Skip to content

07. 컨테이너 오케스트레이션 ​

컨테이너가 수십 개가 되면 손으로 띄우고 지킬 수 없습니다. "이렇게 떠 있어야 한다"를 적어 두면 맞춰 주는 도구가 필요해집니다.

선언적 모델 ​

명령이 아니라 원하는 상태를 적습니다.

명령형:  컨테이너를 3개 띄워라
선언형:  이 컨테이너가 항상 3개 떠 있어야 한다

하나가 죽으면 컨트롤러가 알아서 새로 띄웁니다. 노드가 사라지면 다른 노드에 다시 배치합니다. 사람이 개입하는 자리는 원하는 상태를 바꿀 때뿐입니다.

클러스터 구조 ​

하는 일
API 서버모든 요청의 입구, 인증과 검증
etcd클러스터 상태 저장소
스케줄러새 Pod를 어느 노드에 둘지 결정
컨트롤러 매니저현재 상태를 원하는 상태로 맞춤
kubelet각 노드에서 컨테이너를 실제로 띄움

관리형 서비스(EKS, GKE, AKS)를 쓰면 컨트롤 플레인은 제공자가 운영합니다. 직접 세우는 쪽은 etcd 백업까지 책임져야 하므로, 처음에는 관리형이 거의 항상 낫습니다.

워크로드 ​

리소스쓰는 곳
Pod컨테이너 한 묶음 (직접 만들지는 않음)
Deployment상태 없는 애플리케이션
StatefulSet고유한 이름과 저장소가 필요한 것 (DB 등)
DaemonSet모든 노드에 하나씩 (로그 수집기, 노드 감시)
Job / CronJob한 번 실행 / 주기 실행

Pod는 직접 만들지 않습니다. 죽으면 그대로 사라지기 때문에, 항상 컨트롤러를 통해 만듭니다.

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  replicas: 3
  selector:
    matchLabels: { app: api }
  template:
    metadata:
      labels: { app: api }
    spec:
      containers:
        - name: api
          image: registry.example.com/api:v1.4.0
          ports:
            - containerPort: 3000
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef: { name: api-secret, key: database-url }
          resources:
            requests: { cpu: 100m, memory: 128Mi }
            limits:   { memory: 512Mi }
          readinessProbe:
            httpGet: { path: /health/ready, port: 3000 }
            initialDelaySeconds: 5
          livenessProbe:
            httpGet: { path: /health/live, port: 3000 }
            periodSeconds: 10

자원 요청과 제한 ​

뜻
requests스케줄러가 자리를 잡을 때 쓰는 기준
limits이 이상 쓰면 제한

requests를 적지 않으면 스케줄러가 판단할 근거가 없어 한 노드에 몰립니다.

CPU limit은 대체로 걸지 않습니다. CPU는 제한에 닿으면 스로틀링이 걸려 응답이 느려지는데, 남는 CPU가 있어도 쓰지 못합니다. 메모리는 반대로 제한을 겁니다. 넘기면 OOM으로 죽지만, 그대로 두면 노드 전체를 끌고 내려갑니다.

프로브 ​

프로브실패하면
readinessService에서 제외 (트래픽 안 보냄)
liveness컨테이너 재시작
startup시작이 느린 앱에서 다른 프로브를 미룸

liveness에 DB 연결 확인을 넣으면 안 됩니다. DB가 잠깐 흔들릴 때 모든 Pod가 동시에 재시작되어 장애가 커집니다. liveness는 "프로세스가 응답하는가"만 봅니다.

네트워크 ​

yaml
apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  selector: { app: api }
  ports:
    - port: 80
      targetPort: 3000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
spec:
  tls:
    - hosts: [api.example.com]
      secretName: api-tls
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api
                port: { number: 80 }

Service는 Pod가 바뀌어도 변하지 않는 안쪽 주소입니다. Ingress는 바깥에서 들어오는 HTTP 경로를 Service로 보냅니다. Ingress 자체는 규칙일 뿐이고, 실제로 트래픽을 받는 것은 Ingress 컨트롤러(ingress-nginx 등)입니다. 설치하지 않으면 아무 일도 일어나지 않습니다.

설정과 비밀 ​

yaml
apiVersion: v1
kind: ConfigMap
metadata: { name: api-config }
data:
  LOG_LEVEL: info

Secret은 base64 인코딩일 뿐 암호화가 아닙니다. 클러스터에 접근할 수 있으면 그대로 읽힙니다. 저장소에 커밋하려면 Sealed Secrets나 External Secrets로 실제 값을 바깥에 두고 참조만 올립니다(보너스. 데브섹옵스).

kubectl ​

bash
kubectl get pods -n prod
kubectl describe pod api-7d9f-x2k4 -n prod      # 이벤트까지 보임
kubectl logs -f deploy/api -n prod
kubectl logs api-7d9f-x2k4 --previous           # 죽기 직전 로그
kubectl rollout status deploy/api -n prod
kubectl rollout undo deploy/api -n prod         # 롤백

describe의 아래쪽 Events를 먼저 봅니다. 이미지를 못 받았는지, 자리가 없어 배치되지 않았는지가 대부분 여기 적혀 있습니다.

상태원인
ImagePullBackOff이미지 이름 오타, 레지스트리 인증
CrashLoopBackOff컨테이너가 계속 죽음 — logs --previous
Pending자원 부족, 노드 셀렉터와 맞는 노드 없음
OOMKilled메모리 제한 초과

Helm ​

YAML이 환경 수만큼 복제되는 문제를 템플릿으로 풉니다.

bash
helm upgrade --install api ./charts/api \
  -n prod -f values.prod.yaml \
  --set image.tag=v1.4.0 --wait

--wait를 붙이면 롤아웃이 끝날 때까지 기다리고, 실패하면 0이 아닌 코드로 끝납니다. CI에서 반드시 필요합니다.

도입 시점 ​

쿠버네티스는 운영 비용이 큽니다. 서비스 두세 개를 돌리는 데에는 과합니다.

상황맞는 선택
서비스 한두 개컨테이너를 그냥 띄우기, PaaS
트래픽이 일정하지 않음서버리스, 관리형 컨테이너 서비스
서비스 여러 개, 팀 여러 개쿠버네티스

"언젠가 필요할 것 같아서" 미리 도입하면, 정작 제품에 쓸 시간을 클러스터 운영에 씁니다.