Notice
Recent Posts
Recent Comments
Link
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
Archives
Today
Total
관리 메뉴

근묵자흑

Kubernetes Pattern: Immutable Configuration & Kubernetes Churn 본문

k8s/kubernetes-pattern

Kubernetes Pattern: Immutable Configuration & Kubernetes Churn

Luuuuu 2025. 12. 27. 19:24

Kubernetes 환경에서 애플리케이션 설정을 관리할 때 ConfigMap을 사용하는 것이 일반적입니다. 하지만 설정 파일의 크기가 커지거나 복잡한 구조를 가지게 되면 ConfigMap만으로는 한계가 있죠. 또한 ConfigMap을 자주 변경하면 클러스터 전체에 영향을 주는 "Churn" 현상이 발생할 수 있습니다.

 

이 글에서는 Immutable Configuration 패턴을 소개하고, 이 패턴이 클러스터 성능에 미치는 영향을 Churn 관점에서 살펴보겠습니다.

사전 지식

ConfigMap과 Secret의 기본 이해

ConfigMap과 Secret은 Kubernetes에서 설정 데이터를 저장하는 기본 리소스입니다. 설정 정의와 사용을 분리하여 애플리케이션 수명주기와 독립적으로 관리할 수 있다는 장점이 있습니다. Kubernetes 1.21부터는 immutable: true 필드를 설정하여 리소스를 불변으로 선언할 수 있게 되었습니다.

apiVersion: v1
kind: Secret
metadata:
  name: random-config
data:
  user: cm9sYW5k
immutable: true

불변 ConfigMap과 Secret은 생성 후 수정이 불가능합니다. API 서버가 변경 사항을 모니터링할 필요가 없어 클러스터 성능이 향상되는데, 이 부분은 뒤에서 Churn과 연결하여 자세히 설명하겠습니다.

Init Container 패턴

Init Container는 Pod의 메인 컨테이너가 시작되기 전에 실행되는 초기화 전용 컨테이너입니다. 이 패턴에서는 설정 데이터를 공유 볼륨으로 복사하는 역할을 담당하게 됩니다. Init Container는 순차적으로 실행되며, 모든 Init Container가 성공적으로 완료되어야 메인 컨테이너가 시작됩니다.

emptyDir 볼륨

emptyDir은 Pod가 노드에 할당될 때 생성되는 빈 디렉토리입니다. Pod 내 모든 컨테이너가 동일한 emptyDir 볼륨을 마운트하여 데이터를 공유할 수 있습니다. Pod가 삭제되면 emptyDir의 데이터도 함께 삭제됩니다.

설정 관리의 현실적인 문제들

Kubernetes에서 애플리케이션을 운영하다 보면 다음과 같은 상황을 마주하게 됩니다.

  1. 첫 번째는 ConfigMap의 크기 제한입니다. ConfigMap은 1MB까지만 저장할 수 있어서, 머신러닝 모델의 설정 데이터나 사전 계산된 룩업 테이블처럼 용량이 큰 설정 파일은 ConfigMap에 담을 수 없습니다.
  2. 두 번째는 복잡한 설정 파일의 가독성 문제입니다. 애플리케이션 서버의 XML 설정이나 복잡한 YAML 구조를 ConfigMap에 넣으면 들여쓰기 관리가 어렵고 오류가 발생하기 쉽습니다. YAML 안에 YAML을 중첩하는 구조는 유지보수를 어렵게 만들죠.
  3. 세 번째는 다수의 설정 파일 관리입니다. 애플리케이션에 수십 개의 설정 파일이 필요한 경우, 이를 하나의 ConfigMap에서 관리하는 것은 현실적으로 어렵습니다.
  4. 네 번째는 설정 변경이 클러스터에 미치는 영향입니다. ConfigMap을 변경하면 API 서버와 etcd에 부하가 발생하고, 해당 ConfigMap을 사용하는 모든 Pod에 이벤트가 전파됩니다. 이것이 바로 Churn 문제입니다.

Kubernetes Churn 이해하기

Churn이란 무엇인가

"Churn"이라는 단어는 원래 "휘젓다", "뒤섞다"라는 뜻을 가지고 있습니다.

 

버터를 만들 때 우유를 계속 휘젓는 것처럼, Kubernetes 클러스터에서도 리소스가 계속해서 생성되고, 수정되고, 삭제되는 현상을 churn이라고 부릅니다.

 

