sayu.day

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가 있는 노드에도 배포하려면 nodeSelectortolerations를 사용합니다.

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 포트

동작 순서는 다음과 같습니다.

  1. Service 생성 시 ClusterIP 할당 (예: 10.96.0.100)
  2. Endpoints 객체 자동 생성 (selector와 일치하는 Pod IP 목록)
  3. kube-proxy가 iptables/IPVS 규칙 생성
  4. 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

흔한 원인은 다음과 같습니다.

  1. 이미지 Pull 실패: ImagePullBackOff
  2. 리소스 부족: Pending 상태
  3. Liveness/Readiness Probe 실패: CrashLoopBackOff
  4. maxUnavailable 0: 기존 Pod 삭제 안 됨

워크로드: StatefulSet Pod가 Pending일 때

# PVC 상태 확인
kubectl get pvc

# 스토리지 이벤트 확인
kubectl describe pvc data-mysql-0

흔한 원인은 다음과 같습니다.

  1. StorageClass 없음: 기본 StorageClass 미설정
  2. 가용 영역(AZ) 불일치: EBS는 같은 AZ에서만 마운트
  3. 용량 부족: AWS EBS 한도 초과

워크로드: CronJob이 실행되지 않을 때

# CronJob 상태 확인
kubectl get cronjob

# 최근 Job 확인
kubectl get jobs --sort-by=.metadata.creationTimestamp

# 마지막 스케줄 시간 확인
kubectl describe cronjob daily-report | grep "Last Schedule"

흔한 원인은 다음과 같습니다.

  1. startingDeadlineSeconds 초과: 스케줄 시간 놓침
  2. concurrencyPolicy: Forbid + 긴 실행 시간: 이전 Job이 계속 실행 중
  3. 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)

흔한 원인은 다음과 같습니다.

  1. Cloud Controller Manager 미설치 (On-prem)
  2. IAM 권한 부족
  3. 서브넷 태그 누락 (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'

흔한 원인은 다음과 같습니다.

  1. OIDC Provider 미설정
  2. ServiceAccount와 IAM Role 연결 누락
  3. 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 외부 저장소 연동 저장소 외부화

참고 자료: