환경마다 YAML을 복사하면 생기는 문제

쿠버네티스를 운영하다 보면 dev, staging, prod가 거의 같은 Deployment를 쓴다. 차이라곤 replicas 수, 이미지 태그, 리소스 limit, 환경변수 정도다. 이걸 디렉터리별로 통째로 복사하면 공통 부분을 고칠 때마다 세 곳을 똑같이 수정해야 한다. 한 곳을 빠뜨리면 prod만 구버전 probe 설정이 남는 식의 드리프트가 생긴다. Helm으로 넘어가는 팀도 많지만, 단순한 값 치환에 템플릿 문법과 values 계층까지 도입하는 건 과할 때가 있다. kustomize는 순수 YAML을 유지하면서 base + overlay로 이 중복을 없앤다.

base와 overlay의 기본 구조

kustomize는 kubectl에 내장돼 있어(kubectl kustomize) 별도 설치 없이 쓸 수 있다. 공통 리소스를 base에 두고, 환경별 차이만 overlay에 patch로 얹는다.

myapp/
├── base/
│   ├── deployment.yaml
│   ├── service.yaml
│   └── kustomization.yaml
└── overlays/
    ├── dev/
    │   └── kustomization.yaml
    └── prod/
        ├── kustomization.yaml
        └── deployment-patch.yaml

base의 kustomization.yaml은 어떤 리소스를 묶을지 선언한다.

# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml
commonLabels:
  app: myapp

overlay에서 환경별 차이만 얹기

prod overlay는 base를 참조하고 필요한 값만 덮어쓴다. 이미지 태그는 images로, replicas 같은 필드는 patch로 처리한다.

# overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
namePrefix: prod-
images:
  - name: myapp
    newTag: v1.8.3
patches:
  - path: deployment-patch.yaml
replicas:
  - name: myapp
    count: 6
# overlays/prod/deployment-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      containers:
        - name: myapp
          resources:
            limits:
              cpu: "2"
              memory: 2Gi

렌더 결과는 kubectl kustomize overlays/prod로 확인하고, 적용은 kubectl apply -k overlays/prod로 한다. dev overlay는 replicas 1, 이미지 태그만 다르게 두면 된다.

strategic merge patch vs JSON 6902 patch

덮어쓰기 방식은 두 가지다. 위 예시의 strategic merge patch는 리소스 조각을 그대로 겹쳐 병합한다. 리스트 항목 하나를 정밀하게 지우거나 특정 인덱스를 바꿔야 할 때는 JSON 6902 patch가 명확하다.

구분strategic mergeJSON 6902
표현부분 YAML 조각op/path 연산
가독성직관적경로 지정 필요
적합한 경우필드 추가·교체리스트 항목 삭제·치환
patches:
  - target:
      kind: Deployment
      name: myapp
    patch: |-
      - op: remove
        path: /spec/template/spec/containers/0/livenessProbe

ConfigMap과 Secret은 generator로

환경변수나 설정 파일은 configMapGenerator로 관리하면 좋다. kustomize가 내용 해시를 이름 접미사로 붙여, 값이 바뀌면 ConfigMap 이름이 바뀌고 이를 참조하는 Pod가 자동으로 롤링된다. 수동으로 재배포를 유발하던 문제를 없앤다.

# overlays/prod/kustomization.yaml (일부)
configMapGenerator:
  - name: myapp-config
    literals:
      - LOG_LEVEL=info
      - FEATURE_X=true
    files:
      - app.properties
generatorOptions:
  disableNameSuffixHash: false

Secret은 리터럴을 커밋하지 말고 secretGenerator의 envs로 외부 파일을 참조하되, 실제 값은 SOPS나 외부 시크릿 매니저에 맡긴다.

실무에서 주의할 점

  • base는 배포 가능한 완결 상태로 두지 말고, 환경별로 반드시 채워야 하는 값(리소스 limit 등)은 overlay에서 강제하는 관례를 만든다. base만으로 prod에 실수 적용되는 사고를 줄인다.
  • namePrefix/nameSuffix는 편리하지만 Service 이름이 바뀌면 이를 참조하는 외부 설정도 함께 깨진다. kustomize는 리소스 간 참조는 갱신해주지만 클러스터 밖 참조는 모른다.
  • patch 대상은 이름으로 매칭되므로, base에서 리소스 이름을 바꾸면 overlay patch가 조용히 무시된다. CI에서 kubectl kustomize 출력에 예상 필드가 있는지 검증하는 단계를 넣어라.
  • 오버레이 중첩은 2단계까지가 실용적이다. region → env → tenant처럼 깊어지면 렌더 결과 추적이 어려워진다.

정리

kustomize는 템플릿 없이 순수 YAML을 유지하면서 환경별 중복을 제거한다. 공통은 base에, 차이는 overlay patch와 generator에 두면 드리프트가 사라지고 diff가 리뷰하기 쉬워진다. 복잡한 조건 분기나 패키지 배포가 필요하면 Helm이 낫지만, "거의 같은데 몇 군데만 다른" 매니페스트라면 kustomize가 가장 단순하고 안전한 선택이다.