5. Kubernetes YAML 매니페스트¶
Kubernetes에서 모든 리소스는 YAML 매니페스트로 정의됩니다. 이번 챕터에서는 YAML 문법 기초부터 시작하여, K8s 매니페스트의 공통 구조, 주요 리소스별 상세 작성법, 그리고 관리 도구까지 폭넓게 다룹니다.
1. YAML 기초 복습¶
Kubernetes 매니페스트를 작성하기 전에, YAML 문법을 확실히 짚고 넘어가겠습니다.
들여쓰기 규칙¶
YAML에서 구조는 들여쓰기(indentation)로 표현합니다.
- 탭(Tab) 사용 금지 — 반드시 스페이스만 사용
- 일반적으로 2칸 스페이스를 사용 (K8s 공식 예제 기준)
- 같은 레벨의 키는 반드시 같은 들여쓰기 깊이를 유지
흔한 실수
YAML 파싱 에러의 80% 이상이 들여쓰기 문제입니다.
에디터에서 탭을 스페이스로 자동 변환하는 설정을 반드시 켜두세요.
VS Code에서는 editor.insertSpaces: true, editor.tabSize: 2로 설정합니다.
맵(Map)¶
맵은 키-값(key-value) 쌍의 모음입니다. JSON의 객체(object)에 해당합니다.
# 블록 스타일 (가독성이 좋아 매니페스트에서 주로 사용)
metadata:
name: my-app
namespace: production
labels:
app: my-app
tier: frontend
# 플로우 스타일 (한 줄 표현, 짧은 맵에 적합)
labels: {app: my-app, tier: frontend}
리스트(List)¶
리스트는 순서가 있는 항목의 모음입니다. JSON의 배열(array)에 해당합니다.
# 블록 스타일
containers:
- name: nginx
image: nginx:1.25
- name: sidecar
image: busybox:latest
# 플로우 스타일
ports: [80, 443, 8080]
맵과 리스트의 조합¶
K8s 매니페스트는 맵과 리스트가 깊게 중첩되는 구조입니다.
spec:
containers: # 리스트
- name: web # 리스트 항목 1 (맵)
image: nginx:1.25
ports: # 리스트 안의 리스트
- containerPort: 80
- containerPort: 443
env: # 리스트
- name: DB_HOST
value: "db.example.com"
- name: logger # 리스트 항목 2 (맵)
image: fluentd:v1.16
들여쓰기 팁
리스트 항목의 - 기호는 부모 키보다 2칸 안쪽에 위치합니다.
- 뒤의 첫 번째 키와 그 아래 키들은 같은 레벨로 정렬합니다.
멀티라인 문자열¶
긴 문자열을 여러 줄로 표현할 때 두 가지 방식을 사용합니다.
리터럴 블록 (|) — 줄바꿈 보존¶
결과: [database]\nhost=db.example.com\nport=5432\nname=mydb\n
접힘 블록 (>) — 줄바꿈을 스페이스로 변환¶
# > 를 사용하면 줄바꿈이 스페이스로 합쳐집니다
metadata:
annotations:
description: >
이 서비스는 사용자 인증을
담당하는 마이크로서비스입니다.
JWT 토큰 발급 및 검증을 수행합니다.
결과: 이 서비스는 사용자 인증을 담당하는 마이크로서비스입니다. JWT 토큰 발급 및 검증을 수행합니다.\n
후행 줄바꿈 제어¶
# | : 마지막에 줄바꿈 1개 (기본)
# |- : 마지막 줄바꿈 제거 (strip)
# |+ : 마지막 줄바꿈 전부 보존 (keep)
script: |-
#!/bin/bash
echo "Hello"
exit 0
ConfigMap과 멀티라인
ConfigMap에 설정 파일을 통째로 넣을 때 |를 많이 사용합니다.
줄바꿈이 그대로 보존되므로 설정 파일 원본과 동일한 형태를 유지할 수 있습니다.
앵커(&)와 별칭(*)¶
반복되는 값을 재사용할 때 앵커(anchor)와 별칭(alias)을 사용합니다.
# 앵커 정의: &이름
defaults: &default-resources
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
# 별칭 참조: *이름
containers:
- name: app
image: my-app:v1
resources: *default-resources # 위에서 정의한 값을 그대로 사용
- name: sidecar
image: sidecar:v1
resources: *default-resources # 동일한 리소스 제한 적용
맵 병합 (<<)¶
앵커의 값을 기반으로 일부만 오버라이드할 수 있습니다.
defaults: &defaults
env: production
replicas: 3
image: my-app:latest
# <<: *앵커 로 병합 후 특정 필드만 오버라이드
staging:
<<: *defaults
env: staging
replicas: 1
결과적으로 staging은 {env: staging, replicas: 1, image: my-app:latest}가 됩니다.
K8s에서의 앵커 사용
앵커와 별칭은 YAML 파서 수준에서 동작하므로 kubectl apply 시
정상적으로 해석됩니다. 단, kubectl이 응답으로 돌려주는 YAML에는
앵커가 해제(resolve)된 상태로 출력됩니다.
Kustomize나 Helm에서는 별도의 변수 시스템을 제공하므로,
앵커보다는 해당 도구의 기능을 사용하는 것이 더 일반적입니다.
2. K8s 매니페스트 공통 구조¶
모든 Kubernetes 리소스 매니페스트는 4개의 최상위 필드로 구성됩니다.
apiVersion: apps/v1 # API 그룹 및 버전
kind: Deployment # 리소스 종류
metadata: # 리소스 메타데이터
name: my-app
spec: # 리소스의 원하는 상태 (desired state)
replicas: 3
4대 필드 상세¶
| 필드 | 필수 여부 | 설명 |
|---|---|---|
apiVersion |
필수 | API 그룹과 버전. 리소스 종류에 따라 결정 |
kind |
필수 | 리소스의 종류 (Pod, Deployment, Service 등) |
metadata |
필수 | 리소스를 식별하는 메타정보 (name, namespace, labels 등) |
spec |
대부분 필수 | 리소스의 상세 사양. 종류마다 구조가 다름 |
apiVersion¶
리소스가 속한 API 그룹과 버전을 지정합니다.
| 리소스 | apiVersion | 비고 |
|---|---|---|
| Pod | v1 |
core 그룹 (그룹명 생략) |
| Service | v1 |
core 그룹 |
| ConfigMap | v1 |
core 그룹 |
| Secret | v1 |
core 그룹 |
| Namespace | v1 |
core 그룹 |
| Deployment | apps/v1 |
apps 그룹 |
| StatefulSet | apps/v1 |
apps 그룹 |
| DaemonSet | apps/v1 |
apps 그룹 |
| ReplicaSet | apps/v1 |
apps 그룹 |
| Ingress | networking.k8s.io/v1 |
networking 그룹 |
| NetworkPolicy | networking.k8s.io/v1 |
networking 그룹 |
| CronJob | batch/v1 |
batch 그룹 |
| Job | batch/v1 |
batch 그룹 |
| HPA | autoscaling/v2 |
autoscaling 그룹 |
| PVC | v1 |
core 그룹 |
apiVersion 확인 방법
사용 가능한 API 리소스와 버전을 확인하려면:
kind¶
리소스의 종류를 PascalCase로 지정합니다.
kubectl api-resources 명령으로 확인할 수 있는 KIND 열의 값과 동일합니다.
metadata¶
리소스를 식별하고 분류하는 메타데이터입니다.
metadata:
name: my-web-app # 리소스 이름 (필수)
namespace: production # 네임스페이스 (생략 시 default)
labels: # 라벨 (선택, 리소스 선택/필터링용)
app.kubernetes.io/name: my-web-app
app.kubernetes.io/version: "1.0"
app.kubernetes.io/component: frontend
app.kubernetes.io/part-of: web-platform
app.kubernetes.io/managed-by: kubectl
annotations: # 어노테이션 (선택, 비식별 메타정보)
description: "메인 웹 애플리케이션"
owner: "platform-team"
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
name¶
- 네임스페이스 내에서 고유해야 함
- 영문 소문자, 숫자,
-,.만 사용 가능 - 최대 253자
- DNS 서브도메인 규칙을 따름
namespace¶
- 리소스가 속할 네임스페이스
- 생략하면
default네임스페이스에 생성 - Node, PersistentVolume, Namespace 등 일부 리소스는 네임스페이스에 속하지 않음 (cluster-scoped)
labels vs annotations¶
| 구분 | labels | annotations |
|---|---|---|
| 용도 | 리소스 선택(select) 및 그룹핑 | 부가 정보 저장 |
| 셀렉터 사용 | 가능 (kubectl -l, selector) |
불가능 |
| 값 제한 | 63자 이내, 영숫자/-/_/. |
제한 없음 (대용량 텍스트 가능) |
| 예시 | app: nginx, tier: frontend |
description: "...", prometheus.io/scrape: "true" |
추천 라벨 키
Kubernetes 공식 문서에서 권장하는 라벨 키 접두사:
app.kubernetes.io/name— 애플리케이션 이름app.kubernetes.io/instance— 인스턴스 식별자app.kubernetes.io/version— 버전app.kubernetes.io/component— 컴포넌트 역할app.kubernetes.io/part-of— 상위 시스템 이름app.kubernetes.io/managed-by— 관리 도구 (kubectl, helm 등)
spec¶
리소스의 원하는 상태(desired state)를 정의합니다.
리소스 종류(kind)에 따라 내부 구조가 완전히 다릅니다.
Kubernetes 컨트롤러는 spec에 정의된 상태와 현재 상태를 비교하여
지속적으로 조정(reconciliation)을 수행합니다.
3. Labels와 Selectors 심화¶
Label 네이밍 규칙¶
라벨 키는 접두사(prefix)와 이름(name) 두 부분으로 구성됩니다.
| 부분 | 규칙 |
|---|---|
| 접두사 (선택) | DNS 서브도메인 형식, 최대 253자, /로 이름과 구분 |
| 이름 (필수) | 최대 63자, 영문자/숫자로 시작·끝, 중간에 -, _, . 허용 |
| 값 | 최대 63자, 빈 문자열 허용, 영문자/숫자로 시작·끝 |
# 좋은 라벨 예시
labels:
app.kubernetes.io/name: api-gateway
app.kubernetes.io/version: "2.1.0"
environment: production
team: platform
# 피해야 할 라벨 예시
labels:
App Name: My App # 공백 불가
version: 2.1.0 # 따옴표 없으면 float으로 파싱될 수 있음
a-very-long-label-name-that-exceeds-sixty-three-characters-limit-here: bad # 63자 초과
matchLabels vs matchExpressions¶
Deployment, Service 등에서 Pod를 선택할 때 두 가지 방식을 사용합니다.
matchLabels (등호 기반)¶
모든 라벨이 AND 조건으로 정확히 일치해야 합니다.
matchExpressions (집합 기반)¶
더 유연한 조건을 표현할 수 있습니다.
selector:
matchExpressions:
- key: app
operator: In
values: [my-app, my-app-canary] # app이 둘 중 하나
- key: tier
operator: NotIn
values: [backend] # tier가 backend가 아닌 것
- key: release
operator: Exists # release 라벨이 존재하는 것
- key: deprecated
operator: DoesNotExist # deprecated 라벨이 없는 것
| 연산자 | 설명 | values 필요 여부 |
|---|---|---|
In |
값이 목록 중 하나에 포함 | 필요 |
NotIn |
값이 목록에 포함되지 않음 | 필요 |
Exists |
해당 키를 가진 라벨이 존재 | 불필요 |
DoesNotExist |
해당 키를 가진 라벨이 존재하지 않음 | 불필요 |
matchLabels + matchExpressions 혼용
두 조건을 동시에 사용하면 모두 AND로 결합됩니다.
kubectl에서 Label 기반 필터링¶
# 등호 기반 필터
kubectl get pods -l app=my-app
kubectl get pods -l app=my-app,tier=frontend
# 부등호
kubectl get pods -l 'tier!=backend'
# 집합 기반 필터
kubectl get pods -l 'app in (my-app, my-api)'
kubectl get pods -l 'environment notin (test, staging)'
kubectl get pods -l 'release' # release 라벨이 존재하는 것
kubectl get pods -l '!canary' # canary 라벨이 없는 것
# 라벨 컬럼 표시
kubectl get pods --show-labels
kubectl get pods -L app,tier # 특정 라벨만 컬럼으로 표시
# 라벨 추가/수정/삭제
kubectl label pod my-pod environment=staging
kubectl label pod my-pod environment=production --overwrite
kubectl label pod my-pod environment- # 삭제 (키 뒤에 -)
4. 주요 리소스별 매니페스트 상세¶
Pod¶
Pod는 Kubernetes의 최소 배포 단위입니다. 하나 이상의 컨테이너를 포함하며, 네트워크와 스토리지를 공유합니다.
apiVersion: v1 # core API 그룹
kind: Pod # 리소스 종류: Pod
metadata:
name: my-web-pod # Pod 이름
namespace: default # 네임스페이스
labels:
app: my-web # 라벨: 서비스 셀렉터가 이 라벨로 Pod를 찾음
tier: frontend
annotations:
description: "샘플 웹 애플리케이션 Pod"
spec:
restartPolicy: Always # 재시작 정책: Always | OnFailure | Never
containers: # 컨테이너 목록 (1개 이상)
- name: web # 컨테이너 이름
image: nginx:1.25-alpine # 컨테이너 이미지
imagePullPolicy: IfNotPresent # 이미지 풀 정책: Always | IfNotPresent | Never
ports:
- name: http # 포트 이름 (Service에서 참조 가능)
containerPort: 80 # 컨테이너가 리슨하는 포트
protocol: TCP # 프로토콜: TCP | UDP | SCTP
env: # 환경 변수
- name: APP_ENV # 직접 값 지정
value: "production"
- name: DB_PASSWORD # Secret에서 가져오기
valueFrom:
secretKeyRef:
name: db-secret # Secret 리소스 이름
key: password # Secret 내 키
- name: CONFIG_VALUE # ConfigMap에서 가져오기
valueFrom:
configMapKeyRef:
name: app-config # ConfigMap 리소스 이름
key: config-key # ConfigMap 내 키
resources: # 리소스 요청 및 제한
requests: # 최소 보장 리소스 (스케줄링 기준)
cpu: "100m" # 100 밀리코어 = 0.1 코어
memory: "128Mi" # 128 메비바이트
limits: # 최대 허용 리소스
cpu: "500m" # 500 밀리코어 = 0.5 코어
memory: "256Mi" # 초과 시 OOMKilled
volumeMounts: # 볼륨 마운트 지점
- name: config-volume # 아래 volumes에서 정의한 이름
mountPath: /etc/config # 컨테이너 내 마운트 경로
readOnly: true # 읽기 전용 여부
- name: data-volume
mountPath: /var/data
livenessProbe: # 생존 프로브: 실패 시 컨테이너 재시작
httpGet: # HTTP GET 방식
path: /healthz # 헬스체크 경로
port: 80 # 대상 포트
initialDelaySeconds: 15 # 시작 후 첫 체크까지 대기 시간
periodSeconds: 10 # 체크 간격
timeoutSeconds: 3 # 타임아웃
failureThreshold: 3 # 연속 실패 횟수 → 재시작
successThreshold: 1 # 연속 성공 횟수 → 정상 판정
readinessProbe: # 준비 프로브: 실패 시 서비스 트래픽 제외
httpGet:
path: /ready
port: 80
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
startupProbe: # 스타트업 프로브: 앱 시작이 느린 경우
httpGet:
path: /healthz
port: 80
failureThreshold: 30 # 30 x 10초 = 최대 5분까지 시작 대기
periodSeconds: 10
volumes: # Pod 레벨 볼륨 정의
- name: config-volume # ConfigMap을 볼륨으로 마운트
configMap:
name: app-config
- name: data-volume # emptyDir: Pod 수명 동안 유지
emptyDir: {}
프로브(Probe) 방식 비교
| 방식 | 설명 | 사용 예 |
|---|---|---|
httpGet |
HTTP GET 요청, 200~399 응답이면 성공 | 웹 서버 헬스체크 |
tcpSocket |
TCP 연결 시도, 연결되면 성공 | DB, Redis 등 |
exec |
컨테이너 내 명령 실행, exit code 0이면 성공 | 파일 존재 확인, 커스텀 스크립트 |
grpc |
gRPC 헬스체크 프로토콜 사용 | gRPC 서비스 |
resources를 반드시 설정하세요
resources를 생략하면 Pod가 노드의 리소스를 무제한으로 사용할 수 있어,
다른 Pod에 영향을 줄 수 있습니다.
운영 환경에서는 반드시 requests와 limits를 설정하세요.
requests: 스케줄러가 Pod를 배치할 때 참고하는 최소 보장 리소스limits: 컨테이너가 사용할 수 있는 최대 리소스. 메모리 초과 시 OOMKilled 발생
Deployment¶
Deployment는 ReplicaSet을 관리하여 Pod의 선언적 업데이트와 롤백을 지원합니다. 실무에서 Pod를 직접 생성하는 경우는 거의 없고, 대부분 Deployment를 통해 관리합니다.
apiVersion: apps/v1 # apps API 그룹
kind: Deployment # 리소스 종류: Deployment
metadata:
name: my-web-deployment # Deployment 이름
namespace: default
labels:
app: my-web
annotations:
kubernetes.io/change-cause: "nginx 1.25로 업데이트" # 롤아웃 히스토리에 표시
spec:
replicas: 3 # 유지할 Pod 수
revisionHistoryLimit: 10 # 보관할 ReplicaSet 히스토리 수 (롤백용)
selector: # 관리 대상 Pod 선택 (template.labels와 일치해야 함)
matchLabels:
app: my-web
strategy: # 배포 전략
type: RollingUpdate # RollingUpdate | Recreate
rollingUpdate:
maxSurge: 1 # 롤링 업데이트 시 초과 허용 Pod 수 (또는 %)
maxUnavailable: 0 # 롤링 업데이트 시 이용 불가 허용 Pod 수 (또는 %)
template: # Pod 템플릿 (여기 정의된 대로 Pod가 생성됨)
metadata:
labels: # Pod에 부여할 라벨 (selector.matchLabels와 일치 필수)
app: my-web
version: "1.25"
spec: # Pod 스펙 (위의 Pod spec과 동일한 구조)
containers:
- name: web
image: nginx:1.25-alpine
ports:
- name: http
containerPort: 80
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 80
initialDelaySeconds: 5
periodSeconds: 5
배포 전략 비교¶
| 전략 | 동작 | 다운타임 | 사용 시나리오 |
|---|---|---|---|
RollingUpdate |
새 Pod를 점진적으로 생성하며 기존 Pod를 제거 | 없음 | 대부분의 무중단 배포 |
Recreate |
기존 Pod를 모두 제거한 뒤 새 Pod를 생성 | 있음 | DB 마이그레이션, 볼륨 충돌 방지 등 |
RollingUpdate 파라미터¶
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25% # replicas=4일 때 최대 5개까지 동시 실행 가능
maxUnavailable: 25% # replicas=4일 때 최소 3개는 항상 가용 상태
maxSurge: 업데이트 중replicas보다 많이 생성할 수 있는 Pod 수maxUnavailable: 업데이트 중 사용 불가능할 수 있는 Pod 수- 둘 다 0으로 설정할 수는 없음 (업데이트가 진행되지 않으므로)
롤아웃 관리 명령어
# 롤아웃 상태 확인
kubectl rollout status deployment/my-web-deployment
# 롤아웃 히스토리 확인
kubectl rollout history deployment/my-web-deployment
# 특정 리비전 상세 확인
kubectl rollout history deployment/my-web-deployment --revision=2
# 이전 버전으로 롤백
kubectl rollout undo deployment/my-web-deployment
# 특정 리비전으로 롤백
kubectl rollout undo deployment/my-web-deployment --to-revision=3
# 롤아웃 일시 정지/재개
kubectl rollout pause deployment/my-web-deployment
kubectl rollout resume deployment/my-web-deployment
Service¶
Service는 Pod 집합에 대한 안정적인 네트워크 엔드포인트를 제공합니다. Pod는 생성/삭제될 때마다 IP가 변경되지만, Service는 고정된 ClusterIP와 DNS 이름을 제공합니다.
apiVersion: v1 # core API 그룹
kind: Service # 리소스 종류: Service
metadata:
name: my-web-service # Service 이름 → DNS: my-web-service.default.svc.cluster.local
namespace: default
labels:
app: my-web
spec:
type: ClusterIP # Service 타입
selector: # 트래픽을 전달할 Pod 선택 (Pod의 labels와 매칭)
app: my-web
ports:
- name: http # 포트 이름
protocol: TCP # 프로토콜
port: 80 # Service가 노출하는 포트
targetPort: 80 # Pod에서 리슨하는 포트 (포트 이름도 가능)
- name: https
protocol: TCP
port: 443
targetPort: 8443
sessionAffinity: None # None | ClientIP (동일 클라이언트를 같은 Pod로)
Service 타입 비교¶
| 타입 | 접근 범위 | 설명 | 사용 시나리오 |
|---|---|---|---|
ClusterIP |
클러스터 내부만 | 기본값. 클러스터 내부 IP를 할당 | 내부 마이크로서비스 간 통신 |
NodePort |
클러스터 외부 | 모든 노드의 고정 포트(30000-32767)로 접근 | 개발/테스트 환경, 간단한 외부 노출 |
LoadBalancer |
클러스터 외부 | 클라우드 로드밸런서를 프로비저닝 | 프로덕션 외부 서비스 노출 |
ExternalName |
클러스터 내부 | 외부 DNS 이름에 대한 CNAME 매핑 | 외부 서비스를 클러스터 내 DNS로 참조 |
NodePort 예시¶
apiVersion: v1
kind: Service
metadata:
name: my-web-nodeport
spec:
type: NodePort
selector:
app: my-web
ports:
- port: 80 # 클러스터 내부 접근 포트
targetPort: 80 # Pod 포트
nodePort: 30080 # 노드에서 노출할 포트 (30000-32767, 생략 시 자동 할당)
LoadBalancer 예시¶
apiVersion: v1
kind: Service
metadata:
name: my-web-lb
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "nlb" # AWS NLB 사용
service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
spec:
type: LoadBalancer
selector:
app: my-web
ports:
- port: 80
targetPort: 80
ExternalName 예시¶
apiVersion: v1
kind: Service
metadata:
name: external-db
spec:
type: ExternalName
externalName: db.example.com # 외부 DNS 이름
# selector와 ports를 정의하지 않음
Service DNS 규칙
클러스터 내에서 Service는 다음 DNS 이름으로 접근할 수 있습니다:
- 같은 네임스페이스:
my-web-service - 다른 네임스페이스:
my-web-service.production - FQDN:
my-web-service.production.svc.cluster.local
5. kubectl apply vs create vs replace¶
Kubernetes 리소스를 관리하는 방식은 크게 선언형(declarative)과 명령형(imperative)으로 나뉩니다.
명령형 관리 (Imperative)¶
# kubectl create: 리소스를 처음 생성 (이미 존재하면 에러)
kubectl create -f deployment.yaml
# kubectl replace: 기존 리소스를 완전히 교체 (존재하지 않으면 에러)
kubectl replace -f deployment.yaml
# kubectl delete: 리소스 삭제
kubectl delete -f deployment.yaml
선언형 관리 (Declarative)¶
# kubectl apply: 리소스가 없으면 생성, 있으면 업데이트
kubectl apply -f deployment.yaml
# 디렉토리 내 모든 매니페스트 적용
kubectl apply -f ./manifests/
# 재귀적으로 하위 디렉토리까지 적용
kubectl apply -f ./manifests/ -R
비교표¶
| 항목 | create |
apply |
replace |
|---|---|---|---|
| 패러다임 | 명령형 | 선언형 | 명령형 |
| 리소스 없을 때 | 생성 | 생성 | 에러 |
| 리소스 있을 때 | 에러 | 부분 업데이트 (패치) | 전체 교체 |
| 변경 이력 추적 | 불가 | last-applied-configuration 어노테이션으로 추적 |
불가 |
| GitOps 호환성 | 낮음 | 높음 | 낮음 |
| 권장 사용 시나리오 | 일회성 작업, 스크립트 | 운영 환경 표준 | 완전한 교체가 필요한 경우 |
운영 환경에서는 kubectl apply를 사용하세요
apply는 선언형 관리의 핵심입니다.
YAML 파일에 원하는 상태를 정의하고, apply로 적용하면
Kubernetes가 현재 상태와 비교하여 필요한 변경만 수행합니다.
이 패턴은 GitOps와 가장 잘 어울립니다.
dry-run과 diff¶
실제 적용 전에 변경 사항을 미리 확인하는 것은 매우 중요합니다.
# 클라이언트 사이드 dry-run: 문법 검증만 수행 (서버 통신 없음)
kubectl apply -f deployment.yaml --dry-run=client
# 서버 사이드 dry-run: 서버에서 검증하되 실제 적용하지 않음
kubectl apply -f deployment.yaml --dry-run=server
# diff: 현재 상태와 매니페스트의 차이점을 확인
kubectl diff -f deployment.yaml
dry-run=client vs dry-run=server
--dry-run=client는 YAML 문법만 검증하므로, API 서버의 유효성 검사
(예: 잘못된 필드 이름, 필수 필드 누락)를 잡아내지 못합니다.
가능하면 --dry-run=server를 사용하세요.
# 실무 워크플로우 예시
# 1. diff로 변경 사항 확인
kubectl diff -f deployment.yaml
# 2. dry-run으로 유효성 검증
kubectl apply -f deployment.yaml --dry-run=server
# 3. 문제 없으면 실제 적용
kubectl apply -f deployment.yaml
# 4. 롤아웃 상태 확인
kubectl rollout status deployment/my-web-deployment
6. Kustomize & Helm 간단 소개¶
매니페스트를 직접 관리하는 것만으로는 환경별 설정 차이, 반복적인 구조 등을 효율적으로 처리하기 어렵습니다. Kustomize와 Helm은 이 문제를 해결하는 대표적인 도구입니다.
Kustomize¶
Kustomize는 템플릿 없이 기존 YAML을 오버레이 방식으로 수정합니다. kubectl에 내장되어 있어 별도 설치가 필요 없습니다.
디렉토리 구조¶
k8s/
├── base/ # 공통 매니페스트
│ ├── kustomization.yaml
│ ├── deployment.yaml
│ └── service.yaml
├── overlays/
│ ├── dev/ # 개발 환경 오버레이
│ │ ├── kustomization.yaml
│ │ └── replica-patch.yaml
│ ├── staging/ # 스테이징 환경 오버레이
│ │ ├── kustomization.yaml
│ │ └── replica-patch.yaml
│ └── production/ # 프로덕션 환경 오버레이
│ ├── kustomization.yaml
│ ├── replica-patch.yaml
│ └── hpa.yaml
base/kustomization.yaml¶
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
commonLabels:
app: my-web
overlays/production/kustomization.yaml¶
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base # base 매니페스트를 가져옴
namespace: production # 모든 리소스에 네임스페이스 지정
commonLabels:
environment: production
patches: # 패치 적용
- path: replica-patch.yaml
configMapGenerator: # ConfigMap 자동 생성
- name: app-config
literals:
- APP_ENV=production
- LOG_LEVEL=warn
images: # 이미지 태그 오버라이드
- name: nginx
newTag: "1.25-alpine"
overlays/production/replica-patch.yaml¶
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-web-deployment
spec:
replicas: 5 # base에서 정의한 replicas를 5로 변경
Kustomize 사용법¶
# 결과 미리보기 (렌더링만 수행)
kubectl kustomize overlays/production/
# 직접 적용
kubectl apply -k overlays/production/
# diff 확인
kubectl diff -k overlays/production/
Kustomize의 장점
- 별도 도구 설치 불필요 (kubectl 내장)
- 원본 YAML을 수정하지 않고 환경별 차이만 관리
- 학습 곡선이 낮음
- GitOps 워크플로우에 적합
Helm¶
Helm은 Kubernetes의 패키지 매니저입니다. 차트(chart)라는 패키지 형태로 매니페스트를 템플릿화하여 관리합니다.
Chart 기본 구조¶
my-web-chart/
├── Chart.yaml # 차트 메타데이터 (이름, 버전, 설명)
├── values.yaml # 기본 설정값
├── templates/ # Go 템플릿 기반 매니페스트
│ ├── _helpers.tpl # 재사용 가능한 템플릿 헬퍼
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── configmap.yaml
│ ├── hpa.yaml
│ └── NOTES.txt # 설치 후 출력되는 안내 메시지
└── charts/ # 의존성 차트 (서브 차트)
Chart.yaml¶
apiVersion: v2
name: my-web-chart
description: 샘플 웹 애플리케이션 Helm 차트
type: application
version: 0.1.0 # 차트 버전
appVersion: "1.25.0" # 애플리케이션 버전
values.yaml¶
replicaCount: 3
image:
repository: nginx
tag: "1.25-alpine"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
templates/deployment.yaml (Go 템플릿)¶
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-web-chart.fullname" . }}
labels:
{{- include "my-web-chart.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "my-web-chart.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "my-web-chart.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: 80
resources:
{{- toYaml .Values.resources | nindent 12 }}
Helm 사용법¶
# 차트 설치
helm install my-release ./my-web-chart
# 환경별 values 파일로 설치
helm install my-release ./my-web-chart -f values-production.yaml
# 특정 값만 오버라이드
helm install my-release ./my-web-chart --set replicaCount=5
# 렌더링 결과만 확인 (실제 설치하지 않음)
helm template my-release ./my-web-chart
# 업그레이드
helm upgrade my-release ./my-web-chart
# 롤백
helm rollback my-release 1
# 릴리스 목록
helm list
# 삭제
helm uninstall my-release
Kustomize vs Helm 비교¶
| 항목 | Kustomize | Helm |
|---|---|---|
| 접근 방식 | 오버레이 (패치) | 템플릿 (Go template) |
| 설치 | kubectl 내장 | 별도 설치 필요 |
| 학습 곡선 | 낮음 | 중간~높음 |
| 패키지 배포 | 불가 | 차트 리포지토리를 통한 배포 가능 |
| 조건부 로직 | 제한적 | if/else, range 등 Go 템플릿 문법 |
| 커뮤니티 차트 | 없음 | ArtifactHub에 수천 개의 공개 차트 |
| 적합한 상황 | 자체 앱, 환경별 설정 차이 관리 | 공유 가능한 패키지, 복잡한 조건부 설정 |
Kustomize + Helm 조합
두 도구를 함께 사용하는 것도 가능합니다. Helm으로 차트를 렌더링한 결과를 Kustomize로 추가 패치하는 방식으로, 공개 차트를 커스터마이징할 때 유용합니다.
정리¶
이번 챕터에서 다룬 핵심 내용을 정리합니다.
| 주제 | 핵심 포인트 |
|---|---|
| YAML 기초 | 스페이스 들여쓰기, 맵/리스트 조합, \|/> 멀티라인, 앵커/별칭 |
| 매니페스트 구조 | apiVersion, kind, metadata, spec 4대 필드 |
| Labels/Selectors | matchLabels(등호), matchExpressions(집합), kubectl -l 필터링 |
| Pod | containers, probes, resources, volumeMounts |
| Deployment | replicas, strategy(RollingUpdate/Recreate), template, 롤백 |
| Service | ClusterIP, NodePort, LoadBalancer, ExternalName |
| 리소스 관리 | apply(선언형) vs create/replace(명령형), dry-run, diff |
| 도구 | Kustomize(오버레이), Helm(템플릿+패키지) |
다음 단계
YAML 매니페스트 작성에 익숙해졌다면, 실제로 매니페스트를 작성하고
kubectl apply로 적용해보세요.
kubectl explain <리소스>.<필드>를 활용하면 매니페스트 작성 시
필드 이름과 타입을 빠르게 확인할 수 있습니다.