Docker Compose는 단일 서버에서 여러 컨테이너를 관리하는 데 잘 동작해요. 하지만 서버가 10대, 100대로 늘어나면? 서버가 죽으면 누가 컨테이너를 다시 띄워주나요? 트래픽이 몰리면 자동으로 확장할 수는 없나요?

Kubernetes(K8s) 는 이 문제를 해결하는 컨테이너 오케스트레이션 도구예요. 원하는 상태를 선언하면, Kubernetes가 그 상태를 유지해줍니다.

이번 주에는 먼저 로컬에서 kind(Kubernetes IN Docker) 로 빠르게 실습한 뒤, EC2의 k3s 클러스터에 배포할 거예요.

공부할 내용

1. YAML 문법

Kubernetes는 리소스를 YAML 파일로 정의해요. JSON과 비슷하지만 더 읽기 쉬워요. 기본 문법을 익히고 오세요.

참고 자료

2. Kubernetes 핵심 개념

아키텍처

Control Plane(클러스터 관리)과 Worker Node(워크로드 실행)의 구조를 이해하세요.

핵심 철학: 선언적(Declarative)

  • 명령적: “컨테이너를 3개 시작해라” → docker run을 3번 실행하는 방식
  • 선언적: “컨테이너 3개가 항상 실행 중이어야 한다” → Kubernetes 방식

Docker Compose도 YAML로 상태를 선언하지만, docker compose up 실행 시점에만 상태를 맞춰요. Kubernetes는 한 발 더 나아가서, 컨트롤 루프가 현재 상태를 지속적으로 확인하며 선언한 상태에 맞춰줘요. 컨테이너가 죽으면 자동으로 다시 만들어요.

핵심 오브젝트

아래 오브젝트들의 역할과 관계를 이해하세요:

  • Pod: 가장 작은 배포 단위. Pod는 언제든 죽을 수 있는 일회성 존재.
  • Deployment: Pod의 복제본을 관리. 롤링 업데이트와 롤백 처리.
  • Service: Pod들에 대한 안정적인 네트워크 엔드포인트. Pod는 재시작 시 IP가 바뀌지만, Service는 변하지 않는 DNS와 IP를 제공. ClusterIP, NodePort, LoadBalancer 타입의 차이를 알아보세요.
  • ConfigMap / Secret: 설정값과 민감한 정보를 Pod에 주입.
  • Namespace: 클러스터 내 논리적 격리.

참고 자료


프로젝트 실습

EC2에 경량 Kubernetes인 k3s를 설치하고, 앱을 배포할 거예요.

Step 1: EC2 인스턴스 준비

요구사항:

  • OS: Ubuntu 24.04 LTS
  • 인스턴스 타입: t3.small 이상 (2 CPU, 2GB RAM)
  • 스토리지: SSD 20GB 이상
  • Security Group: 22(SSH), 80(HTTP), 443(HTTPS), 6443(K8s API), 31000(NodePort) 인바운드 허용

Step 2: k3s 설치

# Docker 설치 (이미지 빌드 및 push용 — k3s 자체는 내장 containerd를 사용합니다)
curl -fsSL https://get.docker.com | sudo sh -

# k3s 설치
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server --tls-san=$(curl -s ifconfig.me) --write-kubeconfig-mode=644" sh

Step 3: 로컬에서 원격 kubectl 접속 설정

EC2의 /etc/rancher/k3s/k3s.yaml을 로컬에 복사하고, server 항목의 IP를 EC2 Public IP로 변경하세요. KUBECONFIG 환경 변수를 설정한 뒤 kubectl get nodes로 연결을 확인하세요.

Step 4: Docker Hub에 이미지 Push

k3s가 이미지를 pull할 수 있도록, Session 3에서 빌드한 이미지를 Docker Hub에 push하세요.

docker tag my-server:v1 <your-dockerhub-username>/my-server:v1
docker push <your-dockerhub-username>/my-server:v1

Step 5: Deployment 작성 및 배포

Deployment YAML을 직접 작성하세요. 아래 스펙을 만족해야 합니다:

  • 이미지: Docker Hub에 올린 본인의 서버 이미지
  • Replicas: 3
  • livenessProbe: /health 엔드포인트 사용 — Session 1에서 만든 그 endpoint! 실패하면 컨테이너 재시작.
  • readinessProbe: /health 엔드포인트 사용 — 실패하면 Service에서 트래픽 제외 (죽이지는 않음).
  • resources: requests와 limits를 설정하세요. requests = 보장되는 최소치 (스케줄링에 사용), limits = 최대 사용량. CPU는 limits를 넘으면 쓰로틀링되고, 메모리는 limits를 넘으면 OOM으로 컨테이너가 종료됩니다.

Step 6: Service 작성

Service YAML을 작성하세요. NodePort 타입으로, 외부 포트 31000을 Pod의 3000으로 매핑하세요.

NodePort는 클러스터의 모든 노드에서 지정한 포트(31000)를 열어서 외부 트래픽을 Pod로 전달해요. EC2의 IP와 이 포트로 직접 접속할 수 있습니다.

Step 7: 배포 및 확인

kubectl apply -f deployment.yaml
kubectl apply -f service.yaml

kubectl get pods -w
kubectl get svc
kubectl describe pod <pod-name>
kubectl logs <pod-name>

# 외부에서 접속
curl http://[EC2-IP]:31000/
curl http://[EC2-IP]:31000/api/info
curl http://[EC2-IP]:31000/health

실험해보기

  1. Pod 삭제: kubectl delete pod <pod-name> → Deployment가 즉시 새 Pod 생성! 이게 선언적 관리의 핵심.
  2. 스케일링: kubectl scale deployment my-server --replicas=5 → 5개로 확장. 다시 --replicas=2로 축소.
  3. 잘못된 이미지: 이미지 태그를 오타로 변경해보세요 → ImagePullBackOff. kubectl describe pod로 이벤트를 확인.
  4. 셸 접속: kubectl exec -it <pod-name> -- sh → 컨테이너 안에서 curl, env 등 실행.

(선택) ALB 연결

Session 5에서 만든 ALB의 Target Group에 EC2 인스턴스의 31000 포트를 추가하면, ALB를 통해서도 접속할 수 있어요.

디버깅 치트시트

Pod가 안 뜨나요?
├── kubectl get pods              → 상태 확인
│   ├── Pending?                  → kubectl describe pod → 이벤트 확인
│   │    (리소스 부족? 이미지 pull 실패?)
│   ├── CrashLoopBackOff?         → kubectl logs <pod>  → 앱 에러 확인
│   │                               kubectl logs <pod> --previous → 이전 로그
│   └── Running인데 안 되나요?     → 아래로
│
├── kubectl get svc               → Service 확인
│   └── Endpoints가 비어있나?      → Labels 불일치!
│
└── kubectl exec -it <pod> -- sh  → 컨테이너 안에서 직접 확인
     ├── curl localhost:3000       → 앱이 내부에서 동작하나?
     ├── env                       → 환경 변수가 주입됐나?
     └── nslookup <service-name>   → DNS가 동작하나?