공유한다
네트워크 네임스페이스 — 같은 IP, 같은 포트 공간 → 서로를 localhost로 부른다.
볼륨 — spec.volumes를 각 컨테이너가 마운트.
IPC 네임스페이스(기본)와 수명 — 함께 스케줄되고 함께 정리된다.
컨테이너가 아니라 Pod이 단위인 이유
같은 노드에서, 같은 네트워크와 볼륨을 공유하며, 함께 뜨고 함께 죽는 컨테이너 묶음.
Pod은 일회용이다. 재시작되면 이름도 IP도 바뀐다. 그래서 Pod을 직접 만들지 않고 Deployment 같은 컨트롤러에 맡긴다 (워크로드).
공유한다
네트워크 네임스페이스 — 같은 IP, 같은 포트 공간 → 서로를 localhost로 부른다.
볼륨 — spec.volumes를 각 컨테이너가 마운트.
IPC 네임스페이스(기본)와 수명 — 함께 스케줄되고 함께 정리된다.
공유하지 않는다
파일시스템 — 컨테이너마다 별개 (볼륨으로 명시적으로 공유해야 한다).
PID 네임스페이스 — 기본은 분리, shareProcessNamespace로 켤 수 있다.
리소스 request/limit은 컨테이너별로 정한다.
네트워크를 공유하니 같은 Pod 안에서 포트가 겹치면 안 된다. 두 컨테이너가 모두 8080을 열 수 없다.
pause(sandbox) 컨테이너가 하나 더 있다kubectl get pods에는 안 보이고, 노드에서 crictl ps로 보면 보인다sudo crictl pods # Pod 샌드박스 = pause 컨테이너이걸 알면 “컨테이너는 죽었는데 Pod IP는 그대로”가 자연스러워진다. Pod IP가 바뀌는 것은 Pod 자체가 새로 만들어질 때다.
pause 자체가 문제로 나오는 일은 드물다 — “컨테이너 재시작 ≠ Pod 재생성”을 구분하고,
노드에서 crictl ps에 앱 컨테이너만 보이는 이유를 이해하는 배경 지식으로 이만큼이면 충분하다.
이 YAML이 시작하기 전에에서 말한 “선언된 상태”(spec) 그 자체다. kubectl apply로 API 서버에
넣는 순간 일이 끝나는 게 아니라 시작된다 — 스케줄러가 노드를 정하고 kubelet이 컨테이너를
띄우며, 실제 상태(status)를 여기 적힌 모습에 맞춰 간다.
apiVersion: v1kind: Podmetadata: name: web labels: app: webspec: containers: - name: nginx image: nginx:1.27 ports: - containerPort: 80 env: - name: LOG_LEVEL value: debug resources: requests: { cpu: 100m, memory: 128Mi } limits: { cpu: 500m, memory: 256Mi } volumeMounts: - name: data mountPath: /usr/share/nginx/html volumes: - name: data emptyDir: {} restartPolicy: Alwayscontainers: - name: app image: busybox command: ["sh", "-c", "echo hello; sleep 3600"]| Kubernetes | Dockerfile | 생략하면 |
|---|---|---|
command | ENTRYPOINT | 이미지의 ENTRYPOINT 사용 |
args | CMD | 이미지의 CMD 사용 |
어느 쪽을 지정했는지에 따라 실제로 실행되는 것이 달라진다. 공식 명령/인자 문서의 표를 요약하면:
command | args | 실행되는 것 |
|---|---|---|
| 없음 | 없음 | 이미지 ENTRYPOINT + CMD |
| 없음 | 지정 | 이미지 ENTRYPOINT + args (CMD 무시) |
| 지정 | 없음 | command만 (ENTRYPOINT·CMD 둘 다 무시) |
| 지정 | 지정 | command + args (둘 다 무시) |
# 명령형으로 만들 때는 -- 뒤에 쓴다 (args로 들어간다)kubectl run busy --image=busybox --restart=Never -- sleep 3600kubectl run busy --image=busybox --restart=Never --command -- sleep 3600 # command로사이드카 (sidecar)
주 컨테이너를 보조한다. 로그 수집기, 서비스 메시 프록시.
앰배서더 (ambassador)
바깥과의 통신을 대신한다. DB 프록시, 커넥션 풀.
어댑터 (adapter)
출력 형식을 변환한다. 앱 로그를 Prometheus 형식으로.
세 이름 중 커리큘럼에 올라 있는 것은 사이드카뿐이다 (아래 “네이티브 사이드카”). 앰배서더·어댑터는 분류용 이름이라 깊게 팔 필요는 없다 — 이름만 알아두자.
함께 넣을지는 세 질문으로 판단한다.
셋 다 “예”가 아니면 별도 Pod으로 나누고 Service로 연결하는 게 낫다.
spec: initContainers: - name: wait-for-db image: busybox:1.36 command: ['sh', '-c', 'until nslookup db; do sleep 2; done'] containers: - name: app image: myapp:1.0containers가 시작한다restartPolicy에 따라 재시도한다 (Never면 Pod이 실패)Init:0/2, Init:CrashLoopBackOff 처럼 나온다initContainers에 restartPolicy: Always 를 주면 사이드카가 된다.
spec: initContainers: - name: log-shipper image: fluent-bit:3.0 restartPolicy: Always # ← 이 한 줄이 사이드카로 만든다 volumeMounts: - name: logs mountPath: /var/log/app containers: - name: app image: myapp:1.0예전 방식(그냥 containers에 두 개)도 여전히 동작한다. 다만 순서 보장이 없다.
네이티브 사이드카는 커리큘럼에 이름이 올라 있는 항목이다 —
“initContainers에 넣고 restartPolicy: Always를 준다”는 조합 자체를 기억해 두자.
시험 중 init 컨테이너·사이드카 YAML 뼈대가 필요하면 공식 Init Containers 문서의 예시를 복사해 이름·이미지·command만 바꾸는 게 가장 빠르다.
매니페스트가 “선언된 상태”라면 이 절은 전부 반대쪽, “실제 상태”(status) 이야기다 — 시작하기 전에의 루프에서 차이를 판정할 때 읽는 재료가 여기 모여 있다. 문제가 생기면 결국 이 status를 읽는 것부터 시작한다.
kubectl get pod web -o jsonpath='{.status.phase}'| phase | 의미 |
|---|---|
| Pending | 아직 스케줄되지 않았거나, 이미지를 받는 중 |
| Running | 노드에 배정되었고 컨테이너가 최소 하나 이상 살아 있다 |
| Succeeded | 모든 컨테이너가 성공 종료. 재시작하지 않는다 |
| Failed | 모든 컨테이너가 종료되었고 하나 이상이 실패 |
| Unknown | 노드와 통신이 안 되어 상태를 모른다 |
kubectl get pod web -o jsonpath='{.status.containerStatuses[0].state}'kubectl describe pod web # 사람이 읽기엔 이쪽컨테이너는 셋 중 하나다: Waiting / Running / Terminated. 각각 reason이 붙는다.
| reason | 뜻 | 볼 곳 |
|---|---|---|
ContainerCreating | 볼륨 마운트·네트워크 설정 중 | Events |
ImagePullBackOff / ErrImagePull | 이미지를 못 받는다 | Events (이름·태그·인증) |
CrashLoopBackOff | 시작했다가 계속 죽는다 | logs --previous |
CreateContainerConfigError | ConfigMap/Secret이 없다 | Events |
OOMKilled | 메모리 limit 초과로 커널이 죽였다 | describe 의 Last State |
Error | 0이 아닌 코드로 종료 | logs |
spec: restartPolicy: Always # Always(기본) | OnFailure | Never| 값 | 재시작 조건 | 쓰는 곳 |
|---|---|---|
Always | 종료 코드와 무관하게 항상 | Deployment/StatefulSet/DaemonSet의 Pod |
OnFailure | 0이 아닌 코드로 끝났을 때만 | Job |
Never | 재시작하지 않음 | 일회성 작업, 디버그 Pod |
restartPolicy: Always만 예외)Always만 쓸 수 있다에러 이름이 아니다. “재시작을 지연시키고 있다”는 상태다.
진단 순서
kubectl describe pod web # Last State / Exit Code / Reason 확인kubectl logs web --previous # ★ 죽기 직전 로그kubectl get pod web -o yaml # 정확한 exitCode| Exit Code | 대개의 원인 |
|---|---|
0 | 정상 종료인데 restartPolicy: Always — 명령이 바로 끝난다 |
1 / 2 | 애플리케이션 에러. 로그를 보라 |
137 | SIGKILL — OOMKilled 이거나 강제 종료 |
143 | SIGTERM — 정상적인 종료 신호를 받았다 |
프로브가 없으면 kubelet은 프로세스가 살아 있는지만 안다 — 행에 걸려 응답을 못 해도 재시작되지 않고, 준비가 안 된 컨테이너에도 트래픽이 간다. “제대로 동작하는가”는 프로브로 kubelet에게 알려줘야 한다.
가장 중요한 구분: liveness는 죽이고, readiness는 트래픽만 끊는다.
livenessProbe: httpGet: # 2xx~3xx면 성공 path: /healthz port: 8080 httpHeaders: - name: Custom-Header value: check initialDelaySeconds: 10 periodSeconds: 10 timeoutSeconds: 1 failureThreshold: 3 successThreshold: 1
readinessProbe: exec: # 종료 코드 0이면 성공 command: ["cat", "/tmp/ready"]
startupProbe: tcpSocket: # 연결이 되면 성공 port: 8080 failureThreshold: 30 periodSeconds: 10 # 최대 300초까지 기다린다grpc: 방식도 있다 (port + service). gRPC 헬스체크 규약을 쓰는 앱용.
시험에 주로 나오는 건 httpGet과 exec다 — grpc:는 이런 게 있다는 것만 알아두면 된다.
| 필드 | 기본값 | 의미 |
|---|---|---|
initialDelaySeconds | 0 | 컨테이너 시작 후 첫 검사까지 대기 |
periodSeconds | 10 | 검사 주기 |
timeoutSeconds | 1 | 응답 대기 시간 |
failureThreshold | 3 | 몇 번 연속 실패해야 실패로 볼 것인가 |
successThreshold | 1 | 몇 번 연속 성공해야 복귀할 것인가 (liveness는 1 고정) |
실제 반응 시간 = initialDelaySeconds + periodSeconds × failureThreshold
기본값이면 컨테이너가 죽고 나서 최대 30초 뒤에 재시작이 걸린다.
1번과 2번은 동시에 시작한다. 그래서 엔드포인트 제거(Service의 대상 목록에서 빼는 것 — Service)가
전파되기 전에 SIGTERM이 도착할 수 있다 — preStop에 sleep 5 를 넣는 관행이 여기서 나온다.
containers: - name: app image: myapp:1.0 lifecycle: postStart: exec: command: ["sh", "-c", "echo started > /tmp/status"] preStop: exec: command: ["sh", "-c", "sleep 5; nginx -s quit"]spec: terminationGracePeriodSeconds: 60postStart — 컨테이너 시작과 동시에 실행된다 (ENTRYPOINT보다 먼저라는 보장은 없다)preStop — SIGTERM 전에 실행된다. 여기서 시간을 쓰면 grace period에서 차감된다훅 자체를 깊게 묻는 일은 드물다 — 위 종료 흐름에서 preStop에 sleep을 넣는
관행이 왜 생겼는지 이해하는 정도면 충분하다.
env: - name: LOG_LEVEL # 직접 value: "debug"
- name: MY_NODE # Downward API — Pod 자신의 정보 valueFrom: fieldRef: fieldPath: spec.nodeName # status.podIP, metadata.name 등도 가능
- name: CPU_LIMIT # 자기 리소스 값 valueFrom: resourceFieldRef: containerName: app resource: limits.cpu
- name: DB_PASSWORD # Secret에서 (설정과 리소스) valueFrom: secretKeyRef: name: db-secret key: passwordfieldRef·resourceFieldRef로 값을 끌어오는 통로가 Downward API다 —
Pod이 자기 자신의 정보(이름·노드·IP·리소스 값)를 API 서버에 물어보지 않고
환경변수나 파일로 받는 방법이다. 앱이 “내가 어느 노드에 떠 있나”를 로그에 남길 때 같은 데 쓴다.
깊게 팔 필요는 없다 — 이름과 “자기 정보를 받는 통로”라는 용도만 알아두자.
envFrom: 을 쓰면 ConfigMap/Secret의 모든 키를 한 번에 환경변수로 넣을 수 있다 (설정과 리소스).
spec: securityContext: # Pod 수준 — 모든 컨테이너에 적용 runAsUser: 1000 runAsGroup: 3000 fsGroup: 2000 # 볼륨의 그룹 소유권 containers: - name: app image: myapp:1.0 securityContext: # 컨테이너 수준 — Pod 설정을 덮어쓴다 runAsNonRoot: true allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: add: ["NET_ADMIN"] drop: ["ALL"]capabilities는 컨테이너 수준에만 있다fsGroup은 Pod 수준에만 있다스태틱 Pod은 kubelet이 API 서버 없이, 디렉터리에 놓인 매니페스트 파일만 보고 직접 만드는 Pod이다. 컨트롤 플레인 컴포넌트 자신이 이 방식으로 뜬다 (아키텍처). API 서버에 보이는 것은 kubelet이 보고용으로 올린 미러(mirror) Pod라서, API 쪽에서 지워도 kubelet이 파일을 보고 다시 만든다.
어느 디렉터리를 보는지 확인
sudo grep staticPodPath /var/lib/kubelet/config.yaml그 디렉터리에 YAML을 놓는다 — 그게 전부다
sudo kubectl run web --image=nginx --dry-run=client -o yaml \ | sudo tee /etc/kubernetes/manifests/web.yaml잠시 후 확인 — 이름 뒤에 노드 이름이 붙는다
kubectl get pods# web-node01 1/1 Runningkubectl delete pod는 소용없다ssh로 들어가서 만들어야 한다resources는 원래 만들고 나면 못 바꾸는 필드였다 — 조정하려면 Pod을 지우고
다시 만드는 수밖에 없었고, 그때마다 재시작이 따라왔다.
이제 Pod을 다시 만들지 않고 CPU·메모리를 바꿀 수 있다.
containers: - name: app resizePolicy: - resourceName: cpu restartPolicy: NotRequired # 재시작 없이 적용 (기본) - resourceName: memory restartPolicy: RestartContainer # 컨테이너를 다시 시작해서 적용kubectl patch pod web --subresource=resize \ -p '{"spec":{"containers":[{"name":"app","resources":{"requests":{"cpu":"200m"}}}]}}'status.containerStatuses[*].resources 에 실제 적용된 값이 보인다v1.35에서 GA가 되며 커리큘럼에 올라온 항목이다 —
kubectl patch에 --subresource=resize를 붙이는 것이 핵심이다.
pause 컨테이너가 네트워크 네임스페이스를 쥐고 있어 컨테이너가 죽어도 IP가 유지된다restartPolicy: Always를 주면 사이드카(v1.33 GA)CrashLoopBackOff는 에러가 아니라 지연 중이라는 뜻 → logs --previous