· 57분 읽기
Kubernetes 내부 동작 원리: 컨트롤러, 서비스 네트워킹, 설정 관리
kubectl apply로 리소스를 선언하면 실제 Pod를 만들고 유지하는 것은 컨트롤러입니다. Kubernetes의 거의 모든 구성요소는 "현재 상태를 원하는 상태에 맞춘다"는 Reconciliation Loop 하나로 설명할 수 있습니다. 이 글에서는 이 조정 루프를 축으로 워크로드 컨트롤러의 내부 동작, kube-proxy와 Service 타입별 네트워킹, ConfigMap과 Secrets 관리까지 이어서 살펴봅니다. AWS 환경(ALB/NLB, Secrets Manager) 통합과 트러블슈팅 부록도 함께 정리합니다.
컨트롤러 패턴과 Reconciliation Loop
Kubernetes의 모든 것은 컨트롤러 패턴으로 동작합니다. 사용자가 Deployment YAML을 제출하면 API Server가 이를 etcd에 저장하고, Controller Manager 안의 각 컨트롤러가 변경을 감지해 실제 리소스를 만들어 냅니다.
flowchart LR
subgraph User [사용자]
YAML[Deployment YAML]
end
subgraph ControlPlane [Control Plane]
API[API Server]
ETCD[(etcd)]
CM[Controller Manager]
subgraph Controllers [컨트롤러들]
DC[Deployment Controller]
RSC[ReplicaSet Controller]
STC[StatefulSet Controller]
DSC[DaemonSet Controller]
JC[Job Controller]
CJC[CronJob Controller]
end
end
subgraph Node [Worker Node]
Kubelet[kubelet]
Pod1[Pod]
Pod2[Pod]
end
YAML --> API --> ETCD
API <--> CM --> Controllers
DC --> API
RSC --> API
API <--> Kubelet --> Pod1 & Pod2
모든 컨트롤러는 동일한 패턴으로 동작합니다.
무한 루프:
1. 현재 상태(Current State) 관찰
2. 원하는 상태(Desired State)와 비교
3. 차이가 있으면 조정(Reconcile)
4. 다음 이벤트 대기
// 실제 Kubernetes 컨트롤러의 핵심 구조 (간략화)
func (c *Controller) Run(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
// 1. 작업 큐에서 아이템 가져오기
key, shutdown := c.workqueue.Get()
if shutdown {
return
}
// 2. Reconcile 실행
err := c.reconcile(key.(string))
if err != nil {
// 재시도 큐에 추가
c.workqueue.AddRateLimited(key)
} else {
c.workqueue.Forget(key)
}
c.workqueue.Done(key)
}
}
}
중요: Kubernetes 컨트롤러는 Edge-triggered가 아닌 Level-triggered 방식입니다. "상태가 변했다"(Edge)가 아니라 "현재 상태가 이것이다"(Level)를 기준으로 동작하기 때문에, 컨트롤러가 재시작되어도 현재 상태를 다시 읽어 정상적으로 조정할 수 있습니다.
Deployment: 가장 널리 쓰이는 워크로드
Deployment → ReplicaSet → Pod
Deployment는 Pod를 직접 관리하지 않습니다. ReplicaSet을 통해 간접적으로 관리합니다.
flowchart TB
subgraph Deployment [Deployment: my-app]
D[spec.replicas: 3<br/>spec.strategy: RollingUpdate]
end
subgraph RS [ReplicaSet: my-app-7d4f5b8c9]
RS1[spec.replicas: 3<br/>ownerReference: my-app]
end
subgraph Pods [Pods]
P1[my-app-7d4f5b8c9-abc12]
P2[my-app-7d4f5b8c9-def34]
P3[my-app-7d4f5b8c9-ghi56]
end
Deployment --> RS --> Pods
Rolling Update 동작 원리
Deployment를 업데이트하면 새 ReplicaSet이 생성되고, 점진적으로 트래픽이 이동합니다.
sequenceDiagram
participant User as 사용자
participant API as API Server
participant DC as Deployment Controller
participant RSC as ReplicaSet Controller
User->>API: kubectl apply (새 이미지)
API->>DC: Deployment 변경 감지
DC->>API: 새 ReplicaSet 생성 (replicas: 1)
DC->>API: 기존 ReplicaSet scale down (replicas: 2)
loop maxSurge/maxUnavailable 조건 충족까지
RSC->>API: 새 Pod 생성
RSC->>API: 기존 Pod 삭제
DC->>API: ReplicaSet replicas 조정
end
DC->>API: 기존 ReplicaSet replicas: 0
Note over API: 롤아웃 완료
업데이트 전략 비교
# RollingUpdate (기본값) - 무중단 배포
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25% # 최대 추가 Pod 수
maxUnavailable: 25% # 최대 사용 불가 Pod 수
# Recreate - 모든 Pod 삭제 후 재생성
spec:
strategy:
type: Recreate
# 주의: 다운타임 발생!
# 사용 사례: DB 마이그레이션, 싱글 인스턴스 제약
| 전략 | 다운타임 | 리소스 사용 | 사용 사례 |
|---|---|---|---|
| RollingUpdate | 없음 | 일시적 증가 | 대부분의 상황 |
| Recreate | 있음 | 동일 | 싱글 인스턴스, 볼륨 공유 불가 시 |
롤백 동작 원리
# 롤아웃 히스토리 확인
kubectl rollout history deployment/my-app
# 특정 리비전으로 롤백
kubectl rollout undo deployment/my-app --to-revision=2
롤백은 새 ReplicaSet을 생성하는 것이 아닙니다. 기존 ReplicaSet을 다시 Scale Up합니다.
flowchart LR
subgraph Before [롤백 전]
RS1_B[RS v1: 0개]
RS2_B[RS v2: 0개]
RS3_B[RS v3: 3개 ✓]
end
subgraph After [v2로 롤백 후]
RS1_A[RS v1: 0개]
RS2_A[RS v2: 3개 ✓]
RS3_A[RS v3: 0개]
end
Before --> |kubectl rollout undo<br/>--to-revision=2| After
팁:
revisionHistoryLimit(기본값 10)으로 유지할 ReplicaSet 수를 제한할 수 있습니다. 너무 많으면 etcd 부담이 증가합니다.
StatefulSet: 상태 유지가 필요한 워크로드
Deployment와의 핵심 차이
| 특성 | Deployment | StatefulSet |
|---|---|---|
| Pod 이름 | 랜덤 (app-7d4f5b8c9-abc12) |
순차적 (app-0, app-1, app-2) |
| 생성/삭제 순서 | 병렬 | 순차적 (0→1→2, 삭제는 역순) |
| 네트워크 ID | 없음 | Headless Service로 고정 DNS |
| 스토리지 | Pod 삭제 시 PVC도 삭제 가능 | Pod 삭제해도 PVC 유지 |
순차적 생성과 삭제
sequenceDiagram
participant API as API Server
participant STC as StatefulSet Controller
Note over STC: 생성 (순차적)
STC->>API: Pod-0 생성
API-->>STC: Pod-0 Ready
STC->>API: Pod-1 생성
API-->>STC: Pod-1 Ready
STC->>API: Pod-2 생성
API-->>STC: Pod-2 Ready
Note over STC: 삭제 (역순)
STC->>API: Pod-2 삭제
API-->>STC: Pod-2 Terminated
STC->>API: Pod-1 삭제
API-->>STC: Pod-1 Terminated
STC->>API: Pod-0 삭제
Headless Service와 고정 DNS
# Headless Service (ClusterIP: None)
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
clusterIP: None # Headless!
selector:
app: mysql
ports:
- port: 3306
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql # Headless Service 이름
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
이렇게 설정하면 각 Pod에 고정 DNS가 부여됩니다.
mysql-0.mysql.default.svc.cluster.local
mysql-1.mysql.default.svc.cluster.local
mysql-2.mysql.default.svc.cluster.local
중요: 일반 Service는 Pod IP가 변해도 Service IP로 로드밸런싱됩니다. StatefulSet + Headless Service는 각 Pod를 직접 지정할 수 있어 MySQL 레플리케이션, Kafka 브로커 등에 필수입니다.
VolumeClaimTemplates
spec:
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: gp3
resources:
requests:
storage: 100Gi
Pod가 삭제되어도 PVC는 그대로 유지됩니다. mysql-0이 다시 생성되면 같은 데이터를 사용합니다.
flowchart TB
subgraph StatefulSet [StatefulSet: mysql]
P0[mysql-0]
P1[mysql-1]
P2[mysql-2]
end
subgraph PVCs [PersistentVolumeClaims]
PVC0[data-mysql-0<br/>100Gi]
PVC1[data-mysql-1<br/>100Gi]
PVC2[data-mysql-2<br/>100Gi]
end
subgraph PVs [PersistentVolumes - AWS EBS]
PV0[vol-abc...]
PV1[vol-def...]
PV2[vol-ghi...]
end
P0 --> PVC0 --> PV0
P1 --> PVC1 --> PV1
P2 --> PVC2 --> PV2
podManagementPolicy
spec:
podManagementPolicy: Parallel # 기본값: OrderedReady
| 정책 | 설명 | 사용 사례 |
|---|---|---|
OrderedReady |
순차적 생성/삭제, 이전 Pod Ready 대기 | MySQL, ZooKeeper |
Parallel |
병렬 생성/삭제, 순서 무관 | Elasticsearch (빠른 스케일링 필요) |
DaemonSet: 모든 노드에 Pod 배포
DaemonSet Controller는 각 노드에 정확히 하나의 Pod가 실행되도록 보장합니다. 노드가 추가되면 자동으로 해당 노드에 Pod를 생성하고, 노드가 삭제되면 해당 노드의 Pod도 함께 삭제합니다.
flowchart TB
subgraph Cluster [Kubernetes 클러스터]
subgraph Node1 [Node 1]
P1[fluentd]
end
subgraph Node2 [Node 2]
P2[fluentd]
end
subgraph Node3 [Node 3]
P3[fluentd]
end
subgraph Node4 [Node 4 - 신규]
P4[fluentd]:::new
end
end
DS[DaemonSet: fluentd]
DS --> P1 & P2 & P3 & P4
classDef new fill:#90EE90
특정 노드에만 배포하거나 taint가 있는 노드에도 배포하려면 nodeSelector와 tolerations를 사용합니다.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nvidia-driver
spec:
selector:
matchLabels:
app: nvidia-driver
template:
spec:
# 특정 노드에만 배포
nodeSelector:
hardware: gpu
# taint를 허용
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
containers:
- name: nvidia-driver
image: nvidia/driver:latest
| 사용 사례 | 예시 |
|---|---|
| 로그 수집 | Fluentd, Fluent Bit, Filebeat |
| 모니터링 | Node Exporter, cAdvisor |
| 네트워크 | Calico, Cilium (CNI 플러그인) |
| 스토리지 | CSI 드라이버, Rook-Ceph |
| 보안 | Falco, Sysdig |
Job과 CronJob: 배치 작업
Job: 일회성 작업
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
spec:
backoffLimit: 3 # 실패 시 최대 재시도 횟수
activeDeadlineSeconds: 600 # 최대 실행 시간 (10분)
ttlSecondsAfterFinished: 3600 # 완료 후 1시간 뒤 자동 삭제
template:
spec:
restartPolicy: Never # Job에서는 OnFailure 또는 Never만 가능
containers:
- name: migration
image: my-app:latest
command: ["./migrate.sh"]
CronJob: 정기적 작업
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-report
spec:
schedule: "0 2 * * *" # 매일 02:00 (timeZone 미지정 시 UTC)
timeZone: "Asia/Seoul" # K8s 1.27+ 지원
# 동시 실행 정책
concurrencyPolicy: Forbid # 이전 Job이 실행 중이면 새 Job 생성 안함
# 히스토리 제한
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
# 스케줄 놓쳤을 때 정책
startingDeadlineSeconds: 300 # 5분 내 시작 못하면 스킵
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: report
image: report-generator:latest
concurrencyPolicy 비교
gantt
title CronJob concurrencyPolicy 비교
dateFormat HH:mm
axisFormat %H:%M
section Allow
Job 1 :active, 00:00, 30m
Job 2 :active, 00:30, 30m
Job 3 (동시실행됨) :active, 00:45, 30m
section Forbid
Job 1 :active, 00:00, 60m
Job 2 (스킵됨) :crit, 00:30, 5m
Job 2 (실행) :active, 01:00, 30m
section Replace
Job 1 :active, 00:00, 30m
Job 2 (Job 1 중단) :active, 00:30, 30m
| 정책 | 동작 | 사용 사례 |
|---|---|---|
Allow |
동시 실행 허용 (기본값) | 독립적인 작업 |
Forbid |
이전 Job 실행 중이면 스킵 | 중복 실행 방지 필요 시 |
Replace |
이전 Job 중단하고 새로 시작 | 최신 데이터만 중요할 때 |
Service의 본질: Pod 추상화
Pod는 생성될 때마다 IP가 바뀝니다. Service는 이 변화하는 Pod들 앞에 안정적인 엔드포인트를 제공합니다.
flowchart LR
subgraph Before [Pod IP 직접 사용]
Client1[클라이언트]
P1[Pod 10.0.1.5]
P2[Pod 10.0.1.6]
Client1 --> P1
Client1 -.->|Pod 재시작<br/>IP 변경!| P2
end
subgraph After [Service 사용]
Client2[클라이언트]
SVC[Service<br/>10.96.0.100]
P3[Pod 10.0.1.5]
P4[Pod 10.0.1.6]
P5[Pod 10.0.1.7]
Client2 --> SVC --> P3 & P4 & P5
end
Service 타입별 동작 원리
ClusterIP: 클러스터 내부 통신
apiVersion: v1
kind: Service
metadata:
name: backend-api
spec:
type: ClusterIP # 기본값, 생략 가능
selector:
app: backend
ports:
- port: 80 # Service 포트
targetPort: 8080 # Pod 포트
동작 순서는 다음과 같습니다.
- Service 생성 시 ClusterIP 할당 (예: 10.96.0.100)
- Endpoints 객체 자동 생성 (selector와 일치하는 Pod IP 목록)
- kube-proxy가 iptables/IPVS 규칙 생성
- ClusterIP로 오는 트래픽을 Pod IP로 로드밸런싱
# Endpoints 확인 (실제 Pod IP 목록)
kubectl get endpoints backend-api
# 출력 예시:
# NAME ENDPOINTS AGE
# backend-api 10.0.1.5:8080,10.0.1.6:8080,10.0.1.7:8080 1h
NodePort: 외부에서 노드 IP로 접근
apiVersion: v1
kind: Service
metadata:
name: backend-api
spec:
type: NodePort
selector:
app: backend
ports:
- port: 80
targetPort: 8080
nodePort: 30080 # 30000-32767 범위, 생략 시 자동 할당
flowchart LR
subgraph External [외부 네트워크]
Client[클라이언트]
end
subgraph Cluster [Kubernetes 클러스터]
subgraph Node1 [Node 1 - 192.168.1.10]
NP1[NodePort :30080]
P1[Pod :8080]
end
subgraph Node2 [Node 2 - 192.168.1.11]
NP2[NodePort :30080]
P2[Pod :8080]
end
subgraph Node3 [Node 3 - 192.168.1.12]
NP3[NodePort :30080]
end
end
Client -->|192.168.1.10:30080| NP1
Client -->|192.168.1.11:30080| NP2
Client -->|192.168.1.12:30080| NP3
NP1 --> P1 & P2
NP2 --> P1 & P2
NP3 --> P1 & P2
중요: NodePort는 모든 노드에서 열립니다. Node3에 Pod가 없어도 30080 포트로 접근하면 다른 노드의 Pod로 트래픽이 전달됩니다.
externalTrafficPolicy: 트래픽 경로 제어
spec:
type: NodePort
externalTrafficPolicy: Local # 기본값: Cluster
| 정책 | 동작 | 장점 | 단점 |
|---|---|---|---|
Cluster |
다른 노드 Pod로도 전달 | 균등한 로드밸런싱 | 추가 홉, 클라이언트 IP 손실 |
Local |
해당 노드의 Pod로만 전달 | 클라이언트 IP 보존, 낮은 지연 | 불균등 분배 가능 |
flowchart TB
subgraph Cluster_Policy [externalTrafficPolicy: Cluster]
C1[Client → Node1:30080]
N1_1[Node1]
N2_1[Node2]
P1_1[Pod on Node2]
C1 --> N1_1 -->|SNAT| N2_1 --> P1_1
Note1[클라이언트 IP가 Node1 IP로 변경됨]
end
subgraph Local_Policy [externalTrafficPolicy: Local]
C2[Client → Node2:30080]
N2_2[Node2]
P1_2[Pod on Node2]
C2 --> N2_2 --> P1_2
Note2[클라이언트 IP 보존됨]
end
LoadBalancer: 클라우드 로드밸런서 통합
LoadBalancer 타입은 "NodePort + 클라우드 LB 자동 프로비저닝"입니다.
apiVersion: v1
kind: Service
metadata:
name: backend-api
annotations:
# AWS NLB 사용 (기본은 CLB)
service.beta.kubernetes.io/aws-load-balancer-type: nlb
# 인터널 LB
service.beta.kubernetes.io/aws-load-balancer-internal: "true"
spec:
type: LoadBalancer
selector:
app: backend
ports:
- port: 80
targetPort: 8080
flowchart LR
subgraph AWS [AWS]
Internet[인터넷]
NLB[Network Load Balancer<br/>abc123.elb.amazonaws.com]
end
subgraph K8s [Kubernetes 클러스터]
subgraph Node1 [Node 1]
NP1[NodePort :30080]
P1[Pod]
end
subgraph Node2 [Node 2]
NP2[NodePort :30080]
P2[Pod]
end
end
Internet --> NLB --> NP1 & NP2
NP1 --> P1
NP2 --> P2
ExternalName: 외부 서비스 CNAME
apiVersion: v1
kind: Service
metadata:
name: external-db
spec:
type: ExternalName
externalName: mydb.us-east-1.rds.amazonaws.com
클러스터 내에서 external-db로 DNS 조회하면 RDS 주소가 반환됩니다.
# 클러스터 포드 내에서
nslookup external-db.default.svc.cluster.local
# → mydb.us-east-1.rds.amazonaws.com
Headless Service의 DNS 해석
앞서 StatefulSet에서 본 Headless Service(clusterIP: None)는 DNS 조회 시 Service IP가 아닌 Pod IP 목록을 직접 반환합니다.
# 일반 Service: ClusterIP 반환
nslookup backend-api.default.svc.cluster.local
# → 10.96.0.100
# Headless Service: 모든 Pod IP 반환
nslookup mysql.default.svc.cluster.local
# → 10.0.1.5
# → 10.0.1.6
# → 10.0.1.7
flowchart TB
subgraph Normal [일반 Service]
DNS1[DNS 조회] --> ClusterIP[10.96.0.100]
ClusterIP --> |kube-proxy 로드밸런싱| P1 & P2 & P3
end
subgraph Headless [Headless Service]
DNS2[DNS 조회] --> PodIPs[10.0.1.5, 10.0.1.6, 10.0.1.7]
PodIPs --> |클라이언트가 직접 선택| P4[Pod]
end
| 사용 사례 | 이유 |
|---|---|
| StatefulSet | 각 Pod에 고유 DNS 필요 (mysql-0.mysql.svc) |
| 클라이언트 측 로드밸런싱 | gRPC, 커스텀 LB 알고리즘 |
| 서비스 디스커버리 | Consul, etcd 같은 클러스터 |
kube-proxy: Service의 구현체
Service는 결국 각 노드의 kube-proxy가 만드는 패킷 라우팅 규칙으로 구현됩니다.
flowchart TB
subgraph IptablesMode ["iptables 모드"]
IT["iptables 규칙"]
Rule1["규칙 1"]
Rule2["규칙 2"]
RuleN["규칙 N"]
Target["Pod"]
IT --> Rule1 --> Rule2 --> RuleN --> Target
end
subgraph IPVSMode ["IPVS 모드"]
IV["IPVS 해시 테이블"]
Target2["Pod"]
IV --> Target2
end
subgraph NftablesMode ["nftables 모드 (권장)"]
NFT["nftables 규칙"]
Target3["Pod"]
NFT --> Target3
end
| 특성 | iptables | IPVS | nftables |
|---|---|---|---|
| 조회 시간 | O(n) | O(1) | O(1) |
| 권장 여부 | 안정적 | Deprecated 예정 | 권장 |
| 로드밸런싱 | 랜덤 | rr, lc, dh 등 | 다양 |
| 성능 | 개선됨 | 양호 | 최고 |
| 최소 버전 | 모든 버전 | - | K8s 1.31+ (1.33 Stable) |
주의: Kubernetes 공식 문서에 따르면 IPVS 모드는 Kubernetes Services API와의 불일치로 인해 향후 deprecated될 예정입니다. 새로운 클러스터에서는 nftables 모드(K8s 1.33+ stable) 또는 iptables 모드를 권장합니다.
# nftables 모드 (K8s 1.31+, 권장)
apiVersion: v1
kind: ConfigMap
metadata:
name: kube-proxy
namespace: kube-system
data:
config.conf: |
mode: "nftables"
# IPVS 모드 (deprecated 예정)
apiVersion: v1
kind: ConfigMap
metadata:
name: kube-proxy
namespace: kube-system
data:
config.conf: |
mode: "ipvs"
ipvs:
scheduler: "rr" # round-robin
AWS Load Balancer Controller
AWS Load Balancer Controller는 Ingress와 Service를 ALB/NLB로 프로비저닝합니다.
flowchart TB
subgraph K8s [Kubernetes]
Ingress[Ingress 리소스]
SVC[Service type: LoadBalancer]
CTRL[AWS Load Balancer Controller]
end
subgraph AWS [AWS]
ALB[Application Load Balancer]
NLB[Network Load Balancer]
TG[Target Group]
R53[Route 53]
end
Ingress --> CTRL
SVC --> CTRL
CTRL --> ALB & NLB
ALB & NLB --> TG
TG --> |Pod IP 직접 등록| Pods[Pods]
Ingress로 ALB 생성
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
annotations:
# ALB Ingress Controller
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip # Pod IP 직접 연결
# SSL/TLS
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:...
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
alb.ingress.kubernetes.io/ssl-redirect: '443'
# 헬스체크
alb.ingress.kubernetes.io/healthcheck-path: /health
alb.ingress.kubernetes.io/healthcheck-interval-seconds: '15'
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: backend-api
port:
number: 80
target-type: ip vs instance
flowchart LR
subgraph Instance [target-type: instance]
ALB1[ALB] --> NP[NodePort] --> Pod1[Pod]
end
subgraph IP [target-type: ip]
ALB2[ALB] --> Pod2[Pod]
end
| target-type | 경로 | 장점 | 단점 |
|---|---|---|---|
instance |
ALB → NodePort → Pod | 간단, 모든 네트워크에서 동작 | 추가 홉, 지연 |
ip |
ALB → Pod (직접) | 낮은 지연, 효율적 | VPC CNI 필요, ENI 제한 |
NLB for TCP/UDP
apiVersion: v1
kind: Service
metadata:
name: tcp-service
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-scheme: "internet-facing"
spec:
type: LoadBalancer
selector:
app: my-app
ports:
- port: 9000
targetPort: 9000
protocol: TCP
EndpointSlices: 대규모 클러스터 최적화
Endpoints 객체는 Service당 하나이며 모든 Pod IP를 담습니다. Pod가 수천 개면 문제가 됩니다.
flowchart TB
subgraph Endpoints [기존 Endpoints]
EP[Endpoints 객체<br/>5000개 Pod IP<br/>매우 큼!]
end
subgraph EndpointSlices [EndpointSlices]
ES1[EndpointSlice 1<br/>100개 Pod]
ES2[EndpointSlice 2<br/>100개 Pod]
ES3[...<br/>...]
ES50[EndpointSlice 50<br/>100개 Pod]
end
EndpointSlices의 장점은 다음과 같습니다.
- 최대 100개 엔드포인트씩 분할
- 변경 시 해당 슬라이스만 업데이트
- etcd 부하 감소
# EndpointSlices 확인
kubectl get endpointslices -l kubernetes.io/service-name=my-service
ConfigMap: 설정의 외부화
컨테이너 이미지에 설정을 하드코딩하면 환경별(dev, staging, prod)로 이미지를 따로 빌드해야 하고, 설정 변경 시 재배포가 필요하며, 설정을 재사용할 수 없습니다. ConfigMap은 설정과 코드를 분리합니다.
flowchart LR
subgraph Before [하드코딩된 설정]
IMG1[이미지-dev]
IMG2[이미지-staging]
IMG3[이미지-prod]
end
subgraph After [ConfigMap 사용]
IMG[단일 이미지]
CM1[ConfigMap-dev]
CM2[ConfigMap-staging]
CM3[ConfigMap-prod]
IMG --> CM1 & CM2 & CM3
end
ConfigMap 생성 방법
# 직접 정의
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
# 키-값 쌍
DATABASE_HOST: mysql.default.svc.cluster.local
DATABASE_PORT: "3306"
LOG_LEVEL: info
# 파일 내용
nginx.conf: |
server {
listen 80;
location / {
proxy_pass http://backend:8080;
}
}
# 파일에서 생성
kubectl create configmap nginx-config --from-file=nginx.conf
# 리터럴에서 생성
kubectl create configmap app-config \
--from-literal=DATABASE_HOST=mysql \
--from-literal=LOG_LEVEL=debug
사용 패턴 1: 환경변수로 주입
spec:
containers:
- name: app
env:
# 개별 키 선택
- name: DB_HOST
valueFrom:
configMapKeyRef:
name: app-config
key: DATABASE_HOST
# 전체 ConfigMap을 환경변수로
envFrom:
- configMapRef:
name: app-config
사용 패턴 2: 볼륨으로 마운트
spec:
containers:
- name: nginx
volumeMounts:
- name: config-volume
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf # 특정 키만 파일로
volumes:
- name: config-volume
configMap:
name: nginx-config
불변(Immutable) ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
immutable: true # K8s 1.21+
data:
LOG_LEVEL: info
API Server 부하가 감소(watch 제거)하고 실수로 인한 변경을 방지할 수 있습니다. 대신 값을 변경하려면 새 ConfigMap을 만들고 Pod를 재배포해야 합니다.
Secrets: 민감 정보 관리
| 특성 | ConfigMap | Secrets |
|---|---|---|
| 용도 | 일반 설정 | 민감 정보 |
| 저장 | 평문 | Base64 인코딩 |
| 크기 제한 | 1MB | 1MB |
| etcd 저장 | 평문 | 암호화 가능 (별도 설정) |
| kubectl 출력 | 그대로 표시 | 기본적으로 숨김 |
주의: Base64는 인코딩이지 암호화가 아닙니다. 누구나 쉽게 디코딩할 수 있으므로 반드시 Secrets at Rest Encryption을 활성화해야 합니다.
Secret 타입
# Opaque (기본값) - 일반 시크릿
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
data:
username: YWRtaW4= # echo -n 'admin' | base64
password: cGFzc3dvcmQ= # echo -n 'password' | base64
# 또는 stringData 사용 (자동 인코딩)
stringData:
username: admin
password: password
# TLS 인증서
apiVersion: v1
kind: Secret
metadata:
name: tls-secret
type: kubernetes.io/tls
data:
tls.crt: <base64-encoded-cert>
tls.key: <base64-encoded-key>
# Docker Registry 인증
apiVersion: v1
kind: Secret
metadata:
name: regcred
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: <base64-encoded-docker-config>
Secrets 사용 패턴
spec:
containers:
- name: app
# 환경변수로 주입
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
# 볼륨으로 마운트 (파일)
volumeMounts:
- name: secrets-volume
mountPath: /etc/secrets
readOnly: true
volumes:
- name: secrets-volume
secret:
secretName: db-credentials
모든 Pod에는 ServiceAccount 토큰이 자동 마운트됩니다. 필요 없다면 비활성화하는 것이 안전합니다.
spec:
automountServiceAccountToken: false
containers:
- name: app
Secrets at Rest Encryption
etcd에 저장된 Secrets를 암호화합니다.
# /etc/kubernetes/encryption-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
# AWS KMS 사용
- kms:
apiVersion: v2
name: aws-encryption-provider
endpoint: unix:///var/run/kmsplugin/socket.sock
cachesize: 1000
timeout: 3s
# 폴백: identity (암호화 안 함)
- identity: {}
EKS는 AWS KMS를 사용한 Envelope Encryption을 지원합니다. Secret은 랜덤 생성된 DEK(Data Encryption Key)로 암호화되고, DEK는 다시 KMS의 CMK로 암호화되어 함께 저장됩니다.
flowchart LR
subgraph K8s [Kubernetes]
Secret[Secret 데이터]
DEK[Data Encryption Key<br/>랜덤 생성]
end
subgraph KMS [AWS KMS]
CMK[Customer Master Key]
end
subgraph etcd [etcd]
Encrypted[암호화된 Secret +<br/>암호화된 DEK]
end
Secret -->|DEK로 암호화| Encrypted
DEK -->|CMK로 암호화| Encrypted
CMK -.->|복호화 시 사용| DEK
# EKS 클러스터에 KMS 키 연결
aws eks associate-encryption-config \
--cluster-name my-cluster \
--encryption-config '[{
"resources": ["secrets"],
"provider": {
"keyArn": "arn:aws:kms:ap-northeast-2:123456789012:key/12345678-1234-1234-1234-123456789012"
}
}]'
AWS Secrets Manager와 CSI Driver
Kubernetes Secrets에는 한계가 있습니다. 시크릿이 etcd에 저장되어 클러스터 침해 시 노출될 수 있고, 로테이션이 어려우며, 감사(Audit) 기능이 제한적이고, 버전 관리가 없습니다. 외부 시크릿 저장소를 연동하면 이 한계를 보완할 수 있습니다.
flowchart TB
subgraph AWS [AWS]
SM["Secrets Manager"]
end
subgraph K8s ["Kubernetes 클러스터"]
CSI["Secrets Store CSI Driver"]
ASCP["AWS Secrets & Config Provider"]
Pod["Application Pod"]
subgraph Mount ["Pod 내부"]
File["파일: db-password"]
Env["ENV: DB_PASSWORD"]
end
end
SM --> ASCP --> CSI --> Pod
Pod --> Mount
설치
# Secrets Store CSI Driver 설치
helm repo add secrets-store-csi-driver \
https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm install csi-secrets-store \
secrets-store-csi-driver/secrets-store-csi-driver \
--namespace kube-system \
--set syncSecret.enabled=true \
--set enableSecretRotation=true
# AWS Provider 설치
kubectl apply -f https://raw.githubusercontent.com/aws/secrets-store-csi-driver-provider-aws/main/deployment/aws-provider-installer.yaml
IRSA (IAM Roles for Service Accounts) 설정
# OIDC Provider 연결 (EKS 클러스터 생성 시 자동)
eksctl utils associate-iam-oidc-provider \
--cluster my-cluster \
--approve
# ServiceAccount용 IAM Role 생성
eksctl create iamserviceaccount \
--cluster my-cluster \
--namespace default \
--name my-app-sa \
--attach-policy-arn arn:aws:iam::123456789012:policy/SecretsManagerReadPolicy \
--approve
SecretProviderClass 정의
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: aws-secrets
spec:
provider: aws
parameters:
objects: |
- objectName: "prod/myapp/db-credentials"
objectType: "secretsmanager"
jmesPath:
- path: username
objectAlias: db-username
- path: password
objectAlias: db-password
- objectName: "prod/myapp/api-key"
objectType: "secretsmanager"
# Kubernetes Secret으로도 동기화 (optional)
secretObjects:
- secretName: db-credentials-k8s
type: Opaque
data:
- objectName: db-username
key: username
- objectName: db-password
key: password
Pod에서 사용
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
serviceAccountName: my-app-sa # IRSA 연결된 SA
containers:
- name: app
image: my-app:latest
# 파일로 마운트된 시크릿 경로
volumeMounts:
- name: secrets
mountPath: /mnt/secrets
readOnly: true
# 환경변수로도 사용 가능 (secretObjects 사용 시)
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials-k8s
key: password
volumes:
- name: secrets
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: aws-secrets
시크릿 로테이션
# Secrets Store CSI Driver 설치 시 옵션
helm upgrade csi-secrets-store \
secrets-store-csi-driver/secrets-store-csi-driver \
--namespace kube-system \
--set enableSecretRotation=true \
--set rotationPollInterval=2m # 2분마다 체크
sequenceDiagram
participant SM as AWS Secrets Manager
participant CSI as CSI Driver
participant Vol as Volume (파일)
participant App as Application
Note over SM: 시크릿 로테이션 발생
SM->>SM: 새 버전 생성
loop 매 2분
CSI->>SM: 현재 시크릿 조회
SM-->>CSI: 새 값 반환
CSI->>Vol: 파일 업데이트
end
App->>Vol: 파일 다시 읽기
Note over App: 새 시크릿 사용
팁: 애플리케이션이 파일 변경을 감지하거나 주기적으로 파일을 다시 읽도록 구현해야 합니다. 환경변수로 동기화하는 경우에는 Pod 재시작이 필요합니다.
External Secrets Operator (대안)
CSI Driver의 대안으로, 외부 시크릿 저장소의 값을 Kubernetes Secret으로 직접 동기화하는 방식입니다.
flowchart TB
subgraph AWS [AWS]
SM[Secrets Manager]
end
subgraph K8s [Kubernetes]
ESO[External Secrets Operator]
ES[ExternalSecret CR]
Secret[Kubernetes Secret]
Pod[Pod]
end
ES --> ESO
ESO --> SM
SM --> ESO --> Secret --> Pod
| 특성 | CSI Driver | External Secrets Operator |
|---|---|---|
| 시크릿 저장 위치 | Pod 로컬 볼륨 (tmpfs) | Kubernetes Secret (etcd) |
| 설치 복잡도 | DaemonSet + Provider | Deployment |
| GitOps 친화성 | 낮음 | 높음 (ExternalSecret CRD) |
| etcd에 저장 | 안 됨 | 됨 |
| 호환성 | CSI 지원 필요 | 모든 환경 |
설정 관리 베스트 프랙티스
1. 환경별 네임스페이스 분리
# dev 네임스페이스
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: dev
data:
LOG_LEVEL: debug
---
# prod 네임스페이스
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: prod
data:
LOG_LEVEL: warn
2. RBAC로 시크릿 접근 제한
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: secret-reader
namespace: prod
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["db-credentials"] # 특정 시크릿만
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-secret-binding
namespace: prod
subjects:
- kind: ServiceAccount
name: my-app-sa
namespace: prod
roleRef:
kind: Role
name: secret-reader
apiGroup: rbac.authorization.k8s.io
3. 감사 로깅 활성화
# Audit Policy
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
4. 시크릿 값 변경 시 Pod 재시작
# Deployment 템플릿에 checksum 추가
spec:
template:
metadata:
annotations:
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
트러블슈팅 부록
워크로드: Deployment 롤아웃이 멈출 때
# 상태 확인
kubectl rollout status deployment/my-app
# 이벤트 확인
kubectl describe deployment my-app
# ReplicaSet 상태 확인
kubectl get rs -l app=my-app
흔한 원인은 다음과 같습니다.
- 이미지 Pull 실패: ImagePullBackOff
- 리소스 부족: Pending 상태
- Liveness/Readiness Probe 실패: CrashLoopBackOff
- maxUnavailable 0: 기존 Pod 삭제 안 됨
워크로드: StatefulSet Pod가 Pending일 때
# PVC 상태 확인
kubectl get pvc
# 스토리지 이벤트 확인
kubectl describe pvc data-mysql-0
흔한 원인은 다음과 같습니다.
- StorageClass 없음: 기본 StorageClass 미설정
- 가용 영역(AZ) 불일치: EBS는 같은 AZ에서만 마운트
- 용량 부족: AWS EBS 한도 초과
워크로드: CronJob이 실행되지 않을 때
# CronJob 상태 확인
kubectl get cronjob
# 최근 Job 확인
kubectl get jobs --sort-by=.metadata.creationTimestamp
# 마지막 스케줄 시간 확인
kubectl describe cronjob daily-report | grep "Last Schedule"
흔한 원인은 다음과 같습니다.
- startingDeadlineSeconds 초과: 스케줄 시간 놓침
- concurrencyPolicy: Forbid + 긴 실행 시간: 이전 Job이 계속 실행 중
- suspend: true: CronJob이 일시 중지됨
네트워킹: Service에 연결되지 않을 때
# 1. Endpoints 확인 (비어있으면 selector 문제)
kubectl get endpoints my-service
# 2. Pod label 확인
kubectl get pods --show-labels
# 3. Pod readiness 확인
kubectl get pods -o wide
네트워킹: NodePort로 외부 접속이 안 될 때
# 1. NodePort 확인
kubectl get svc my-service
# 2. 방화벽/Security Group 확인
# AWS: EC2 Security Group에서 NodePort 범위 허용
# 3. iptables 규칙 확인
sudo iptables -t nat -L KUBE-SERVICES -n
네트워킹: LoadBalancer EXTERNAL-IP가 <pending>일 때
# 1. AWS Load Balancer Controller 로그 확인
kubectl logs -n kube-system -l app.kubernetes.io/name=aws-load-balancer-controller
# 2. 서비스 이벤트 확인
kubectl describe svc my-service
# 3. IAM 권한 확인 (IRSA)
흔한 원인은 다음과 같습니다.
- Cloud Controller Manager 미설치 (On-prem)
- IAM 권한 부족
- 서브넷 태그 누락 (
kubernetes.io/role/elb)
설정: CSI Driver가 시크릿을 읽지 못할 때
# 1. Provider Pod 로그 확인
kubectl logs -n kube-system -l app=csi-secrets-store-provider-aws
# 2. IRSA 설정 확인
kubectl describe sa my-app-sa
# 3. IAM Role의 Trust Policy 확인
aws iam get-role --role-name MyAppRole --query 'Role.AssumeRolePolicyDocument'
흔한 원인은 다음과 같습니다.
- OIDC Provider 미설정
- ServiceAccount와 IAM Role 연결 누락
- Secrets Manager 접근 권한 누락
설정: ConfigMap 변경이 Pod에 반영되지 않을 때
# 1. 환경변수로 주입한 경우: Pod 재시작 필요
kubectl rollout restart deployment/my-app
# 2. 볼륨으로 마운트한 경우: kubelet sync 대기 (기본 1분)
# 즉시 반영 여부 확인:
kubectl exec -it my-pod -- cat /etc/config/my-key
중요: 볼륨 마운트 시 파일은 자동 업데이트되지만, 애플리케이션이 파일을 캐싱하고 있다면 재시작이 필요합니다.
정리
Kubernetes의 내부 동작을 한 문장으로 요약하면 "모든 것은 Reconciliation Loop"입니다. 워크로드 컨트롤러가 Pod의 수와 순서를 조정하고, kube-proxy가 Service 규칙을 노드에 조정하며, CSI Driver가 외부 시크릿을 볼륨에 조정합니다.
| 워크로드 | 핵심 특징 | 사용 사례 |
|---|---|---|
| Deployment | ReplicaSet 관리, Rolling Update | 무상태 웹 서비스 |
| StatefulSet | 순차적 생성, 고정 네트워크 ID, PVC 유지 | DB, 메시지 큐 |
| DaemonSet | 노드당 1개 Pod 보장 | 로그/모니터링 에이전트 |
| Job / CronJob | 일회성 완료 작업 / 정기 스케줄링 | 마이그레이션, 리포트 |
| 네트워킹 구성요소 | 역할 |
|---|---|
| ClusterIP / NodePort / LoadBalancer / ExternalName | 내부 가상 IP / 노드 포트 노출 / 클라우드 LB / 외부 CNAME |
| kube-proxy | iptables/IPVS/nftables로 패킷 라우팅 |
| AWS LB Controller | Ingress → ALB, Service → NLB |
| Headless Service | 각 Pod에 직접 DNS 접근 |
| 설정 구성요소 | 용도 | 보안 수준 |
|---|---|---|
| ConfigMap | 일반 설정 | 평문 |
| Secrets | 민감 정보 (etcd) | Base64 + (암호화 가능) |
| Secrets at Rest | etcd 암호화 | KMS 암호화 |
| CSI Driver / External Secrets | 외부 저장소 연동 | 저장소 외부화 |
참고 자료: