공유한다
네트워크 네임스페이스 — 같은 IP, 같은 포트 공간 → 서로를 localhost로 부른다.
볼륨 — spec.volumes를 각 컨테이너가 마운트.
IPC 네임스페이스(기본)와 수명 — 함께 스케줄되고 함께 정리된다.
컨테이너가 아니라 Pod이 단위인 이유
같은 노드에서, 같은 네트워크와 볼륨을 공유하며, 함께 뜨고 함께 죽는 컨테이너 묶음.
Pod은 일회용이다. 재시작되면 이름도 IP도 바뀐다. 그래서 Pod을 직접 만들지 않고 Deployment 같은 컨트롤러에 맡긴다 (5장).
flowchart TB
subgraph POD["Pod — 10.244.1.5"]
PAUSE["pause 컨테이너<br/>네트워크 네임스페이스 소유"]
C1["컨테이너 app<br/>:8080"]
C2["컨테이너 sidecar<br/>:9090"]
VOL[("volumes<br/>emptyDir · configMap …")]
end
C1 -->|"localhost:9090"| C2
C1 --> VOL
C2 --> VOL
C1 -.->|"합류"| PAUSE
C2 -.->|"합류"| PAUSE
FS1["app 의 파일시스템<br/>별개 ❌"] -.- C1
FS2["sidecar 의 파일시스템<br/>별개 ❌"] -.- C2
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef sh fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef no fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
class PAUSE key
class VOL sh
class FS1,FS2 no
공유한다
네트워크 네임스페이스 — 같은 IP, 같은 포트 공간 → 서로를 localhost로 부른다.
볼륨 — spec.volumes를 각 컨테이너가 마운트.
IPC 네임스페이스(기본)와 수명 — 함께 스케줄되고 함께 정리된다.
공유하지 않는다
파일시스템 — 컨테이너마다 별개 (볼륨으로 명시적으로 공유해야 한다).
PID 네임스페이스 — 기본은 분리, shareProcessNamespace로 켤 수 있다.
리소스 request/limit은 컨테이너별로 정한다.
네트워크를 공유하니 같은 Pod 안에서 포트가 겹치면 안 된다. 두 컨테이너가 모두 8080을 열 수 없다.
sequenceDiagram
participant KL as kubelet
participant PA as pause 컨테이너
participant AP as app 컨테이너
KL->>PA: 먼저 생성 — 네트워크 네임스페이스 확보
Note over PA: Pod IP 10.244.1.5 를 쥐고 있다
KL->>AP: 생성 — pause 의 네임스페이스에 합류
AP--xAP: 크래시
KL->>AP: 재시작 후 같은 네임스페이스에 다시 합류
Note over PA,AP: Pod IP 는 그대로 · pause 가 살아 있으므로
pause(sandbox) 컨테이너가 하나 더 있다kubectl get pods에는 안 보이고, 노드에서 crictl ps로 보면 보인다sudo crictl pods # Pod 샌드박스 = pause 컨테이너이걸 알면 **“컨테이너는 죽었는데 Pod IP는 그대로”**가 자연스러워진다. Pod IP가 바뀌는 것은 Pod 자체가 새로 만들어질 때다.
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"] # Dockerfile의 ENTRYPOINT 를 덮어쓴다 args: ["echo hello; sleep 3600"] # Dockerfile의 CMD 를 덮어쓴다| Kubernetes | Dockerfile | 생략하면 |
|---|---|---|
command |
ENTRYPOINT |
이미지의 ENTRYPOINT 사용 |
args |
CMD |
이미지의 CMD 사용 |
# 명령형으로 만들 때는 -- 뒤에 쓴다 (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 형식으로.
함께 넣을지는 세 질문으로 판단한다.
flowchart TD
Q1{"같은 노드에<br/>있어야만 하는가"}
Q1 -->|아니오| SEP["별도 Pod + Service 로 연결 ✅"]
Q1 -->|예| Q2{"같은 볼륨 · localhost 를<br/>써야 하는가"}
Q2 -->|아니오| SEP
Q2 -->|예| Q3{"수명이 같아야 하는가"}
Q3 -->|아니오| SEP
Q3 -->|예| SAME["같은 Pod 에 넣는다<br/>함께 스케일되고 함께 죽는다"]
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class SEP ok
class SAME key
class Q1,Q2,Q3 mute
셋 다 “예”가 아니면 별도 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.0flowchart LR
S["Pod 스케줄됨"] --> I1["init 1<br/>실행 → exit 0"]
I1 --> I2["init 2<br/>실행 → exit 0"]
I2 --> C["containers 시작"]
I1 -->|"exit 0 이 아니면"| R["restartPolicy 에 따라 재시도<br/>Init:CrashLoopBackOff"]
R --> I1
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class C ok
class R bad
class S,I1,I2 mute
containers가 시작한다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.0flowchart LR
ST["Pod 시작"] --> SC["사이드카 시작<br/>init 자리 · restartPolicy Always"]
SC --> AP["app 컨테이너 시작"]
AP --> RUN["둘 다 동작"]
RUN --> APE["app 종료"]
APE --> SCE["사이드카 종료<br/>★ 나중에 끝난다"]
SCE --> DONE["Job 이라면 여기서 완료 ✅"]
classDef side fill:#fef3c7,stroke:#d97706,color:#78350f
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class SC,SCE side
class DONE ok
class ST,AP,RUN,APE mute
예전 방식(그냥 containers에 두 개)도 여전히 동작한다. 다만 순서 보장이 없다.
kubectl get pod web -o jsonpath='{.status.phase}'stateDiagram-v2
[*] --> Pending: 생성
Pending --> Running: 스케줄 · 이미지 pull · 컨테이너 시작
Running --> Succeeded: 모든 컨테이너가 성공 종료
Running --> Failed: 하나 이상이 실패 종료
Running --> Unknown: 노드와 통신 두절
Unknown --> Running: 통신 복구
Succeeded --> [*]
Failed --> [*]
note right of Pending
아직 스케줄되지 않았거나
이미지를 받는 중
end note
| phase | 의미 |
|---|---|
| Pending | 아직 스케줄되지 않았거나, 이미지를 받는 중 |
| Running | 노드에 배정되었고 컨테이너가 최소 하나 이상 살아 있다 |
| Succeeded | 모든 컨테이너가 성공 종료. 재시작하지 않는다 |
| Failed | 모든 컨테이너가 종료되었고 하나 이상이 실패 |
| Unknown | 노드와 통신이 안 되어 상태를 모른다 |
kubectl get pod web -o jsonpath='{.status.containerStatuses[0].state}'kubectl describe pod web # 사람이 읽기엔 이쪽컨테이너는 셋 중 하나다: Waiting / Running / Terminated. 각각 reason이 붙는다.
flowchart LR
ST{"컨테이너 state"}
ST -->|Waiting| W1["ContainerCreating → Events"]
ST -->|Waiting| W2["ImagePullBackOff · ErrImagePull → Events"]
ST -->|Waiting| W3["CrashLoopBackOff → ★ logs --previous"]
ST -->|Waiting| W4["CreateContainerConfigError → Events"]
ST -->|Running| R["정상 ✅"]
ST -->|Terminated| T1["OOMKilled → describe 의 Last State"]
ST -->|Terminated| T2["Error → logs"]
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
class R ok
class W3,T1,T2 bad
class W1,W2,W4 warn
| 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 |
Always만 쓸 수 있다에러 이름이 아니다. “재시작을 지연시키고 있다”는 상태다.
flowchart LR
C["컨테이너 크래시"] --> R1["10초 후 재시작"]
R1 -->|"또 크래시"| R2["20초"]
R2 -->|"또"| R3["40초"]
R3 -->|"또"| R4["지수적으로 증가"]
R4 --> MAX["최대 5분에서 멈춤"]
R1 -->|"10분간 정상"| RESET["카운터 초기화 ✅"]
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class MAX bad
class RESET ok
class C,R1,R2,R3,R4 mute
진단 순서
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 — 정상적인 종료 신호를 받았다 |
가장 중요한 구분: liveness는 죽이고, readiness는 트래픽만 끊는다.
flowchart LR
KL["kubelet"]
KL --> LP["livenessProbe<br/>살아 있나?"]
KL --> RP["readinessProbe<br/>요청 받을 준비 됐나?"]
KL --> SP["startupProbe<br/>기동이 끝났나?"]
LP -->|실패| A1["컨테이너 재시작 ❌<br/>행에 걸린 앱을 구제"]
RP -->|실패| A2["Service 엔드포인트에서 제외 ⚠️<br/>재시작하지 않는다"]
SP -->|"성공할 때까지"| A3["다른 두 프로브를 멈춘다<br/>느리게 뜨는 앱용"]
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class A1 bad
class A2 warn
class A3 key
class KL,LP,RP,SP mute
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 헬스체크 규약을 쓰는 앱용.
| 필드 | 기본값 | 의미 |
|---|---|---|
initialDelaySeconds |
0 | 컨테이너 시작 후 첫 검사까지 대기 |
periodSeconds |
10 | 검사 주기 |
timeoutSeconds |
1 | 응답 대기 시간 |
failureThreshold |
3 | 몇 번 연속 실패해야 실패로 볼 것인가 |
successThreshold |
1 | 몇 번 연속 성공해야 복귀할 것인가 (liveness는 1 고정) |
flowchart LR
S["컨테이너 시작"] -->|"initialDelaySeconds<br/>기본 0"| P1["1차 검사"]
P1 -->|"periodSeconds<br/>기본 10"| P2["2차 검사"]
P2 -->|"10초"| P3["3차 검사"]
P3 -->|"failureThreshold 3 도달"| ACT["실패로 판정 → 재시작"]
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class ACT bad
class S,P1,P2,P3 mute
실제 반응 시간 = initialDelaySeconds + periodSeconds × failureThreshold
기본값이면 컨테이너가 죽고 나서 최대 30초 뒤에 재시작이 걸린다.
flowchart TB
D["삭제 요청"] --> P1["1. Endpoint 에서 제거<br/>새 트래픽 차단"]
D --> P2["2. preStop 훅 실행"]
P2 --> S["3. 컨테이너에 SIGTERM"]
S --> W["terminationGracePeriodSeconds<br/>대기 · 기본 30초"]
W -->|"끝났다"| OK["정상 종료 ✅"]
W -->|"시간 초과"| K["4. SIGKILL — 강제 종료 ❌"]
classDef ok fill:#dcfce7,stroke:#16a34a,color:#14532d
classDef bad fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class OK ok
class K bad
class P1,P2 key
class D,S,W mute
1번과 2번은 동시에 시작한다. 그래서 엔드포인트 제거가 전파되기 전에
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에서 차감된다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에서 (6장) valueFrom: secretKeyRef: name: db-secret key: passwordenvFrom: 을 쓰면 ConfigMap/Secret의 모든 키를 한 번에 환경변수로 넣을 수 있다 (6장).
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"]flowchart LR
P["Pod securityContext<br/>runAsUser · runAsGroup · fsGroup"] --> C["컨테이너 securityContext<br/>runAsNonRoot · capabilities …"]
C -->|"겹치면 컨테이너가 이긴다"| W["최종 적용 값"]
ONLY1["fsGroup 은 Pod 수준에만"] -.- P
ONLY2["capabilities 는 컨테이너 수준에만"] -.- C
classDef key fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
classDef warn fill:#fef3c7,stroke:#d97706,color:#78350f
classDef mute fill:#f1f5f9,stroke:#94a3b8,color:#334155
class C key
class ONLY1,ONLY2 warn
class P,W mute
capabilities는 컨테이너 수준에만 있다fsGroup은 Pod 수준에만 있다어느 디렉터리를 보는지 확인
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로 들어가서 만들어야 한다이제 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 에 실제 적용된 값이 보인다pause 컨테이너가 네트워크 네임스페이스를 쥐고 있어 컨테이너가 죽어도 IP가 유지된다restartPolicy: Always를 주면 사이드카(v1.33 GA)CrashLoopBackOff는 에러가 아니라 지연 중이라는 뜻 → logs --previous