카페를 운영한다고 가정해볼까요. 손님이 주문을 하면(Create), 주문 내용을 수정하고(Update), 음료가 완성되면 주문표를 버립니다(Delete). 손님이 적당히 오면 문제없지만, 갑자기 100명이 동시에 몰려와서 주문을 넣고, 수정하고, 취소하면 어떻게 될까요. 바리스타(API Server)는 과부하에 걸리고, 주문 시스템(etcd)은 느려지며, 전체 카페 운영이 마비됩니다.

Kubernetes에서의 churn도 마찬가지입니다. 리소스의 빈번한 생성, 수정, 삭제가 클러스터 전체의 성능과 안정성에 영향을 미치게 됩니다.

Churn의 네 가지 유형

Kubernetes에서 churn은 크게 네 가지 유형으로 분류됩니다.

 

Pod Churn은 Pod의 빈번한 생성과 삭제를 의미합니다. Deployment 롤링 업데이트, HPA 스케일링, Job 완료, 노드 eviction 등이 주요 원인이 됩니다.

 

Object Churn은 Deployment, Service, ConfigMap 등 Kubernetes 객체 전반의 변경률입니다. 각 객체 변경 시 resourceVersion이 증가하고 watch 이벤트가 발생하여 API 서버 처리량과 etcd 쓰기 부하에 직접적 영향을 줍니다.

 

Event Churn은 Kubernetes Event 객체의 빈번한 생성을 말합니다. Event는 기본 1시간 후 만료되지만, 동일한 Reason/Message를 루프마다 기록하면 이벤트가 스팸처럼 쌓이고 API 부하도 증가하게 됩니다.

 

Watch Churn은 Watch 연결의 빈번한 생성과 종료를 의미합니다. Watch 연결이 끊기면 re-list가 발생하고, 410 Gone 에러 시 클라이언트는 전체 재동기화가 필요해 API 서버 부하가 급증합니다.

이 글에서는 Immutable Configuration 패턴과 직접 관련된 Object ChurnWatch Churn에 집중해서 설명하겠습니다.

SIG-Scalability의 20 mutations/second 기준

Kubernetes SIG-Scalability에서 정의한 SLOs(Service Level Objectives)에 따르면, 클러스터의 안정적인 운영 기준은 초당 20개 이하의 mutation(변경 작업)입니다. 이를 초과하면 다음과 같은 문제가 연쇄적으로 발생하게 됩니다.

flowchart TB
    A[높은 Churn 발생] --> B[API Server 요청 처리 지연]
    B --> C[etcd 쓰기/읽기 부하 증가]
    C --> D[Watch 이벤트 전파 지연]
    D --> E[Controller reconciliation 지연]
    E --> F[사용자 체감 성능 저하]
    F --> G[Pod 생성 지연, 타임아웃 발생]

 

mutation rate는 세 가지 핵심 컴포넌트에 직접적인 영향을 미칩니다.

 

API Server는 각 mutation을 admission controller를 통해 처리하고, 객체를 직렬화하며, watch 이벤트를 배포해야 합니다. 기본 동시성 제한은 읽기 요청 400개, 변경 요청 200개(총 600개)로 설정되어 있습니다.

etcd는 모든 영구 저장소를 처리합니다. 각 mutation은 Write-Ahead Log(WAL) fsync가 완료되어야 확인되는데, 디스크가 느리면 하트비트 누락과 리더 선출 문제가 연쇄적으로 발생할 수 있습니다.

Watch consumer(kubelet, controller)는 모든 mutation에 대한 이벤트를 수신합니다. churn이 높으면 watch 캐시가 넘쳐서 비용이 큰 re-list 작업이 발생하게 됩니다.

항목 기준값 출처
정상 상태 churn rate 20 mutations/second 이하 SIG-Scalability SLOs
Mutating API 지연시간 (p99) 1초 이하 SIG-Scalability SLOs
클러스터당 최대 노드 5,000개 SIG-Scalability
클러스터당 최대 Pod 150,000개 SIG-Scalability
etcd 저장소 제한 최대 8GB etcd 권장사항

Object Churn의 동작 원리

모든 Kubernetes 객체는 metadata에 resourceVersion 필드를 가지고 있습니다. 이 필드는 etcd의 mod_revision에 직접 대응하죠. 객체를 생성, 수정, 삭제할 때마다 새로운 resourceVersion이 할당되고, 이는 모든 구독자에게 watch 이벤트를 발생시킵니다.

sequenceDiagram
    participant Client as 클라이언트
    participant API as API Server
    participant etcd as etcd
    participant Watch as Watch Cache
    participant Kubelet as Kubelet

    Client->>API: ConfigMap 수정 요청
    API->>etcd: 변경 사항 저장
    etcd-->>API: 새 mod_revision 반환
    API->>Watch: Watch Cache 업데이트
    Watch->>Kubelet: MODIFIED 이벤트 전송
    Note over Kubelet: ConfigMap을 사용하는<br/>모든 Pod에 전파

