HPA/VPA 오토스케일링, RBAC 보안, StatefulSet·DaemonSet·CronJob 워크로드, GPU 기반 LLM 추론 서버 배포와 카나리 롤아웃·큐 기반 오토스케일링 같은 AI 모델 운영 전략, 업그레이드·장애 복구, 차트 구조·Hook·서브차트까지 다루는 Helm 심화, Contour/Gateway API 기반 Ingress 전환, LB·Contour 버전별 Proxy Protocol 설정과 내부 호출 대응, 모니터링까지 — 프로덕션 Kubernetes 운영에 필요한 심화 주제를 다룹니다.
자동 스케일링 & 무중단 배포RBAC 기반 권한 관리StatefulSet/DaemonSet/CronJob 운영GPU 노드 기반 LLM 추론 서버 & AI 모델 운영 전략Helm 차트 심화 & GitOps 연계Contour/Gateway API 기반 Ingress 전환 & 정책 설정Proxy Protocol 기반 클라이언트 IP 보존 & 내부 호출 대응
HPA/VPA 오토스케일링, RBAC 보안, StatefulSet·DaemonSet·CronJob 워크로드, GPU 기반 LLM 추론 서버 배포와 카나리 롤아웃·큐 기반 오토스케일링 같은 AI 모델 운영 전략, 업그레이드·장애 복구, 차트 구조·Hook·서브차트까지 다루는 Helm 심화, Contour/Gateway API 기반 Ingress 전환, LB·Contour 버전별 Proxy Protocol 설정과 내부 호출 대응, 모니터링까지 — 프로덕션 Kubernetes 운영에 필요한 심화 주제를 다룹니다. 이 가이드는 개념을 나열하기보다, 실제 프로젝트에서 판단해야 하는 순서대로 내용을 따라갈 수 있게 구성했습니다.
핵심 관점
인프라 / 운영
설치 명령을 외우기보다 트래픽, 런타임, 관측, 장애 대응이 어떤 순서로 이어지는지 파악합니다.
자동 스케일링 & 무중단 배포RBAC 기반 권한 관리StatefulSet/DaemonSet/CronJob 운영GPU 노드 기반 LLM 추론 서버 & AI 모델 운영 전략Helm 차트 심화 & GitOps 연계Contour/Gateway API 기반 Ingress 전환 & 정책 설정Proxy Protocol 기반 클라이언트 IP 보존 & 내부 호출 대응
구조 다이어그램
글로 읽은 내용을 머릿속에 오래 남기려면 먼저 흐름을 그림으로 잡는 편이 좋습니다. 아래 두 그림은 Kubernetes 심화/실무를 학습할 때 계속 되돌아볼 수 있는 기준 지도입니다.
학습 흐름
다이어그램 렌더링 중…
아키텍처 관점
다이어그램 렌더링 중…
HPA & VPA 오토스케일링
Kubernetes 심화/실무를 처음 펼칠 때는 세부 명령보다 큰 그림이 먼저입니다. 이 섹션에서는 앞으로 배울 개념들이 어떤 문제를 풀기 위해 등장했는지부터 잡아봅니다.
트래픽이 일정하지 않은 서비스는 고정 replicas 대신 오토스케일러로 부하에 맞춰 Pod 수(HPA)나 컨테이너 리소스(VPA)를 자동 조정해야 합니다. 두 방식을 같은 리소스에 CPU/메모리 타깃으로 동시에 켜면 서로 충돌하므로, HPA는 CPU/커스텀 메트릭 기준으로, VPA는 요청값 추천 전용(updateMode: Off)으로 분리 운용하는 것이 안전합니다.
# Metrics Server 설치 (HPA 필수 의존성)kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml# 상태 확인kubectl get hpa myapp-hpa --watchkubectl describe hpa myapp-hpa # 현재 메트릭 값, 스케일 이벤트 확인kubectl top pods # Pod별 실시간 CPU/메모리
여기서는 StatefulSet vs Stateless(Deployment) 비교을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
Kubernetes에서 워크로드를 선택할 때 가장 중요한 기준은 "Pod이 상태(데이터)를 직접 보관하는가"입니다. Deployment는 Pod을 교체 가능한 복제본으로 다루지만, StatefulSet은 각 Pod에 고유한 ID, 안정적 네트워크 이름, 독립적 스토리지를 부여합니다.
다이어그램 렌더링 중…
구분
Deployment (Stateless)
StatefulSet (Stateful)
Pod 이름
랜덤 suffix (app-7d4f9c-xkz)
순번 고정 (app-0, app-1, app-2)
Pod 교체
랜덤 순서로 생성·삭제 가능
순서 보장 (0→1→2 생성, 2→1→0 삭제)
네트워크 ID
Service ClusterIP 공유, Pod IP 가변
Headless Service로 app-0.svc, app-1.svc 고정 DNS
스토리지
Pod 삭제 시 데이터 소멸 (PVC 미보장)
volumeClaimTemplates → 각 Pod 전용 PVC 유지
스케일 아웃
순서 없이 즉시 병렬 확장
0, 1, 2 순서로 순차 확장
롤링 업데이트
랜덤 교체 (maxSurge/maxUnavailable)
역순 (2→1→0) 순차 교체
주요 용도
API 서버, 웹 앱, AI 추론 서버 등
DB(PostgreSQL, MySQL), 메시지 브로커(Kafka), 분산 캐시(Redis Cluster)
PodDisruptionBudget
선택 사항
운영 환경에서 필수 권장
StatefulSet 완전 가이드
여기서는 StatefulSet 완전 가이드을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
PostgreSQL을 StatefulSet으로 배포하는 예제로 Headless Service, volumeClaimTemplates, 안정적 네트워크 ID, 운영 운용 패턴까지 단계별로 정리합니다.
# 배포kubectl apply -f postgres-statefulset.yaml# Pod 상태 확인 (순서 보장: postgres-0 먼저 Running 후 postgres-1 생성)kubectl get pods -l app=postgres -w# Pod별 고정 DNS 확인# postgres-0.postgres-svc.default.svc.cluster.local# postgres-1.postgres-svc.default.svc.cluster.localkubectl exec -it postgres-0 -- psql -U postgres -c "SELECT inet_server_addr();"# PVC는 Pod 삭제 후에도 유지됨kubectl delete pod postgres-2kubectl get pvc | grep postgres # postgres-data-postgres-2 그대로 존재# 스케일 다운 (2→1→0 역순 삭제)kubectl scale statefulset postgres --replicas=1# 롤링 업데이트 (updateStrategy: RollingUpdate, 역순 2→1→0)kubectl set image statefulset/postgres postgres=postgres:17-alpinekubectl rollout status statefulset/postgres# 특정 Pod만 재시작 (PVC 유지)kubectl delete pod postgres-1
pdb.yamlYAML
# PodDisruptionBudget — 노드 드레인 등 자발적 중단 시 최소 2개 유지apiVersion: policy/v1kind: PodDisruptionBudgetmetadata: name: postgres-pdbspec: minAvailable: 2 selector: matchLabels: app: postgres
# 4. Headless Service → StatefulSet 순서로 배포 (순서 필수)kubectl apply -f postgres-headless-svc.yaml -n databasekubectl apply -f postgres-statefulset.yaml -n database# 5. Pod 기동 순서 모니터링 (postgres-0 Running 후 postgres-1 시작)kubectl get pods -n database -l app=postgres -w# Ready 상태 확인kubectl rollout status statefulset/postgres -n database# 6. PodDisruptionBudget 적용kubectl apply -f postgres-pdb.yaml -n database# 전체 리소스 확인kubectl get all,pvc,pdb -n database -l app=postgres
05-scale-update.shBASH
# ── 스케일 업/다운 ────────────────────────────────────────────# 스케일 업: 3 → 5 (3, 4 순서로 순차 생성)kubectl scale statefulset postgres --replicas=5 -n databasekubectl get pods -n database -l app=postgres -w# 스케일 다운: 5 → 3 (4, 3 순서로 역순 삭제, PVC는 유지)kubectl scale statefulset postgres --replicas=3 -n database# PVC는 삭제되지 않음 — 수동 정리 필요 시kubectl delete pvc postgres-data-postgres-3 -n databasekubectl delete pvc postgres-data-postgres-4 -n database# ── 롤링 업데이트 ─────────────────────────────────────────────# 이미지 업데이트 (역순: pod-2 → pod-1 → pod-0)kubectl set image statefulset/postgres \ postgres=postgres:17-alpine -n database# 업데이트 진행 상황kubectl rollout status statefulset/postgres -n database# 업데이트 일시 중단 (카나리 배포 패턴)kubectl patch statefulset postgres -n database \ -p '{"spec":{"updateStrategy":{"rollingUpdate":{"partition":2}}}}'# partition=2 → pod-2만 새 버전, pod-0·pod-1은 유지# 검증 후 partition=0으로 전체 업데이트# ── 롤백 ─────────────────────────────────────────────────────kubectl rollout undo statefulset/postgres -n databasekubectl rollout history statefulset/postgres -n database
06-disaster-recovery.shBASH
# ── 장애 복구 시나리오 ───────────────────────────────────────# Pod 강제 재시작 (PVC 유지, 데이터 보존)kubectl delete pod postgres-1 -n databasekubectl get pods -n database -w # 자동 재생성 확인# Pod가 Pending 상태일 때 진단kubectl describe pod postgres-0 -n databasekubectl get events -n database --sort-by='.lastTimestamp'# PVC 볼륨 상태 확인kubectl get pvc -n databasekubectl describe pvc postgres-data-postgres-0 -n database# 노드 장애 시 강제 Pod 재스케줄 (taint 제거가 안 될 때)kubectl delete pod postgres-0 -n database --grace-period=0 --force# 특정 Pod 로그 확인 (이전 컨테이너 로그 포함)kubectl logs postgres-0 -n database --previouskubectl logs postgres-0 -n database -f
실전 샘플
여기서는 실전 샘플을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
Redis Sentinel, MySQL Primary-Replica, Elasticsearch 세 가지 프로덕션 패턴을 정리합니다. 각 샘플은 Headless Service + StatefulSet + volumeClaimTemplates 구조를 기반으로 합니다.
# ── 공통 배포 검증 스크립트 ──────────────────────────────────# 1. 모든 Pod Running 확인kubectl get pods -A -l app in (postgres,redis,mysql,elasticsearch) \ --field-selector=status.phase!=Running# 2. PVC Bound 확인kubectl get pvc -A | grep -v Bound# 3. StatefulSet 롤아웃 완료 확인for sts in postgres redis mysql elasticsearch; do echo "=== $sts ===" kubectl rollout status statefulset/$sts --timeout=120s 2>/dev/null || truedone# 4. Endpoint 통신 확인# PostgreSQLkubectl exec -it postgres-0 -n database -- \ psql -U postgres -c "SELECT version();"# Rediskubectl exec -it redis-0 -n cache -- redis-cli pingkubectl exec -it redis-0 -n cache -- \ redis-cli info replication | grep -E "role|connected_slaves"# Elasticsearchkubectl exec -it elasticsearch-0 -n search -- \ curl -s -u elastic:$ELASTIC_PASSWORD \ http://localhost:9200/_cluster/health?pretty | jq '.status'# 5. 리소스 사용량kubectl top pods -A -l app in (postgres,redis,mysql,elasticsearch)
StatefulSet 심화 — Parallel · 볼륨 확장 · 스냅샷
여기서는 StatefulSet 심화 — Parallel · 볼륨 확장 · 스냅샷을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
기본 StatefulSet 운영에 익숙해졌다면 podManagementPolicy로 기동 순서를 제어하고, PVC를 무중단으로 확장하고, VolumeSnapshot으로 데이터를 백업·복구하는 실무 패턴을 알아야 합니다. 특히 volumeClaimTemplates는 StatefulSet 생성 후에는 직접 수정할 수 없다는 제약이 자주 실수를 유발합니다.
# ── PVC 온라인 확장 (StorageClass에 allowVolumeExpansion: true 필수) ──# ⚠️ volumeClaimTemplates는 StatefulSet 생성 후 직접 patch/edit 불가 (immutable field)# → 이미 생성된 PVC들을 개별적으로 patch 해야 함# 1. 각 PVC 용량을 직접 확장 (10Gi → 50Gi)for i in 0 1 2; do kubectl patch pvc postgres-data-postgres-$i -n database \ -p '{"spec":{"resources":{"requests":{"storage":"50Gi"}}}}'done# 2. 확장 진행 상태 확인 (FileSystemResizePending → 완료 시 사라짐)kubectl get pvc -n database -wkubectl describe pvc postgres-data-postgres-0 -n database | grep -A3 Conditions# 3. StatefulSet manifest의 volumeClaimTemplates 용량 값도 함께 맞춰 반영# (다음 재생성/신규 replica 추가 시 새 PVC가 같은 크기로 생성되도록)# spec.volumeClaimTemplates 필드 자체는 수정 불가하므로,# 변경이 필요하면 --cascade=orphan으로 Pod을 보존한 채 StatefulSet만 재생성한다kubectl delete statefulset postgres -n database --cascade=orphankubectl apply -f postgres-statefulset.yaml -n database # 용량 값을 50Gi로 수정 후 재적용
volumesnapshot-backup.yamlYAML
# ── VolumeSnapshot 기반 백업 & 복구 (CSI 드라이버 + VolumeSnapshotClass 필요) ──apiVersion: snapshot.storage.k8s.io/v1kind: VolumeSnapshotClassmetadata: name: csi-snapclassdriver: ebs.csi.aws.com # GKE: pd.csi.storage.gke.io | NCP: nks-block-storagedeletionPolicy: Retain # 원본 PVC 삭제되어도 스냅샷 유지---# 1. 스냅샷 생성 — 배포 전 / 대규모 마이그레이션 전 백업apiVersion: snapshot.storage.k8s.io/v1kind: VolumeSnapshotmetadata: name: postgres-snap-before-upgrade namespace: databasespec: volumeSnapshotClassName: csi-snapclass source: persistentVolumeClaimName: postgres-data-postgres-0---# 2. 스냅샷으로부터 새 PVC 복구 — 장애 발생 시 별도 이름으로 복원해 데이터 검증 후 교체apiVersion: v1kind: PersistentVolumeClaimmetadata: name: postgres-data-restored namespace: databasespec: storageClassName: fast-ssd dataSource: name: postgres-snap-before-upgrade kind: VolumeSnapshot apiGroup: snapshot.storage.k8s.io accessModes: ["ReadWriteOnce"] resources: requests: storage: 50Gi
snapshot-ops.shBASH
# 스냅샷 상태 확인 (readyToUse: true 될 때까지 대기)kubectl get volumesnapshot -n database -wkubectl describe volumesnapshot postgres-snap-before-upgrade -n database# 복구된 PVC를 검증용 Pod에 마운트해 데이터 확인 후, 문제없으면 기존 PVC와 교체kubectl run pg-verify --image=postgres:16-alpine --restart=Never \ --overrides='{"spec":{"containers":[{"name":"pg-verify","image":"postgres:16-alpine","command":["sleep","3600"],"volumeMounts":[{"name":"data","mountPath":"/data"}]}],"volumes":[{"name":"data","persistentVolumeClaim":{"claimName":"postgres-data-restored"}}]}}' \ -n database
podManagementPolicy
동작
적합한 워크로드
OrderedReady (기본값)
Pod-0이 Running & Ready가 되어야 Pod-1을 생성. 삭제도 역순으로 하나씩
PostgreSQL Primary-Replica처럼 기동 순서가 데이터 정합성에 영향을 주는 경우
Parallel
모든 Pod을 동시에 생성/삭제. 순서 보장 없음, 대신 훨씬 빠름
Elasticsearch·Cassandra·Kafka처럼 각 노드가 독립적으로 클러스터에 join하는 경우
DaemonSet 완전 가이드
여기서는 DaemonSet 완전 가이드을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
DaemonSet은 클러스터의 모든(또는 라벨로 선택된) 노드에 Pod을 정확히 1개씩 배포합니다. 새 노드가 추가되면 자동으로 Pod이 생성되고, 노드가 제거되면 함께 정리됩니다. 로그 수집기, 모니터링 에이전트, CNI/CSI 플러그인처럼 "노드마다 하나씩 있어야 하는" 인프라 컴포넌트에 적합합니다.
# 노드별 배포 현황 확인 — READY가 노드 수와 일치해야 함kubectl get daemonset fluent-bit -n loggingkubectl get pods -n logging -l app=fluent-bit -o wide# 특정 노드에서만 실행되도록 제한 (예: GPU 노드에만 device-plugin 배포)kubectl label node gpu-node-1 workload=gpu# spec.template.spec.nodeSelector: { workload: gpu } 를 매니페스트에 추가# 롤링 업데이트kubectl set image daemonset/fluent-bit fluent-bit=fluent/fluent-bit:3.1 -n loggingkubectl rollout status daemonset/fluent-bit -n loggingkubectl rollout history daemonset/fluent-bit -n logging# 신규 노드 추가 시 자동 스케줄 확인kubectl get nodes -w
특성
DaemonSet
Deployment
replicas 지정
불가 — 대상 노드 수만큼 자동 결정
명시적으로 지정
스케줄링
노드당 정확히 1개, nodeSelector/affinity로 대상 노드 제한 가능
스케줄러가 리소스 여유가 있는 노드에 자유 배치
Control-plane 노드 배포
tolerations로 NoSchedule taint를 허용해야 배포됨
기본적으로 배제됨
업데이트 전략
RollingUpdate(maxUnavailable) 또는 OnDelete
RollingUpdate(maxSurge/maxUnavailable) 또는 Recreate
여기서는 CronJob & Job 완전 가이드을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
Job은 완료가 보장되어야 하는 일회성 작업(배치, 마이그레이션)을, CronJob은 그 Job을 cron 스케줄에 따라 주기적으로 생성합니다. 실패 시 재시도 횟수(backoffLimit), 최대 실행 시간(activeDeadlineSeconds), 동시 실행 정책(concurrencyPolicy)을 명확히 정의하지 않으면 배치 작업이 무한 재시도하거나 중복 실행되는 사고로 이어집니다.
db-migration-job.yamlYAML
apiVersion: batch/v1kind: Jobmetadata: name: db-migration-v2 namespace: productionspec: backoffLimit: 3 # 3회 실패하면 포기 (무한 재시도 방지) activeDeadlineSeconds: 600 # 10분 넘으면 강제 종료 ttlSecondsAfterFinished: 3600 # 완료 1시간 후 자동 정리 template: spec: restartPolicy: Never containers: - name: migrate image: myapp-migrator:2.1.0 command: ["./migrate.sh", "--target=v2"] resources: requests: { cpu: 200m, memory: 256Mi } limits: { cpu: 500m, memory: 512Mi } envFrom: - secretRef: { name: db-credentials }
nightly-backup-cronjob.yamlYAML
apiVersion: batch/v1kind: CronJobmetadata: name: postgres-nightly-backup namespace: databasespec: schedule: "0 3 * * *" # 매일 새벽 3시 (클러스터 kube-controller-manager 시간대 기준) timeZone: "Asia/Seoul" # K8s 1.27+ — CronJob 자체에 타임존 지정 가능 concurrencyPolicy: Forbid # 이전 백업이 아직 실행 중이면 이번 회차는 건너뜀 startingDeadlineSeconds: 300 # 5분 안에 시작 못하면 이번 회차는 포기 successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 5 # 실패 이력은 조금 더 오래 남겨 원인 분석 jobTemplate: spec: backoffLimit: 2 activeDeadlineSeconds: 1800 template: spec: restartPolicy: OnFailure containers: - name: backup image: postgres:16-alpine command: - sh - -c - | pg_dump -h postgres-svc -U postgres mydb | \ gzip > /backup/mydb-$(date +%Y%m%d).sql.gz volumeMounts: - name: backup-storage mountPath: /backup volumes: - name: backup-storage persistentVolumeClaim: { claimName: backup-pvc }
job-cronjob-ops.shBASH
# CronJob으로부터 즉시 1회 수동 실행 (스케줄 기다리지 않고 테스트)kubectl create job --from=cronjob/postgres-nightly-backup manual-backup-test -n database# 실행 이력 확인kubectl get cronjob postgres-nightly-backup -n databasekubectl get jobs -n database -l job-name --sort-by=.metadata.creationTimestamp# 특정 Job의 Pod 로그 확인kubectl logs -n database -l job-name=postgres-nightly-backup-28912345 --tail=100# CronJob 일시 중지/재개 (배포 중이거나 장애 조사 중일 때)kubectl patch cronjob postgres-nightly-backup -n database -p '{"spec":{"suspend":true}}'kubectl patch cronjob postgres-nightly-backup -n database -p '{"spec":{"suspend":false}}'# 실패한 Job 정리 (ttlSecondsAfterFinished 미설정 시 수동 정리)kubectl delete job --field-selector status.successful=1 -n database
필드
적용 대상
역할
backoffLimit
Job
실패 시 재시도 최대 횟수 (기본 6) — 초과하면 Job이 Failed로 종료
activeDeadlineSeconds
Job
전체 실행 제한 시간 — 초과 시 강제 종료, 무한 실행 방지
restartPolicy
Job Pod
Never(재시도 시 새 Pod 생성) 또는 OnFailure(같은 Pod 재시작) — Always 불가
ttlSecondsAfterFinished
Job
완료 후 N초 뒤 Job/Pod 자동 삭제 — 완료된 Job이 계속 쌓이는 것 방지
concurrencyPolicy
CronJob
Allow(기본, 중복 허용) / Forbid(이전 실행 중이면 스킵) / Replace(이전 실행 취소 후 새로 시작)
# PodDisruptionBudget — 최소 1개 Pod 항상 유지apiVersion: policy/v1kind: PodDisruptionBudgetmetadata: name: java-agent-pdb namespace: ai-agentspec: minAvailable: 1 selector: matchLabels: app: java-agent
deploy-and-verify.shBASH
# ── 전체 배포 순서 ────────────────────────────────────────────kubectl apply -f configmap.yamlkubectl apply -f statefulset.yaml # Headless SVC + ClusterIP SVC + StatefulSetkubectl apply -f ingress.yamlkubectl apply -f pdb.yaml# Pod 기동 확인 (java-agent-0 먼저 Running 후 java-agent-1)kubectl get pods -n ai-agent -l app=java-agent -w# PVC 2개씩 생성 확인 (Pod당 agent-state + agent-logs)kubectl get pvc -n ai-agent# 예상 출력:# agent-state-java-agent-0 Bound 20Gi# agent-logs-java-agent-0 Bound 10Gi# agent-state-java-agent-1 Bound 20Gi# agent-logs-java-agent-1 Bound 10Gi# Actuator 헬스 확인kubectl exec -it java-agent-0 -n ai-agent -- \ curl -s http://localhost:8080/actuator/health | jq .# 데이터 디렉터리 확인 (FAISS 인덱스, H2 DB)kubectl exec -it java-agent-0 -n ai-agent -- ls -lh /app/data/kubectl exec -it java-agent-0 -n ai-agent -- ls -lh /app/logs/# 로그 실시간 확인kubectl logs java-agent-0 -n ai-agent -f# ── 롤링 업데이트 ─────────────────────────────────────────────kubectl set image statefulset/java-agent \ java-agent=registry.aidevops.kr/java-agent:1.1.0 \ -n ai-agentkubectl rollout status statefulset/java-agent -n ai-agent# ── Pod 재시작 (PVC 데이터 보존) ──────────────────────────────kubectl delete pod java-agent-1 -n ai-agentkubectl get pods -n ai-agent -w # 자동 재생성 확인# ── PVC 용량 확인 ─────────────────────────────────────────────kubectl exec -it java-agent-0 -n ai-agent -- df -h /app/data /app/logs
PVC 이름
마운트 경로
용도
크기
agent-state
/app/data
H2 embedded DB, 벡터 인덱스(FAISS), 에이전트 메모리, 파일 업로드
20 Gi
agent-logs
/app/logs
감사 로그, 트레이스 로그 (Logback 아카이브, 90일 보관)
10 Gi
GPU 워크로드 & LLM 추론 서버 배포
여기서는 GPU 워크로드 & LLM 추론 서버 배포을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
AI Agent/LLM 서빙 워크로드는 GPU 리소스를 정확히 요청하고, 일반 CPU Pod와 같은 노드에 스케줄되지 않도록 격리해야 합니다. NVIDIA device plugin이 GPU를 노드 리소스(nvidia.com/gpu)로 노출하면, requests/limits에 정수 단위로만 지정할 수 있습니다 — GPU는 CPU/메모리처럼 분할(fractional) 요청이 불가능합니다.
BASH
# NVIDIA device plugin 설치 — GPU 노드를 nvidia.com/gpu 리소스로 노출kubectl create -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/main/deployments/static/nvidia-device-plugin.yml# GPU 노드 확인kubectl get nodes -o json | jq '.items[].status.capacity."nvidia.com/gpu"'# GPU 노드에 전용 taint 부여 — 일반 Pod가 실수로 스케줄되는 것을 방지kubectl taint nodes gpu-node-1 nvidia.com/gpu=present:NoSchedule
# GPU Pod 오토스케일링 — CPU가 아닌 큐 길이/동시 요청 수 기반 커스텀 메트릭 권장apiVersion: autoscaling/v2kind: HorizontalPodAutoscalermetadata: name: llm-inference-hpa namespace: ai-agentspec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: llm-inference minReplicas: 1 # GPU는 고가 자원 — 최소치를 낮게 유지 maxReplicas: 6 # 클러스터 내 확보 가능한 GPU 수로 상한 설정 metrics: - type: Pods pods: metric: name: inference_queue_length # Prometheus Adapter로 노출한 커스텀 메트릭 target: type: AverageValue averageValue: "5"
구성 요소
역할
비고
NVIDIA device plugin (DaemonSet)
GPU 노드를 nvidia.com/gpu 스케줄링 리소스로 노출
GPU 노드에만 자동 배포됨
nodeSelector / taint-toleration
GPU Pod를 GPU 노드에만 스케줄, 일반 Pod는 배제
GPU 노드에 NoSchedule taint를 걸어 두는 것이 일반적
모델 가중치 스토리지
컨테이너 이미지에 넣지 않고 PVC/오브젝트 스토리지에서 마운트
이미지 크기·배포 시간 단축, 모델 버전 교체 용이
Readiness probe
모델 로딩이 끝나기 전 트래픽 유입 차단
LLM은 로딩에 수십 초~수 분 소요 — initialDelaySeconds를 넉넉히
AI 모델 배포 전략 & 운영
여기서는 AI 모델 배포 전략 & 운영을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
LLM 추론 서버는 일반 웹 서비스와 운영 리스크가 다릅니다. GPU는 비싸서 최소 복제본을 낮게 유지해야 하고, 모델 하나를 바꾸는 것이 곧 수십 GB 가중치의 재검증을 의미하며, CPU/메모리 지표만으로는 지금 큐에 얼마나 많은 요청이 밀려 있는지 알 수 없습니다. 이 특성에 맞춰 점진적 모델 롤아웃, 큐 기반 오토스케일링, GPU 노드 장애 격리를 별도로 설계해야 합니다.
다이어그램 렌더링 중…
model-canary-route.yaml — Gateway API로 두 모델 버전에 트래픽 분할YAML
apiVersion: gateway.networking.k8s.io/v1kind: HTTPRoutemetadata: name: llm-inference-routespec: parentRefs: - name: main-gateway rules: - backendRefs: - name: llm-inference-stable port: 8000 weight: 95 # 검증된 기존 모델 - name: llm-inference-canary port: 8000 weight: 5 # 신규 모델 — 점진적으로 weight를 늘려감
keda-scaledobject.yaml — 큐 길이 기반 오토스케일링YAML
# Prometheus Adapter 없이도 Redis/SQS 등 외부 큐 지표로 바로 스케일링apiVersion: keda.sh/v1alpha1kind: ScaledObjectmetadata: name: llm-inference-scaler namespace: ai-agentspec: scaleTargetRef: name: llm-inference minReplicaCount: 1 # GPU는 고가 자원 — 유휴 상태에서는 0까지도 검토 가능 maxReplicaCount: 6 cooldownPeriod: 300 # 스케일 다운 전 대기 — GPU Pod는 축소·재확보 비용이 크므로 여유 있게 triggers: - type: redis metadata: address: redis.ai-agent.svc:6379 listName: inference-queue listLength: "5" # 큐에 5개 이상 쌓이면 스케일 아웃
gpu-pdb.yaml — GPU Pod 동시 축출 방지YAML
apiVersion: policy/v1kind: PodDisruptionBudgetmetadata: name: llm-inference-pdb namespace: ai-agentspec: minAvailable: 1 # 노드 유지보수 중에도 최소 1개는 서비스 가능 상태 유지 selector: matchLabels: { app: llm-inference }
운영 항목
일반 웹 서비스
LLM 추론 서버
배포 리스크
이미지 하나 교체
모델 가중치가 바뀌면 응답 품질 자체가 달라질 수 있음 — 카나리로 먼저 검증 필요
스케일링 신호
CPU/메모리 사용률로 충분한 경우가 많음
큐 길이·동시 요청 수 등 외부 지표 기반 스케일링이 더 정확
장애 시 복구 비용
Pod 재시작 수 초
GPU 노드 재확보·모델 재로딩까지 수 분 — PodDisruptionBudget으로 동시 축출 방지 필수
운영 & 업그레이드
여기서는 운영 & 업그레이드을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
kubectl 치트시트, 노드 유지보수, 클러스터 버전 업그레이드, etcd 백업·복구, 장애 진단 패턴을 정리합니다. 클러스터를 구축하는 것보다 몇 배 더 오래 지속되는 것이 일상 운영이므로, 노드를 안전하게 비우고 복귀시키는 절차와 etcd 백업처럼 장애 시 되돌릴 수 있는 수단을 미리 손에 익혀두는 것이 실제 사고 대응 속도를 좌우합니다.
kubectl-cheatsheet.shBASH
# ── kubectl 필수 명령어 ───────────────────────────────────────# 컨텍스트 전환kubectl config get-contextskubectl config use-context production# 네임스페이스 기본 설정kubectl config set-context --current --namespace=production# Pod 상태 실시간 감시kubectl get pods -n production -wkubectl get pods -n production -o wide # 노드 배치 포함# 로그kubectl logs <pod> -f # 실시간kubectl logs <pod> --previous # 이전 컨테이너 로그kubectl logs <pod> -c <container> --tail=100 # 멀티 컨테이너# 디버깅kubectl describe pod <pod> -n productionkubectl exec -it <pod> -- /bin/shkubectl debug <pod> -it --image=busybox # 임시 디버그 컨테이너# 포트 포워딩 (로컬 테스트)kubectl port-forward svc/postgres-svc 5432:5432 -n databasekubectl port-forward pod/redis-0 6379:6379 -n cache# 리소스 사용량kubectl top nodeskubectl top pods -n production --sort-by=cpu# 이벤트 확인kubectl get events -n production --sort-by='.lastTimestamp'# 강제 재시작 (Deployment 롤링 재시작)kubectl rollout restart deployment/<name> -n production# 리소스 상세 출력kubectl get all -n productionkubectl get all,pvc,ingress,cm,secret -n production
node-maintenance.shBASH
# ── 노드 유지보수 (cordon + drain) ───────────────────────────# 1. 새 Pod 스케줄 차단 (기존 Pod 유지)kubectl cordon k8s-worker-01# 2. 기존 Pod를 다른 노드로 이동 (PodDisruptionBudget 존중)kubectl drain k8s-worker-01 \ --ignore-daemonsets \ # DaemonSet Pod는 드레인 제외 --delete-emptydir-data \ # emptyDir 볼륨 Pod 삭제 허용 --grace-period=30 \ # 종료 대기 시간 --timeout=300s # 전체 타임아웃# 3. OS 패치, 커널 업그레이드, 디스크 교체 등 유지보수 수행# 4. 노드 복구 후 스케줄 재개kubectl uncordon k8s-worker-01# 5. 노드 상태 확인kubectl get node k8s-worker-01 -o widekubectl describe node k8s-worker-01 | grep -A5 "Conditions:"
cluster-upgrade.shBASH
# ── 클러스터 버전 업그레이드 (1.35 → 1.36, 예시) ──────────────────# kubeadm은 minor 버전을 한 단계씩만 건너뛸 수 있음 — 1.34에서 1.36으로 직행 불가, 1.35를 반드시 경유# 반드시 Control Plane → Worker 순서# 0. 사전 체크kubeadm upgrade plan# 1. Control Plane 업그레이드# kubeadm 업데이트apt-mark unhold kubeadmapt-get install -y kubeadm=1.36.0-00apt-mark hold kubeadm# 업그레이드 실행kubeadm upgrade apply v1.36.0# kubelet, kubectl 업데이트apt-mark unhold kubelet kubectlapt-get install -y kubelet=1.36.0-00 kubectl=1.36.0-00apt-mark hold kubelet kubectlsystemctl daemon-reloadsystemctl restart kubelet# 2. Worker 노드 업그레이드 (각 노드 반복)# Control Plane에서kubectl cordon k8s-worker-01kubectl drain k8s-worker-01 --ignore-daemonsets --delete-emptydir-data# Worker 노드에서apt-mark unhold kubeadm kubelet kubectlapt-get install -y kubeadm=1.36.0-00 kubelet=1.36.0-00 kubectl=1.36.0-00apt-mark hold kubeadm kubelet kubectlkubeadm upgrade nodesystemctl daemon-reload && systemctl restart kubelet# Control Plane에서kubectl uncordon k8s-worker-01# 3. 업그레이드 완료 확인kubectl get nodes
etcd-backup.shBASH
# ── etcd 백업 & 복구 ─────────────────────────────────────────# etcd는 클러스터의 모든 상태 저장소 — 정기 백업 필수# 백업 (Control Plane에서)ETCD_BACKUP_DIR=/backup/etcd/$(date +%Y%m%d-%H%M%S)mkdir -p $ETCD_BACKUP_DIRETCDCTL_API=3 etcdctl snapshot save $ETCD_BACKUP_DIR/snapshot.db \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key# 백업 검증ETCDCTL_API=3 etcdctl snapshot status $ETCD_BACKUP_DIR/snapshot.db \ --write-out=table# cron 정기 백업 (매일 새벽 2시)echo "0 2 * * * root ETCDCTL_API=3 etcdctl snapshot save /backup/etcd/$(date +%Y%m%d).db --endpoints=https://127.0.0.1:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key" >> /etc/crontab# ── 복구 절차 ────────────────────────────────────────────────# 1. 모든 Control Plane에서 kubelet 중지systemctl stop kubelet# 2. etcd 데이터 디렉터리 백업 후 삭제mv /var/lib/etcd /var/lib/etcd.bak# 3. 스냅샷 복구ETCDCTL_API=3 etcdctl snapshot restore /backup/etcd/20240101.db \ --data-dir=/var/lib/etcd \ --name=k8s-master-01 \ --initial-cluster=k8s-master-01=https://192.168.1.10:2380 \ --initial-cluster-token=etcd-cluster-1 \ --initial-advertise-peer-urls=https://192.168.1.10:2380# 4. kubelet 재시작systemctl start kubeletkubectl get nodes
troubleshoot.shBASH
# ── 장애 진단 패턴 ───────────────────────────────────────────# Pod가 Pending 상태일 때kubectl describe pod <pod> -n <ns># → Events 항목 확인:# "Insufficient cpu" → ResourceQuota 또는 노드 자원 부족# "No nodes available" → 모든 노드 taint/cordon 상태# "PVC not found" → StorageClass 또는 PV 문제# Pod가 CrashLoopBackOff일 때kubectl logs <pod> --previous -n <ns>kubectl describe pod <pod> -n <ns># → 종료 코드 확인:# Exit Code 1: 앱 오류 (로그 확인)# Exit Code 137: OOM Kill → limits.memory 증가# 노드 NotReady일 때kubectl describe node k8s-worker-01# → Conditions: MemoryPressure, DiskPressure, PIDPressure 확인ssh k8s-worker-01systemctl status kubeletjournalctl -u kubelet -f# 서비스 접근 불가일 때kubectl get endpoints <service> -n <ns> # Pod IP 등록 여부kubectl exec <pod> -- curl http://<service>.<ns>.svc.cluster.localkubectl exec <pod> -- nslookup <service> # DNS 확인# 이미지 Pull 실패kubectl describe pod <pod> | grep -A3 "Failed"# → ErrImagePull / ImagePullBackOff# private registry: imagePullSecrets 설정 확인kubectl create secret docker-registry regcred \ --docker-server=registry.aidevops.kr \ --docker-username=<user> \ --docker-password=<pass># 클러스터 전체 헬스 체크 스크립트echo "=== Nodes ===" && kubectl get nodesecho "=== System Pods ===" && kubectl get pods -n kube-system | grep -v Runningecho "=== Failed Pods ===" && kubectl get pods -A | grep -E "Error|CrashLoop|Pending|OOMKilled"echo "=== PVC ===" && kubectl get pvc -A | grep -v Boundecho "=== Recent Events ===" && kubectl get events -A --sort-by='.lastTimestamp' | tail -20
Helm 차트
여기서는 Helm 차트을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
Helm은 여러 YAML 매니페스트를 하나의 배포 단위(차트)로 묶고, 환경마다 달라지는 값(이미지 태그, 레플리카 수, GPU 개수 등)만 values 파일로 바꿔 재사용할 수 있게 해줍니다. kubectl apply를 반복하는 것과 근본적으로 다른 점은 리비전 히스토리입니다 — helm upgrade를 실행할 때마다 그 시점의 매니페스트와 values 전체가 스냅샷으로 기록되어, helm rollback 한 줄로 이전 리비전 전체 상태로 되돌릴 수 있습니다.
다이어그램 렌더링 중…
차트 디렉터리 구조TEXT
llm-inference-chart/
├── Chart.yaml # 차트 이름, 버전, 의존성(서브차트) 선언
├── values.yaml # 기본값 — 모든 환경의 출발점
├── values-staging.yaml # 스테이징 전용 override (레플리카 1, GPU 1개)
├── values-production.yaml # 운영 전용 override (레플리카 3, GPU 타입 지정)
├── templates/
│ ├── deployment.yaml # {{ .Values.* }} 로 값이 채워지는 템플릿
│ ├── service.yaml
│ ├── hpa.yaml
│ ├── _helpers.tpl # 여러 템플릿에서 재사용할 이름·라벨 헬퍼 함수
│ └── hooks/
│ └── pre-upgrade-migration.yaml
└── charts/ # 의존 서브차트 (예: postgresql) — helm dependency update로 채워짐
templates/hooks/pre-upgrade-migration.yaml — 업그레이드 전 자동 실행되는 JobYAML
apiVersion: batch/v1kind: Jobmetadata: name: {{ include "llm-inference.fullname" . }}-migrate annotations: "helm.sh/hook": pre-upgrade,pre-install "helm.sh/hook-delete-policy": hook-succeeded # 성공한 Job은 정리, 실패 시 원인 조사를 위해 남김spec: template: spec: restartPolicy: Never containers: - name: migrate image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" command: ["python", "scripts/sync_model_registry.py"]
BASH
# 설치 & 배포helm create myapp # 기본 차트 스캐폴딩 생성helm install myapp ./myapp -f values-production.yaml -n production --atomic --wait# --atomic: 실패 시 자동으로 이전 상태로 롤백# --wait: Pod가 Ready 상태가 될 때까지 명령이 종료되지 않고 대기# 실제 적용 전에 렌더링 결과 확인 (클러스터에 아무것도 적용하지 않음)helm template myapp ./myapp -f values-production.yaml# 업그레이드 & 롤백helm upgrade myapp ./myapp -f values-production.yaml --set image.tag=1.3.0 --atomichelm history myapp # 리비전별 변경 요약helm rollback myapp 2 # 특정 리비전으로 즉시 복귀# 서브차트(postgresql 등) 의존성 다운로드helm dependency update ./myapp# 차트를 OCI 레지스트리에 패키징 & 배포 (최신 Helm 표준 배포 방식)helm package ./myapphelm push myapp-1.0.0.tgz oci://ghcr.io/myorg/charts
명령어/플래그
역할
helm template
클러스터에 아무것도 적용하지 않고 렌더링된 YAML만 미리 확인 (dry-run)
--atomic
업그레이드 실패 시 자동으로 이전 리비전으로 롤백 — 운영 배포에는 거의 필수
--wait
Pod가 실제로 Ready 상태가 될 때까지 대기 후 명령 종료 — CI 파이프라인에서 배포 성공 여부를 정확히 판단
helm.sh/hook (pre-upgrade 등)
DB 마이그레이션처럼 배포 전후에 한 번만 실행해야 하는 작업을 차트에 포함
charts/ (서브차트)
postgresql, redis 같은 의존 컴포넌트를 다른 팀이 만든 차트 그대로 가져와 조합 (umbrella chart)
Contour & Gateway API — NGINX Ingress 대체
여기서는 Contour & Gateway API — NGINX Ingress 대체을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
Contour는 Envoy Proxy를 데이터 플레인으로 쓰는 L7 Ingress Controller입니다. NGINX Ingress와 달리 설정 변경 시 워커 프로세스 reload가 필요 없이 xDS API로 Envoy 설정을 무중단으로 갱신하며, 대규모 라우트에서도 reload로 인한 커넥션 드롭이 없습니다. 라우팅 정책은 두 가지 방식으로 선언할 수 있습니다 — Contour 전용 CRD인 HTTPProxy(팀 간 위임·세밀한 정책에 강점)와, 컨트롤러 벤더에 종속되지 않는 Kubernetes 표준 Gateway API(GatewayClass/Gateway/HTTPRoute 3계층 모델)입니다. 신규 구축이면 Gateway API를, 팀별 네임스페이스 위임이나 세밀한 IP/Rate limit 정책이 필요하면 HTTPProxy를 우선 검토하세요.
contour-install.shBASH
# 방법 A — HTTPProxy 중심 (정적 매니페스트, IngressClass "contour" 생성)kubectl apply -f https://projectcontour.io/quickstart/contour.yamlkubectl get pods -n projectcontour -wkubectl get svc envoy -n projectcontour # LoadBalancer 외부 IP 확인# 방법 B — Gateway API 중심 (Contour Gateway Provisioner)# 1) Gateway API CRD 설치 (standard channel)kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.1.0/standard-install.yaml# 2) Contour + Gateway Provisioner 설치 — GatewayClass "contour" 자동 생성kubectl apply -f https://projectcontour.io/quickstart/contour-gateway-provisioner.yamlkubectl get gatewayclass contour# 이후 Gateway 리소스를 생성하면 Provisioner가 해당 Gateway 전용 Contour+Envoy 워크로드를 자동 배포합니다.
여기서는 Proxy Protocol — LB 뒤에서 클라이언트 IP 보존하기을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
Contour(Envoy) 앞에 L4 LoadBalancer를 두면 Envoy 액세스 로그나 애플리케이션이 보는 클라이언트 IP가 실제 사용자 IP가 아니라 LB나 노드의 IP로 찍히는 문제가 자주 생깁니다. LB가 SNAT을 하거나(AWS NLB의 ip 타깃 모드, Classic ELB, 여러 매니지드 LB), kube-proxy가 externalTrafficPolicy: Cluster에서 노드 간 전달 시 SNAT을 하기 때문입니다. HTTPS는 Envoy가 TLS를 종료하므로 L4 LB는 암호화된 바이트를 들여다볼 수 없어 X-Forwarded-For 헤더를 넣어줄 수도 없습니다.
Proxy Protocol은 HAProxy가 제안한 규약으로, L4 LB가 백엔드로 TCP 연결을 맺자마자 연결 맨 앞에 원본 클라이언트 IP/포트를 담은 짧은 헤더를 먼저 보내고, 그 뒤에 원래의 HTTP/TLS 바이트를 그대로 흘려보내는 방식입니다. v1은 사람이 읽을 수 있는 텍스트 한 줄, v2는 바이너리 형식이며 v2는 TLV 확장(AWS VPC Endpoint ID 등)을 실을 수 있습니다. Envoy의 envoy.filters.listener.proxy_protocol 리스너 필터는 v1/v2를 자동 인식해 헤더를 읽어낸 뒤, 이후 요청의 downstream remote address를 헤더 속 원본 IP로 바꿉니다 — 그래서 Envoy가 업스트림으로 보내는 X-Forwarded-For, X-Envoy-External-Address, 액세스 로그, ipAllowFilterPolicy 판정이 모두 실제 클라이언트 IP 기준으로 동작합니다.
⚠️
핵심 규칙 — 보내는 쪽과 받는 쪽이 반드시 동시에 켜져야 합니다 Proxy Protocol 스펙은 수신 측이 헤더 유무를 "추측"하지 말고, 헤더가 필요한 리스너에서는 헤더 없는 연결을 거부하도록 정의합니다. Contour에서 Proxy Protocol을 켜면 Envoy의 모든 트래픽 리스너(HTTP 8080, HTTPS 8443)에 이 필터가 붙기 때문에, LB를 거치지 않고 Envoy로 직접 들어오는 클러스터 내부 호출은 전부 끊깁니다. 반대로 LB만 켜고 Envoy를 켜지 않으면 헤더가 HTTP/TLS 데이터로 해석되어 400 Bad Request나 TLS 핸드셰이크 실패가 납니다. 이 가이드의 이후 하위 섹션은 이 두 가지 문제를 LB·Contour 버전·대체 방법별로 풀어갑니다.
다이어그램 렌더링 중…
proxy-protocol-header-example.txtTEXT
# v1 (텍스트) — 연결 직후 한 줄이 먼저 전송되고, 그 뒤에 원래 HTTP/TLS 데이터가 이어짐
PROXY TCP4 203.0.113.7 10.0.12.34 51234 443\r\n
# 프로토콜 원본IP LB/수신IP 원본포트 목적지포트
# v2 (바이너리) — 12바이트 시그니처로 시작
\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A + 버전/명령 + 주소 + TLV(선택)
# Envoy 리스너 필터 구성 (Contour가 useProxyProtocol=true일 때 생성하는 형태)
listener_filters:
- name: envoy.filters.listener.proxy_protocol
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.listener.proxy_protocol.v3.ProxyProtocol
# allow_requests_without_proxy_protocol: false ← 기본값. Contour는 이 값을 바꿀 옵션을 제공하지 않음
- name: envoy.filters.listener.tls_inspector # HTTPS 리스너는 SNI 확인을 위해 뒤이어 실행
클라이언트 IP 보존 방식
적합한 환경
장점
주의점
externalTrafficPolicy: Local
LB가 SNAT 없이 패킷을 그대로 전달(패스스루)하는 경우 — GKE/AKS L4 LB, MetalLB, AWS NLB instance 타깃
Service 설정 한 줄, 내부 호출에 영향 없음
Envoy Pod가 없는 노드는 LB 헬스체크에서 빠짐 → 노드별 트래픽 불균형 가능
Proxy Protocol
LB가 SNAT/프록시를 하는 경우 — AWS NLB ip 타깃, Classic ELB, NCP, DigitalOcean, HAProxy/F5 등
TLS를 Envoy가 종료해도 원본 IP 전달, externalTrafficPolicy와 무관
LB·Envoy 동시 설정 필요, 헤더 없는 내부 호출 차단, 헤어핀 트래픽 문제
L7 LB + X-Forwarded-For
ALB, Application Gateway 등 L7 LB가 TLS를 먼저 종료하는 경우
Proxy Protocol 불필요, WAF 연동 쉬움
Contour num-trusted-hops 설정 필수, TLS가 두 번 종료됨
LB별 Proxy Protocol 설정
여기서는 LB별 Proxy Protocol 설정을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
Proxy Protocol 헤더를 붙이는 주체는 LB입니다. Kubernetes에서는 Envoy를 노출하는 type: LoadBalancer Service에 클라우드별 annotation을 달아 Cloud Controller Manager(또는 AWS Load Balancer Controller)가 LB 리스너/타깃 그룹에 Proxy Protocol을 켜도록 합니다. Proxy Protocol을 쓰면 원본 IP가 헤더로 전달되므로 externalTrafficPolicy는 Cluster로 두어도 되고, 노드 간 트래픽 균형 측면에서는 오히려 유리합니다.
헬스체크 주의: LB 헬스체크가 트래픽 포트로 연결할 때 PROXY 헤더를 보내지 않거나, 반대로 헤더를 이해하지 못하는 포트(Envoy의 8002 /ready, kube-proxy healthCheckNodePort)로 헤더를 보내면 타깃이 unhealthy로 빠집니다. 클라우드 문서에서 "헬스체크에도 Proxy Protocol 헤더가 포함되는지"를 확인하고, 애매하면 트래픽 포트에 대한 TCP 헬스체크로 시작해 동작을 확인한 뒤 HTTP 헬스체크로 옮기세요.
Envoy의 리스너 필터는 v1과 v2를 모두 자동 인식하므로 LB가 어떤 버전을 보내든 Contour 쪽 설정은 동일합니다.
envoy-service-aws-nlb.yamlYAML
# 정적 매니페스트(contour.yaml)로 설치한 경우 — envoy Service에 annotation 추가# Gateway Provisioner를 쓴다면 이 Service는 Provisioner가 관리하므로 직접 수정하지 말고# ContourDeployment.spec.envoy.networkPublishing.serviceAnnotations로 지정 (다음 섹션)apiVersion: v1kind: Servicemetadata: name: envoy namespace: projectcontour annotations: service.beta.kubernetes.io/aws-load-balancer-type: external service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*" # 모든 타깃 그룹에 PP v2spec: type: LoadBalancer externalTrafficPolicy: Cluster # 원본 IP는 PROXY 헤더로 전달되므로 Cluster로도 충분 selector: app: envoy ports: - name: http port: 80 targetPort: 8080 protocol: TCP - name: https port: 443 targetPort: 8443 protocol: TCP
aws-verify-target-group.shBASH
# NLB 타깃 그룹에 Proxy Protocol v2가 실제로 켜졌는지 확인aws elbv2 describe-target-groups \ --load-balancer-arn "$NLB_ARN" \ --query "TargetGroups[].TargetGroupArn" --output text |tr '\t' '\n' | while read -r TG; do echo "== $TG" aws elbv2 describe-target-group-attributes --target-group-arn "$TG" \ --query "Attributes[?Key=='proxy_protocol_v2.enabled']"done
haproxy.cfg (온프레미스 L4 앞단)TEXT
frontend fe_https
mode tcp
bind :443
default_backend be_envoy_https
backend be_envoy_https
mode tcp
balance roundrobin
# send-proxy-v2: 백엔드(Envoy NodePort)로 연결 시 PROXY v2 헤더 전송
# check-send-proxy: 헬스체크 연결에도 헤더를 붙여 Envoy 리스너가 거부하지 않도록 함
server node1 10.0.0.11:30443 send-proxy-v2 check check-send-proxy
server node2 10.0.0.12:30443 send-proxy-v2 check check-send-proxy
nginx-stream.conf (온프레미스 L4 앞단)NGINX
stream { upstream envoy_https { server 10.0.0.11:30443; server 10.0.0.12:30443; } server { listen 443; proxy_pass envoy_https; proxy_protocol on; # 업스트림(Envoy)으로 PROXY v1 헤더 전송 }}
MetalLB(L2/BGP)는 Proxy Protocol을 붙이지 않음 → Local 정책 사용
Contour 버전별 Envoy Proxy Protocol 설정 (1.28 ~ 1.33)
여기서는 Contour 버전별 Envoy Proxy Protocol 설정 (1.28 ~ 1.33)을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
Contour에서 Envoy가 PROXY 헤더를 받도록 하는 방법은 설치 방식에 따라 세 가지입니다. 어느 방식이든 결과는 같습니다 — Contour가 Envoy의 ingress_http/ingress_https 리스너 listener_filters 맨 앞에 envoy.filters.listener.proxy_protocol을 추가합니다. 통계용 리스너(8002)와 admin 인터페이스에는 적용되지 않습니다.
설치 방식
설정 위치
필드/플래그
정적 매니페스트 (quickstart contour.yaml)
contour Deployment의 contour serve 인자
--use-proxy-protocol
ContourConfiguration CRD 사용 (contour serve --contour-config-name)
주의: Contour 설정 파일(contour ConfigMap의 contour.yaml)에는 Proxy Protocol에 대응하는 키가 없습니다. ConfigMap에 넣어도 무시되므로 플래그나 CRD를 사용하세요.
아래 버전 표는 Contour 공식 compatibility matrix 기준입니다. 1.28 ~ 1.33 사이에서 Proxy Protocol 설정 방식 자체는 바뀌지 않았고, 달라지는 것은 번들 Envoy 버전과 지원 Kubernetes·Gateway API 버전입니다. 특히 1.33은 패치 릴리스 도중(1.33.6) 번들 Envoy가 1.35 → 1.38로 크게 올라갔으므로, 1.33.x 안에서의 패치 업그레이드라도 Envoy 릴리스 노트의 동작 변경 사항을 확인하고 스테이징에서 먼저 검증하세요.
# [방법 B] ContourConfiguration CRD — contour serve --contour-config-name=contour 로 실행할 때 사용apiVersion: projectcontour.io/v1alpha1kind: ContourConfigurationmetadata: name: contour namespace: projectcontourspec: envoy: listener: useProxyProtocol: true # 모든 Envoy 트래픽 리스너에 proxy_protocol 리스너 필터 추가 network: numTrustedHops: 0 # 앞단이 L4(PP)만 있으면 0 — L7 프록시가 한 단계 더 있으면 그 수만큼 # ... 기존 설정(gateway, httpproxy, tls 등)은 그대로 유지
# [참고] Helm 차트로 설치한 경우 — 차트마다 키 이름이 다르므로 반드시 확인:# helm show values <repo>/contour --version <차트버전> | grep -n -i -E "extraArgs|proxy|annotations"contour: extraArgs: - --use-proxy-protocolenvoy: service: externalTrafficPolicy: Cluster annotations: service.beta.kubernetes.io/aws-load-balancer-type: external service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip service.beta.kubernetes.io/aws-load-balancer-proxy-protocol: "*"
check-contour-envoy-version.shBASH
# 현재 클러스터의 Contour / Envoy 이미지 버전 확인 → 위 표에서 해당 행 찾기kubectl -n projectcontour get deploy,ds \ -o custom-columns='NAME:.metadata.name,IMAGES:.spec.template.spec.containers[*].image'# NAME IMAGES# contour ghcr.io/projectcontour/contour:v1.33.x# envoy ghcr.io/projectcontour/contour:v1.33.x,docker.io/envoyproxy/envoy:v1.35.x# 설정이 실제 Envoy 리스너에 반영됐는지 확인 (Envoy admin: Pod 내부 9001)ENVOY_POD=$(kubectl -n projectcontour get pod -l app=envoy -o jsonpath='{.items[0].metadata.name}')kubectl -n projectcontour port-forward "$ENVOY_POD" 9001:9001 >/dev/null &sleep 2curl -s localhost:9001/config_dump | grep -c "envoy.filters.listener.proxy_protocol"# 2 이상 (ingress_http + ingress_https) 이면 적용됨, 0이면 미적용# ※ Provisioner로 만든 Envoy는 라벨이 다를 수 있음: kubectl get pod -n projectcontour --show-labels
Contour
번들 Envoy
Kubernetes
Gateway API
Proxy Protocol 설정
PROXY 없는 연결 허용 (allow_requests_without_proxy_protocol)
1.28.x
1.29.x
1.27 ~ 1.29
v1.0.0
플래그 / ContourConfiguration / ContourDeployment
Envoy는 지원, Contour 미노출 → 설정 불가
1.29.x
1.30.x
1.27 ~ 1.29
v1.0.0
동일
설정 불가
1.30.x
1.31.x
1.28 ~ 1.30
v1.1.0
동일
설정 불가
1.31.x
1.34.x
1.30 ~ 1.32
v1.2.1
동일
설정 불가
1.32.x
1.34.x
1.31 ~ 1.33
v1.2.1
동일
설정 불가
1.33.0 ~ 1.33.5
1.35.x
1.32 ~ 1.34
v1.3.0
동일
설정 불가
1.33.6 ~
1.38.x
1.32 ~ 1.34
v1.3.0
동일
설정 불가 — 대체 방법은 다음 섹션
PROXY 헤더 없는 내부 호출 허용 & 대체 방법
여기서는 PROXY 헤더 없는 내부 호출 허용 & 대체 방법을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
Proxy Protocol을 켠 뒤 가장 흔하게 터지는 문제는 LB를 거치지 않고 Envoy로 들어오는 호출입니다. 대표적으로 세 가지 경로가 있습니다.
① 클러스터 내부 Pod → Envoy Service(ClusterIP) 직접 호출 — 예: 사내 API 게이트웨이 정책을 타기 위해 envoy.projectcontour.svc.cluster.local로 요청. PROXY 헤더가 없으므로 Envoy가 연결을 끊습니다. ② 클러스터 내부 Pod → 외부 도메인(LB IP) 호출(헤어핀) — LB가 Service status.loadBalancer.ingress에 IP를 기록하면 kube-proxy가 그 IP로 가는 패킷을 노드에서 가로채 LB를 거치지 않고 Envoy로 바로 보냅니다. 결과적으로 PROXY 헤더 없이 도착해 실패합니다. AWS NLB처럼 hostname만 기록하는 LB에서는 보통 발생하지 않고, DigitalOcean·Hetzner·일부 온프레미스 LB처럼 IP를 기록하는 환경에서 발생합니다. ③ 사내망/VPN → 내부 LB → Envoy — 내부 LB에 Proxy Protocol을 켜지 않았다면 역시 실패합니다.
Envoy의 allow_requests_without_proxy_protocol Envoy의 proxy_protocol 리스너 필터에는 allow_requests_without_proxy_protocol: true 옵션이 있어, 헤더가 있으면 읽고 없으면 그냥 통과시키는 "선택적 허용"이 가능합니다. Contour 1.28 ~ 1.33이 번들하는 Envoy는 모두 이 필드를 지원합니다. 하지만 Contour는 이 필드를 노출하지 않습니다 — Contour 설정 파일·contour serve 플래그·ContourConfiguration/ContourDeployment 어디에도 대응 옵션이 없고, Contour가 xDS로 리스너를 계속 재생성하므로 Istio의 EnvoyFilter 같은 임의 패치 경로도 없습니다. 따라서 Contour 1.33까지는 "PROXY 있으면 읽고 없어도 허용"을 설정으로 만들 수 없고, 아래 대체 방법 중 하나를 선택해야 합니다(새 버전에서 옵션이 추가되었는지는 Contour 릴리스 노트로 확인).
⚠️
allow 옵션을 쓸 수 있더라도 주의할 점 Envoy 문서는 이 옵션이 Proxy Protocol 스펙 준수를 깨뜨리므로 "리스너로 들어오는 모든 트래픽이 신뢰할 수 있는 출처일 때만" 켜라고 경고합니다. 헤더 없는 연결은 TCP 소스 IP(노드/SNAT IP)가 클라이언트 IP로 기록되므로 IP 기반 허용 정책이 섞일 수 있고, 반대로 외부에서 LB를 우회해 직접 들어온 연결이 PROXY 헤더를 위조하면 임의의 IP로 가장할 수 있습니다. 또한 v2 시그니처와 일치하는 12바이트 이하(v1은 6바이트 이하)의 아주 짧은 요청은 Envoy가 헤더인지 판단하지 못해 타임아웃됩니다. Envoy 리스너(NodePort/Pod IP)로의 직접 접근은 NetworkPolicy·보안그룹으로 LB와 신뢰 대역만 허용하세요.
# [② 정적 매니페스트 버전] Provisioner 없이 두 번째 Contour+Envoy 세트를 운영하는 경우# - 두 번째 세트는 별도 네임스페이스(예: projectcontour-internal)에 quickstart 매니페스트를 복제 배포# - contour serve 인자로 담당 IngressClass를 구분하고, PP 플래그는 외부 세트에만 추가# 외부: contour serve ... --ingress-class-name=contour-external --use-proxy-protocol# 내부: contour serve ... --ingress-class-name=contour-internalapiVersion: projectcontour.io/v1kind: HTTPProxymetadata: name: api-internal namespace: productionspec: ingressClassName: contour-internal # 내부 세트만 이 HTTPProxy를 처리 virtualhost: fqdn: api.internal.aidevops.kr routes: - conditions: - prefix: / services: - name: api-service port: 8080
option3-client-sends-proxy-header.shBASH
# [③] 클라이언트가 직접 PROXY 헤더를 보내는 방법# curl — 테스트·헬스체크 스크립트용 (v1 헤더 전송)curl --haproxy-protocol -H "Host: api.aidevops.kr" \ http://envoy.projectcontour.svc.cluster.local/healthz# curl 8.2+ — 헤더에 넣을 클라이언트 IP 지정curl --haproxy-protocol --haproxy-clientip 10.1.2.3 -H "Host: api.aidevops.kr" \ http://envoy.projectcontour.svc.cluster.local/# 애플리케이션 트래픽은 HTTP 라이브러리가 PROXY 헤더를 지원하지 않는 경우가 대부분이므로# 같은 Pod에 TCP 프록시 사이드카를 두고 localhost로 호출하게 합니다 (HAProxy 예시):# frontend local_in# mode tcp# bind 127.0.0.1:18080# default_backend envoy# backend envoy# mode tcp# server contour envoy.projectcontour.svc.cluster.local:80 send-proxy-v2# 앱은 http://127.0.0.1:18080 으로 요청 + Host 헤더 지정
option4-hairpin.yamlYAML
# [④] 헤어핀 차단 — 내부 Pod가 외부 도메인을 호출해도 반드시 LB를 거치게 만들기# (a) Kubernetes 1.32+ (LoadBalancerIPMode GA): Cloud Controller가 ipMode: Proxy를 기록하면# kube-proxy가 LB IP를 노드에서 가로채지 않고 실제 LB로 보냄 → PROXY 헤더가 붙음# 확인:# kubectl -n projectcontour get svc envoy -o jsonpath='{.status.loadBalancer.ingress}'# [{"ip":"203.0.113.10","ipMode":"Proxy"}] ← Proxy면 OK, VIP 또는 미표시면 헤어핀 발생# ipMode는 사용자가 아니라 Cloud Controller Manager가 설정하는 값 — CCM 지원 여부 확인# (b) IP 대신 hostname을 status에 기록하게 하는 LB별 annotationapiVersion: v1kind: Servicemetadata: name: envoy namespace: projectcontour annotations: # DigitalOcean service.beta.kubernetes.io/do-loadbalancer-enable-proxy-protocol: "true" service.beta.kubernetes.io/do-loadbalancer-hostname: "lb.aidevops.kr" # Hetzner Cloud # load-balancer.hetzner.cloud/uses-proxyprotocol: "true" # load-balancer.hetzner.cloud/hostname: "lb.aidevops.kr"spec: type: LoadBalancer selector: app: envoy ports: - name: https port: 443 targetPort: 8443
option6-envoy-gateway.yamlYAML
# [⑥] Envoy Gateway로 이전 시 — Proxy Protocol 활성화 + 헤더 없는 연결 선택적 허용# 1) ClientTrafficPolicy로 Proxy Protocol 활성화# (최신 Envoy Gateway는 proxyProtocol 블록의 optional 설정을 제공 — 사용 버전의 API 문서 확인)apiVersion: gateway.envoyproxy.io/v1alpha1kind: ClientTrafficPolicymetadata: name: enable-proxy-protocol namespace: envoy-gateway-systemspec: targetRefs: - group: gateway.networking.k8s.io kind: Gateway name: eg enableProxyProtocol: true---# 2) optional 필드가 없는 버전이라면 EnvoyPatchPolicy로 Envoy 필드를 직접 패치# (EnvoyGateway 설정에 extensionApis.enableEnvoyPatchPolicy: true 필요)apiVersion: gateway.envoyproxy.io/v1alpha1kind: EnvoyPatchPolicymetadata: name: pp-allow-without-header namespace: envoy-gateway-systemspec: targetRef: group: gateway.networking.k8s.io kind: Gateway name: eg type: JSONPatch jsonPatches: - type: "type.googleapis.com/envoy.config.listener.v3.Listener" name: envoy-gateway-system/eg/http # <namespace>/<gateway>/<listener> operation: op: add path: "/listener_filters/0/typed_config/allow_requests_without_proxy_protocol" value: true# 적용 후 egctl config envoy-proxy listener 로 listener_filters 순서(0번이 proxy_protocol인지) 확인
대체 방법
해결하는 경로
장점
단점
① 내부 호출은 앱 Service 직접 호출 (svc.cluster.local)
Pod → Envoy
가장 단순, 추가 리소스 없음, 홉 1개 감소
Envoy의 Rate limit·IP 정책·TLS·라우팅 규칙을 거치지 않음
② 내부 전용 Gateway(Envoy) 분리 — 권장
Pod → Envoy, 사내망 → 내부 LB
HTTPRoute 하나를 외부/내부 Gateway에 동시 연결해 라우팅 규칙 재사용
Contour+Envoy 세트 추가 운영(리소스·모니터링)
③ 클라이언트가 PROXY 헤더를 직접 전송
Pod → Envoy
기존 Envoy 재사용
일반 HTTP 클라이언트 라이브러리는 대부분 미지원 — 중간 TCP 프록시(HAProxy/Envoy 사이드카) 필요
④ 헤어핀 차단 (ipMode: Proxy / hostname annotation)
Pod → 외부 도메인
내부 트래픽도 LB를 거쳐 PROXY 헤더가 붙음
Cloud Controller 지원 필요, LB 경유로 지연·비용 증가
⑤ Proxy Protocol 대신 externalTrafficPolicy: Local
모든 경로 (PP 자체를 제거)
내부 호출 문제가 원천적으로 사라짐
LB가 패스스루(SNAT 없음)일 때만 가능
⑥ Envoy Gateway로 이전 + 선택적 PP
모든 경로
allow_requests_without_proxy_protocol을 직접 켤 수 있음
컨트롤러 교체(HTTPProxy는 이전 불가, HTTPRoute는 재사용 가능)
⑦ Contour 포크/패치 빌드
모든 경로
Contour 유지
리스너 필터 생성 코드를 패치해 직접 빌드 — 업그레이드마다 재적용, 운영 부담 큼 (비권장)
Proxy Protocol 검증 & 트러블슈팅
여기서는 Proxy Protocol 검증 & 트러블슈팅을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
Proxy Protocol 문제는 대부분 "LB와 Envoy의 설정 불일치" 또는 "LB를 거치지 않는 경로" 둘 중 하나입니다. 증상만 보면 네트워크 장애처럼 보이지만, 증상의 형태로 어느 쪽이 켜져 있는지 거의 정확히 추론할 수 있습니다. 아래 표로 원인을 좁힌 뒤, 코드 블록의 순서대로 Envoy 설정 → 헤더 유무별 요청 → 통계 → 액세스 로그를 확인하세요.
pp-verify.shBASH
NS=projectcontourENVOY_POD=$(kubectl -n $NS get pod -l app=envoy -o jsonpath='{.items[0].metadata.name}')# 1) Envoy 리스너에 proxy_protocol 필터가 붙었는지kubectl -n $NS port-forward "$ENVOY_POD" 9001:9001 >/dev/null &PF_PID=$!; sleep 2curl -s localhost:9001/config_dump \ | grep -B2 -A4 "envoy.filters.listener.proxy_protocol"# 2) Proxy Protocol 관련 통계 — 헤더 파싱 실패/거부 카운터가 증가하는지curl -s localhost:9001/stats | grep -i proxy_protokill $PF_PID# 3) 헤더 유무별 요청 비교 (클러스터 내부 디버그 Pod)kubectl run pp-test -n default --rm -it --restart=Never --image=curlimages/curl -- sh -c ' echo "--- PROXY 헤더 없이 (PP on이면 실패가 정상)"; curl -sv -m 5 -H "Host: api.aidevops.kr" http://envoy.projectcontour.svc.cluster.local/ 2>&1 | tail -3; echo "--- PROXY v1 헤더 포함 (PP on이면 성공해야 정상)"; curl -sv -m 5 --haproxy-protocol -H "Host: api.aidevops.kr" http://envoy.projectcontour.svc.cluster.local/ 2>&1 | tail -3'# 4) 외부에서 요청 후 Envoy 액세스 로그에 실제 공인 IP가 찍히는지curl -s https://api.aidevops.kr/ -o /dev/nullkubectl -n $NS logs "$ENVOY_POD" -c envoy --tail=5# 기본 로그 포맷의 x-forwarded-for / downstream 주소가 내 공인 IP여야 정상# 5) 헤어핀 여부 — status에 IP가 있고 ipMode가 Proxy가 아니면 내부 호출은 LB를 우회kubectl -n $NS get svc -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.loadBalancer.ingress}{"\n"}{end}'
외부 요청 전부 실패 — curl "Empty reply from server", "Connection reset", HTTPS는 핸드셰이크 도중 끊김
Envoy만 PP on, LB는 off
LB annotation / 타깃 그룹 proxy_protocol_v2.enabled 확인
HTTP는 400 Bad Request, HTTPS는 "wrong version number"·SSL 오류
LB만 PP on, Envoy는 off (PROXY 헤더가 HTTP/TLS 데이터로 해석됨)
config_dump에 proxy_protocol 필터 존재 여부 확인 → useProxyProtocol 적용
외부는 정상, 내부 Pod → Envoy Service 호출만 실패
PROXY 헤더 없는 내부 호출
내부 전용 Gateway 분리 또는 앱 Service 직접 호출
외부는 정상, 내부 Pod → 외부 도메인 호출만 실패
kube-proxy 헤어핀 (LB IP를 노드에서 가로챔)
Service status의 ipMode 확인, hostname annotation 적용
LB 타깃이 모두 unhealthy
헬스체크 연결의 PROXY 헤더 유무와 대상 포트 불일치
트래픽 포트 TCP 헬스체크로 전환 후 재확인
정상 동작하지만 로그의 클라이언트 IP가 여전히 노드/LB IP
PP 미적용, 또는 앞단에 L7 프록시가 한 단계 더 있음
config_dump 확인, L7이 있다면 numTrustedHops를 홉 수만큼 설정
아주 짧은 요청이 간헐적으로 타임아웃 (선택적 허용 사용 시)
12바이트(v1: 6바이트) 이하 요청을 헤더 대기 중으로 판단
allow 옵션 대신 리스너 분리 구성으로 전환
Contour/Envoy 모니터링 — Prometheus & Grafana
여기서는 Contour/Envoy 모니터링 — Prometheus & Grafana을 실제 코드와 함께 확인합니다. 예제를 그대로 따라 하기보다, 입력과 출력, 그리고 바뀌기 쉬운 부분이 어디인지 보면서 읽어보세요.
Contour는 자체 컨트롤 플레인 메트릭을 :8000/metrics에 노출하고, 데이터 플레인인 Envoy는 :8002/stats/prometheus에서 xDS 반영 상태·업스트림 커넥션·요청 처리 결과를 노출합니다. kube-prometheus-stack을 이미 설치했다면(모니터링 스택 구성 참고) ServiceMonitor 2개만 추가하면 됩니다. Envoy의 admin 인터페이스(9001)는 클러스터 내부에서만 접근하도록 외부 노출을 반드시 차단하세요.
# Envoy 공식 Grafana 대시보드 (grafana.com/dashboards) — Import ID 입력만으로 사용 가능# Envoy Global: Dashboard ID 11021# Envoy Clusters: Dashboard ID 11022# Contour 자체 대시보드는 grafana.com에 등록되어 있지 않으므로 저장소에서 JSON을 직접 가져옵니다git clone --depth 1 https://github.com/projectcontour/contour.git /tmp/contour# /tmp/contour/examples/grafana/ 하위 JSON을 Grafana UI → Dashboards → Import 로 업로드# (버전에 따라 경로가 변경될 수 있으니 실제 clone한 태그의 examples/ 디렉터리를 확인)
다음 단계
이 섹션은 다음 단계을 실무 관점에서 정리합니다. 개념을 외우기보다, 어떤 상황에서 이 기준을 꺼내 쓸지에 초점을 맞춰보세요.