개요
기본편이 “Kubernetes의 구성요소와 사용법"이었다면, 이 글은 Probe, 리소스 requests/limits, HPA, 배포 전략, RBAC, NetworkPolicy, 스케줄링 제어를 다룹니다.
1. Probe — 상태 점검
정리 보기
“떠 있는 것"과 “정상 동작하는 것"은 다르다
프로세스는 살아있는데 응답을 못 하는 경우가 있습니다. DB 커넥션 풀이 고갈되거나, 데드락에 빠지거나. Probe는 K8s가 Pod의 실제 건강 상태를 주기적으로 확인하는 메커니즘입니다.
3종류의 Probe
| Probe | 질문 | 실패 시 동작 |
|---|---|---|
| livenessProbe | “살아있냐?” | kubelet이 해당 컨테이너를 죽이고 restartPolicy에 따라 재시작 |
| readinessProbe | “요청 받을 준비 됐냐?” | 엔드포인트 컨트롤러가 해당 Pod의 IP를 매칭되는 모든 Service의 엔드포인트에서 제거 (컨테이너는 계속 실행) |
| startupProbe | “아직 부팅 중이냐?” | kubelet이 컨테이너를 죽이고 restartPolicy에 따라 재시작 |
- 재시작 대상은 Pod 전체가 아니라 문제가 된 컨테이너입니다. 다른 컨테이너는 영향을 받지 않습니다
- startupProbe의 “liveness/readiness 비활성화"는 실패 시 동작이 아니라, startupProbe가 성공하기 전까지의 동작입니다. startupProbe가 설정돼 있으면 성공할 때까지 liveness/readiness를 실행하지 않습니다
- startup과 liveness는 한 번만 실행되는 게 아니라
failureThreshold횟수만큼 연속 실패해야 재시작으로 이어집니다. readiness는 컨테이너 수명 전체에 걸쳐 계속 실행됩니다
YAML 예시
spec:
containers:
- name: app
image: myapp:1.0
ports:
- containerPort: 8080
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10 # 시작 후 10초 대기 (기본값 0)
periodSeconds: 15 # 15초마다 체크 (기본값 10)
failureThreshold: 3 # 3번 연속 실패하면 재시작 (기본값 3)
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
failureThreshold: 2 # 2번 연속 실패하면 Ready=false
startupProbe:
httpGet:
path: /healthz
port: 8080
failureThreshold: 30 # 30번까지 허용 (× periodSeconds = 최대 대기)
periodSeconds: 10공식 기본값
| 필드 | 기본값 | 비고 |
|---|---|---|
| initialDelaySeconds | 0 | 최소 0 |
| periodSeconds | 10 | 최소 1 |
| timeoutSeconds | 1 | 최소 1 |
| successThreshold | 1 | liveness/startup은 1만 허용 |
| failureThreshold | 3 | 최소 1 |
즉 아무것도 지정하지 않으면 probe는 컨테이너 시작 직후부터 10초 간격, 1초 타임아웃으로 실행되고, 연속 3회 실패가 재시작 기준이 됩니다.
Probe 방식 4가지
# 1. HTTP GET — 지정한 경로/포트로 HTTP GET 요청
httpGet:
path: /healthz
port: 8080
# 2. TCP 소켓 — 포트 열려있는지만 확인 (DB 등)
tcpSocket:
port: 3306
# 3. 명령어 실행 — 종료 코드 0이면 성공
exec:
command: ["pg_isready", "-U", "postgres"]
# 4. gRPC — gRPC 헬스체크 프로토콜 사용
grpc:
port: 9000probe별 확인 대상
- readiness: 요청 처리 가능 여부 (DB 연결, 캐시 워밍업 등 포함 가능)
- startup: 시작이 느린 컨테이너를 위한 probe
2. Resources — 리소스 관리
정리 보기
requests = “최소 이만큼 보장해줘”, limits = “최대 이만큼만 써”
리소스를 지정하지 않으면 한 Pod가 노드 자원을 다 먹어서 다른 Pod까지 죽일 수 있습니다(noisy neighbor). Docker 심화편의 --memory/--cpus와 같은 개념입니다.
spec:
containers:
- name: app
image: myapp:1.0
resources:
requests:
memory: "256Mi" # 스케줄러가 이만큼 남은 노드에 배치
cpu: "250m" # 0.25 코어
limits:
memory: "512Mi" # 초과 시 OOMKill (종료 코드 137)
cpu: "1000m" # 1 코어. 초과 시 쓰로틀링 (죽이진 않음)단위 설명
| 리소스 | 단위 | 의미 |
|---|---|---|
| CPU | cpu, 밀리코어(m) | 1 cpu = 물리 코어 1개(또는 vCPU 1개). 250m = 0.25 cpu, 1000m = 1 코어 |
| 메모리 | E, P, T, G, M, k / Ei, Pi, Ti, Gi, Mi, Ki | 바이트 단위. 접미사 없는 정수도 가능 |
메모리 접미사에서 Mi와 MB를 같은 것으로 적으면 안 됩니다. Mi는 2의 거듭제곱 단위라서 256Mi = 256 × 2^20 = 268,435,456바이트이고, 10의 거듭제곱인 256M은 256,000,000바이트입니다. 1Gi도 1GB가 아니라 2^30바이트입니다. 대소문자도 의미가 다릅니다. M은 메가바이트, m은 밀리바이트(1/1000 바이트)라서 400m은 0.4바이트 요청이 됩니다.
- CPU는 1m(0.001 cpu)보다 세밀하게 지정할 수 없습니다
- 접미사가 서로 다른 값이라도 바이트 수가 같으면 같은 양입니다:
128974848,129e6,129M,128974848000m,123Mi
CPU vs 메모리 초과의 차이
| CPU limits 초과 | 메모리 limits 초과 | |
|---|---|---|
| 동작 | 커널이 다음 스케줄링 구간까지 실행을 미룸 (쓰로틀링) | 커널 OOM 서브시스템이 개입해 메모리를 할당하려던 프로세스를 종료 |
| Pod 상태 | 계속 Running. 컨테이너 런타임은 CPU 과다 사용으로 컨테이너를 종료하지 않음 | 종료된 프로세스가 컨테이너의 PID 1이고 재시작 대상이면 컨테이너 재시작 (reason: OOMKilled, exitCode: 137) |
| 위험도 | 응답 지연 | 컨테이너 재시작으로 이어질 수 있음 |
- limits를 넘지 않아도, requests를 초과한 상태에서 노드 메모리가 부족해지면 그 Pod는 축출(eviction) 대상이 됩니다
QoS 클래스 — K8s가 자원 부족 시 누구를 먼저 죽이나
| QoS 클래스 | 조건 | 축출 순서 |
|---|---|---|
| Guaranteed | 모든 컨테이너가 CPU/메모리 requests와 limits를 0보다 크게 가지고, 각각 requests = limits | 가장 마지막 |
| Burstable | Guaranteed 조건은 아니지만, 컨테이너 중 하나 이상이 CPU 또는 메모리의 request나 limit을 가짐 | 중간 |
| BestEffort | 어떤 컨테이너에도 CPU/메모리 request와 limit이 없음 | 가장 먼저 |
- 노드 리소스가 부족하면 BestEffort → Burstable → Guaranteed 순으로 축출됩니다. 다만 리소스 압박으로 인한 축출에서는 requests를 초과한 Pod만 후보가 됩니다
- QoS 클래스는 Pod 생성 시점에 결정되고 수명 동안 바뀌지 않습니다
- kube-scheduler가 선점(preemption) 대상을 고를 때는 QoS 클래스를 보지 않습니다
# Guaranteed 예시 (requests = limits)
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "512Mi"
cpu: "500m"참고
- 스케줄러는 각 리소스 타입별로 “스케줄된 컨테이너들의 requests 합 < 노드 capacity"를 확인합니다. requests를 안 적으면 그 컨테이너는 0으로 계산되니, 실제 사용량이 늘어난 뒤 노드가 부족해질 수 있습니다
- 메모리 requests는 주로 스케줄링에 쓰이고, CPU requests는 경합 시 가중치로 동작합니다. 즉 CPU requests가 큰 워크로드가 더 많은 CPU 시간을 배분받습니다
- CPU limits는 하드 실링이라 초과하면 커널이 실행을 미룹니다. 반면 CPU 과다 사용으로 컨테이너가 종료되지는 않습니다
kubectl top pods는 Metrics API로 실제 사용량을 보여줍니다
# 리소스 사용량 확인
kubectl top pods
kubectl top nodes
# 특정 Pod의 리소스 설정 확인
kubectl get pod <이름> -o jsonpath='{.spec.containers[*].resources}'3. HPA — 오토스케일링
정리 보기
HPA = 목표 사용률을 정해두면 컨트롤러가 레플리카 수를 조절하는 것
HPA(Horizontal Pod Autoscaler)는 메트릭(CPU, 메모리, 커스텀)을 보고 Pod 수를 자동으로 늘리고 줄입니다.
YAML 예시
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
minReplicas: 2 # 최소 2개
maxReplicas: 10 # 최대 10개
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # CPU 평균 70% 넘으면 스케일 아웃
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80동작 원리
공식 계산식은 desiredReplicas = ceil[currentReplicas × (currentMetricValue / desiredMetricValue)]입니다. 소수점은 버림이 아니라 올림입니다.
현재 Pod 3개, 평균 CPU 85%
→ 비율 = 85/70 = 1.214
→ 필요 Pod 수 = ceil(3 × 1.214) = ceil(3.64) = 4개
→ 스케일 아웃- 목표값 주변에는 허용 오차(tolerance)가 있고, 이 안에서는 스케일링이 일어나지 않습니다.
behavior에서 지정하지 않으면 클러스터 전체 기본값 10%가 적용됩니다(kube-controller-manager의--horizontal-pod-autoscaler-tolerance) - 메트릭을 여러 개 지정하면 각각에 대해 계산한 뒤 가장 큰 replica 수를 선택합니다
전제 조건
- 사용률(Utilization) 계산은 Pod의
resources.requests를 기준으로 하므로 requests가 설정돼 있어야 합니다 - Resource 메트릭은 Metrics API에서 오고, 일반적으로
metrics-server가 이를 제공합니다
명령어
# HPA 생성 (명령어)
kubectl autoscale deployment web-app --min=2 --max=10 --cpu-percent=70
# HPA 상태 확인
kubectl get hpa
# 상세 정보 (현재 메트릭, 타겟, 현재 replica 수)
kubectl describe hpa web-hpa스케일링 기본 동작
- 스케일 아웃은 빠르고 스케일 인은 느립니다. 공식 기본값으로
scaleUp.stabilizationWindowSeconds는 0,scaleDown.stabilizationWindowSeconds는 300초(5분)입니다. 후자는 kube-controller-manager의--horizontal-pod-autoscaler-downscale-stabilization플래그로 바꿀 수 있고 그 기본값도 5분입니다 - 안정화 기간 동안 컨트롤러는 해당 윈도우 내 권고값 중 가장 높은 값을 선택합니다. 그래서 메트릭이 요동칠 때 축소가 완만하게 일어납니다
- 기본 정책은 15초마다 스케일 아웃 시 현재 replica의 100% 또는 4개까지 추가, 스케일 인 시 현재 replica의 100%까지 제거입니다
4. 배포 전략 — 무중단 배포
정리 보기
RollingUpdate와 Recreate
Deployment의 .spec.strategy.type은 Recreate와 RollingUpdate 두 가지이고, 기본값은 RollingUpdate입니다. 새 Pod를 띄우면서 이전 Pod를 내립니다. Recreate는 기존 Pod를 전부 종료한 뒤에 새 Pod를 만듭니다.
Rolling Update 동작
v1 v1 v1 ← 현재 상태 (3개)
v1 v1 v1 v2 ← 새 Pod 1개 추가
v1 v1 v2 v2 ← 이전 Pod 1개 제거, 새 Pod 1개 추가
v1 v2 v2 v2 ← 반복
v2 v2 v2 ← 완료배포 전략 설정
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 원하는 수보다 1개 더 띄울 수 있음
maxUnavailable: 0 # 동시에 내리는 Pod 0개 (항상 전체 수 유지)| 설정 | 의미 | 효과 |
|---|---|---|
| maxSurge: 1, maxUnavailable: 0 | 하나씩 추가 후 제거 | 가용 replica 수를 유지, 교체 시간은 길어짐 |
| maxSurge: 25%, maxUnavailable: 25% | 동시에 여러 개 | 빠르지만 일시적 용량 감소 |
maxSurge,maxUnavailable모두 기본값은 25%입니다. 아무것도 지정하지 않으면 두 번째 줄이 적용됩니다- 퍼센트를 절대값으로 환산할 때
maxSurge는 올림,maxUnavailable은 버림입니다 - 둘 다 0으로 둘 수는 없습니다. 한쪽이 0이면 다른 쪽은 0이 아니어야 합니다
종료 절차에 관여하는 설정
- readinessProbe가 성공하기 전에는 Pod IP가 매칭되는 Service의 EndpointSlice에 추가되지 않습니다
- 컨테이너 PID 1에 TERM 시그널이 전달되고, grace period가 끝나면 남아있는 프로세스에 SIGKILL이 전달됩니다
- preStop hook은 컨테이너에 TERM이 전달되기 전에 실행됩니다
spec:
containers:
- name: app
lifecycle:
preStop:
exec:
command: ["sleep", "5"] # preStop hook은 TERM 전송 전에 실행됨 (5는 예시값)
terminationGracePeriodSeconds: 30 # 기본값 30초. 초과하면 강제 종료왜 preStop이 필요한가
1. Pod에 종료 시각과 grace period가 기록됨 (STATUS는 Terminating)
2. kubelet이 로컬 Pod 종료 절차 시작 → preStop hook 실행
동시에, 컨트롤 플레인이 해당 Pod를 EndpointSlice에서 제거할지 평가
3. preStop 완료 후 컨테이너 PID 1에 TERM 시그널 전송
4. 앱이 graceful shutdown
5. grace period가 끝나도 남아있는 프로세스에는 SIGKILL
문제: EndpointSlice 업데이트 전파보다 preStop이 먼저 끝나면
→ TERM을 받은 뒤에도 이미 라우팅된 트래픽이 들어올 수 있음terminationGracePeriodSeconds기본값은 30초입니다.kubectl delete도 기본 grace period 30초로 동작합니다- preStop hook이 grace period를 넘겨서 실행 중이면 kubelet이 2초를 한 번 더 줍니다. preStop이 그보다 오래 걸려야 한다면
terminationGracePeriodSeconds를 늘려야 합니다 - 종료 중인 엔드포인트가 EndpointSlice에서 즉시 사라지는 것은 아니고, terminating 상태가 노출됩니다. 이 엔드포인트의 ready는 항상 false라서 로드밸런서가 일반 트래픽에 쓰지 않습니다
terminationGracePeriodSeconds: 0이면 preStop hook은 실행되지 않습니다- 사이드카 컨테이너(restartPolicy가 Always인 init 컨테이너)를 쓰면 kubelet이 메인 컨테이너 종료 후에 사이드카에 TERM을 보내므로, 순서 제어용 preStop을 대체할 수 있습니다
롤백
# 직전 리비전으로 롤백
kubectl rollout undo deployment/web-app
# 특정 버전으로 롤백
kubectl rollout undo deployment/web-app --to-revision=3
# 배포 히스토리
kubectl rollout history deployment/web-appBlue-Green / Canary
Deployment의 strategy가 제공하는 것은 RollingUpdate와 Recreate 두 가지입니다. Blue-Green이나 Canary는 strategy 필드로 지정하는 값이 아닙니다.
다만 Deployment를 릴리스별로 여러 개 만들어서 Canary 패턴을 구성할 수 있습니다. 즉 “쓸 수 없다"가 아니라 “레이블과 Service로 직접 구성하거나 별도 도구를 쓴다"에 가깝습니다.
| 전략 | 방식 | 구현 |
|---|---|---|
| Rolling Update | 점진적 교체 | Deployment strategy.type: RollingUpdate (기본값) |
| Recreate | 전부 종료 후 새로 생성 | Deployment strategy.type: Recreate |
| Blue-Green | 새 환경 전체 준비 후 한번에 전환 | Service selector 전환으로 직접 구성, 또는 Argo Rollouts / Flagger |
| Canary | 일부 트래픽만 새 버전으로 | 릴리스별 Deployment + 레이블 분리, 또는 Istio / Argo Rollouts |
5. RBAC — 권한 관리
정리 보기
RBAC = “누가 무엇을 할 수 있는가”
RBAC(Role-Based Access Control)은 사용자나 서비스가 클러스터에서 어떤 작업을 할 수 있는지 제어합니다.
4가지 구성요소
| 리소스 | 역할 | 범위 |
|---|---|---|
| Role | 권한 정의 | 네임스페이스 내 |
| ClusterRole | 권한 정의 | 클러스터 전체 (네임스페이스에 속하지 않음) |
| RoleBinding | 권한 부여 (Role 또는 ClusterRole → 사용자/그룹/SA) | 네임스페이스 내 |
| ClusterRoleBinding | 권한 부여 (ClusterRole → 사용자/그룹/SA) | 클러스터 전체 |
- RoleBinding은 같은 네임스페이스의 Role뿐 아니라 ClusterRole도 참조할 수 있습니다. 이때 부여되는 권한은 그 RoleBinding이 속한 네임스페이스로 한정됩니다. 공통 ClusterRole을 하나 만들어 여러 네임스페이스에서 재사용하는 방식입니다
- ClusterRole을 모든 네임스페이스에 걸쳐 부여하고 싶을 때만 ClusterRoleBinding을 씁니다
흐름
사용자/ServiceAccount
│
▼
RoleBinding / ClusterRoleBinding (누구에게)
│
▼
Role / ClusterRole (무엇을 허용)
│
▼
리소스 (pods, services, deployments 등)Role 예시 — dev 네임스페이스에서 Pod 조회/로그만 허용
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: dev
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]RoleBinding — 위 Role을 특정 사용자에게 부여
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods-binding
namespace: dev
subjects:
- kind: User
name: junior-dev
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.ioServiceAccount — Pod가 API Server에 접근할 때 사용하는 계정
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-bot
namespace: dev# ServiceAccount 목록
kubectl get serviceaccounts -n dev
# 현재 사용자의 권한 확인
kubectl auth can-i create deployments --namespace dev
# 다른 사용자의 권한 확인
kubectl auth can-i delete pods --namespace dev --as junior-dev6. NetworkPolicy — 네트워크 접근 제어
정리 보기
NetworkPolicy = “이 Pod는 저 Pod하고만 통신 가능"이라는 방화벽 규칙
기본적으로 K8s의 모든 Pod는 서로 자유롭게 통신할 수 있습니다. NetworkPolicy를 설정하면 허용된 트래픽만 통과시킬 수 있습니다.
예시 — backend Pod는 frontend에서만 접근 가능
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-allow-frontend
namespace: production
spec:
podSelector:
matchLabels:
app: backend # 이 정책이 적용되는 대상
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend # frontend에서 오는 것만 허용
ports:
- protocol: TCP
port: 8080모든 인바운드 차단 (기본 거부 정책)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
namespace: production
spec:
podSelector: {} # 네임스페이스 내 모든 Pod
policyTypes:
- Ingress
ingress: [] # 허용 목록 없음이 상태에서 필요한 통신만 하나씩 열어주는 방식으로 씁니다.
- 다만 “아무것도 못 들어온다"는 아닙니다. ingress 격리 상태에서도 Pod가 실행 중인 노드에서 오는 연결은 항상 허용됩니다. kubelet의 probe가 막히지 않는 이유가 여기 있습니다
- 허용된 연결의 응답 트래픽은 암묵적으로 허용됩니다
- 정책은 서로 충돌하지 않고 합집합으로 더해집니다(additive)
- 기본값은 egress/ingress 모두 비격리입니다. 해당 Pod를 선택하고
policyTypes에 그 방향이 들어있는 NetworkPolicy가 하나라도 있을 때만 그 방향이 격리됩니다
전제 조건
- NetworkPolicy는 네트워크 플러그인이 구현합니다. 이를 구현하는 컨트롤러 없이 NetworkPolicy 리소스만 만들면 아무 효과가 없습니다
- NetworkPolicy 지원 여부는 네트워크 플러그인마다 다릅니다
- 적용 범위는 IP/포트 레벨(OSI 3~4계층)이고 TCP, UDP, SCTP를 다룹니다
# NetworkPolicy 목록
kubectl get networkpolicies -n production
# 상세 확인
kubectl describe networkpolicy backend-allow-frontend -n production정책 구성 예
| 시나리오 | 정책 |
|---|---|
| DB는 backend에서만 | DB Pod에 backend만 ingress 허용 |
| 외부 API 호출 제한 | egress 정책으로 특정 IP만 허용 |
| 네임스페이스 간 격리 | namespaceSelector로 제어 |
7. Pod 스케줄링 제어
정리 보기
“이 Pod는 특정 노드에만 / 특정 Pod와 같이 / 떨어져서 배치해줘”
Scheduler의 기본 배치 외에 배치 조건을 지정하는 필드가 있습니다.
nodeSelector — 노드 레이블로 배치 제한
spec:
nodeSelector:
disk: ssd # label이 disk=ssd인 노드에만 배치# 노드에 label 추가
kubectl label node node-1 disk=ssdTaint & Toleration — “이 노드에 아무나 못 옴”
Taint는 노드에 설정하고, Toleration은 Pod가 해당 Taint를 허용하는 값입니다.
# 노드에 taint 추가 (GPU 전용 노드)
kubectl taint nodes gpu-node-1 gpu=true:NoSchedule# 이 Pod만 gpu 노드에 배치 가능
spec:
tolerations:
- key: "gpu"
operator: "Equal"
value: "true"
effect: "NoSchedule"Affinity — 세밀한 조건 배치
spec:
affinity:
# 노드 어피니티: SSD 노드 선호
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 80
preference:
matchExpressions:
- key: disk
operator: In
values: ["ssd"]
# Pod 안티어피니티: 같은 앱의 Pod끼리 다른 노드에 분산
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: web
topologyKey: kubernetes.io/hostname용도 정리
| 기능 | 용도 | 예시 |
|---|---|---|
| nodeSelector | 특정 노드에 고정 | SSD 노드에 DB |
| Taint/Toleration | 특정 Pod만 허용 | GPU 노드에 ML 워크로드만 |
| podAntiAffinity | 같은 앱 분산 배치 | 가용성 확보 (노드 장애 대비) |
| podAffinity | 같이 배치 | 레이턴시 최소화 |