[k8s] Kustomize manifest template 설계 — base/overlay

Woong·2026년 4월 10일

Docker, k8s

목록 보기
33/38

개요

  • Kustomize 의 base/overlay 패턴으로 K8s manifest 를 관리하는 구조를 정리
    • base 에 공통 리소스 골격을 정의하고, overlay 에서 환경별/앱별 차이를 주입
    • 새 서비스 온보딩 시 _template/ 를 복사하고 placeholder 를 치환하는 워크플로우

base/overlay 구조

  • Kustomize 는 K8s manifest 를 환경별로 관리하기 위해 만들어진 도구

    • base/overlay 는 Kustomize 공식 문서에서 권장하는 표준 패턴
    • base 에 환경에 무관한 공통 리소스를 정의하고, overlay 에서 환경별 차이만 patch 로 덮어쓰는 구조
  • Helm 과의 비교

    • Helm: 템플릿 엔진. values.yaml 로 변수를 주입해 manifest 를 렌더링
    • Kustomize: 원본 manifest 를 그대로 두고, 필요한 부분만 patch 로 덮어쓰기
    • 자체 앱 배포에는 Kustomize 가 더 직관적 — manifest 가 그대로 읽히므로 리뷰가 쉬움
    • 서드파티 인프라(Redis, PostgreSQL 등)는 Helm chart 가 제공되므로 Helm 사용
    • → Kustomize(자체 앱) + Helm(서드파티 인프라) 으로 역할 분리
  • base/overlay 로 해결하는 문제

    • 동일한 앱을 DEV / LIVE 에 배포하되, 환경마다 다른 설정을 적용해야 함
    • 이미지 태그, 리소스 요청/제한, 노드 스케줄링, 환경변수 등이 환경마다 다름
    • base 1벌 + overlay(dev/live) 조합으로 manifest 중복 없이 환경 분리
k8s/
├── base/              # 공통 골격 (1벌)
└── overlays/
    ├── dev/
    │   └── my-app/    # DEV 전용 설정
    └── live/
        └── my-app/    # LIVE 전용 설정
  • _template/ 디렉토리는 Kustomize 자체 기능이 아니라 자체 운영 워크플로우
    • 새 서비스 온보딩 시 placeholder 치환만으로 빠르게 overlay 를 생성하기 위한 구조

base 구조

  • 모든 서비스가 공유하는 최소한의 리소스 골격
    • overlay 에서 patch 로 덮어쓰는 구조이므로, base 는 가능한 비워두는 것이 포인트
k8s/base/
├── deployment.yaml
├── service.yaml
└── kustomization.yaml
kustomization.yaml
resources:
  - deployment.yaml
  - service.yaml
deployment.yaml
  • overlay 에서 spec 전체를 patch 로 주입하므로, base 는 최소한의 뼈대만 정의
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
  selector:
    matchLabels:
      app: app
  template:
    metadata:
      labels:
        app: app
    spec:
      containers:
        - name: app
service.yaml
  • ports 를 base 에서 지정하면 overlay 와 merge 되어 중복이 생기므로 비워둠
apiVersion: v1
kind: Service
metadata:
  name: svc
  labels:
    app: svc
spec:
  selector:
    app: app

overlay 템플릿 구조

  • _template/ 디렉토리에 새 서비스 배포를 위한 표준 템플릿을 관리
    • __PLACEHOLDER__ 형태의 치환 변수를 사용
_template/
├── kustomization.yaml      # Kustomize 설정 (base 참조, patch, image)
├── patch-deployment.yaml   # Deployment patch (보안, 리소스, 프로브)
├── patch-service.yaml      # Service patch (ClusterIP, port)
├── hpa.yaml                # HorizontalPodAutoscaler
├── pdb.yaml                # PodDisruptionBudget
├── network-policy.yaml     # NetworkPolicy
├── ingress.yaml            # Ingress (새 namespace 일 때만)
└── .env.example            # 환경변수 템플릿

kustomization.yaml (overlay)

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ../../../base
  - network-policy.yaml
  - pdb.yaml
  - hpa.yaml
  # 새 namespace 에 배포하는 경우 주석 해제
  # - ingress.yaml

# 모든 리소스의 metadata.name 에 prefix 적용
namePrefix: __APP_NAME__-

# 배포 대상 namespace
namespace: __NAMESPACE__

