컨테이너화 & 오케스트레이션 심화 가이드
멀티 스테이지 빌드, 이미지 보안, 고급 스케줄링, 서비스 메시 등 컨테이너 심화 운영
Contents
컨테이너는 실행 환경을 패키지로 묶고, 규모가 커지면 오케스트레이션이 배포와 복구를 자동화한다.
컨테이너는 애플리케이션과 실행 환경을 하나의 패키지로 묶어 어디서든 동일하게 동작하도록 보장한다. 컨테이너 수가 늘어나면 배포, 네트워킹, 스토리지, 장애 복구를 사람이 직접 다루기 어려워진다. 이때 오케스트레이션이 이 작업들을 선언적 구성으로 자동화한다.
고급 컨테이너 빌드 전략
멀티 스테이지 빌드
빌드 도구와 컴파일러를 최종 이미지에서 제외해 런타임 이미지 크기를 줄인다. 빌드 스테이지에서 바이너리를 만든 뒤, 런타임 스테이지로 결과물만 복사한다.
# 빌드 스테이지
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 컴파일러와 소스 코드는 최종 이미지에 남지 않는다.
이미지 레이어 최적화
도커는 레이어 단위로 캐시하므로, 자주 바뀌는 파일을 늦게 복사할수록 캐시가 오래 유지된다. 의존성 정의 파일을 먼저 복사해 설치한 뒤 소스 코드를 복사하면, 코드만 바뀌어도 의존성 설치 레이어는 캐시를 재사용한다.
# Bad: 캐시 효율 낮음
COPY . .
RUN npm install
# Good: 캐시 효율 높음
COPY package*.json ./
RUN npm install
COPY . .이미지 보안
| 도구 | 용도 |
|---|---|
| Trivy | 취약점 스캔 |
| cosign / Notation | 이미지 서명 |
| distroless | 경량 베이스 이미지 |
이미지 서명은 한때 Notary(v1)가 표준에 가까웠으나, 현재는 Sigstore cosign과 Notation으로 옮겨가는 추세다. distroless는 셸과 패키지 매니저가 없는 베이스 이미지여서 공격 표면을 줄인다.
# Trivy로 취약점 스캔
trivy image my-app:latest리소스 격리와 튜닝
cgroups & Namespaces
control groups(cgroups)는 CPU, 메모리, I/O, 네트워크 사용량을 Linux 커널 레벨에서 격리한다. Namespaces는 프로세스, 네트워크, 파일시스템의 가시 범위를 분리한다.
# Kubernetes 리소스 제한
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"requests는 스케줄러가 Pod를 배치할 때 보장하는 최소 자원이고, limits는 컨테이너가 넘을 수 없는 상한이다. 메모리가 limits를 넘으면 컨테이너가 종료(OOMKilled)되고, CPU는 상한까지만 스로틀링된다.
컨테이너 런타임
| 런타임 | 특징 |
|---|---|
| containerd | 경량, K8s 기본 |
| CRI-O | K8s 전용 |
| gVisor | 보안 샌드박스 |
Rootless 컨테이너
호스트의 루트 권한 없이 컨테이너를 실행해, 컨테이너 탈출이 일어나도 호스트 루트로 이어지지 않게 한다.
# Podman rootless 모드
podman run --user 1000:1000 nginx고급 스케줄링
Node Affinity
노드 레이블 조건에 맞는 노드에만 Pod를 배치한다. 아래 예시는 gpu=true 레이블이 붙은 노드에만 스케줄링하도록 강제한다.
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: gpu
operator: In
values:
- "true"Pod Anti-Affinity
같은 종류의 Pod를 서로 다른 노드에 분산해, 한 노드가 죽어도 전체가 함께 내려가지 않게 한다. topologyKey는 분산 단위를 정하는 기준으로, kubernetes.io/hostname이면 노드 단위로 흩어놓는다.
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: web
topologyKey: kubernetes.io/hostnameTaint & Toleration
노드에 Taint를 걸면 그 Taint를 견디는(Toleration) Pod만 해당 노드에 배치된다. Node Affinity가 Pod 쪽에서 노드를 고르는 방식이라면, Taint는 노드 쪽에서 Pod를 밀어내는 방식이다.
# 노드에 Taint 추가
kubectl taint nodes gpu-node gpu=true:NoSchedule# Pod에 Toleration 추가
tolerations:
- key: "gpu"
operator: "Equal"
value: "true"
effect: "NoSchedule"서비스 메쉬
서비스 메쉬는 마이크로서비스 사이의 통신을 별도 인프라 레이어에서 처리한다. 애플리케이션 코드 대신 사이드카가 트래픽 제어, 보안, 관찰성을 맡는다.
| 기능 | 설명 |
|---|---|
| 트래픽 제어 | 라우팅, 로드밸런싱 |
| 보안 | mutual TLS(mTLS) 자동 적용 |
| 관찰성 | 분산 트레이싱 |
주요 솔루션
| 솔루션 | 특징 |
|---|---|
| Istio | 가장 많은 기능, 복잡도 높음 |
| Linkerd | 경량, 사용 쉬움 |
| Consul Connect | HashiCorp 생태계 |
Sidecar 패턴
애플리케이션 컨테이너 옆에 Envoy 프록시를 사이드카로 붙여, 코드 수정 없이 네트워크 기능을 추가한다. 모든 인바운드·아웃바운드 트래픽이 프록시를 거치므로 mTLS와 트레이싱을 일괄 적용할 수 있다.
스토리지 오케스트레이션
CSI (Container Storage Interface)
Container Storage Interface(CSI)는 AWS EBS, GCP PD, NFS 같은 서로 다른 스토리지를 일관된 인터페이스로 다루게 한다.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp3
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumervolumeBindingMode: WaitForFirstConsumer는 Pod가 실제로 스케줄링될 때까지 볼륨 생성을 미룬다. 덕분에 볼륨이 Pod와 같은 가용 영역에 만들어져, 영역이 어긋나 마운트에 실패하는 문제를 막는다.
동적 프로비저닝
PersistentVolumeClaim(PVC)을 만들면 StorageClass가 그에 맞는 PersistentVolume(PV)을 자동으로 생성해 연결한다. 관리자가 미리 볼륨을 만들어 둘 필요가 없다.
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개 사이에서 복제본 수를 조정한다.
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: 70KEDA (Kubernetes Event-Driven Autoscaling)
CPU·메모리 대신 큐 길이, 메시지 적체 같은 외부 이벤트 지표로 스케일링한다. 아래 예시의 lagThreshold: "100"은 Kafka 컨슈머가 처리하지 못한 메시지가 파티션당 100건을 넘으면 컨슈머 Pod를 늘린다는 뜻이다.
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로 변경 이력을 커밋에 남기고, 메트릭·로그·트레이싱을 묶어 장애를 추적한다.