Skip to content
Go back

EC2에 K3s 올리기 - 개발 일지

Published:  at  10:08 PM

개인 프로젝트를 위한 인프라를 고민하던 중, DevOps 경험도 쌓을 겸 쿠버네티스를 도입하기로 했습니다. 하지만 EKS 같은 완전 관리형 서비스는 비용이 부담스러웠고, 결국 가장 저렴한 EC2 인스턴스에 가벼운 쿠버네티스 배포판인 K3s를 직접 설치하는 길을 선택했습니다.

이 글은 당시 제가 작성했던 설치 셸 스크립트를 기반으로, 각 단계가 어떤 의미를 가지며 왜 그렇게 구성했는지 적어두었습니다.

자동화 스크립트

목표는 git push를 통해 자동으로 컨테이너 이미지를 빌드하고, 클러스터에 무중단으로 배포하는 CI/CD 파이프라인을 구축하는 것입니다. 이 모든 과정을 자동화하기 위해 아래와 같은 셸 스크립트를 작성했습니다.

전체 설치 스크립트 보기 (install.sh)
#!/bin/bash
set -euo pipefail

# 🎯 환경별 설정 파일 로드
ENVIRONMENT=${1:-staging}  # 기본값: staging
CONFIG_FILE="configs/${ENVIRONMENT}.env"

if [[ ! -f "$CONFIG_FILE" ]]; then
  echo "❌ 설정 파일을 찾을 수 없습니다: $CONFIG_FILE"
  echo "사용 가능한 환경: dev, staging, prod"
  echo "사용법: $0 [dev|staging|prod]"
  exit 1
fi

echo "📋 환경 설정 로드 중: $ENVIRONMENT"
source "$CONFIG_FILE"

echo "🚀 K3s 클러스터 설치를 시작합니다..."
echo "🌐 환경: $ENVIRONMENT"
echo "📍 도메인: $K8S_DOMAIN"
echo "📧 이메일: $EMAIL"

# 🌐 공인 IP 획득
echo "🌐 서버 공인 IP 확인 중..."
SERVER_PUBLIC_IP=$(curl -4 ifconfig.me 2>/dev/null)
echo "🌐 감지된 IP: $SERVER_PUBLIC_IP"

# 📦 시스템 업데이트
echo "📦 시스템 업데이트 중..."
sudo apt update && sudo apt upgrade -y

# ☸️  K3s 설치
echo "☸️  K3s 설치 중..."
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="\
  --tls-san $K8S_DOMAIN \
  --disable traefik \
  --disable metrics-server \
  --disable local-storage" sh -

# 🔧 kubeconfig 설정
echo "🔧 kubeconfig 설정 중..."
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
chmod 600 ~/.kube/config
echo 'export KUBECONFIG=~/.kube/config' >> ~/.bashrc
export KUBECONFIG=~/.kube/config

# ⚙️ Helm 설치
echo "⚙️  Helm 설치 중..."
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt update && sudo apt install helm -y

# 📚 Helm 저장소 추가
echo "📚 Helm 저장소 추가 중..."
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add jetstack https://charts.jetstack.io --force-update
helm repo update

if [ "$WITH_METALLB" = true ]; then
  echo "🔗 MetalLB 설치 중..."
  kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.12/config/manifests/metallb-native.yaml

  echo "⏳ MetalLB 초기화 대기..."
  kubectl wait --namespace metallb-system \
    --for=condition=ready pod \
    --selector=app=metallb \
    --timeout=90s

  cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: external-ip-pool
  namespace: metallb-system
spec:
  addresses:
    - $SERVER_PUBLIC_IP/32
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: l2-adv
  namespace: metallb-system
spec:
  ipAddressPools:
    - external-ip-pool
EOF
fi

# 📄 Nginx Ingress 설정 파일 생성
echo "📄 Nginx Ingress Values 파일 생성..."
cat > nginx-ingress-controller.yaml <<EOF
service:
  type: LoadBalancer
  loadBalancerIP: $SERVER_PUBLIC_IP
  annotations:
    metallb.universe.tf/address-pool: external-ip-pool

controller:
  ingressClassResource:
    default: true
  ingressClass: nginx
  watchIngressWithoutClass: true

  resources:
    requests:
      cpu: $NGINX_CPU_REQUEST
      memory: $NGINX_MEMORY_REQUEST
    limits:
      cpu: $NGINX_CPU_LIMIT
      memory: $NGINX_MEMORY_LIMIT

  admissionWebhooks:
    enabled: true
    patch:
      resources:
        requests:
          cpu: $WEBHOOK_CPU_REQUEST
          memory: $WEBHOOK_MEMORY_REQUEST
        limits:
          cpu: $WEBHOOK_CPU_LIMIT
          memory: $WEBHOOK_MEMORY_LIMIT

defaultBackend:
  enabled: false
EOF

# 🌐 Nginx Ingress 설치
echo "🌐 Nginx Ingress Controller 설치 중..."
helm upgrade --install nginx-ingress ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --values nginx-ingress-controller.yaml

if [ "$WITH_CERT_MANAGER" = true ]; then
  echo "🔐 Cert-Manager 설치 중..."
  helm upgrade --install cert-manager jetstack/cert-manager \
    --namespace cert-manager \
    --create-namespace \
    --version v1.17.2 \
    --set crds.enabled=true \
    --set resources.requests.cpu=$CERTMANAGER_CPU_REQUEST \
    --set resources.requests.memory=$CERTMANAGER_MEMORY_REQUEST \
    --set resources.limits.cpu=$CERTMANAGER_CPU_LIMIT \
    --set resources.limits.memory=$CERTMANAGER_MEMORY_LIMIT

  echo "📜 Let's Encrypt ClusterIssuer 생성 중..."
  cat > cluster-issuer.yaml <<EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    email: $EMAIL
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
    - http01:
        ingress:
          ingressClassName: nginx
EOF
  kubectl apply -f cluster-issuer.yaml
fi

if [ "$WITH_GITHUB_SECRET" = true ]; then
  echo "📦 GitHub Container Registry Secret 설정 필요 (수동)..."
  echo "kubectl create secret docker-registry ghcr-secret \\"
  echo "  --docker-server=ghcr.io \\"
  echo "  --docker-username=\$GITHUB_USERNAME \\"
  echo "  --docker-password=\$GITHUB_TOKEN \\"
  echo "  --namespace=default"
fi

# ✅ 설치 완료 및 상태 확인
echo ""
echo "🎉 K3s 클러스터 설치 완료!"
echo ""
echo "📋 설치된 구성요소:"
echo "  ✅ K3s (Kubernetes)"
echo "  ✅ Helm"
echo "  ✅ Nginx Ingress Controller"
[ "$WITH_METALLB" = true ] && echo "  ✅ MetalLB (LoadBalancer)"
[ "$WITH_CERT_MANAGER" = true ] && echo "  ✅ Cert-Manager (Let's Encrypt)"
echo ""
echo "🌐 클러스터 정보:"
echo "  📍 환경: $ENVIRONMENT"
echo "  📍 도메인: $K8S_DOMAIN"
echo "  🌍 공인 IP: $SERVER_PUBLIC_IP"
echo "  📧 이메일: $EMAIL"
echo ""
echo "🔍 상태 확인 명령어:"
echo "  kubectl get nodes"
echo "  kubectl get pods -n ingress-nginx"
[ "$WITH_CERT_MANAGER" = true ] && echo "  kubectl get pods -n cert-manager"
[ "$WITH_METALLB" = true ] && echo "  kubectl get pods -n metallb-system"
echo "  kubectl get svc -n ingress-nginx"
echo ""
echo "📋 kubeconfig (GitHub Secrets에 저장할 내용):"
echo "----------------------------------------"
cat ~/.kube/config | sed "s|https://127.0.0.1:6443|https://$K8S_DOMAIN:6443|g"
echo "----------------------------------------"
echo ""
echo "🚀 이제 애플리케이션을 배포할 수 있습니다!"

이제 이 스크립트의 각 단계를 자세히 살펴보겠습니다.

1단계: 기본 환경 설정

스크립트의 가장 처음은 서버의 기본 상태를 준비하는 과정입니다.

# 📦 시스템 업데이트
echo "📦 시스템 업데이트 중..."
sudo apt update && sudo apt upgrade -y

# 🌐 공인 IP 획득
echo "🌐 서버 공인 IP 확인 중..."
SERVER_PUBLIC_IP=$(curl -4 ifconfig.me 2>/dev/null)
echo "🌐 감지된 IP: $SERVER_PUBLIC_IP"

2단계: K3s 코어 설치 및 설정

가장 핵심인 K3s를 설치하고, kubectl로 클러스터를 제어할 수 있도록 준비합니다.

# ☸️  K3s 설치
echo "☸️  K3s 설치 중..."
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="\
  --tls-san $K8S_DOMAIN \
  --disable traefik \
  --disable metrics-server \
  --disable local-storage" sh -

# 🔧 kubeconfig 설정
echo "🔧 kubeconfig 설정 중..."
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
chmod 600 ~/.kube/config

3단계: Helm 설치 및 저장소 추가