# 환경변수를 ConfigMap 으로 주입
configMapGenerator:
  - name: config
    envs:
      - ./.env

patches:
  - path: patch-deployment.yaml
  - path: patch-service.yaml

# CI/CD 에서 newTag 를 자동 업데이트
images:
  - name: __ECR_REPO__/__ECR_IMAGE_NAME__
    newTag: __IMAGE_TAG__
  • namePrefix 로 모든 리소스에 앱 이름을 prefix 로 붙임
    • ex) appmy-service-app, svcmy-service-svc
  • imagesnewTag 는 GitLab CI 에서 kustomize edit set image 로 자동 교체

patch-deployment.yaml

  • Deployment 의 핵심 설정을 모두 포함하는 patch 파일
Node Scheduling
  • Karpenter NodePool 의 label/taint 에 맞춰 배포 대상 노드를 지정
spec:
  template:
    spec:
      tolerations:
        - key: "nodegroup"
          operator: "Equal"
          value: "__NODE_POOL__"
          effect: "NoSchedule"
      nodeSelector:
        nodegroup: __NODE_POOL__
Rolling Update
  • maxUnavailable: 0 으로 무중단 배포
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
Security Context
  • Pod 레벨 + Container 레벨 보안 설정
# Pod 레벨
securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  runAsGroup: 1000
  fsGroup: 1000

# Service Account 토큰 마운트 방지
automountServiceAccountToken: false
# Container 레벨
securityContext:
  allowPrivilegeEscalation: false   # 권한 상승 금지
  readOnlyRootFilesystem: true      # 루트 FS 읽기 전용
  capabilities:
    drop:
      - ALL                         # 모든 capability 제거
  • readOnlyRootFilesystem: true 사용 시 앱이 파일 쓰기가 필요하면 emptyDir 볼륨 마운트 필요
volumeMounts:
  - name: data
    mountPath: /data

volumes:
  - name: data
    emptyDir: {}
Resources
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi
  • requests : 스케줄링 기준. Karpenter 가 노드를 프로비저닝할 때 이 값을 참고
  • limits : 쓰로틀링/OOMKill 기준
Health Probes
  • 3가지 프로브를 모두 설정
# 1. Startup Probe: 앱 시작 완료 확인 (Cold Start 대응)
startupProbe:
  httpGet:
    path: /health
    port: http
  initialDelaySeconds: 5
  periodSeconds: 5
  timeoutSeconds: 3
  failureThreshold: 30      # 5s × 30 = 최대 150초 대기
  successThreshold: 1

# 2. Readiness Probe: 트래픽 수신 가능 여부
#    실패 시 → Service endpoints 에서 제거
readinessProbe:
  httpGet:
    path: /ready
    port: http
  initialDelaySeconds: 10
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3
  successThreshold: 1

# 3. Liveness Probe: 컨테이너 생존 여부
#    실패 시 → Pod 재시작
#    주의: 외부 의존성(DB 등) 체크 금지
livenessProbe:
  httpGet:
    path: /health
    port: http
  initialDelaySeconds: 15
  periodSeconds: 10
  timeoutSeconds: 3
  failureThreshold: 3
  successThreshold: 1
  • 앱에서 구현 필요한 엔드포인트
엔드포인트용도응답
GET /healthliveness/startup200 OK
GET /readyreadiness (의존성 포함)200 OK or 503
  • ex) FastAPI
@app.get("/health")
async def health():
    return {"status": "ok"}

@app.get("/ready")
async def ready():
    try:
        await db.execute("SELECT 1")
        return {"status": "ready"}
    except:
        raise HTTPException(status_code=503)

patch-service.yaml

apiVersion: v1
kind: Service
metadata:
  name: svc
  labels:
    app: __APP_NAME__
spec:
  type: ClusterIP
  selector:
    app: __APP_NAME__
  ports:
    - name: http
      port: 80
      targetPort: http    # Deployment 의 containerPort name 참조
      protocol: TCP
  • ClusterIP + Ingress 조합 권장
    • LoadBalancer 는 비용이 비싸므로 비권장

hpa.yaml

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: app
  minReplicas: 1
  maxReplicas: 2
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 90
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
      policies:
        - type: Pods
          value: 2
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Pods
          value: 1
          periodSeconds: 60
  • scaleUp : 빠르게 (30초 안정화, 1분에 2개씩)
  • scaleDown : 천천히 (5분 안정화, 1분에 1개씩)
  • 사전 조건: metrics-server 설치 필요
    • kubectl get deployment metrics-server -n kube-system