ConfigMap과 Secret의 경우 이 영향이 더 커집니다. kubelet은 마운트된 각 설정에 대해 개별 watch를 유지하기 때문입니다. 예를 들어 1,000개의 Pod가 동일한 ConfigMap을 사용하고 이 Pod들이 100개 노드에 분산되어 있다면, 한 번의 ConfigMap 업데이트가 100개의 watch 이벤트와 후속 kubelet 처리를 발생시키게 됩니다.

Watch Churn과 410 Gone 에러

Watch churn은 watch 연결의 비효율적인 순환으로 인해 비용이 큰 re-list 작업이 필요한 상황을 말합니다. Kubernetes watch 메커니즘은 list-watch 패턴을 사용합니다. 클라이언트가 먼저 LIST를 수행하여 현재 상태와 resourceVersion을 얻고, 그 다음 WATCH 연결을 열어 ADDED, MODIFIED, DELETED 이벤트를 스트리밍으로 받는 방식이죠.

410 Gone 에러는 watch churn의 주요 원인입니다. watch의 resourceVersion이 "너무 오래된" 경우, 즉 요청한 히스토리가 etcd나 watch 캐시에서 압축되어 사라진 경우 서버는 HTTP 410 Gone을 반환합니다.

{
  "type": "ERROR",
  "object": {
    "kind": "Status",
    "message": "too old resource version: 1 (4034675)",
    "reason": "Gone",
    "code": 410
  }
}

410 Gone을 받으면 클라이언트는 전체 re-list를 수행한 후에야 새 watch를 설정할 수 있습니다. 대규모 클러스터에서 re-list 작업은 수백만 개의 객체를 가져올 수 있어 API 서버 CPU, 메모리, 네트워크 대역폭을 상당히 소비하게 됩니다.

 

410 Gone이 발생하는 일반적인 상황은 다음과 같습니다. 네트워크 파티션으로 인한 watch 연결 끊김, API 서버 재시작으로 watch 캐시가 재초기화되는 경우, 느린 consumer가 이벤트 스트림을 따라가지 못하는 상황, etcd 압축으로 이전 revision이 제거되는 경우 등이 있습니다.

Immutable Configuration이 Churn을 줄이는 방법

불변 ConfigMap/Secret의 Watch 최적화

Kubernetes 1.21에서 GA가 된 immutable ConfigMap/Secret은 KEP-1412에 문서화되어 있습니다. immutable: true가 설정되면 kubelet은 해당 리소스에 대한 watch 설정을 완전히 건너뜁니다. watch도 없고, 폴링도 없고, 업데이트 처리도 없는 것이죠.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config-v1
data:
  database-host: "postgres.default.svc"
  cache-size: "256"
immutable: true

성능 영향은 상당합니다. 100개 노드에 10,000개의 ConfigMap이 있는 클러스터에서 mutable 설정은 10,000개의 활성 watch가 필요합니다. 반면 immutable 설정은 0개의 watch가 필요합니다.

 

SIG-Scalability 테스트에서는 Pod의 100%가 immutable ConfigMap과 Secret을 마운트해도 기존 SLO를 위반하지 않음을 확인했습니다. 이전 테스트에서는 10%만 마운트했었는데, 이 기능이 규모 개선을 가능하게 한다는 것을 증명한 셈입니다.

 

enhancements/keps/sig-storage/1412-immutable-secrets-and-configmaps/README.md at master · kubernetes/enhancements

Enhancements tracking repo for Kubernetes. Contribute to kubernetes/enhancements development by creating an account on GitHub.

github.com

 

flowchart LR
    subgraph "Mutable ConfigMap"
        A1[ConfigMap 변경] --> B1[Watch 이벤트 발생]
        B1 --> C1[모든 노드에 전파]
        C1 --> D1[kubelet 처리]
        D1 --> E1[Pod 볼륨 업데이트]
    end

    subgraph "Immutable ConfigMap"
        A2[ConfigMap 생성] --> B2[Watch 설정 안함]
        B2 --> C2[변경 불가]
        C2 --> D2[이벤트 전파 없음]
    end

불변성 제약과 버전 기반 명명 패턴

불변성에는 몇 가지 제약이 있습니다. immutable 필드는 한번 설정하면 되돌릴 수 없습니다. data와 binaryData 필드는 영구적으로 잠기게 됩니다. 업데이트하려면 객체를 삭제하고 다른 이름으로 새로 생성해야 하며, 기존 Pod는 재생성될 때까지 이전 객체에 대한 마운트를 유지합니다.

