jongkwan.dev
개발 · Essay №023

컨테이너화 & 오케스트레이션 심화 가이드

멀티 스테이지 빌드, 이미지 보안, 고급 스케줄링, 서비스 메시 등 컨테이너 심화 운영

이종관2025년 11월 22일11 min read
Contents

컨테이너는 실행 환경을 패키지로 묶고, 규모가 커지면 오케스트레이션이 배포와 복구를 자동화한다.

컨테이너는 애플리케이션과 실행 환경을 하나의 패키지로 묶어 어디서든 동일하게 동작하도록 보장한다. 컨테이너 수가 늘어나면 배포, 네트워킹, 스토리지, 장애 복구를 사람이 직접 다루기 어려워진다. 이때 오케스트레이션이 이 작업들을 선언적 구성으로 자동화한다.

고급 컨테이너 빌드 전략

멀티 스테이지 빌드

빌드 도구와 컴파일러를 최종 이미지에서 제외해 런타임 이미지 크기를 줄인다. 빌드 스테이지에서 바이너리를 만든 뒤, 런타임 스테이지로 결과물만 복사한다.

dockerfile
# 빌드 스테이지
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o main .
 
# 런타임 스테이지
FROM alpine:3.18
COPY --from=builder /app/main /main
CMD ["/main"]

COPY --from=builder가 빌드 스테이지의 산출물만 가져오므로, Go 컴파일러와 소스 코드는 최종 이미지에 남지 않는다.

이미지 레이어 최적화

도커는 레이어 단위로 캐시하므로, 자주 바뀌는 파일을 늦게 복사할수록 캐시가 오래 유지된다. 의존성 정의 파일을 먼저 복사해 설치한 뒤 소스 코드를 복사하면, 코드만 바뀌어도 의존성 설치 레이어는 캐시를 재사용한다.

dockerfile
# Bad: 캐시 효율 낮음
COPY . .
RUN npm install
 
# Good: 캐시 효율 높음
COPY package*.json ./
RUN npm install
COPY . .

이미지 보안

도구용도
Trivy취약점 스캔
cosign / Notation이미지 서명
distroless경량 베이스 이미지

이미지 서명은 한때 Notary(v1)가 표준에 가까웠으나, 현재는 Sigstore cosign과 Notation으로 옮겨가는 추세다. distroless는 셸과 패키지 매니저가 없는 베이스 이미지여서 공격 표면을 줄인다.

bash
# Trivy로 취약점 스캔
trivy image my-app:latest

리소스 격리와 튜닝

cgroups & Namespaces

control groups(cgroups)는 CPU, 메모리, I/O, 네트워크 사용량을 Linux 커널 레벨에서 격리한다. Namespaces는 프로세스, 네트워크, 파일시스템의 가시 범위를 분리한다.

yaml
# Kubernetes 리소스 제한
resources:
  requests:
    memory: "64Mi"
    cpu: "250m"
  limits:
    memory: "128Mi"
    cpu: "500m"

requests는 스케줄러가 Pod를 배치할 때 보장하는 최소 자원이고, limits는 컨테이너가 넘을 수 없는 상한이다. 메모리가 limits를 넘으면 컨테이너가 종료(OOMKilled)되고, CPU는 상한까지만 스로틀링된다.

컨테이너 런타임

런타임특징
containerd경량, K8s 기본
CRI-OK8s 전용
gVisor보안 샌드박스

Rootless 컨테이너

호스트의 루트 권한 없이 컨테이너를 실행해, 컨테이너 탈출이 일어나도 호스트 루트로 이어지지 않게 한다.

bash
# Podman rootless 모드
podman run --user 1000:1000 nginx

고급 스케줄링

Node Affinity

노드 레이블 조건에 맞는 노드에만 Pod를 배치한다. 아래 예시는 gpu=true 레이블이 붙은 노드에만 스케줄링하도록 강제한다.

yaml
affinity:
  nodeAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      nodeSelectorTerms:
      - matchExpressions:
        - key: gpu
          operator: In
          values:
          - "true"

Pod Anti-Affinity

같은 종류의 Pod를 서로 다른 노드에 분산해, 한 노드가 죽어도 전체가 함께 내려가지 않게 한다. topologyKey는 분산 단위를 정하는 기준으로, kubernetes.io/hostname이면 노드 단위로 흩어놓는다.

yaml
affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
    - labelSelector:
        matchLabels:
          app: web
      topologyKey: kubernetes.io/hostname

Taint & Toleration

노드에 Taint를 걸면 그 Taint를 견디는(Toleration) Pod만 해당 노드에 배치된다. Node Affinity가 Pod 쪽에서 노드를 고르는 방식이라면, Taint는 노드 쪽에서 Pod를 밀어내는 방식이다.

