_template/ 를 복사하고 placeholder 를 치환하는 워크플로우Kustomize 는 K8s manifest 를 환경별로 관리하기 위해 만들어진 도구
Helm 과의 비교
values.yaml 로 변수를 주입해 manifest 를 렌더링base/overlay 로 해결하는 문제
k8s/
├── base/ # 공통 골격 (1벌)
└── overlays/
├── dev/
│ └── my-app/ # DEV 전용 설정
└── live/
└── my-app/ # LIVE 전용 설정
_template/ 디렉토리는 Kustomize 자체 기능이 아니라 자체 운영 워크플로우k8s/base/
├── deployment.yaml
├── service.yaml
└── kustomization.yaml
resources:
- deployment.yaml
- service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
apiVersion: v1
kind: Service
metadata:
name: svc
labels:
app: svc
spec:
selector:
app: app
_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 # 환경변수 템플릿
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 로 붙임app → my-service-app, svc → my-service-svcimages 의 newTag 는 GitLab CI 에서 kustomize edit set image 로 자동 교체spec:
template:
spec:
tolerations:
- key: "nodegroup"
operator: "Equal"
value: "__NODE_POOL__"
effect: "NoSchedule"
nodeSelector:
nodegroup: __NODE_POOL__
maxUnavailable: 0 으로 무중단 배포spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
# 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:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
requests : 스케줄링 기준. Karpenter 가 노드를 프로비저닝할 때 이 값을 참고limits : 쓰로틀링/OOMKill 기준# 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 /health | liveness/startup | 200 OK |
GET /ready | readiness (의존성 포함) | 200 OK or 503 |
@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)
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 는 비용이 비싸므로 비권장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-systemapiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: pdb
spec:
maxUnavailable: 1
selector:
matchLabels:
app: __APP_NAME__
maxUnavailable: 1 + HPA minReplicas: 1 → 드레인 시 순간 다운타임 감수minReplicas: 2 이상으로 설정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__
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 를 공유 (비용 절감)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"
cp -r k8s/overlays/_template k8s/overlays/${ENV}/${APP_NAME}
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
# .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
k8s/overlays/<env>/<app_name>