권장 패턴은 버전 기반 이름을 사용하는 것입니다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config-v2
data:
  database-host: "postgres-v2.default.svc"
immutable: true
---
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      volumes:
      - name: config
        configMap:
          name: app-config-v2

Kustomize 같은 도구는 자동으로 콘텐츠 해시 접미사(예: my-config-k5m92h)를 생성하여 버전 관리 패턴을 자동화하고 설정 내용이 변경될 때 deployment가 롤링되도록 보장해줍니다.

Watch Bookmark를 통한 추가 최적화

Watch bookmark(KEP-956)는 Kubernetes 1.17에서 GA가 되었으며, 실제 데이터를 전송하지 않고 클라이언트의 resourceVersion을 업데이트하는 주기적인 체크포인트 이벤트를 보내 410 Gone 문제를 완화합니다.

{
  "type": "BOOKMARK",
  "object": {
    "metadata": {
      "resourceVersion": "12746"
    }
  }
}

bookmark가 없으면 오랜 시간 유휴 상태인 watch는 원래 resourceVersion을 유지하며 결국 만료됩니다. bookmark가 있으면 클라이언트의 resourceVersion이 최신 상태를 유지하여 re-list 없이 watch를 재개할 수 있습니다. KEP-956 테스트에서는 처리되는 init 이벤트가 40배 감소함을 확인했습니다.

Immutable Configuration 패턴 구현

Kubernetes에서의 구현

Kubernetes는 컨테이너 간 직접적인 볼륨 공유를 지원하지 않습니다. 대신 Init Container를 사용하여 이 패턴을 구현하게 됩니다.

sequenceDiagram
    participant Node as Kubernetes Node
    participant InitC as Init Container
    participant Volume as emptyDir Volume
    participant AppC as Application Container

    Node->>Volume: emptyDir 볼륨 생성
    Node->>InitC: Init Container 시작
    InitC->>Volume: 설정 파일 복사 (cp)
    InitC-->>Node: 완료 및 종료
    Node->>AppC: Application Container 시작
    AppC->>Volume: 설정 파일 읽기

Init Container는 Pod의 메인 컨테이너가 시작되기 전에 실행되는 초기화 컨테이너입니다. 설정 이미지를 Init Container로 실행하고, 이미지에 포함된 설정 파일을 emptyDir 볼륨으로 복사합니다. 복사가 완료되면 Init Container는 종료되고, 메인 애플리케이션 컨테이너가 시작되어 볼륨에서 설정을 읽게 됩니다.

설정 이미지를 만들기 위한 Dockerfile은 다음과 같습니다.

FROM busybox:1.36
ARG ENV=dev
COPY config-src/${ENV}/ /config-src/
ENTRYPOINT ["sh", "-c", "cp -r /config-src/* $1", "--"]

이 Dockerfile은 busybox를 베이스로 사용합니다. 환경별 설정 파일을 이미지에 포함하고, 실행 시 전달받은 경로로 파일을 복사하는 스크립트를 엔트리포인트로 설정하는 방식이죠.

Deployment 매니페스트에서는 Init Container와 Application Container가 동일한 emptyDir 볼륨을 공유하도록 설정합니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      initContainers:
      - name: config-init
        image: k8spatterns/immutable-config-dev:1.0
        imagePullPolicy: Never
        args: ["/config"]
        volumeMounts:
        - name: config-volume
          mountPath: /config

      containers:
      - name: app
        image: myapp:1.0
        volumeMounts:
        - name: config-volume
          mountPath: /app/config
          readOnly: true

      volumes:
      - name: config-volume
        emptyDir: {}

Init Container는 /config 경로로 설정 파일을 복사하고, 애플리케이션 컨테이너는 /app/config에서 설정을 읽습니다. 애플리케이션 컨테이너는 읽기 전용으로 마운트하여 실수로 설정이 변경되는 것을 방지합니다.

Churn 모니터링 방법

Prometheus 메트릭

효과적인 churn 모니터링을 위해서는 API 서버 요청 패턴, etcd 지연시간, watch 연결 상태, controller 큐 깊이를 추적해야 합니다.

핵심 API 서버 메트릭은 다음과 같습니다.

메트릭 용도
apiserver_request_total verb, resource, code별 총 요청 수
apiserver_request_duration_seconds 작업별 지연시간 히스토그램
apiserver_longrunning_requests 활성 WATCH/CONNECT 요청
apiserver_current_inflight_requests 동시 요청 포화도
apiserver_flowcontrol_rejected_requests_total APF 쓰로틀링 지표

 