쿠버네티스의 패키지 매니저인 Helm을 설치합니다. 애플리케이션을 차트(Chart) 단위로 쉽게 배포하고 관리할 수 있게 해주는 필수 도구입니다.

# ⚙️ Helm 설치
echo "⚙️  Helm 설치 중..."
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt update && sudo apt install helm -y

# 📚 Helm 저장소 추가
echo "📚 Helm 저장소 추가 중..."
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add jetstack https://charts.jetstack.io
helm repo update

4단계: 네트워크 인프라 구축 (MetalLB & Nginx Ingress)

외부 트래픽이 클러스터 내부의 서비스까지 도달할 수 있는 길을 만듭니다.

MetalLB: LoadBalancer 서비스 구현

EC2 같은 단일 노드 환경은 클라우드 플랫폼처럼 LoadBalancer 타입의 서비스를 지원하지 않습니다. MetalLB는 이러한 베어메탈 환경에서 LoadBalancer 서비스에 공인 IP를 할당해주는 역할을 합니다.

# 🔗 MetalLB 설치
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.12/config/manifests/metallb-native.yaml

kubectl wait --namespace metallb-system --for=condition=ready pod --selector=app=metallb --timeout=90s

# 🔗 MetalLB 설정 적용
cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: external-ip-pool
  namespace: metallb-system
spec:
  addresses:
    - $SERVER_PUBLIC_IP/32
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: l2-adv
  namespace: metallb-system
spec:
  ipAddressPools:
    - external-ip-pool
EOF

Nginx Ingress Controller: 클러스터의 관문

Ingress Controller는 도메인과 경로 규칙에 따라 트래픽을 적절한 내부 서비스로 라우팅하는 ‘교통 경찰’입니다. Helm으로 설치하되, 설정을 values.yaml 파일로 분리하여 관리의 용이성을 높였습니다.

# 📄 Nginx Ingress Values 파일 생성
cat > nginx-ingress-values.yaml <<EOF
controller:
  ingressClassResource:
    default: true
  resources:
    requests:
      cpu: 100m
      memory: 128Mi
  service:
    type: LoadBalancer
EOF

# 🌐 Nginx Ingress Controller 설치
helm upgrade --install nginx-ingress ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  -f nginx-ingress-values.yaml

5단계: SSL/TLS 인증서 자동화 (Cert-Manager)

Let’s Encrypt와 연동하여 HTTPS 통신에 필요한 SSL 인증서를 자동으로 발급하고 갱신합니다.

# 🔐 Cert-Manager 설치
helm upgrade --install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set crds.enabled=true

# 📜 Let's Encrypt ClusterIssuer 생성
cat > cluster-issuer.yaml <<EOF
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    email: your-email@example.com # 실제 이메일로 변경
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-key
    solvers:
    - http01:
        ingress:
          ingressClassName: nginx
EOF

kubectl apply -f cluster-issuer.yaml

6단계: CI/CD 연동 및 최종 확인

외부 CI/CD 시스템이 클러스터에 배포할 수 있도록 준비하고, 모든 컴포넌트가 정상적으로 동작하는지 확인합니다.

CI/CD용 kubeconfig 생성

GitHub Actions 같은 외부 환경은 서버의 기본 kubeconfig 파일(server 주소가 127.0.0.1로 되어 있음)을 사용할 수 없습니다. 따라서 서버 주소를 공인 도메인으로 변경한 버전을 생성해야 합니다.

echo "📋 kubeconfig (GitHub Secrets에 저장할 내용):"
echo "----------------------------------------"
cat ~/.kube/config | sed "s|https://127.0.0.1:6443|https://$K8S_DOMAIN:6443|g"
echo "----------------------------------------"

위 명령어로 출력된 내용을 복사하여 GitHub 저장소의 Settings > Secrets and variables > ActionsKUBECONFIG_CONTENT와 같은 이름의 Secret으로 저장하면 됩니다.

최종 설치 확인

아래 명령어들을 통해 각 컴포넌트들이 정상적으로 설치되고 실행 중인지 확인할 수 있습니다.

# 전체 노드 상태 확인
kubectl get nodes -o wide

# 모든 네임스페이스의 Pod 상태 확인
kubectl get pods -A

# Ingress-Nginx 서비스의 EXTERNAL-IP 할당 확인
kubectl get svc -n ingress-nginx

# Cert-Manager Pod 동작 확인
kubectl get pods -n cert-manager

ingress-nginx-controller 서비스의 EXTERNAL-IP가 서버의 공인 IP로 할당되었다면 성공입니다. 이제 샘플 애플리케이션을 배포하여 최종 테스트를 진행할 수 있습니다.


Share this post on:

Previous Post
왜 프리랜서를 그만두게 되었는가