bash
# 노드에 Taint 추가
kubectl taint nodes gpu-node gpu=true:NoSchedule
yaml
# Pod에 Toleration 추가
tolerations:
- key: "gpu"
  operator: "Equal"
  value: "true"
  effect: "NoSchedule"

서비스 메쉬

서비스 메쉬는 마이크로서비스 사이의 통신을 별도 인프라 레이어에서 처리한다. 애플리케이션 코드 대신 사이드카가 트래픽 제어, 보안, 관찰성을 맡는다.

기능설명
트래픽 제어라우팅, 로드밸런싱
보안mutual TLS(mTLS) 자동 적용
관찰성분산 트레이싱

주요 솔루션

솔루션특징
Istio가장 많은 기능, 복잡도 높음
Linkerd경량, 사용 쉬움
Consul ConnectHashiCorp 생태계

Sidecar 패턴

애플리케이션 컨테이너 옆에 Envoy 프록시를 사이드카로 붙여, 코드 수정 없이 네트워크 기능을 추가한다. 모든 인바운드·아웃바운드 트래픽이 프록시를 거치므로 mTLS와 트레이싱을 일괄 적용할 수 있다.

스토리지 오케스트레이션

CSI (Container Storage Interface)

Container Storage Interface(CSI)는 AWS EBS, GCP PD, NFS 같은 서로 다른 스토리지를 일관된 인터페이스로 다루게 한다.

yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp3
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer

volumeBindingMode: WaitForFirstConsumer는 Pod가 실제로 스케줄링될 때까지 볼륨 생성을 미룬다. 덕분에 볼륨이 Pod와 같은 가용 영역에 만들어져, 영역이 어긋나 마운트에 실패하는 문제를 막는다.

동적 프로비저닝

PersistentVolumeClaim(PVC)을 만들면 StorageClass가 그에 맞는 PersistentVolume(PV)을 자동으로 생성해 연결한다. 관리자가 미리 볼륨을 만들어 둘 필요가 없다.

yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-pvc
spec:
  storageClassName: fast
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 20Gi

오토 스케일링

HPA (Horizontal Pod Autoscaler)

CPU나 메모리 사용률을 목표값에 맞춰 Pod 수를 늘리거나 줄인다. 아래 예시는 평균 CPU 사용률 70%를 기준으로 2개에서 10개 사이에서 복제본 수를 조정한다.

yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

KEDA (Kubernetes Event-Driven Autoscaling)

CPU·메모리 대신 큐 길이, 메시지 적체 같은 외부 이벤트 지표로 스케일링한다. 아래 예시의 lagThreshold: "100"은 Kafka 컨슈머가 처리하지 못한 메시지가 파티션당 100건을 넘으면 컨슈머 Pod를 늘린다는 뜻이다.

yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: kafka-scaler
spec:
  scaleTargetRef:
    name: consumer
  triggers:
  - type: kafka
    metadata:
      topic: orders
      lagThreshold: "100"

Observability

모니터링 스택

  • 메트릭: Prometheus -> Grafana
  • 로그: Fluentd -> Elasticsearch -> Kibana
  • 트레이싱: Jaeger 또는 Zipkin

GitOps

Git 저장소를 단일 진실 공급원(Single Source of Truth)으로 두고, Argo CD나 Flux가 저장소 상태와 클러스터 상태를 지속적으로 맞춘다. 변경은 Git 커밋으로만 일어나므로 배포 이력과 롤백이 커밋 단위로 남는다.

정리

영역핵심 기술
빌드멀티 스테이지, 이미지 스캔
스케줄링Affinity, Taint/Toleration
네트워킹서비스 메쉬, Container Network Interface(CNI)
스토리지CSI, 동적 프로비저닝
스케일링HPA, Vertical Pod Autoscaler(VPA), KEDA
운영GitOps, Observability

컨테이너 오케스트레이션은 선언적 구성으로 배포·복구·스케일링을 자동화하고, 그 결과를 관찰성으로 검증하는 흐름이다. 이미지 단계에서는 멀티 스테이지 빌드로 크기를 줄이고 스캔·서명으로 신뢰를 확보한다. 스케줄링은 노드 특성에 따라 Affinity와 Taint/Toleration으로 배치를 통제하고, 스토리지는 CSI와 동적 프로비저닝으로 볼륨을 자동 연결한다. 스케일링은 CPU·메모리 기반이면 HPA, 큐 적체 같은 이벤트 기반이면 KEDA로 나눠 고른다. 운영은 GitOps로 변경 이력을 커밋에 남기고, 메트릭·로그·트레이싱을 묶어 장애를 추적한다.