4·6·10강 · 워크로드 (Deployment · ConfigMap · Job · DaemonSet · 사이드카)¶
영상: 4강 파드에서 Deployment 까지 (1~3) · 6강 설정을 이미지 밖으로 (4~5) · 10강 Deployment 로는 안 되는 일 (6~8) · 이어서 풀 채점 세트:
04-pods·06-config·10-workloads
실습 목표¶
- 명령으로 YAML 뼈대를 뽑고(
$do), 필드를kubectl explain으로 찾아 채우고,apply·edit·replace --force를 상황에 맞게 고른다. - Deployment 가 ReplicaSet 을 갈아 끼우는 방식으로 업데이트·롤백하고, 레이블이 소유 관계를 정한다는 것을 눈으로 확인한다.
maxSurge·maxUnavailable·Recreate를 바꿔 가며 롤아웃 모양이 어떻게 달라지는지 비교하고,pause·resume으로 변경을 묶는다.- ConfigMap · Secret 을 환경변수로 넣을 때와 볼륨으로 넣을 때 값이 바뀌는 시점이 다르다는 것을 확인하고, Deployment 에는
rollout restart로 반영한 뒤immutable로 잠근다. - Job(
completions·parallelism·backoffLimit) · CronJob · DaemonSet(updateStrategy) 이 Deployment 와 무엇이 다른지 직접 띄워 보고 비교한다. - 일반 initContainer 와 네이티브 사이드카의 차이를 확인한다.
0) 준비¶
이 페이지의 모든 리소스는 guide-wl 네임스페이스에 만듭니다.
kubectl create namespace guide-wl
mkdir -p ~/guide-wl && cd ~/guide-wl # 이 페이지에서 만드는 YAML 파일을 모아 둔다
echo $do # --dry-run=client -o yaml 이 나오면 준비 완료
$do 가 비어 있으면 한 번 정의합니다.
매번 -n guide-wl 을 붙입니다
kubectl config set-context --current --namespace=... 로 기본 네임스페이스를 바꿀 수도 있지만,
시험에서는 문제마다 네임스페이스가 달라서 -n 을 직접 붙이는 습관이 더 안전합니다.
1) 명령으로 뼈대 뽑고, 고치기 — $do · explain · apply / edit¶
왜 — 시험에서 YAML 을 처음부터 쓰면 들여쓰기 하나로 몇 분이 날아갑니다. 명령으로 뼈대를 뽑고, 명령으로 못 넣는 필드만 손으로 추가하는 것이 가장 빠르고 안전합니다.
뼈대 뽑기¶
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null # ← 지워도 되고 둬도 된다
labels:
run: probe # ← kubectl run 은 run=<이름> 레이블을 붙인다
name: probe
namespace: guide-wl
spec:
containers:
- args: # ← `--` 뒤는 args 로 들어간다 (command 가 아니다)
- sleep
- "3600"
image: busybox:1.36
name: probe
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Never # ← --restart=Never 가 없으면 Always
status: {}
| 명령 | 만들어지는 것 | -- 뒤의 명령이 들어가는 곳 |
|---|---|---|
kubectl run <이름> --image=... |
Pod | args (--command 를 붙이면 command) |
kubectl create deployment <이름> --image=... --replicas=N |
Deployment | command |
kubectl create job <이름> --image=... |
Job | command |
kubectl create cronjob <이름> --image=... --schedule='...' |
CronJob | command |
kubectl create configmap / secret generic |
ConfigMap / Secret | — |
kubectl expose deployment <이름> --port=... |
Service | — |
| (없음) | DaemonSet · StatefulSet | Deployment 뼈대를 뽑아 고친다 → 7) |
필드 이름은 explain 으로¶
인터넷 없이, 클러스터 안에서 필드 구조와 설명을 봅니다.
KIND: Pod
VERSION: v1
FIELD: envFrom <[]EnvFromSource>
DESCRIPTION:
List of sources to populate environment variables in the container. ...
FIELDS:
configMapRef <ConfigMapEnvSource>
The ConfigMap to select from
prefix <string>
An optional identifier to prepend to each key in the ConfigMap. ...
secretRef <SecretEnvSource>
The Secret to select from
kubectl explain deployment.spec --recursive | head -30 # 구조를 통째로
kubectl create cronjob --help | grep -A1 -- '--schedule' # 플래그가 기억 안 날 때
고치는 세 가지 방법¶
| 방법 | 무엇을 기준으로 | 언제 |
|---|---|---|
kubectl apply -f <파일> |
파일 — 파일과 클러스터의 차이만 반영 | 파일로 관리할 때. 여러 번 실행해도 안전 |
kubectl edit <리소스> <이름> |
클러스터의 현재 상태 — 에디터에서 바로 고침 | 한두 줄 빠르게 고칠 때 |
kubectl replace --force -f <파일> |
파일 — 지우고 다시 만든다 | 파드처럼 바꿀 수 없는 필드를 바꿔야 할 때 |
깨 보기 — 파드는 대부분의 필드를 바꿀 수 없다¶
kubectl run probe -n guide-wl --image=busybox:1.36 --restart=Never -- sleep 3600
kubectl edit pod probe -n guide-wl
# spec.containers[0] 아래에 아래 두 줄을 추가하고 저장
# env:
# - {name: GREETING, value: hi}
저장하면 에디터가 다시 열리고 맨 위에 거절 이유가 주석으로 붙어 있습니다.
# pods "probe" was not valid:
# * spec: Forbidden: pod updates may not change fields other than `spec.containers[*].image`, ...
그대로 :q! 로 나오면 kubectl 이 고친 내용을 파일로 남겨 줍니다.
A copy of your changes has been stored to "/tmp/kubectl-edit-1234567890.yaml"
error: Edit cancelled, no valid changes were saved.
kubectl apply 로 해도 같은 이유로 거절됩니다.
파드는 이미지 · activeDeadlineSeconds · toleration 추가 등 일부 필드만 바꿀 수 있습니다. 저장된 사본으로 지우고 다시 만듭니다.
kubectl replace --force -f /tmp/kubectl-edit-1234567890.yaml # 실제 파일 이름은 에러 메시지에서 복사
kubectl exec probe -n guide-wl -- env | grep GREETING
관찰 포인트 — Deployment 는 파드 템플릿을 바꾸면 새 파드로 교체해 주므로 이런 제약이 없습니다. 파드를 직접 고칠 일이 생기면 replace --force 를 떠올리세요.
create 로 만든 것을 나중에 apply 하면
처음 한 번은 Warning: resource ... is missing the kubectl.kubernetes.io/last-applied-configuration annotation ... 경고가 뜹니다.
동작은 정상이고 kubectl 이 어노테이션을 알아서 붙입니다. 두 번째부터는 뜨지 않습니다.
2) Deployment — 업데이트와 롤백은 ReplicaSet 교체다¶
왜 — Deployment 는 파드를 직접 만들지 않습니다. ReplicaSet 을 만들고, ReplicaSet 이 파드를 만듭니다. 이 3단 구조를 알면 업데이트 · 롤백 · 레이블 문제가 전부 설명됩니다.
flowchart LR
D["Deployment shop<br/>원하는 템플릿 · replicas 3"]
D -->|revision 1| RS1["ReplicaSet shop-6d4f…<br/>nginx:1.26"]
D -->|revision 2| RS2["ReplicaSet shop-7b9c…<br/>nginx:1.27"]
RS1 --> P1["Pod ×3 → ×0<br/>(RS 는 0 으로 남는다)"]
RS2 --> P2["Pod ×0 → ×3"]
Deployment 만들기¶
kubectl create deployment shop -n guide-wl --image=nginx:1.26 --replicas=3
kubectl rollout status deployment/shop -n guide-wl
Deployment 를 만들면 ReplicaSet 이 하나 생기고, 그 ReplicaSet 이 파드 3개를 만듭니다.
NAME READY UP-TO-DATE AVAILABLE
deployment.apps/shop 3/3 3 3
NAME DESIRED CURRENT READY
replicaset.apps/shop-6d4f8b7c9d 3 3 3
NAME READY STATUS
pod/shop-6d4f8b7c9d-2xk8p 1/1 Running
...
파드 이름 가운데의 6d4f8b7c9d 는 ReplicaSet 의 해시입니다. 파드 템플릿이 바뀌면 이 해시도 바뀝니다.
NAME READY STATUS LABELS
shop-6d4f8b7c9d-2xk8p 1/1 Running app=shop,pod-template-hash=6d4f8b7c9d
...
pod-template-hash 레이블 덕분에 새 ReplicaSet 과 옛 ReplicaSet 이 서로의 파드를 헷갈리지 않습니다.
변경 이유 남기기¶
rollout history 의 CHANGE-CAUSE 칸은 kubernetes.io/change-cause 어노테이션에서 옵니다.
(예전의 --record 옵션은 폐기되었습니다.)
롤링 업데이트¶
터미널을 하나 더 열어 ReplicaSet 을 지켜보면서 업데이트합니다.
nginx= 는 컨테이너 이름입니다
kubectl create deployment 는 이미지 이름으로 컨테이너 이름을 짓습니다. 확실하지 않으면 먼저 확인하세요.
터미널 1 에서 새 ReplicaSet 의 DESIRED 가 1 → 2 → 3 으로 늘고, 옛 ReplicaSet 은 3 → 2 → 1 → 0 으로 줄어드는 것이 보입니다. 이것이 롤링 업데이트의 실체입니다.
NAME DESIRED CURRENT READY
shop-6d4f8b7c9d 0 0 0 ← nginx:1.26 (지워지지 않고 0 으로 남는다)
shop-7b9c5d6f8a 3 3 3 ← nginx:1.27
옛 ReplicaSet 이 0 으로 남아 있는 이유가 바로 롤백 때문입니다. 몇 개까지 남길지는 spec.revisionHistoryLimit(기본 10) 이 정합니다.
무엇을 바꿔야 새 ReplicaSet 이 생기나
파드 템플릿(spec.template) 이 바뀔 때만 생깁니다. kubectl scale 로 replicas 만 바꾸면 기존 ReplicaSet 의 숫자만 바뀌고 리비전도 늘지 않습니다.
롤백¶
kubectl rollout undo deployment/shop -n guide-wl
kubectl rollout status deployment/shop -n guide-wl
kubectl get rs -n guide-wl
이번에는 0 이던 옛 ReplicaSet 이 다시 3 으로 올라갑니다. 새 ReplicaSet 을 만들지 않고 보관해 둔 것을 되살린 것입니다.
리비전 1 이 사라지고 3 이 생겼습니다. 롤백도 "옛 템플릿으로 새 리비전을 만드는 일" 이기 때문입니다.
특정 리비전으로 돌아가려면?
레이블이 소유 관계를 정한다¶
ReplicaSet 은 파드를 이름이 아니라 레이블(셀렉터)로 셉니다. 파드 하나에서 레이블을 떼어 봅니다.
POD=$(kubectl get pods -n guide-wl -l app=shop -o jsonpath='{.items[0].metadata.name}')
kubectl label pod $POD -n guide-wl app- # 키 뒤에 하이픈 = 레이블 삭제
kubectl get pods -n guide-wl -L app
NAME READY STATUS APP
shop-6d4f8b7c9d-2xk8p 1/1 Running ← 레이블이 없어져 셀렉터에서 빠졌다
shop-6d4f8b7c9d-9pl4n 1/1 Running shop
shop-6d4f8b7c9d-h7wqs 1/1 Running shop
shop-6d4f8b7c9d-x2m5r 1/1 Running shop ← ReplicaSet 이 "2개뿐" 이라 보고 새로 만들었다
관찰 포인트 — 레이블을 뗀 파드는 주인 없는 파드가 되어 계속 돌고(ownerReferences 도 빠집니다), ReplicaSet 은 3개를 맞추려고 하나를 더 만듭니다.
kubectl get pod $POD -n guide-wl -o jsonpath='{.metadata.ownerReferences}{"\n"}' # 비어 있다
kubectl delete pod $POD -n guide-wl
레이블을 다루는 명령은 이것이 전부입니다.
kubectl label pod <이름> tier=web env=dev -n guide-wl # 붙이기 (여러 개 한 번에)
kubectl label pod <이름> env=prod --overwrite -n guide-wl # 고치기 — --overwrite 필수
kubectl label pod <이름> env- -n guide-wl # 지우기
kubectl get pods -l 'env in (dev,prod)' -n guide-wl # 여러 값 중 하나
kubectl get pods -l 'app=shop,env!=prod' -n guide-wl # 여러 조건 AND
깨 보기 — selector 와 template labels 가 다르면¶
kubectl create deployment mismatch -n guide-wl --image=nginx:1.27 $do > mismatch.yaml
awk '/app: mismatch/{n++; if(n==3) sub(/app: mismatch/,"app: other")} 1' mismatch.yaml > m.tmp && mv m.tmp mismatch.yaml
grep -n 'app:' mismatch.yaml # 세 번째(template 쪽) 레이블만 바뀌었는지 확인
kubectl apply -f mismatch.yaml
6: app: mismatch ← metadata.labels
13: app: mismatch ← spec.selector.matchLabels
19: app: other ← spec.template.metadata.labels
The Deployment "mismatch" is invalid: spec.template.metadata.labels: Invalid value: map[string]string{"app":"other"}: `selector` does not match template `labels`
selector 는 만든 뒤 바꿀 수 없습니다
apps/v1 의 Deployment · DaemonSet 은 spec.selector 가 불변(immutable) 입니다. 레이블 설계를 바꾸려면 지우고 다시 만들어야 합니다.
$do 로 뽑은 뼈대는 selector 와 template labels 가 처음부터 맞춰져 있으니 한쪽만 고치지 마세요.
3) 롤아웃 전략 — 속도와 안전 사이¶
왜 — 롤링 업데이트는 "한꺼번에 몇 개까지 더 띄우고(maxSurge), 몇 개까지 줄여도 되는가(maxUnavailable)" 두 숫자로 모양이 정해집니다. 잘못된 이미지를 배포했을 때 서비스가 살아남는 것도 이 숫자 덕분입니다.
GROUP: apps
KIND: Deployment
VERSION: v1
FIELD: rollingUpdate <RollingUpdateDeployment>
DESCRIPTION:
Rolling update config params. Present only if DeploymentStrategyType =
RollingUpdate.
...
FIELDS:
maxSurge <IntOrString>
The maximum number of pods that can be scheduled above the desired number of
pods. Value can be an absolute number (ex: 5) or a percentage of desired
pods (ex: 10%). ... Defaults to 25%. ...
maxUnavailable <IntOrString>
The maximum number of pods that can be unavailable during the update. ...
Defaults to 25%. ...
| 설정 | replicas 4 일 때 | 모양 |
|---|---|---|
기본 (25% / 25%) |
최대 5개, 최소 3개 Ready | 하나 올리고 하나 내리기를 겹쳐서 |
maxSurge: 1, maxUnavailable: 0 |
최대 5개, 항상 4개 Ready | 가장 안전, 자원이 1개분 더 필요 |
maxSurge: 0, maxUnavailable: 1 |
최대 4개, 최소 3개 Ready | 자원이 빠듯할 때, 용량이 잠깐 준다 |
type: Recreate |
4 → 0 → 4 | 전부 내리고 전부 올린다. 중단 시간 있음 |
maxSurge 는 올림, maxUnavailable 은 내림으로 계산합니다. replicas 3 에 25% 면 surge = ⌈0.75⌉ = 1, unavailable = ⌊0.75⌋ = 0 입니다.
깨 보기 — 없는 이미지로 업데이트하면¶
2) 의 shop (replicas 3, 기본 전략) 에 존재하지 않는 태그를 넣습니다.
kubectl set image deployment/shop -n guide-wl nginx=nginx:9.99-nope
kubectl rollout status deployment/shop -n guide-wl --timeout=30s
kubectl get pods -n guide-wl -l app=shop
Waiting for deployment "shop" rollout to finish: 1 out of 3 new replicas have been updated...
error: timed out waiting for the condition
NAME READY STATUS RESTARTS
shop-5f7c8d9b4a-kx2wq 0/1 ImagePullBackOff 0 ← 새 파드 1개만 시도
shop-6d4f8b7c9d-9pl4n 1/1 Running 0 ← 옛 파드 3개는 그대로
shop-6d4f8b7c9d-h7wqs 1/1 Running 0
shop-6d4f8b7c9d-x2m5r 1/1 Running 0
관찰 포인트 — replicas 3 의 기본 전략은 maxUnavailable 이 0 이라서 새 파드가 Ready 가 되기 전에는 옛 파드를 하나도 내리지 않습니다. 잘못된 배포가 서비스를 멈추지 않았습니다.
그대로 두면?
spec.progressDeadlineSeconds(기본 600초) 가 지나면 Deployment 의 Progressing 조건이 False / ProgressDeadlineExceeded 가 됩니다.
쿠버네티스가 자동으로 롤백하지는 않습니다. 사람이 rollout undo 해야 합니다.
전략을 바꿔 가며 비교하기¶
비교하기 쉽게 replicas 4 의 새 Deployment 를 YAML 로 만듭니다.
surge.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: surge
namespace: guide-wl
spec:
replicas: 4
minReadySeconds: 5 # ← Ready 후 5초를 더 지켜봐야 "사용 가능". 교체를 눈으로 볼 수 있게 느리게 만든다
selector:
matchLabels:
app: surge
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # ← 원하는 수보다 최대 1개 더
maxUnavailable: 0 # ← 사용 가능한 파드가 4개 밑으로 내려가지 않게
template:
metadata:
labels:
app: surge
spec:
containers:
- name: nginx
image: nginx:1.26
NAME DESIRED CURRENT READY
surge-7c9f6d5b8d 4 4 4 ← 옛 RS
surge-6b8d4f7c9a 1 1 0 ← 새 RS 가 먼저 1개 (합계 5)
surge-6b8d4f7c9a 1 1 1
surge-7c9f6d5b8d 3 4 4 ← 새 것이 사용 가능해진 뒤에야 옛 것을 1개 줄인다
...
surge-6b8d4f7c9a 4 4 4
surge-7c9f6d5b8d 0 0 0
이번에는 반대로 maxSurge: 0, maxUnavailable: 1 로 바꾸고 다시 업데이트합니다.
kubectl patch deployment surge -n guide-wl \
-p '{"spec":{"strategy":{"rollingUpdate":{"maxSurge":0,"maxUnavailable":1}}}}'
kubectl set image deployment/surge -n guide-wl nginx=nginx:1.26
터미널 1 에서 옛 RS 가 먼저 4 → 3 으로 줄고, 그다음 새 RS 가 1 로 느는 순서가 보입니다. 파드 합계가 4 를 넘지 않습니다.
깨 보기 — Recreate 로 바꾸려는데 거절된다¶
The Deployment "surge" is invalid: spec.strategy.rollingUpdate: Forbidden: may not be specified when strategy `type` is 'Recreate'
rollingUpdate 칸이 남아 있기 때문입니다. 같이 지워야 합니다.
kubectl patch deployment surge -n guide-wl --type=merge \
-p '{"spec":{"strategy":{"type":"Recreate","rollingUpdate":null}}}'
kubectl set image deployment/surge -n guide-wl nginx=nginx:1.27
kubectl get pods -n guide-wl -l app=surge -w
관찰 포인트 — 네 파드가 한꺼번에 Terminating 이 되고, 다 사라진 다음에야 새 파드 네 개가 ContainerCreating 으로 뜹니다. 그 사이에는 서비스할 파드가 0개입니다.
여러 변경을 한 번의 롤아웃으로 — pause / resume¶
이미지와 리소스를 둘 다 바꾸면 보통은 롤아웃이 두 번 일어납니다. 일시정지해 두면 한 번으로 묶입니다.
kubectl rollout pause deployment/surge -n guide-wl
kubectl set image deployment/surge -n guide-wl nginx=nginx:1.26
kubectl set resources deployment/surge -n guide-wl -c nginx --requests=cpu=50m,memory=32Mi
kubectl get rs -n guide-wl -l app=surge # 새 RS 가 생기지 않았다
kubectl rollout history deployment/surge -n guide-wl
일시정지 중에는 되돌리기도 막힙니다.
error: cannot rollback a paused deployment; resume it first with 'kubectl rollout resume deployment/surge' and try again
kubectl rollout resume deployment/surge -n guide-wl
kubectl rollout status deployment/surge -n guide-wl
kubectl rollout history deployment/surge -n guide-wl # 리비전이 하나만 늘었다
시험 팁
- 롤아웃 문제는 마지막에
kubectl rollout status로 완료를 확인하고 넘어갑니다. 채점은 Ready 파드 수를 봅니다. - "이전 버전으로" 는
rollout undo, "리비전 N 으로" 는--to-revision=N. 리비전 번호는rollout history로 먼저 확인합니다. - 전략 필드는
kubectl explain deployment.spec.strategy --recursive로 이름을 확인하면 오타가 없습니다.
4) ConfigMap — 환경변수와 볼륨은 반영 시점이 다르다¶
왜 — 설정이 이미지 안에 있으면 환경마다 이미지를 새로 만들어야 합니다. ConfigMap 으로 빼면 같은 이미지를 dev · prod 에 그대로 씁니다. 넣는 방법에 따라 값이 바뀌었을 때의 동작이 다릅니다.
ConfigMap 만들기¶
kubectl create configmap app-settings -n guide-wl \
--from-literal=MODE=dev \
--from-literal=LOG_LEVEL=debug
kubectl get configmap app-settings -n guide-wl -o yaml
파일에서 만드는 방법도 두 가지입니다. 키가 어떻게 정해지는지가 다릅니다.
printf 'server {\n listen 8080;\n}\n' > site.conf
printf 'COLOR=blue\nSIZE=large\n' > ui.env
kubectl create configmap from-file -n guide-wl --from-file=site.conf # 파일 이름이 키, 파일 내용이 값
kubectl create configmap from-env -n guide-wl --from-env-file=ui.env # 줄마다 KEY=VALUE 가 키 하나
kubectl get configmap from-file from-env -n guide-wl -o jsonpath='{range .items[*]}{.metadata.name}: {.data}{"\n"}{end}'
세 가지 방법으로 한 파드에 넣기¶
settings-check.yaml:
apiVersion: v1
kind: Pod
metadata:
name: settings-check
namespace: guide-wl
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
env: # ① 키 하나만 골라서, 이름을 바꿔 넣기
- name: APP_MODE # ← 컨테이너 안에서 쓸 변수 이름
valueFrom:
configMapKeyRef:
name: app-settings # ← ConfigMap 이름
key: MODE # ← 그 안의 키
envFrom: # ② 모든 키를 그대로 넣기
- configMapRef:
name: app-settings
volumeMounts: # ③ 키마다 파일 하나로 넣기
- name: settings # ← 아래 volumes 의 이름과 같아야 한다
mountPath: /etc/settings
volumes:
- name: settings
configMap:
name: app-settings
kubectl apply -f settings-check.yaml
kubectl exec settings-check -n guide-wl -- env | grep -E 'MODE|LOG_LEVEL'
kubectl exec settings-check -n guide-wl -- ls /etc/settings
kubectl exec settings-check -n guide-wl -- cat /etc/settings/MODE
APP_MODE=dev ← ① env
MODE=dev ← ② envFrom
LOG_LEVEL=debug ← ② envFrom
LOG_LEVEL MODE ← ③ 볼륨: 키 이름이 파일 이름
dev
값을 바꾸면?¶
1분쯤 기다린 뒤 다시 확인합니다.
kubectl exec settings-check -n guide-wl -- env | grep MODE
kubectl exec settings-check -n guide-wl -- cat /etc/settings/MODE
| 넣는 방법 | 값이 바뀌면 |
|---|---|
env / envFrom |
파드를 다시 만들어야 반영됩니다. 환경변수는 컨테이너가 시작할 때 한 번 정해집니다 |
| 볼륨 | kubelet 이 주기적으로 파일을 갱신합니다 (보통 1분 이내). 단 subPath 로 마운트하면 갱신되지 않습니다 |
Deployment 에 반영하기 — rollout restart¶
시험에서는 파드 하나가 아니라 Deployment 가 읽는 ConfigMap 을 고치고, 새 값이 실제로 들어갔는지까지 확인하라는 형태로 나옵니다. 끝에 "다시는 못 바꾸게 잠가라(immutable)" 가 붙기도 합니다.
greeter.yaml — 인사말을 ConfigMap 에서 환경변수로 읽는 Deployment:
apiVersion: v1
kind: ConfigMap
metadata:
name: greeter-config
namespace: guide-wl
data:
GREETING: hello
TARGET: world
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: greeter
namespace: guide-wl
spec:
replicas: 2
selector:
matchLabels: { app: greeter }
template:
metadata:
labels: { app: greeter }
spec:
terminationGracePeriodSeconds: 2 # 실습 속도용 — sh 는 SIGTERM 을 무시해서 기본 30초를 다 기다린다
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "echo \"$GREETING, $TARGET\"; sleep 3600"]
env:
- name: GREETING
valueFrom:
configMapKeyRef: { name: greeter-config, key: GREETING }
- name: TARGET
valueFrom:
configMapKeyRef: { name: greeter-config, key: TARGET }
kubectl apply -f greeter.yaml
kubectl rollout status deployment/greeter -n guide-wl
kubectl get pods -n guide-wl -l app=greeter
kubectl exec deploy/greeter -n guide-wl -- printenv GREETING
NAME READY STATUS RESTARTS AGE
greeter-7c9d8b6f5d-4kq2x 1/1 Running 0 15s
greeter-7c9d8b6f5d-tz8wm 1/1 Running 0 15s
hello
① 값 바꾸기 — edit 로 열어 GREETING: hello 를 GREETING: hola 로 고칩니다. (한 줄이면 patch 가 빠릅니다.)
kubectl edit configmap greeter-config -n guide-wl
# 또는
kubectl patch configmap greeter-config -n guide-wl -p '{"data":{"GREETING":"hola"}}'
kubectl get configmap greeter-config -n guide-wl -o jsonpath='{.data.GREETING}{"\n"}'
kubectl exec deploy/greeter -n guide-wl -- printenv GREETING
관찰 포인트 — Deployment 입장에서는 파드 템플릿이 하나도 바뀌지 않았습니다. 그래서 새 ReplicaSet 도, 롤아웃도 일어나지 않습니다. ConfigMap 을 고쳤다고 Deployment 가 알아서 파드를 다시 만들지는 않습니다.
② 파드를 새로 만들기 — rollout restart 는 파드 템플릿에 재시작 시각 annotation 을 넣어서 롤링 업데이트를 일으킵니다.
kubectl rollout restart deployment/greeter -n guide-wl
kubectl rollout status deployment/greeter -n guide-wl
kubectl get pods -n guide-wl -l app=greeter
deployment.apps/greeter restarted
Waiting for deployment "greeter" rollout to finish: 1 out of 2 new replicas have been updated...
...
deployment "greeter" successfully rolled out
NAME READY STATUS RESTARTS AGE
greeter-5f6b9c7d48-8mxlp 1/1 Running 0 6s ← 해시가 바뀐 새 파드
greeter-5f6b9c7d48-q2hvn 1/1 Running 0 9s
③ 확인 — 파드마다 하나씩 — exec deploy/... 는 파드 하나만 고릅니다. 전부 바뀌었는지 보려면 파드마다 확인합니다.
for p in $(kubectl get pods -n guide-wl -l app=greeter -o name); do
echo "$p: $(kubectl exec -n guide-wl $p -- printenv GREETING)"
done
kubectl get deployment greeter -n guide-wl -o jsonpath='{.spec.template.metadata.annotations}{"\n"}'
pod/greeter-5f6b9c7d48-8mxlp: hola
pod/greeter-5f6b9c7d48-q2hvn: hola
{"kubectl.kubernetes.io/restartedAt":"2026-10-04T01:12:31Z"}
막 재시작한 직후에는 옛 파드가 섞여 보입니다
rollout status 가 끝나기 전에 확인하면 Terminating 인 옛 파드도 목록에 나오고, 그 파드는 여전히 hello 를 출력합니다.
rollout status 가 끝난 뒤에 확인하세요.
rollout restart 대신 kubectl delete pod -l app=greeter -n guide-wl 로 파드를 지워도 새 값이 들어갑니다. 다만 한꺼번에 내려가서 잠깐 서비스가 비고, 롤아웃 기록에도 남지 않습니다. Deployment 라면 rollout restart 를 씁니다.
잠그기 — immutable: true¶
immutable: true 를 주면 그 ConfigMap 의 data · binaryData 를 아무도 고칠 수 없습니다. 운영 중 실수로 설정이 바뀌는 것을 막고, kubelet 이 그 ConfigMap 을 계속 감시(watch)하지 않아도 되어 API 서버 부담도 줄어듭니다. Secret 에도 같은 필드가 있습니다.
kubectl patch configmap greeter-config -n guide-wl -p '{"immutable":true}'
kubectl get configmap greeter-config -n guide-wl -o yaml | grep -E '^immutable|GREETING'
시험 팁 — 순서가 곧 정답입니다
"값 바꾸기 → 재배포 → 마지막에 immutable" 처럼 나오면 반드시 그 순서대로 합니다. 먼저 잠가 버리면 값을 고칠 수 없어서 지우고 다시 만드는 수고가 생깁니다.
immutable 은 metadata 아래가 아니라 data 와 같은 높이(최상위) 입니다. kubectl explain configmap.immutable 로 확인할 수 있습니다.
깨 보기 — 잠근 ConfigMap 을 고치려면¶
kubectl patch configmap greeter-config -n guide-wl -p '{"data":{"GREETING":"bonjour"}}'
kubectl patch configmap greeter-config -n guide-wl -p '{"immutable":false}'
The ConfigMap "greeter-config" is invalid: data: Forbidden: field is immutable when `immutable` is set
The ConfigMap "greeter-config" is invalid: immutable: Forbidden: field is immutable when `immutable` is set
관찰 포인트 — 값도, 잠금 자체도 되돌릴 수 없습니다. kubectl edit 로 열어 고쳐도 저장할 때 같은 이유로 거절되고, 고친 내용은 /tmp/kubectl-edit-XXXX.yaml 에 남습니다. (레이블 · annotation 같은 metadata 는 여전히 바꿀 수 있습니다.)
그래도 값을 바꿔야 한다면 지우고 다시 만드는 것이 유일한 방법입니다. 1) 에서 파드 고치기가 거절될 때 쓴 replace --force 가 바로 그것입니다.
kubectl get configmap greeter-config -n guide-wl -o yaml > greeter-config.yaml
# greeter-config.yaml 에서 GREETING: hola → bonjour 로 고친다 (immutable: true 는 그대로 둔다)
kubectl replace --force -f greeter-config.yaml
kubectl get configmap greeter-config -n guide-wl -o jsonpath='{.data.GREETING} {.immutable}{"\n"}'
새 ConfigMap 이 생긴 것이므로 파드는 여전히 hola 입니다. 반영하려면 다시 rollout restart 합니다.
kubectl rollout restart deployment/greeter -n guide-wl
kubectl rollout status deployment/greeter -n guide-wl
kubectl exec deploy/greeter -n guide-wl -- printenv GREETING # bonjour
운영에서는 이름을 바꿔 갈아 끼웁니다
immutable ConfigMap 은 보통 greeter-config-v2 처럼 새 이름으로 만들고, Deployment 의 참조를 바꿉니다.
파드 템플릿이 바뀌므로 롤아웃이 저절로 일어나고, rollout undo 로 옛 설정으로 되돌릴 수도 있습니다.
깨 보기 — 없는 ConfigMap 을 참조하면¶
kubectl run cm-missing -n guide-wl --image=busybox:1.36 --restart=Never $do -- sleep 3600 > cm-missing.yaml
# spec.containers[0] 아래에 추가:
# envFrom:
# - configMapRef:
# name: no-such-config
kubectl apply -f cm-missing.yaml
kubectl get pod cm-missing -n guide-wl
kubectl describe pod cm-missing -n guide-wl | tail -3
NAME READY STATUS RESTARTS
cm-missing 0/1 CreateContainerConfigError 0
Warning Failed 5s (x3 over 20s) kubelet Error: configmap "no-such-config" not found
관찰 포인트 — 파드는 만들어지고 스케줄도 됐지만, 컨테이너를 만들 설정을 못 구해서 멈췄습니다. 참조하는 ConfigMap 을 만들면 kubelet 이 재시도해서 저절로 Running 이 됩니다.
kubectl create configmap no-such-config -n guide-wl --from-literal=K=V
kubectl get pod cm-missing -n guide-wl -w # 잠시 뒤 Running
configMapRef 아래에 optional: true 를 주면 ConfigMap 이 없어도 그냥 시작합니다.
5) Secret — 문법은 ConfigMap 과 같고, base64 는 암호화가 아니다¶
왜 — 비밀번호 · 토큰 · 인증서는 ConfigMap 이 아니라 Secret 에 둡니다. 넣는 방법은 같지만 누가 읽을 수 있는지(RBAC) 를 따로 관리할 수 있고, 노드에서는 디스크가 아니라 메모리(tmpfs) 에 올라갑니다.
만들고, 되돌려 보기¶
kubectl create secret generic api-cred -n guide-wl \
--from-literal=username=svc-bot \
--from-literal=token='s3cr3t!'
kubectl get secret api-cred -n guide-wl -o yaml | grep -A2 '^data'
kubectl get secret api-cred -n guide-wl -o jsonpath='{.data.token}' | base64 -d; echo
base64 는 인코딩이지 암호화가 아닙니다
Secret 을 get 할 수 있는 사람은 값을 읽을 수 있습니다. 실제 보호는 RBAC 으로 Secret 읽기 권한을 제한하는 것(15강)과 etcd 저장 암호화 설정입니다.
--from-literal 에 쓴 값은 셸 히스토리에도 남습니다.
YAML 로 직접 쓸 때는 stringData 를 쓰면 평문으로 적어도 됩니다.
api-cred-yaml.yaml:
apiVersion: v1
kind: Secret
metadata:
name: api-cred-yaml
namespace: guide-wl
type: Opaque # ← 일반 키-값 Secret (generic 으로 만들면 이 타입)
stringData: # ← 평문으로 적으면 저장할 때 base64 로 바꿔 data 에 넣는다
username: svc-bot
token: "s3cr3t!"
kubectl apply -f api-cred-yaml.yaml
kubectl get secret api-cred-yaml -n guide-wl -o jsonpath='{.data}{"\n"}'
data: 에 평문을 그대로 적으면 illegal base64 data 에러가 납니다. 직접 인코딩할 때는 echo -n 으로 줄바꿈을 빼야 합니다(echo -n 's3cr3t!' | base64).
Secret 의 타입¶
kubectl create secret docker-registry regcred -n guide-wl \
--docker-server=registry.example.com --docker-username=demo --docker-password=demo $do | grep -E '^type|^ \.'
openssl req -x509 -nodes -newkey rsa:2048 -days 1 -subj "/CN=demo.local" -keyout tls.key -out tls.crt 2>/dev/null
kubectl create secret tls demo-tls -n guide-wl --cert=tls.crt --key=tls.key
kubectl get secrets -n guide-wl
.dockerconfigjson: eyJhdXRocyI6...
type: kubernetes.io/dockerconfigjson
NAME TYPE DATA AGE
api-cred Opaque 2 3m
api-cred-yaml Opaque 2 1m
demo-tls kubernetes.io/tls 2 2s
| 만드는 명령 | 타입 | 키 | 쓰는 곳 |
|---|---|---|---|
create secret generic |
Opaque |
마음대로 | 앱 설정 · 비밀번호 |
create secret tls |
kubernetes.io/tls |
tls.crt, tls.key |
Ingress TLS (8강) |
create secret docker-registry |
kubernetes.io/dockerconfigjson |
.dockerconfigjson |
파드의 imagePullSecrets |
파드에 넣기¶
cred-check.yaml:
apiVersion: v1
kind: Pod
metadata:
name: cred-check
namespace: guide-wl
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
env:
- name: API_TOKEN
valueFrom:
secretKeyRef: # ← configMapKeyRef 자리에 이것만 바뀐다
name: api-cred
key: token
volumeMounts:
- name: cred
mountPath: /etc/cred
readOnly: true
volumes:
- name: cred
secret: # ← configMap: 자리에 secret:
secretName: api-cred # ← 주의: ConfigMap 은 name, Secret 은 secretName
defaultMode: 0400 # ← 파일 권한 (소유자만 읽기)
kubectl apply -f cred-check.yaml
kubectl exec cred-check -n guide-wl -- sh -c 'echo $API_TOKEN; ls -l /etc/cred/; mount | grep /etc/cred'
s3cr3t! ← 컨테이너 안에서는 디코딩된 평문
total 0
lrwxrwxrwx 1 root root ... token -> ..data/token
lrwxrwxrwx 1 root root ... username -> ..data/username
tmpfs on /etc/cred type tmpfs (ro,relatime,...) ← 디스크가 아니라 메모리
| ConfigMap | Secret | |
|---|---|---|
| 한 키 → 환경변수 | configMapKeyRef |
secretKeyRef |
| 전체 → 환경변수 | envFrom.configMapRef |
envFrom.secretRef |
| 볼륨 | configMap.name |
secret.secretName |
| 저장 형태 | 평문 | base64 (암호화 아님) |
| 노드에서 | 디스크 | tmpfs (메모리) |
깨 보기 — 키 이름 오타¶
cred-check.yaml 의 key: token 을 key: tokn 으로 바꾸고 이름도 cred-typo 로 바꿔 적용해 봅니다.
sed -e 's/key: token/key: tokn/' -e 's/name: cred-check/name: cred-typo/' cred-check.yaml | kubectl apply -f -
kubectl get pod cred-typo -n guide-wl
kubectl describe pod cred-typo -n guide-wl | grep -m1 'Error:'
NAME READY STATUS RESTARTS
cred-typo 0/1 CreateContainerConfigError 0
Warning Failed ... kubelet Error: couldn't find key tokn in Secret guide-wl/api-cred
4) 와 같은 CreateContainerConfigError 입니다. 이 상태를 보면 ConfigMap · Secret 참조부터 의심하세요.
6) Job 과 CronJob — 끝나는 것이 정상인 워크로드¶
왜 — Deployment 의 파드는 끝나면 다시 띄웁니다. Job 의 파드는 성공적으로 끝나면 그걸로 완료입니다. 배치 · 마이그레이션 · 백업처럼 "한 번 하고 끝" 인 일에 씁니다.
Job¶
kubectl create job count -n guide-wl --image=busybox:1.36 -- sh -c 'for i in 1 2 3; do echo $i; sleep 1; done'
kubectl get job,pods -n guide-wl -l job-name=count
NAME STATUS COMPLETIONS DURATION
job.batch/count Complete 1/1 8s
NAME READY STATUS
pod/count-x7k2m 0/1 Completed
여러 번, 동시에 — completions · parallelism¶
kubectl create job 에는 이 필드들의 플래그가 없습니다. 뼈대를 뽑아 추가합니다.
kubectl create job batch-sq -n guide-wl --image=busybox:1.36 $do -- sh -c 'echo "work on $HOSTNAME"; sleep 5' > batch-sq.yaml
완성본 batch-sq.yaml:
apiVersion: batch/v1
kind: Job
metadata:
name: batch-sq
namespace: guide-wl
spec:
completions: 5 # ← 성공한 파드가 5개가 되면 완료
parallelism: 2 # ← 동시에 최대 2개까지
backoffLimit: 3 # ← 실패 재시도 한도 (기본 6)
activeDeadlineSeconds: 120 # ← 전체가 120초 안에 안 끝나면 실패 처리
ttlSecondsAfterFinished: 600 # ← 끝나고 10분 뒤 Job 과 파드를 자동 삭제
template:
spec:
restartPolicy: Never # ← Job 은 Never 또는 OnFailure 만 허용
containers:
- name: batch-sq
image: busybox:1.36
command: ["sh", "-c", "echo \"work on $HOSTNAME\"; sleep 5"]
NAME READY STATUS AGE
batch-sq-4kq2x 0/1 ContainerCreating 0s ← 처음엔 2개만
batch-sq-p8m3n 0/1 ContainerCreating 0s
batch-sq-4kq2x 1/1 Running 2s
batch-sq-p8m3n 1/1 Running 2s
batch-sq-4kq2x 0/1 Completed 7s
batch-sq-w9t6r 0/1 ContainerCreating 7s ← 하나 끝나면 하나 추가
...
관찰 포인트 — Running 인 파드가 2개를 넘지 않고, 5개가 Completed 가 되면 Job 이 끝납니다. 대략 5초 × ⌈5/2⌉ = 15초 남짓 걸립니다.
깨 보기 — 계속 실패하면¶
fail-demo.yaml:
apiVersion: batch/v1
kind: Job
metadata:
name: fail-demo
namespace: guide-wl
spec:
backoffLimit: 2 # ← 두 번까지만 다시 시도
template:
spec:
restartPolicy: Never # ← 실패하면 파드를 새로 만든다 (OnFailure 면 같은 파드 안에서 재시작)
containers:
- name: boom
image: busybox:1.36
command: ["sh", "-c", "echo boom; exit 1"]
kubectl apply -f fail-demo.yaml
kubectl get pods -n guide-wl -l job-name=fail-demo -w # 재시도 간격이 10초, 20초 … 로 늘어난다. 1분쯤 기다린다
kubectl get job fail-demo -n guide-wl
kubectl describe job fail-demo -n guide-wl | tail -5
NAME READY STATUS
fail-demo-6xk2p 0/1 Error
fail-demo-b4m9q 0/1 Error
fail-demo-t2w7z 0/1 Error ← 처음 1번 + 재시도 2번 = 3개
NAME STATUS COMPLETIONS DURATION
fail-demo Failed 0/1 45s
Warning BackoffLimitExceeded 10s job-controller Job has reached the specified backoff limit
Job 에 restartPolicy: Always 를 적으면 만들 때부터 거절됩니다. 이름을 바꿔 새로 만들어 봅니다(이미 있는 Job 의 템플릿은 바꿀 수 없어서 다른 에러가 섞입니다).
sed -e 's/name: fail-demo/name: always-demo/' -e 's/restartPolicy: Never/restartPolicy: Always/' fail-demo.yaml | kubectl apply -f -
The Job "always-demo" is invalid: spec.template.spec.restartPolicy: Unsupported value: "Always": supported values: "OnFailure", "Never"
restartPolicy |
동작 | 쓸 수 있는 곳 |
|---|---|---|
Always (기본) |
종료 코드와 무관하게 항상 재시작 | Pod · Deployment · DaemonSet |
OnFailure |
실패했을 때만 같은 파드 안에서 재시작 | Pod · Job · CronJob |
Never |
재시작 안 함 (Job 이면 새 파드로 재시도) | Pod · Job · CronJob |
CronJob¶
CronJob 은 정해진 시각마다 Job 을 만드는 리소스입니다. 파드를 직접 만들지 않습니다.
1~2분 기다리면 CronJob → Job → Pod 순서로 생긴 것이 보입니다.
NAME SCHEDULE LAST SCHEDULE
cronjob.batch/tick */1 * * * * 25s
NAME STATUS COMPLETIONS
job.batch/tick-29321520 Complete 1/1
NAME READY STATUS
pod/tick-29321520-4hq9z 0/1 Completed
1분을 기다리기 싫다면
CronJob 에서 Job 을 바로 하나 뽑아 테스트할 수 있습니다. 스케줄과 상관없이 즉시 실행됩니다.
CronJob 이 Job 을 무한히 쌓아 두지는 않습니다. 기본값으로 성공 3개, 실패 1개만 남깁니다.
kubectl get cronjob tick -n guide-wl -o jsonpath='{.spec.successfulJobsHistoryLimit} {.spec.failedJobsHistoryLimit}{"\n"}'
| cron 표현식 | 뜻 |
|---|---|
*/5 * * * * |
5분마다 |
0 3 * * * |
매일 03:00 |
30 9 * * 1-5 |
평일 09:30 |
0 0 1 * * |
매월 1일 00:00 |
순서는 분 · 시 · 일 · 월 · 요일입니다. 시각은 기본적으로 kube-controller-manager 의 시간대(보통 UTC)를 따르고, spec.timeZone: "Asia/Seoul" 로 바꿀 수 있습니다.
CronJob 에서 자주 쓰는 필드입니다.
| 필드 | 값 | 뜻 |
|---|---|---|
concurrencyPolicy |
Allow (기본) |
앞 Job 이 안 끝나도 새 Job 을 만든다 |
Forbid |
앞 Job 이 돌고 있으면 이번 회차를 건너뛴다 | |
Replace |
앞 Job 을 지우고 새 Job 으로 바꾼다 | |
suspend |
true |
새 Job 을 만들지 않는다 (이미 돌던 것은 계속) |
startingDeadlineSeconds |
초 | 그 시각을 놓쳤을 때 몇 초까지는 늦게라도 실행 |
일시정지해 봅니다.
kubectl patch cronjob tick -n guide-wl -p '{"spec":{"suspend":true}}'
kubectl get cronjob tick -n guide-wl
1~2분이 지나도 새 Job 이 생기지 않습니다. 다시 켜려면 "suspend":false.
시험 팁
CronJob 의 파드 템플릿은 spec.jobTemplate.spec.template.spec 으로 한 단계 더 깊습니다. 필드를 추가할 때 들여쓰기 위치를 kubectl explain cronjob.spec.jobTemplate.spec.template.spec 로 확인하세요.
7) DaemonSet — 노드마다 하나씩, 그리고 Taint¶
왜 — 로그 수집기 · 모니터링 에이전트 · CNI 처럼 노드마다 정확히 하나 있어야 하는 것은 개수를 적지 않고 노드 수를 따라가게 합니다. 노드가 늘면 자동으로 따라 붙습니다.
DaemonSet 은 kubectl create 명령이 없습니다. Deployment YAML 을 뽑아서 고칩니다.
kubectl create deployment node-agent -n guide-wl --image=busybox:1.36 $do -- sh -c 'sleep infinity' > node-agent.yaml
node-agent.yaml 을 열어 세 군데를 고칩니다.
kind: Deployment→kind: DaemonSetspec.replicas줄 삭제 (노드 수가 곧 파드 수입니다)spec.strategy줄 삭제 (DaemonSet 은updateStrategy를 씁니다)
완성본:
apiVersion: apps/v1
kind: DaemonSet # ← Deployment 에서 바꿨다
metadata:
labels:
app: node-agent
name: node-agent
namespace: guide-wl
spec: # ← replicas · strategy 는 지웠다
selector:
matchLabels:
app: node-agent # ← 아래 template labels 와 같아야 한다
template:
metadata:
labels:
app: node-agent
spec:
containers:
- name: busybox
image: busybox:1.36
command: ["sh", "-c", "sleep infinity"]
kubectl apply -f node-agent.yaml
kubectl get ds -n guide-wl
kubectl get pods -n guide-wl -l app=node-agent -o wide
NAME DESIRED CURRENT READY
node-agent 2 2 2
NAME READY STATUS NODE
node-agent-7wq4k 1/1 Running worker-1
node-agent-m2n8c 1/1 Running worker-2
replicas 를 지우지 않으면
kubectl apply 가 strict decoding error: unknown field "spec.replicas" 로 거절합니다. strategy 도 마찬가지입니다.
노드는 3대인데 파드는 2개입니다. 컨트롤플레인에 무엇이 붙어 있는지 봅니다.
이 Taint 를 견디는 Toleration 을 spec.template.spec 에 추가합니다.
spec:
tolerations:
- key: node-role.kubernetes.io/control-plane # ← Taint 의 키
operator: Exists # ← 값은 따지지 않는다
effect: NoSchedule # ← Taint 의 효과와 같게
containers:
...
이제 컨트롤플레인까지 3개입니다. kube-proxy 나 CNI 에이전트가 모든 노드에 떠 있는 것도 같은 원리입니다.
kubectl get ds -n kube-system
kubectl get ds kube-proxy -n kube-system -o jsonpath='{.spec.template.spec.tolerations}{"\n"}'
업데이트 전략 — RollingUpdate 와 OnDelete¶
기본은 노드 하나씩 옛 파드를 지우고 새 파드를 띄우는 RollingUpdate 입니다. 이미지를 바꿔 봅니다.
kubectl set image ds/node-agent -n guide-wl busybox=busybox:1.37
kubectl rollout status ds/node-agent -n guide-wl
kubectl rollout history ds/node-agent -n guide-wl
Waiting for daemon set "node-agent" rollout to finish: 1 out of 3 new pods have been updated...
Waiting for daemon set "node-agent" rollout to finish: 2 out of 3 new pods have been updated...
daemon set "node-agent" successfully rolled out
REVISION CHANGE-CAUSE
1 <none>
2 <none>
3 <none>
rollout status · history · undo 가 DaemonSet 에도 똑같이 동작합니다(리비전 1·2 는 toleration 추가 전후).
대조 실험 — OnDelete: 파드를 지워야만 새 템플릿이 적용됩니다.
kubectl patch ds node-agent -n guide-wl -p '{"spec":{"updateStrategy":{"type":"OnDelete","rollingUpdate":null}}}'
kubectl set image ds/node-agent -n guide-wl busybox=busybox:1.36
kubectl get pods -n guide-wl -l app=node-agent -o custom-columns='NAME:.metadata.name,NODE:.spec.nodeName,IMAGE:.spec.containers[0].image'
NAME NODE IMAGE
node-agent-4tq8z control-plane busybox:1.37 ← 템플릿은 1.36 인데 그대로
node-agent-9vk2m worker-1 busybox:1.37
node-agent-r6p3x worker-2 busybox:1.37
kubectl delete pod -n guide-wl -l app=node-agent --field-selector spec.nodeName=worker-1
kubectl get pods -n guide-wl -l app=node-agent -o custom-columns='NAME:.metadata.name,NODE:.spec.nodeName,IMAGE:.spec.containers[0].image'
NAME NODE IMAGE
node-agent-4tq8z control-plane busybox:1.37
node-agent-xw5n7 worker-1 busybox:1.36 ← 지운 노드만 새 이미지
node-agent-r6p3x worker-2 busybox:1.37
관찰 포인트 — OnDelete 는 노드별로 사람이 시점을 고르며 바꿀 때 씁니다(예: 노드 점검 순서에 맞춰 에이전트 교체).
| Deployment | DaemonSet | Job | |
|---|---|---|---|
| 파드 수 | replicas |
노드 수 (Taint · nodeSelector 로 거른) | completions |
| 끝나면 | 다시 띄운다 | 다시 띄운다 | 완료 |
| 업데이트 | strategy (RollingUpdate · Recreate) |
updateStrategy (RollingUpdate · OnDelete) |
템플릿 대부분 불변 |
kubectl create |
있음 | 없음 | 있음 |
8) 네이티브 사이드카 — initContainer 인데 끝나지 않는다¶
왜 — 로그 수집기나 프록시처럼 본 앱보다 먼저 뜨고, 본 앱보다 나중에 내려가야 하는 보조 컨테이너가 있습니다. containers 에 나란히 넣으면 시작 순서가 보장되지 않습니다. 1.29 부터 기본 활성화된 네이티브 사이드카가 이 문제를 풉니다.
FIELD: restartPolicy <string>
DESCRIPTION:
RestartPolicy defines the restart behavior of individual containers in a
pod. This field may only be set for init containers, and the only allowed
value is "Always". ...
로그를 쓰는 앱¶
writer.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: writer
namespace: guide-wl
spec:
replicas: 1
selector:
matchLabels: { app: writer }
template:
metadata:
labels: { app: writer }
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "while true; do date >> /data/out.log; sleep 2; done"]
volumeMounts:
- name: shared # ← 사이드카와 같은 볼륨을 쓴다
mountPath: /data
volumes:
- name: shared
emptyDir: {} # ← 파드가 살아 있는 동안만 있는 빈 디렉터리
READY 1/1 입니다.
먼저, 일반 initContainer 로 붙여 보기¶
spec.template.spec 에 아래를 넣고 적용해 봅니다. (containers 와 같은 높이)
initContainers:
- name: reader
image: busybox:1.36
command: ["sh", "-c", "tail -F /data/out.log"]
volumeMounts:
- name: shared
mountPath: /data
NAME READY STATUS
writer-5c8d7b9f6-q2wzx 0/1 Init:0/1 ← 새 파드
writer-8b4c6d7e9-lp4rn 1/1 Running ← 옛 파드 (롤링 업데이트가 안 끝나서 남아 있다)
새 파드가 영원히 Init:0/1 입니다. 일반 initContainer 는 끝나야 본 컨테이너가 시작하는데, tail -F 는 끝나지 않습니다.
새 파드가 Ready 가 되지 않으니 Deployment 는 옛 파드를 지우지 못하고 기다립니다. 3) 에서 본 maxUnavailable 의 보호가 여기서도 작동한 것입니다.
restartPolicy: Always 한 줄¶
reader 에 한 줄을 추가합니다.
initContainers:
- name: reader
image: busybox:1.36
restartPolicy: Always # ← 이 한 줄이 네이티브 사이드카를 만든다
command: ["sh", "-c", "tail -F /data/out.log"]
volumeMounts:
- name: shared
mountPath: /data
kubectl apply -f writer.yaml
kubectl rollout status deployment/writer -n guide-wl
kubectl get pods -n guide-wl -l app=writer
kubectl logs deploy/writer -n guide-wl -c reader --tail=3
NAME READY STATUS
writer-7f6b8c4d5-h9m3t 2/2 Running
Mon Sep 29 06:12:41 UTC 2026
Mon Sep 29 06:12:43 UTC 2026
Mon Sep 29 06:12:45 UTC 2026
READY 2/2 가 되었고 사이드카가 본 컨테이너의 로그를 읽고 있습니다.
| 일반 initContainer | 네이티브 사이드카 | |
|---|---|---|
| 선언 위치 | initContainers |
initContainers |
restartPolicy |
없음 | Always |
| 본 컨테이너 시작 시점 | init 이 끝난 뒤 | 사이드카가 시작된 뒤 (끝날 필요 없음) |
| 파드가 도는 동안 | 이미 종료 | 함께 살아 있음, 죽으면 재시작 |
| READY 칸에 | 세지 않음 | 셉니다 |
그냥 containers 에 두 개 넣는 것과 무엇이 다른가요?
사이드카가 본 컨테이너보다 먼저 뜨고, 나중에 내려가는 것이 보장됩니다. 로그 수집기나 프록시처럼 본 앱보다 먼저 준비돼 있어야 하는 컨테이너에 씁니다. Job 에서도 본 컨테이너가 끝나면 사이드카 때문에 Job 이 안 끝나는 문제가 사라집니다.
시험 팁
기존 Deployment 에 사이드카를 붙이는 문제는 kubectl edit deployment <이름> 이 가장 빠릅니다. initContainers 는 containers 와 같은 들여쓰기, 볼륨 이름은 본 컨테이너가 쓰는 것과 같은 이름입니다.
저장 후 kubectl rollout status 로 완료를 보고, kubectl logs deploy/<이름> -c <사이드카이름> 으로 동작을 확인합니다.
시험에서 빠르게¶
# 뼈대
export do="--dry-run=client -o yaml"
kubectl run <이름> --image=<이미지> --restart=Never $do -- <명령...> > pod.yaml
kubectl create deployment <이름> --image=<이미지> --replicas=N $do > deploy.yaml # DaemonSet 도 여기서 출발
kubectl create job <이름> --image=<이미지> $do -- <명령...> > job.yaml
kubectl create cronjob <이름> --image=<이미지> --schedule='<cron>' $do -- <명령...>
kubectl explain <리소스>.<경로> [--recursive]
# Deployment
kubectl scale deployment <이름> --replicas=N
kubectl set image deployment/<이름> <컨테이너>=<이미지>
kubectl rollout status|history|undo|pause|resume|restart deployment/<이름>
kubectl rollout undo deployment/<이름> --to-revision=N
# 설정
kubectl create configmap <이름> --from-literal=K=V [--from-file=<파일>] [--from-env-file=<파일>]
kubectl create secret generic <이름> --from-literal=K=V
kubectl get secret <이름> -o jsonpath='{.data.<키>}' | base64 -d
kubectl patch configmap <이름> -p '{"data":{"K":"V2"}}' # 값 바꾸기 → env 로 읽으면 rollout restart 필요
kubectl patch configmap <이름> -p '{"immutable":true}' # 잠그기 (되돌릴 수 없다 — 바꾸려면 replace --force)
# Job · CronJob
kubectl create job <이름> --from=cronjob/<크론잡>
kubectl get jobs; kubectl logs job/<이름>
# 레이블
kubectl label <리소스> <이름> k=v [--overwrite] | k-
kubectl get pods -l 'k in (a,b)' --show-labels
# 파드 고치기가 거절되면
kubectl replace --force -f /tmp/kubectl-edit-XXXX.yaml
시험 팁
- 채점은 최종 상태를 봅니다. 마지막에
kubectl get <리소스> -o yaml | grep -A3 <필드>로 실제로 들어갔는지 확인하는 습관을 들이세요. CreateContainerConfigError= ConfigMap · Secret 참조 문제,Init:0/1에서 멈춤 = initContainer 가 안 끝남,ImagePullBackOff= 이미지 이름 · 태그.
정리¶
확인 질문¶
1. kubectl rollout undo 를 하면 새 ReplicaSet 이 생기나요?
생기지 않습니다. 0 으로 남겨 둔 옛 ReplicaSet 의 DESIRED 를 다시 올립니다. 다만 리비전 번호는 새로 매겨집니다. 그래서 history 에서 옛 번호가 사라지고 새 번호가 생깁니다.
2. ConfigMap 값을 바꿨는데 파드의 환경변수가 그대로입니다. 왜 그런가요?
환경변수는 컨테이너가 시작할 때 한 번 정해지기 때문입니다. 파드를 다시 만들어야 합니다
(Deployment 라면 kubectl rollout restart). 볼륨으로 넣은 값은 자동으로 갱신됩니다(subPath 제외).
3. DaemonSet 파드가 노드 수보다 적게 떠 있습니다. 가장 먼저 무엇을 보나요?
노드의 Taint 입니다. kubectl describe node <노드> | grep Taints.
그 Taint 를 견디는 Toleration 이 파드 템플릿에 없으면 그 노드에는 뜨지 않습니다.
4. 네이티브 사이드카를 만드는 조건 두 가지는?
initContainers 아래에 선언하고, restartPolicy: Always 를 줍니다.
5. CronJob 을 1분 기다리지 않고 바로 테스트하는 방법은?
kubectl create job <이름> --from=cronjob/<크론잡이름>
6. replicas 4, maxSurge: 1, maxUnavailable: 0 인 Deployment 를 업데이트하면 파드는 최대 몇 개, 사용 가능한 파드는 최소 몇 개인가요?
최대 5개, 최소 4개입니다. 새 파드를 하나 먼저 띄우고, 그것이 사용 가능해진 뒤에야 옛 파드를 하나 내립니다. 그래서 잘못된 이미지로 업데이트해도 옛 파드는 그대로 남아 서비스가 멈추지 않습니다.
7. 파드가 CreateContainerConfigError 입니다. 무엇을 의심하나요?
참조하는 ConfigMap · Secret 이 없거나 키 이름이 틀린 경우입니다. kubectl describe pod 의 Events 에
configmap "..." not found 나 couldn't find key ... in Secret ... 이 찍힙니다.
8. Job 에 restartPolicy: Always 를 쓰면 어떻게 되나요? backoffLimit 은 무엇을 정하나요?
만들 때 거절됩니다 — Job 은 OnFailure 나 Never 만 허용합니다(끝나야 하는 작업이니까).
backoffLimit 은 실패 재시도 횟수 한도이고, 넘기면 Job 이 Failed 가 되며 BackoffLimitExceeded 이벤트가 남습니다.
9. 파드에서 레이블 하나를 떼었더니 파드가 하나 더 생겼습니다. 왜 그런가요?
ReplicaSet 은 레이블 셀렉터로 자기 파드를 셉니다. 레이블을 뗀 파드는 셀렉터에서 빠져 주인 없는 파드가 되고, ReplicaSet 은 개수가 모자란다고 보고 새 파드를 만듭니다. 뗀 파드는 계속 돌기 때문에 직접 지워야 합니다.
10. immutable: true 인 ConfigMap 의 값을 바꿔야 합니다. 어떻게 하나요?
edit · patch 로는 거절됩니다(data: Forbidden: field is immutable when immutable is set). immutable: false 로 되돌리는 것도 안 됩니다.
get -o yaml > f 로 뽑아 고친 뒤 kubectl replace --force -f f 로 지우고 다시 만들고,
env 로 읽는 파드라면 rollout restart 까지 해야 새 값이 들어갑니다.
다음 단계 — 채점 실습¶
개념이 잡혔으면 시험 형식으로 풀어 봅니다. 이번에는 답이 없습니다.
이 페이지는 세 강의에 걸쳐 있어 채점 세트도 세 개입니다. 강의를 본 순서대로 푸세요.
4강 → 04-pods (1) ~ 3))
| 문제 | 이 페이지에서 연습한 것 |
|---|---|
| Q6 Deployment 만들고 버전 바꾸기 | 2) 3) |
| Q7 멈춘 롤아웃 되돌리기 | 2) 롤백 · rollout undo |
Q1 ~ Q5(파드 · 라벨 · ReplicaSet)는 4강 영상 범위입니다. 이 페이지에는 따로 단계가 없습니다.
6강 → 06-config (4) · 5))
| 문제 | 이 페이지에서 연습한 것 |
|---|---|
| Q1 ConfigMap 생성 + 파드에 환경변수로 주입 | 1) 뼈대 뽑기 · 4) |
| Q2 키 하나만 골라 다른 이름의 환경변수로 | 4) |
| Q3 파일로 만든 ConfigMap 을 볼륨으로 마운트 | 4) 볼륨으로 넣기 |
| Q4 Secret — 풀어 읽기 · 만들기 · 환경변수로 넣기 | 5) |
| Q5 ConfigMap 수정 → 재배포 → immutable | 4) Deployment 에 반영하기 · 잠그기 |
10강 → 10-workloads (6) ~ 8))
| 문제 | 이 페이지에서 연습한 것 |
|---|---|
| Q1 Deployment 생성 · 스케일 · 롤링 업데이트 · 롤백 | 2) 3) (4강 복습) |
| Q2 CronJob | 6) |
| Q3 DaemonSet (hostNetwork · hostPID) | 1) 7) — 필드 위치는 kubectl explain pod.spec 으로 |
| Q4 네이티브 사이드카 | 8) |