0. Contents
1. Overview
Kubernetes의 Pod 실행 방식들
| 방식 | 특징 |
|---|---|
| 베어 Pod | 노드 장애 시 복구 안 됨 (개발/테스트용) |
| ReplicaSet | 계속 실행되어야 하는 서비스용 (웹서버 등) |
| DaemonSet | 모든 노드에 하나씩 (모니터링, 로그 등) |
| Job | 딱 한 번, 끝날 때까지 확실히 실행 ← 이번 챕터! |
Batch Job 패턴
- 격리된 원자적 작업 단위를 관리하기 위한 패턴이다
- 분산 환경에서 완료될 때까지 단기 실행 Pod를 안정적으로 실행하는 Job 리소스를 기반으로 한다
2. Problem
기존 방식의 공통점: 장기 실행 프로세스
- 지금까지 살펴본 Pod 실행 방식들은 모두 장기 실행 프로세스를 위한 것이다
- ReplicaSet: 계속 살아있어야 함 (웹서버)
- DaemonSet: 계속 살아있어야 함 (노드당 1개)
- 베어 Pod: 장애 시 복구 안 됨 (신뢰성 없음)
현실의 다른 요구사항
- 그런데 현실에는 이런 요구사항도 있다
“정해진 작업을 딱 한 번, 확실히 끝내고 종료하고 싶다.”
예시
- 대용량 데이터 배치 처리
- 이미지/영상 파일 변환
- 월말 정산 작업
이런 작업의 조건
- 완료되면 끝나야 하고, 실패하면 재시도되어야 한다
- 기존 primitive로는 이 요구사항을 안정적으로 채울 수 없다
3. Solution: Job
Job의 동작
- Kubernetes의 Job은 ReplicaSet과 비슷하게 하나 이상의 Pod를 생성하고 성공적으로 실행되도록 보장한다
- 차이점: 예상된 수의 Pod가 성공적으로 종료되면 Job이 완료된 것으로 간주된다
- 더 이상 Pod를 만들지 않는다
Job Controller는 어디서 동작하나?
Job Controller의 위치
- Job은 Pod가 아니다
- Control Plane의 Controller Manager 안에서 동작하는 컨트롤러다
Pod가 실행되기까지
- Job Controller는 kubelet에 직접 지시하지 않는다
- Job Controller는 API 서버에 Pod 객체를 만든다
- 스케줄러가 아직 노드가 없는 Pod를 보고 노드를 정한다
- 그 노드의 kubelet이 자기 노드에 배정된 Pod를 받아 실제로 실행한다
Example 7-1. Job 명세
apiVersion: batch/v1 # Job은 batch API 그룹 소속 (core v1이 아님)
kind: Job
metadata:
name: random-generator
spec:
completions: 5 # 성공한 Pod가 5개가 될 때까지 Job은 미완료 상태
parallelism: 2 # 동시에 최대 2개까지만 Pod 실행 (나머지는 대기)
ttlSecondsAfterFinished: 300 # Job/Pod가 Completed 된 뒤 300초 지나면 자동 정리(GC)
template: # 여기부터는 일반 Pod spec과 동일한 템플릿
metadata:
name: random-generator
spec:
restartPolicy: OnFailure # Job에서 허용되는 값은 OnFailure/Never뿐 (Always 불가)
containers:
- image: k8spatterns/random-generator:1.0
name: random-generator
command: [ "java", "RandomRunner", "/numbers.txt", "10000" ]
# 이 컨테이너가 종료(exit)해야 Pod가 완료 처리됨 — 데몬처럼 무한 대기하면 안 됨
템플릿 복제
template.spec.containers에 정의된 명세는 5개 Pod 모두 동일하게 복제된다
완료 판정 기준: Exit Code
판단 기준: exit code
- Kubernetes는 컨테이너 내부 로직을 알지 못한다
- 오직 exit code로만 성공/실패를 판단한다
| exit code | 판정 |
|---|---|
| 0 | 성공 (Completed) |
| 1~ | 실패 (재시작 정책에 따라 처리) |
여러 컨테이너가 있는 Pod
- Pod는 안의 컨테이너가 모두 성공(exit code 0)으로 끝나야 완료된다
- 사이드카를 일반 컨테이너(
containers)로 넣으면, 메인이 끝나도 사이드카가 끝날 때까지 Pod가 완료되지 않는다- 사이드카가 0이 아닌 exit code로 끝나면 Pod는 실패로 처리된다
- 메인 컨테이너의 exit code만 완료 기준이 되는 것은 네이티브 사이드카(아래)일 때뿐이다
- 이때 사이드카는 Job 완료를 막지 않는다
네이티브 사이드카
- 사이드카가 메인보다 오래 살아있으면 Pod 자체가 끝나지 않는 문제가 있었다
- Kubernetes 1.29부터는
initContainers에restartPolicy: Always를 지정해 네이티브 사이드카로 선언할 수 있다 - 이렇게 하면 메인 컨테이너가 완료될 때 사이드카도 함께 자동 종료된다
주의: Pod 레벨과 컨테이너 레벨의 restartPolicy
- 이
restartPolicy: Always는 Pod 레벨 필드가 아니라initContainers[].restartPolicy, 즉 컨테이너 레벨 필드다 - Job의 Pod 레벨
restartPolicy(위 Example 7-1의restartPolicy줄)는 여전히OnFailure/Never만 허용된다Always를 쓸 수 없다
- 이 둘은 이름은 같지만 서로 다른 필드다 → 충돌 없이 공존한다
spec:
restartPolicy: OnFailure # Pod 레벨 — Job이라 Always 불가
initContainers:
- name: sidecar-logger
image: fluentd
restartPolicy: Always # 컨테이너 레벨 — 네이티브 사이드카 지정용, 별개 필드
containers:
- name: main-job
컨테이너 레벨 restartPolicy: Always의 두 가지 의미
- 죽으면 재시작한다
- 사이드카 프로세스가 크래시해도 kubelet이 그 컨테이너만 독립적으로 재시작한다
- Pod의 재시작 정책과 무관하게 동작한다
- 완료 대기 대상에서 제외된다
- 일반 init container는 종료 코드 0으로 끝나야 다음 컨테이너로 진행된다
restartPolicy: Always가 붙은 init container는 (끝나는 대신) Ready 상태가 되는 순간 다음 컨테이너로 진행하도록 kubelet이 다르게 취급한다- envoy, fluentd처럼 원래 끝나지 않는 프로세스를 init container 자리에 넣을 수 있는 이유가 이것이다
- 그리고 메인 컨테이너가 모두 종료되면 kubelet이 이 사이드카에 SIGTERM을 보내 정리한다
일반 init container와의 차이
| 일반 init container | restartPolicy: Always init container | |
|---|---|---|
| 다음 컨테이너로 진행 | 종료(exit code 0) 대기 후 다음 컨테이너로 진행 | Ready 상태가 되면 (안 끝나도) 진행 |
| main 종료 시 | — | kubelet이 SIGTERM으로 정리 |
완료된 Pod는 왜 바로 삭제하지 않을까?
절충안: ttlSecondsAfterFinished
ttlSecondsAfterFinished는 **“로그 볼 시간은 주고, 그 다음엔 치운다”**는 절충안이다
restartPolicy: OnFailure vs Never
Job에서 restartPolicy: Always를 못 쓰는 이유
- Job에서는
restartPolicy: Always를 사용할 수 없다 - 성공한 Pod를 계속 재시작하면 Job이 영원히 끝나지 않기 때문이다
Figure 7-1 해석: parallelism은 “슬롯” 개념
Figure 7-1이 나타내는 것
- 책의 Figure 7-1은 시간 순서가 아니라 동시 실행 슬롯을 나타낸 그림이다
completions: 5,parallelism: 2일 때의 실행 모습은 다음과 같다
“1, 2 완료"의 의미
- 첫 번째·두 번째로 성공한 Pod라는 뜻이다
- 2개씩 병렬로 돌다가, 남은 completions가 1개뿐인 마지막 순간에는 슬롯 하나만 사용된다
단계별 진행
- 슬롯1, 슬롯2 동시 실행 → 2개 완료 (2/5)
- 슬롯1, 슬롯2 동시 실행 → 4개 완료 (4/5)
- 슬롯 하나만 실행 (남은 completions 1개뿐이므로) → 5개 완료 (5/5)
실제 동시 실행 Pod 수
parallelism이 항상 그 숫자만큼 꽉 채워 실행되는 건 아니다- 남은 completions 수, 리소스 쿼터, 스로틀링 등에 따라 실제 동시 실행 Pod 수는 더 적을 수 있다
4. 왜 베어 Pod 대신 Job인가?
| 베어 Pod | Job | |
|---|---|---|
| 클러스터 재시작 생존 | ✓ (Pod 객체도 etcd에 저장) | ✓ (etcd에 영구 기록) |
| 완료 후 기록 보존 | ✓ (직접 지울 때까지 남음) | ✓ |
여러 번 실행 (completions) | ✗ | ✓ |
병렬 실행 (parallelism) | ✗ | ✓ |
일시 중지/재개 (suspend) | ✗ | ✓ |
| 노드 장애 시 대체 Pod | ✗ (실패 상태로 방치, 새로 만드는 주체 없음) | ✓ (Job 컨트롤러가 새 Pod를 만들고 스케줄러가 다른 노드를 고름) |
suspend 동작
suspend: true로 설정하면 실행 중인 Pod가 모두 삭제된다suspend: false로 되돌리면 재개된다
4.1 “etcd에 영구 기록"의 정확한 의미
영구 기록의 뜻: durability
- 위 표의 “영구 기록"은 무한정 저장된다는 뜻이 아니다
- 프로세스가 재시작돼도 사라지지 않는다(durability)는 뜻이다
- 저장 용량이 무한한지(capacity)는 완전히 다른 질문이다
두 질문의 구분
- 질문 A: 재시작하면 사라지나? → durability
- etcd의 답: “예, 안 사라짐”
- 질문 B: 영원히, 무한정 쌓아도 되나? → capacity/retention
- etcd의 답: “아니요, quota와 GC가 있음”
| 질문 | 답 | 근거 |
|---|---|---|
| 재시작해도 살아남나? (durability) | ✅ | 디스크(bbolt)에 저장, WAL 로그로 복구 |
| 무한정 쌓아도 되나? (capacity) | ❌ | 기본 quota 2GB(권장 최대 8GB), 초과 시 읽기 전용 전환 |
| 자동으로 정리되나? | 일부만 | MVCC로 리비전이 계속 쌓임 · compaction(오래된 리비전 삭제)은 API 서버가 자동으로 요청(기본 5분마다) · defrag(디스크 공간 회수)는 직접 실행해야 함 |
etcd 자체의 복제 구조
- etcd 자체도 Raft 알고리즘으로 리더-팔로워 복제 구조를 가진 고가용성 시스템이다
- 이건 Kubernetes의 Control Plane/Worker Node 구조와는 다른 층위의 이야기다
- etcd는 Control Plane 구성 요소 중 하나다
- 그 안에서 자체적으로 한 번 더 리더를 선출해 복제한다
5. Job의 종류
종류를 나누는 두 값
.spec.completions와.spec.parallelism두 값의 조합에 따라 Job의 종류가 나뉜다
Single Pod Job
Pod 1개 실행
- 두 값을 모두 생략하거나 기본값(1)으로 둔다
- Pod 1개만 실행하고 성공하면 즉시 완료된다
Fixed Completion Count Job
적합한 경우
- 작업 개수를 미리 알고 있을 때
- 작업 1개당 Pod 1개를 쓸 만큼 처리 비용이 클 때
Indexed Job
인덱스로 각자 계산
- 각 Pod가
JOB_COMPLETION_INDEX환경변수(또는batch.kubernetes.io/job-completion-index어노테이션)로 자신의 인덱스를 받는다 - 외부 작업 큐 없이 자기가 처리할 작업을 스스로 계산한다
Pod 0~4끼리 통신/동기화 여부
- ❌ 서로 통신 안 함
- ❌ 서로 상태 모름
- ✅ 각자 인덱스로 독립 계산 → 겹치지 않게 처음부터 설계됨
조율이 필요 없는 이유
- 시험지 나눠주듯 “1
30번은 1조, 3160번은 2조"처럼 애초에 겹치지 않게 구간을 나눠주기 때문이다
Example 7-2. Indexed Job
apiVersion: batch/v1
kind: Job
metadata:
name: file-split
spec:
completionMode: Indexed # 각 Pod에 0부터 시작하는 고유 인덱스 부여
completions: 5 # 인덱스는 0, 1, 2, 3, 4 (총 5개)
parallelism: 5 # 5개 전부 동시 실행 (인덱스별로 병렬 처리)
template:
spec:
containers:
- image: alpine
name: split
command:
- "sh"
- "-c"
- |
# JOB_COMPLETION_INDEX는 Kubernetes가 각 Pod에 자동 주입하는 환경변수
# START: 이 Pod 구간 바로 앞 줄 번호, END: 이 Pod가 처리할 마지막 줄 번호
START=$(expr $JOB_COMPLETION_INDEX \* 10000)
END=$(expr $START + 10000)
# NR: awk의 현재 줄 번호 변수 (1부터 시작) → START+1 ~ END 줄, 정확히 10,000줄
# 결과를 인덱스별 파일로 저장
awk -v s=$START -v e=$END 'NR>s && NR<=e' /logs/random.log \
> /logs/random-$JOB_COMPLETION_INDEX.txt
volumeMounts:
- mountPath: /logs
name: log-volume # 모든 Pod가 같은 볼륨을 공유해야 원본 파일에 접근 가능
restartPolicy: OnFailure
Example 7-2가 하는 일
- 50,000줄짜리
random.log파일을 5개 Pod가 동시에 10,000줄씩 나눠서 처리한다
awk의 NR 변수
NR은 awk 내부에서 현재 처리 중인 줄 번호를 나타내는 변수다NR>s && NR<=e조건으로 해당 구간만 잘라낸다NR은 1부터 시작하므로 인덱스 0은 110,000번째 줄, 인덱스 1은 10,00120,000번째 줄을 맡는다
Work Queue Job
설정 방식
completions는 아예 지정하지 않는다parallelism만 1보다 크게 설정한다
Job 종류별 설정 비교
| 종류 | completions | parallelism | completionMode |
|---|---|---|---|
| Single Pod Job | 1 | 1 | — |
| Fixed Completion Count | N | M | — |
| Indexed Job | N | M | Indexed |
| Work Queue Job ← 이번 것 | 없음! | N (>1) | — |
completions를 못 정하는 이유
- 작업이 외부 큐(Redis, RabbitMQ 등)에 쌓여 있다
- 그 큐에 몇 개가 들어있는지 Kubernetes는 알 방법이 없다
- 그래서 숫자로 된 목표치 대신, “큐가 비면 끝"이라는 신호를 Pod가 직접 판단해서 알려주는 방식을 쓴다
완료 조건
- 최소 1개 Pod가 “큐 비었음"을 확인하고 성공 종료
- 나머지 실행 중인 Pod들도 모두 종료
- 두 조건을 모두 채우면 → Job 완료
실행 흐름
완료 신호와 대기
- 가장 먼저 큐가 빈 걸 발견한 Pod가 성공 종료로 완료 신호를 보낸다
- Job 컨트롤러는 나머지 Pod들도 다 끝날 때까지 기다린다
Indexed Job과의 조율 방식 차이
조율 방식: 미리 나눈 구역 vs 실시간 결정
- Indexed Job은 담당 구역이 인덱스로 미리 정해져 있다 → Pod끼리 조율이 필요 없다
- Work Queue Job은 담당 구역이 없다
- “누가 뭘 처리할지"를 Pod들이 큐를 보면서 실시간으로 정해야 한다
- 이 조율은 외부 큐 시스템이 “이 작업은 이미 누가 가져갔다"를 관리해주기 때문에 가능하다
비유: 창구 배정
- Indexed Job = 번호대로 창구 배정 (“1
10번은 1번 창구, 1120번은 2번 창구”) - Work Queue Job = 번호표 뽑아 아무 창구나 이용 (실시간 배정)
언제 쓰나
적합한 경우: 세밀한 작업 항목
- 작업 항목이 세밀(granular) 할 때 쓴다
- 항목 하나하나가 너무 작아서 Pod 하나씩 배정하면 낭비인 경우
- 예: 큐에 작은 메시지 100만 개가 쌓여 있다면
- Indexed Job으로 100만 개 Pod를 띄우는 대신
- Work Queue Job으로 Pod 10개가 큐에서 계속 꺼내며 처리하는 쪽이 합리적이다
| Indexed Job | Work Queue Job | |
|---|---|---|
completions | 지정함(작업 개수를 미리 앎) | 지정 안 함 |
| 작업 분배 | 인덱스로 미리 구간을 나눔 | 공유 큐에서 실시간으로 꺼내감 |
| Pod 간 조율 | 불필요(인덱스로 이미 분리됨) | 필요(큐 시스템이 대신 관리) |
| 완료 조건 | 모든 인덱스 완료 | 1개 이상 성공 + 나머지 종료 |
| 적합한 경우 | 작업량을 미리 알고, 항목이 큼직함 | 작업량 모름, 항목이 잘게 쪼개져 다수 |
Work Queue Job은 몇 개인지 모르는 작업 큐를 여러 Pod가 나눠 처리할 때 쓰는 방식이다.
completions를 아예 빼는 대신, 큐가 비었는지를 Pod 스스로 판단해 Job 컨트롤러에게 완료를 알린다.
6. Partitioning의 한계: 전체 작업자 수를 모른다
Pod 스스로 나누는 Indexed Job
- Work Queue Job은 외부 큐(Redis, RabbitMQ 등)가 이미 작업을 잘라준다
- Indexed Job은 Pod 스스로 작업을 나눠야 한다
- 이때 각 Pod는 다음을 알아야 한다
| 필요한 정보 | 제공 여부 |
|---|---|
| 자기 인덱스 | ✅ (JOB_COMPLETION_INDEX 로 제공됨) |
| 전체 Pod 개수 | ❌ (completions 값 — 2023년 기준 미지원!) |
| 작업 전체 크기 | ✅ (파일 크기 등, 별도로 알아야 함) |
우회책 두 가지
JOB_COMPLETION_TOTAL같은 환경변수가 있으면 편하겠지만 아직 지원되지 않는다- 대신 두 가지 우회책이 있다
방법 1: 하드코딩
int totalPods = 5; // 전체 Pod 개수를 소스 코드에 직접 하드코딩
// completions 값과 반드시 일치해야 하지만 강제할 방법이 없음
코드와 YAML의 강한 결합
completions를 바꾸려면 코드도 수정하고 이미지도 새로 빌드해야 한다- 코드와 YAML이 강하게 결합된다
방법 2: 환경변수로 주입
spec:
completions: 5 # 실제 Job의 완료 목표치
template:
spec:
containers:
- env:
- name: JOB_COMPLETION_TOTAL
value: "5" # 위 completions 값을 사람이 손으로 그대로 복사해 넣음
# → 나중에 completions만 바꾸고 이 값을 깜빡하면 버그로 이어짐
남는 실수 여지
- 이미지를 새로 빌드할 필요는 없다
- 하지만
completions를 바꿀 때 YAML 안 두 곳(completions와 환경변수)을 모두 수정해야 한다- 실수 여지가 남는다
| 하드코딩 | 환경변수 주입 | |
|---|---|---|
| 이미지 재빌드 필요 | ✅ | ❌ |
| 수정 지점 | 코드 + YAML | YAML 2곳 |
| 결합도 | 높음 | 중간 |
7. 작업 항목을 어떻게 Job/Pod에 매핑할까
선택은 개발자 몫
- Job은 작업 항목을 어떻게 나눌지 강제하지 않는다
- 개발자가 아래 두 전략 중 선택해야 한다
- 핵심 차이는 “누가 작업을 나누고 관리하는가” 다
7.1 전략 1: 작업 항목 1개 = Job 1개
Kubernetes가 작업별로 관리
- Kubernetes 자체가 각 작업을 별도의 Job으로 인식하고 관리한다
- 이미지 100장을 변환한다면, 이미지 하나당 Job 하나를 만든다
# job-image-001.yaml (이미지 하나당 파일 하나, 총 100개 필요)
apiVersion: batch/v1
kind: Job
metadata:
name: convert-image-001
spec:
template:
spec:
containers:
- image: image-converter
args: ["--file", "image-001.jpg"]
restartPolicy: OnFailure
kubectl get jobs
NAME COMPLETIONS AGE
convert-image-001 1/1 5m
convert-image-002 0/1 5m ← 이것만 실패!
convert-image-003 1/1 5m
실패한 것만 재실행
- Kubernetes가 Job 단위로 성공/실패를 추적해준다
- 그래서 실패한 것만 콕 집어 재실행할 수 있다
kubectl delete job convert-image-002
kubectl apply -f job-image-002.yaml # 이것만 다시 실행
7.2 전략 2: 작업 항목 전체 = Job 1개
앱 코드가 나눠 처리
- Kubernetes는 전체를 하나의 작업으로만 인식한다
- 100개를 어떻게 나눠 처리할지는 **컨테이너 내부 코드(배치 프레임워크)**가 담당한다
# job-image-batch.yaml (딱 1개)
apiVersion: batch/v1
kind: Job
metadata:
name: convert-all-images
spec:
completions: 1
template:
spec:
containers:
- image: image-converter
command: ["python", "batch_processor.py"]
restartPolicy: OnFailure
# batch_processor.py — 컨테이너 안에서 실행되는 코드
images = get_all_images() # 100개 이미지 목록
for image in images:
try:
convert(image)
mark_as_done(image) # 진행상황을 DB 등에 자체 기록
except Exception as e:
log_error(image, e) # 실패해도 앱이 알아서 처리하고
continue # 다음 이미지로 계속 진행
kubectl get jobs
NAME COMPLETIONS AGE
convert-all-images 1/1 5m
Kubernetes에 보이는 것: 성공 여부뿐
- Kubernetes 입장에서는 “성공했다"만 보인다
- 100개 중 몇 개가 실패했는지는 Kubernetes가 모른다
- 실패 내역은 컨테이너 로그나 앱이 남긴 기록(DB 등)을 직접 확인해야 알 수 있다
7.3 두 전략 비교
| Job per 작업 | Job for 전체 | |
|---|---|---|
| Job 수 | 많음 | 1개 |
| 개별 추적 | ✅ | ❌ |
| 관리 주체 | Kubernetes | 앱 내부 (Spring Batch, JBeret 등) |
| 적합한 경우 | 복잡/중요 | 단순/대량 |
7.4 선택 기준
전략 1이 맞는 경우
- 작업 하나가 실패했을 때 그것만 정확히 찾아 재실행해야 할 때
- 작업마다 우선순위·스케줄이 다를 때
- 예: 사용자별 결제 처리
- 각각 독립적으로 성공/실패를 추적해야 하는 경우
전략 2가 맞는 경우
- 작업 개수가 수천~수만 개라 Job을 그만큼 만들면 API 서버에 부담이 될 때
- 실패 처리를 앱이 이미 잘 하고 있을 때 (재시도 로직, 실패 큐 등)
- 예: 로그 파일 100만 줄 처리
- Job 100만 개보다 앱 내부에서 나눠 처리하는 편이 효율적이다
전략 1은 Kubernetes에게 각 작업을 낱개로 보여주고 관리를 맡기는 방식이고, 전략 2는 Kubernetes에게는 1개로 뭉뚱그려 보여주고, 나누는 일은 컨테이너 안의 앱 코드가 직접 하는 방식이다.
8. Discussion
Job의 범위: 최소한의 기본기
- Job 프리미티브는 스케줄링의 최소한의 기본기만 제공한다
- 복잡한 요구사항은 배치 프레임워크(Java 생태계의 Spring Batch, JBeret 등)와 조합해야 한다
| Job이 해주는 것 | Job이 안 해주는 것 |
|---|---|
| ✅ Pod 실행 보장 | ❌ 작업 항목 세부 관리 |
| ✅ 완료 횟수 관리 | ❌ 진행률 추적 |
| ✅ 병렬 실행 | ❌ 작업 간 의존성 관리 |
| ✅ 실패 시 재시작 |
실행 시점별 리소스
- 모든 서비스가 항상 실행될 필요는 없다
| 실행 시점 | 리소스 |
|---|---|
| 항상 실행 | ReplicaSet / Deployment |
| 모든 노드 | DaemonSet |
| 필요할 때만 | Job ← 이번 챕터 |
| 주기적으로 | CronJob (Job 기반) |
짧은 작업을 Job으로 돌리는 이점
- Job으로 짧은 수명의 작업을 처리하면, ReplicaSet처럼 계속 떠 있는 추상화를 쓰는 것보다 다른 워크로드를 위한 자원을 절약할 수 있다
- Job은 노드의 가용 용량, Pod 배치 정책, 컨테이너 의존성을 고려해 스케줄링된다
Job은 “필요할 때만, 필요한 만큼” 실행되는 워크로드를 위한 primitive다. 다양한 워크로드를 지원하는 것이 Kubernetes의 강점이다.