pdb.yaml

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: pdb
spec:
  maxUnavailable: 1
  selector:
    matchLabels:
      app: __APP_NAME__
  • 노드 드레인/업그레이드 시 동시에 종료 가능한 Pod 수를 제한
    • maxUnavailable: 1 + HPA minReplicas: 1 → 드레인 시 순간 다운타임 감수
    • 고가용성이 필요하면 minReplicas: 2 이상으로 설정

network-policy.yaml

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: network-policy
spec:
  podSelector:
    matchLabels:
      app: __APP_NAME__
  policyTypes:
    - Ingress
    - Egress
  ingress:
    # ALB Ingress Controller 에서 오는 트래픽만 허용
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              app.kubernetes.io/name: aws-load-balancer-controller
      ports:
        - protocol: TCP
          port: __CONTAINER_PORT__
  • 기본: ALB Controller → Pod 트래픽만 허용
  • Egress 는 필요에 따라 주석 해제하여 추가
    • DNS (53), PostgreSQL (5432), Redis (6379), 외부 HTTPS (443) 등

ingress.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: __APP_NAME__-ingress
  namespace: __NAMESPACE__
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/group.name: llmops-alb-group
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
    alb.ingress.kubernetes.io/healthcheck-path: /health
    alb.ingress.kubernetes.io/healthcheck-port: traffic-port
spec:
  ingressClassName: alb
  rules:
    - http:
        paths:
          - path: /__APP_NAME__
            pathType: Prefix
            backend:
              service:
                name: __APP_NAME__-svc
                port:
                  number: 80
  • group.name 이 같으면 하나의 ALB 를 공유 (비용 절감)
  • 기존 namespace 에 서비스를 추가하는 경우: 해당 namespace 의 공통 ingress 에 path 추가
  • 새 namespace 에 배포하는 경우: 이 파일 사용

새 서비스 온보딩 워크플로우

1. 변수 설정
export APP_NAME="my-service"
export NAMESPACE="app"
export ENV="dev"
export CONTAINER_PORT="8080"
export NODE_POOL="np-app-ondemand-s"
export ECR_REPO="<account_id>.dkr.ecr.ap-northeast-1.amazonaws.com/<org>/${APP_NAME}"
export IMAGE_TAG="latest"
2. 템플릿 복사
cp -r k8s/overlays/_template k8s/overlays/${ENV}/${APP_NAME}
3. placeholder 치환
cd k8s/overlays/${ENV}/${APP_NAME}

# macOS
sed -i '' "s/__APP_NAME__/${APP_NAME}/g" *.yaml
sed -i '' "s/__NAMESPACE__/${NAMESPACE}/g" *.yaml
sed -i '' "s/__CONTAINER_PORT__/${CONTAINER_PORT}/g" *.yaml
sed -i '' "s|__ECR_REPO__|${ECR_REPO}|g" *.yaml
sed -i '' "s/__IMAGE_TAG__/${IMAGE_TAG}/g" *.yaml
sed -i '' "s/__NODE_POOL__/${NODE_POOL}/g" *.yaml

# Linux
sed -i "s/__APP_NAME__/${APP_NAME}/g" *.yaml
sed -i "s/__NAMESPACE__/${NAMESPACE}/g" *.yaml
sed -i "s/__CONTAINER_PORT__/${CONTAINER_PORT}/g" *.yaml
sed -i "s|__ECR_REPO__|${ECR_REPO}|g" *.yaml
sed -i "s/__IMAGE_TAG__/${IMAGE_TAG}/g" *.yaml
sed -i "s/__NODE_POOL__/${NODE_POOL}/g" *.yaml
4. 환경변수, 검증
# .env 생성
cp .env.example .env

# kustomize 빌드 테스트
kustomize build k8s/overlays/${ENV}/${APP_NAME}

# dry-run
kubectl apply -k k8s/overlays/${ENV}/${APP_NAME} --dry-run=client
5. ArgoCD Application 등록
  • ArgoCD UI 에서 New App 생성
    • Repository URL: deployment repo 의 Git URL
    • Path: k8s/overlays/<env>/<app_name>
    • Sync Policy: Automatic + Prune Resources + Self Heal

reference

0개의 댓글