Watch 관련 메트릭도 살펴보겠습니다.

메트릭 용도
apiserver_registered_watchers 현재 활성 watch 수
apiserver_watch_events_total 배포된 이벤트 수
apiserver_watch_events_sizes 이벤트 페이로드 분포

 

etcd 메트릭도 중요합니다.

메트릭 용도
etcd_request_duration_seconds API 서버에서의 요청 지연시간
etcd_disk_wal_fsync_duration_seconds 디스크 쓰기 성능
etcd_mvcc_db_total_size_in_bytes 한계에 근접한 데이터베이스 크기

장단점 분석

장점

  • 버전 관리 측면에서 설정을 Git과 컨테이너 레지스트리로 관리할 수 있습니다. 설정 변경마다 새로운 이미지 태그를 생성하면 언제든지 이전 버전으로 돌아갈 수 있죠.
  • 크기 제한이 없어 대용량 설정 파일도 문제없이 관리할 수 있습니다. 테스트에서는 작은 파일만 사용했지만, 수백 MB의 파일도 이미지에 포함할 수 있습니다.
  • 환경별 설정 분리가 명확합니다. 개발, 스테이징, 운영 환경의 설정을 각각 다른 이미지로 관리하면 환경 간 설정이 섞일 위험이 없습니다.
  • Kubernetes Deployment의 롤백 기능을 그대로 활용할 수 있습니다. 설정 변경으로 문제가 발생하면 즉시 이전 버전으로 되돌릴 수 있죠.
  • Churn 관점에서도 이 패턴은 가치가 있습니다. 설정 이미지는 한번 빌드되면 변경되지 않습니다. ConfigMap처럼 클러스터 내에서 watch 이벤트를 발생시키지 않으므로 Object Churn과 Watch Churn을 줄이는 데 기여합니다.

단점

  • 이미지 빌드와 관리가 추가됩니다. CI/CD 파이프라인에 설정 이미지 빌드 단계를 포함해야 하고, 레지스트리 공간도 더 필요합니다.
  • Init Container를 구성해야 하므로 매니페스트가 복잡해집니다. 단순한 키-값 설정에는 과한 구조일 수 있습니다.
  • 민감한 정보는 여전히 별도로 관리해야 합니다. 비밀번호나 API 키 같은 정보는 Secret이나 외부 시크릿 관리 도구를 사용하는 것이 적절합니다. 각 Pod마다 설정 파일이 복사되므로 emptyDir 볼륨 공간을 사용합니다. 설정 파일이 매우 크고 Pod가 많다면 노드의 디스크 사용량을 모니터링해야 합니다.

적합한 사용 사례

  • 대용량 설정 파일이 필요한 경우 이 패턴이 유용합니다. 머신러닝 모델 설정, 대용량 룩업 테이블, 다국어 번역 파일 등이 해당됩니다.
  • 복잡한 디렉토리 구조가 필요할 때도 적합합니다. 레거시 애플리케이션을 컨테이너화할 때 기존의 복잡한 설정 구조를 그대로 유지해야 하는 경우가 있습니다.
  • 엄격한 변경 관리가 필요한 환경에서 유용합니다. 금융이나 의료 분야처럼 설정 변경 이력을 명확히 추적해야 하는 경우 컨테이너 이미지를 통한 관리가 감사 요구사항을 충족하는 데 도움이 됩니다.
  • 클러스터 규모가 크고 Churn이 우려되는 환경에도 적합합니다. 수천 개의 Pod가 동일한 ConfigMap을 참조하는 상황에서 설정 이미지를 사용하면 watch 이벤트를 완전히 제거할 수 있습니다.

피해야 하는 경우

  • 단순한 키-값 설정은 Immutable ConfigMap으로 충분합니다. 데이터베이스 URL이나 타임아웃 값 같은 간단한 설정에 이 패턴을 사용하면 불필요하게 복잡해집니다. 다만 ConfigMap에 immutable: true를 설정하여 Watch Churn에 대한 고려도 필요합니다.
  • 민감한 정보만 관리하는 경우는 Secret을 사용해야 합니다. 또는 Sealed Secrets, External Secrets Operator 같은 전문 도구를 고려하는 것이 좋습니다.
  • 설정이 자주 변경되는 애플리케이션에는 적합하지 않습니다. 설정이 시간 단위로 바뀌는 경우 매번 이미지를 빌드하는 것은 비효율적입니다. 이런 경우 Spring Cloud Config 같은 외부 설정 서버를 사용하는 것이 낫습니다.

참고 자료