Pod를 직접 만들면 관리는 사람의 몫이 된다. Pod가 죽어도 아무도 다시 만들어주지 않고, 트래픽이 늘어도 알아서 늘어나지 않으며, 버전을 올리려면 일일이 지우고 다시 만들어야 한다.
Controller는 Pod를 대신 관리해 주는 오브젝트다. 사용자가 원하는 상태를 선언해 두면, Controller가 현재 상태를 계속 감시하면서 그 상태에 맞추는 작업을 수행한다.
[ Pod만 직접 만들 때 ] [ Controller를 쓸 때 ]
Pod가 죽음 Pod가 죽음
│ │
▼ ▼
사람이 인지 Controller가 즉시 인지
│ │
▼ ▼
사람이 다시 생성 자동으로 새 Pod 생성
Controller가 제공하는 기능은 네 가지다.
| 기능 | 설명 | 사용하는 Controller |
|---|---|---|
| Auto Healing | 죽은 Pod를 감지해 새로 만든다 | ReplicationController, ReplicaSet, DaemonSet |
| Auto Scaling | 부하에 따라 Pod 수를 늘리고 줄인다 | HPA |
| Software Update | 여러 Pod의 버전을 일괄 업그레이드한다 | Deployment |
| Job | 필요한 순간에만 Pod를 만들어 작업을 수행한다 | Job, CronJob |
Pod가 죽거나 Pod가 올라가 있는 Node 자체에 장애가 발생하면, Controller가 즉각적으로 인지해서 다른 Node에 Pod를 새로 만든다.
┌──────────────────┐
│ Controller │ ← 상태를 계속 감시
└────┬────────┬────┘
│ │
장애 인지 새로 생성
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ Node1 │ │ Node2 │
│ ┌──────┐ │ │ ┌──────┐ │
│ │ Pod1 │ │ │ │ Pod1 │ │ ← 새 Node에 다시 생성
│ │ ✗ │ │ │ │ ✓ │ │
│ └──────┘ │ │ └──────┘ │
│ ⚠ 장애 │ │ │
└──────────┘ └──────────┘
사람이 개입하기 전에 복구가 끝나므로, 엔지니어는 복구 이력만 확인하면 된다.
담당 Controller: ReplicationController, ReplicaSet, DaemonSet
Pod의 리소스 사용량이 limit에 도달한 상태를 Controller가 인지하면, Pod를 하나 더 만들어서 부하를 분산시킨다.
┌──────────────────┐
│ Controller │ ← 리소스 사용량 감시
└────────┬─────────┘
│ Pod1이 limit 도달
▼
┌──────┐ ┌──────────┐
│ Pod1 │ ─┬───▶ │ Pod1 │ 부하 50%
│ ███ │ │ │ ██ │
│ 한계 │ │ └──────────┘
└──────┘ │ ┌──────────┐
└───▶ │ Pod2 │ 부하 50%
│ ██ │ ← 새로 생성
└──────────┘
Pod를 늘려 부하를 나누므로 서비스가 느려지거나 멈추는 상황을 막을 수 있다.
담당 Controller: HPA (Horizontal Pod Autoscaler)
여러 개의 Pod를 한 번에 새로운 버전으로 업그레이드한다. 업데이트 중 문제가 생기면 이전 버전으로 되돌리는 것도 가능하다.
┌──────────────────┐
│ Controller │
└────────┬─────────┘
│
┌────────┴─────────────────────┐
│ │
▼ ▼
v1 v1 v1 v2 v2 v2
┌──┐┌──┐┌──┐ ──────▶ ┌──┐┌──┐┌──┐
│P1││P2││P3│ │P1││P2││P3│
└──┘└──┘└──┘ └──┘└──┘└──┘
v1 v1 v1 v2 v2 v2
┌──┐┌──┐┌──┐ ┌──┐┌──┐┌──┐
│P4││P5││P6│ │P4││P5││P6│
└──┘└──┘└──┘ └──┘└──┘└──┘
담당 Controller: Deployment
일시적인 작업을 해야 하는 경우, Controller가 필요한 순간에만 Pod를 만든다. 작업이 끝나면 Pod가 사용하던 자원을 반납한다.
┌──────────────────┐
│ Controller │
└───┬──────────┬───┘
│ │
▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Pod │ │ Pod │ │ Pod │
│ (없음) │→│ (실행) │→│ (종료) │
└─────────┘ └─────────┘ └─────────┘
Resource Resource Resource
┌─┐┌─┐┌─┐ ┌─┐┌ ┐┌ ┐ ┌─┐┌─┐┌─┐
└─┘└─┘└─┘ └─┘└ ┘└ ┘ └─┘└─┘└─┘
자원 여유 자원 사용중 자원 반납
Pod를 계속 띄워두지 않으므로 자원을 효율적으로 사용할 수 있다.
담당 Controller: Job, CronJob
| Controller | 상태 |
|---|---|
| ReplicationController | Deprecated (더 이상 사용하지 않음) |
| ReplicaSet | ReplicationController를 대체(Replaced) 한 Controller |
역할은 같지만 ReplicaSet이 더 발전된 selector 기능을 갖추면서 ReplicationController를 대체했다. 현재는 ReplicaSet을 사용한다.
두 Controller는 세 가지 요소로 구성된다.
| 구성 요소 | 역할 |
|---|---|
| Template | 새로 만들 Pod의 내용을 정의한다 |
| Replicas | Pod를 몇 개로 유지할지 정한다 |
| Selector | 어떤 Pod를 관리 대상으로 삼을지 고른다 |
Controller를 만들 때 Pod의 내용을 템플릿으로 넣어둔다. Controller가 Pod를 새로 만들어야 할 때 이 템플릿을 그대로 사용한다.
┌──────────────────────────────┐
│ Replication │
│ │ ┌──────────┐
│ selector template │ │ Pod:v2 │
│ ┌────────┐ ┌──────────┐ │ │ │
│ │type:web│ │ Pod:v2 │◀───┼────────│ Template │
│ └────────┘ └──────────┘ │ │ Update │
└───────┬──────────────────────┘ └──────────┘
│
Re-create
│
┌───────┴──────┬──────────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌──────────┐
│ Pod │ │ Pod │ │ Pod:v2 │
│type:web│ │type:web│ │ type:web │
│ ⚠ │ │ ✗ │ │ ✓ │
└────────┘ └────────┘ └──────────┘
기존 Pod 삭제하면 템플릿대로
새로 생성됨 v2로 생성
여기서 중요한 동작이 있다. 템플릿을 v2로 수정해도 이미 실행 중인 Pod는 그대로 남는다. 기존 Pod를 삭제해야 그때 템플릿을 보고 v2 Pod가 새로 만들어진다.
이 성질을 이용하면 Pod를 하나씩 지우면서 버전을 올리는 것이 가능하다. 다만 수작업이므로, 실제 버전 업그레이드는 뒤에 나올 Deployment가 담당한다.
Pod를 직접 만드는 경우
apiVersion: v1
kind: Pod
metadata:
name: pod-1
labels:
type: web
spec:
containers:
- name: container
image: tmkube/app:v1
ReplicationController로 만드는 경우
apiVersion: v1
kind: ReplicationController
metadata:
name: replication-1
spec:
replicas: 1
selector:
type: web
template:
metadata:
name: pod-1
labels:
type: web
spec:
containers:
- name: container
image: tmkube/app:v2
| 항목 | 설명 |
|---|---|
replicas | 유지할 Pod의 개수 |
selector | 관리 대상 Pod를 고르는 조건 |
template | 새 Pod를 만들 때 사용할 내용. Pod YAML의 metadata와 spec이 그대로 들어간다 |
template 안의 내용이 앞의 Pod YAML과 동일한 구조라는 점을 보면 이해하기 쉽다. Pod 정의를 Controller 안에 그대로 넣어둔 것이다.
지정한 개수만큼 Pod가 유지되도록 관리한다.
┌──────────────────┐ ┌──────────────────┐
│ Replication │ │ Replication │
│ │ │ │
│ replicas │ │ replicas template
│ ┌────┐ │ │ ┌────┐ ┌──────┐│
│ │ 3 │ │ │ │ 2 │ │ Pod ││
│ └────┘ │ │ └────┘ └──────┘│
└──┬────┬────┬─────┘ └───┬──────────┬───┘
│ │ │ │ ⚠ │
│ │ │ Scale Out │ Pod 삭제 │
▼ ▼ ▼ ▼ ▼
┌───┐┌───┐┌───┐ ┌───┐ ┌───┐
│Pod││Pod││Pod│ │Pod│ │Pod│
└───┘└───┘└───┘ └───┘ └───┘
새로 생성됨
| 상황 | Controller의 동작 |
|---|---|
replicas를 1에서 3으로 변경 | Pod를 2개 더 만든다 (Scale Out) |
| Pod 하나가 삭제되거나 죽음 | 개수를 채우기 위해 새 Pod를 만든다 (Auto Healing) |
replicas를 3에서 1로 변경 | Pod 2개를 삭제한다 (Scale In) |
여기서 알 수 있는 것은 Auto Healing과 Auto Scaling이 사실 같은 동작이라는 점이다. Controller는 "지정된 개수를 유지한다"는 하나의 원칙만 수행하며, 그 결과가 상황에 따라 복구로도 보이고 확장으로도 보이는 것이다.
또한 죽은 Pod를 되살리는 것이 아니라 항상 새 Pod를 만든다. 따라서 Pod 안에 저장한 데이터는 복구되지 않는다.
어떤 Pod를 관리 대상으로 삼을지 고르는 조건이다. Service의 selector가 트래픽을 보낼 Pod를 고르는 것과 같은 방식이다.
ReplicationController와 ReplicaSet의 가장 큰 차이가 여기에 있다.
ReplicationController ReplicaSet
───────────────────── ──────────────────────────
┌──────────────┐ ┌──────────────────────────┐
│ type: web │ │ matchLabels matchExpressions
│ │ │ ┌────────┐ ┌───────────┐│
│ (값이 정확히│ │ │type:web│ │key: ver ││
│ 같아야 함) │ │ └────────┘ │operator: ││
└──────────────┘ │ │ Exists ││
│ └───────────┘│
└──────────────────────────┘
| 구분 | ReplicationController | ReplicaSet |
|---|---|---|
| 지정 방식 | 값이 정확히 일치하는 조건만 가능 | matchLabels + matchExpressions |
| 표현력 | 단순 | key의 존재 여부, 포함, 제외까지 표현 가능 |
선택 결과 예시
세 개의 Pod가 있다고 하자.
┌────────────┐ ┌────────────┐ ┌────────────┐
│ Pod1 │ │ Pod2 │ │ Pod3 │
│ type: web │ │ type: web │ │ type: db │
│ ver : v1 │ │ ver : alpha│ │ ver : beta │
└────────────┘ └────────────┘ └────────────┘
| Controller | 조건 | 선택되는 Pod |
|---|---|---|
| ReplicationController | type: web | Pod1, Pod2 |
| ReplicaSet | matchLabels: type: web + matchExpressions: {key: ver, operator: Exists} | Pod1, Pod2 |
ReplicaSet에서 두 조건이 모두 지정되면 둘 다 만족하는 Pod만 선택된다.
matchExpressions는 네 가지 연산자를 제공한다.
Exists DoesNotExist
┌──────────────────┐ ┌──────────────────┐
│ ReplicaSet │ │ ReplicaSet │
│ Key: A │ │ Key: A │
└──────────────────┘ └──────────────────┘
┌────┐┌────┐┌────┐ ┌────┐┌────┐┌────┐
│Pod1││Pod2││Pod3│ │Pod1││Pod2││Pod3│
│A:1 ││A:2 ││B:2 │ │A:1 ││A:2 ││B:2 │
└────┘└────┘└────┘ └────┘└────┘└────┘
✓ ✓ ✗ ✗ ✗ ✓
A라는 key가 있으면 선택 A라는 key가 없으면 선택
In NotIn
┌──────────────────┐ ┌──────────────────┐
│ ReplicaSet │ │ ReplicaSet │
│ Key: A │ │ Key: A │
│ Values: 2,3 │ │ Values: 2,3 │
└──────────────────┘ └──────────────────┘
┌────┐┌────┐┌────┐ ┌────┐┌────┐┌────┐
│Pod1││Pod2││Pod3│ │Pod1││Pod2││Pod3│
│A:1 ││A:2 ││A:3 │ │A:1 ││A:2 ││A:3 │
└────┘└────┘└────┘ └────┘└────┘└────┘
✗ ✓ ✓ ✓ ✗ ✗
값이 2 또는 3이면 선택 값이 2, 3이 아니면 선택
| operator | 조건 | 필요한 항목 |
|---|---|---|
Exists | 해당 key가 존재하면 선택 | key |
DoesNotExist | 해당 key가 없으면 선택 | key |
In | key의 값이 values 안에 있으면 선택 | key, values |
NotIn | key의 값이 values 안에 없으면 선택 | key, values |
값을 정확히 알지 못해도 "버전 정보가 붙어 있는 Pod 전부"처럼 유연한 조건을 만들 수 있다는 것이 ReplicaSet의 장점이다.
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: replica-1
spec:
replicas: 3
selector:
matchLabels:
type: web
matchExpressions:
- {key: ver, operator: Exists}
template:
metadata:
name: pod
labels:
type: web
ver: v1
spec:
containers:
- name: container
image: tmkube/app
| 항목 | 설명 |
|---|---|
apiVersion: apps/v1 | ReplicaSet은 v1이 아니라 apps/v1 을 사용한다 |
replicas: 3 | Pod 3개를 유지한다 |
matchLabels | 값이 정확히 일치해야 하는 조건 |
matchExpressions | key의 존재 여부나 포함 관계로 지정하는 조건 |
template | 새 Pod를 만들 때 사용할 정의 |
selector 조건과 template의 labels가 맞지 않으면 문제가 생긴다.
selector: type: web template의 labels: type: db
│ │
│ 조건에 맞는 Pod를 찾음 │ 이 Pod를 만듦
▼ ▼
조건에 맞는 Pod가 없음 ──────▶ 계속 새 Pod 생성
│
무한 반복
Controller가 만든 Pod를 자기 자신이 인식하지 못하므로, 개수를 채우려고 Pod를 계속 만들어내게 된다. 쿠버네티스는 이런 설정을 아예 거부하여 ReplicaSet 생성을 막는다.
Deployment는 Software Update를 담당하는 Controller다. 앞에서 본 ReplicaSet은 Pod의 개수를 유지할 뿐, 버전을 올리는 기능은 없었다.
Deployment는 ReplicaSet을 직접 관리하면서 버전 업그레이드를 처리한다.
Deployment ──관리──▶ ReplicaSet ──관리──▶ Pod
(버전 관리) (개수 유지) (실행)
사용자는 Deployment만 만들면 된다. ReplicaSet은 Deployment가 알아서 생성하고 관리한다.
Deployment는 두 가지 배포 방식을 직접 지원하고, 나머지 두 가지는 Service와 조합해 구현한다.
| 배포 방식 | Deployment 지원 | 다운타임 | 추가 자원 |
|---|---|---|---|
| ReCreate | 지원 (strategy: Recreate) | 발생 | 없음 |
| Rolling Update | 지원 (strategy: RollingUpdate, 기본값) | 없음 | 배포 중 일부 증가 |
| Blue/Green | 미지원 (Controller 2개 + Service 활용) | 없음 | 2배 |
| Canary | 미지원 (Service + Ingress 활용) | 없음 | 일부 증가 |
기존 Pod를 전부 삭제한 뒤 새 버전의 Pod를 만드는 방식이다. 가장 단순하지만 다운타임이 발생한다.
v1 Pod 2개를 v2로 올리는 상황이다.
① 초기 상태
┌──────────────┐
│ Service │
└──┬────────┬──┘
│ │
┌──────▼──┐ ┌───▼─────┐
│ Pod v1 │ │ Pod v1 │
└─────────┘ └─────────┘
자원 사용량: ██ ██ (2개)
서비스 상태: 정상
② v1 Pod를 모두 삭제
┌──────────────┐
│ Service │
└──────────────┘
연결할 Pod 없음
┌─────────┐ ┌─────────┐
│ Pod v1 │ │ Pod v1 │
│ ✗ │ │ ✗ │
└─────────┘ └─────────┘
자원 사용량: □□ □□ (0개, 자원도 함께 반납됨)
서비스 상태: ⚠ Downtime 발생
이 시점에 사용자가 접속하면 연결할 Pod가 없어 서비스가 중단된다.
③ v2 Pod 2개 생성
┌──────────────┐
│ Service │
└──┬────────┬──┘
│ │
┌──────▼──┐ ┌───▼─────┐
│ Pod v2 │ │ Pod v2 │
└─────────┘ └─────────┘
자원 사용량: ██ ██ (2개)
서비스 상태: 정상 복구
사용량
│
2 │ ██ ██ ██ ██
│
0 │ □□ □□
└────────────────────────────▶ 시간
초기 삭제 후 생성 후
(Downtime)
배포 중 자원 사용량이 0으로 떨어진다. 추가 자원이 필요 없다는 것이 유일한 장점이다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-1
spec:
selector:
matchLabels:
type: app
replicas: 2
strategy:
type: Recreate
revisionHistoryLimit: 1
template:
metadata:
labels:
type: app
spec:
containers:
- name: container
image: tmkube/app:v1
| 항목 | 설명 |
|---|---|
strategy.type: Recreate | 전부 삭제 후 재생성하는 방식을 사용한다 |
revisionHistoryLimit | 롤백을 위해 보관할 이전 ReplicaSet의 개수. 기본값은 10 |
Deployment는 ReplicaSet의 replicas 수를 조절하는 방식으로 동작한다.
┌────────────────────────────────────────────┐
│ Deployment │
│ selector replicas template │
│ type:app 2 Pod v2 ◀── 템플릿 변경
└───────┬────────────────────┬───────────────┘
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ ReplicaSet │ │ ReplicaSet │
│ (v1용) │ │ (v2용) │
│ replicas 0 │ │ replicas 2 │
└─────────────┘ └──────┬──────┘
Pod 없음 │
┌──────┴──────┐
▼ ▼
┌─────────┐ ┌─────────┐
│ Pod v2 │ │ Pod v2 │
└─────────┘ └─────────┘
기존 ReplicaSet의 replicas를 2에서 0으로 내리고, 새 ReplicaSet의 replicas를 0에서 2로 올린다. 두 작업이 순차적으로 일어나므로 그 사이에 Pod가 하나도 없는 구간이 생긴다.
기존 ReplicaSet은 삭제되지 않고 replicas: 0 상태로 남는다. 롤백할 때 다시 이 값을 올리기 위해서다.
Pod를 하나씩 교체하는 방식이다. Deployment의 기본값이며, 다운타임이 없다.
replicas: 2, 기본값인 maxSurge: 1, maxUnavailable: 0 기준이다. Pod 개수는 2 → 3 → 2 → 3 → 2로, 3에서 머무르지 않고 매번 2로 돌아온 뒤 다시 늘어난다.
① 초기 상태 (2개)
┌──────────────┐
│ Service │
└──┬────────┬──┘
│ │
┌──────▼──┐ ┌───▼─────┐
│ Pod v1 │ │ Pod v1 │
└─────────┘ └─────────┘
자원 사용량: ██ ██ (2개)
② v2 Pod 하나 생성 → Ready 대기 (3개)
┌──────────────────┐
│ Service │
└──┬───────┬────┬──┘
│ │ │
┌──────▼──┐ ┌──▼───┐ ┌─▼──────┐
│ Pod v1 │ │Pod v1│ │Pod v2 │ ← 새로 생성, Ready 대기
└─────────┘ └──────┘ └────────┘
자원 사용량: ██ ██ ██ (3개, 증가)
서비스 상태: v1과 v2가 동시에 서비스 중
여기가 Rolling Update의 특징이다. 이 시점에 접속하는 사용자는 v1에 연결될 수도 있고 v2에 연결될 수도 있다. Service가 두 버전의 Pod에 모두 트래픽을 분배하기 때문이다.
③ v2가 Ready 되면, v1 하나 삭제 (2개로 감소)
┌──────────────┐
│ Service │
└──┬────────┬──┘
│ │
┌──────▼──┐ ┌───▼─────┐
│ Pod v1 │ │ Pod v2 │
└─────────┘ └─────────┘
자원 사용량: ██ ██ (2개, 다시 감소)
서비스 상태: v1 1개, v2 1개
maxUnavailable: 0이므로 새 Pod가 Ready 상태가 되어야만 기존 Pod를 삭제한다. "삭제"와 "생성"은 동시에 일어나는 것이 아니라 항상 하나씩 순차적으로 일어난다.
④ v2 Pod 하나 더 생성 → Ready 대기 (3개로 증가)
┌──────────────────┐
│ Service │
└──┬───────┬────┬──┘
│ │ │
┌──────▼──┐ ┌──▼───┐ ┌─▼──────┐
│ Pod v1 │ │Pod v2│ │Pod v2 │ ← 새로 생성, Ready 대기
└─────────┘ └──────┘ └────────┘
자원 사용량: ██ ██ ██ (3개, 다시 증가)
서비스 상태: v1 1개, v2 2개
⑤ 남은 v1 삭제 → 완료 (2개)
┌──────────────┐
│ Service │
└──┬────────┬──┘
│ │
┌──────▼──┐ ┌───▼─────┐
│ Pod v2 │ │ Pod v2 │
└─────────┘ └─────────┘
자원 사용량: ██ ██ (2개, 원상 복귀)
서비스 상태: 정상
사용량
│
3 │ ██ ██
│ ╱ ╲ ╱ ╲
2 │ ██ ╱ ██ ╱ ██
│
└──────────────────────────▶ 시간
초기 생성 삭제 생성 삭제
(v2) (v1) (v2) (v1)
3에서 머무르지 않고 생성 → 삭제 → 생성 → 삭제를 한 번에 한 동작씩 순서대로 밟으며 3과 2 사이를 오간다. Pod 개수가 많아질수록 이 오르내림이 그만큼 더 반복된다. 배포 중 일시적으로 추가 자원을 요구한다는 것이 ReCreate와의 차이이며, 대신 다운타임이 없다.
┌────────────────────────────────────────────┐
│ Deployment │
│ selector replicas template │
│ type:app 2 Pod v2 │
└───────┬────────────────────┬───────────────┘
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ ReplicaSet │ │ ReplicaSet │
│ (v1용) │ │ (v2용) │
│ 2 → 2 → 1 → │ │ 0 → 1 → 1 → │
│ 1 → 0 │ │ 2 → 2 │
└─────────────┘ └─────────────┘
한 번에 한 칸씩만 움직인다
한 번에 한 ReplicaSet의 값만 1씩 움직이며, 두 ReplicaSet의 합이 replicas + maxSurge(여기선 3)를 넘지 않고 replicas - maxUnavailable(여기선 2) 밑으로 내려가지도 않도록 조절된다.
ReCreate가 2 → 0 후 0 → 2로 한 번에 크게 움직였다면, Rolling Update는 한 칸씩 번갈아 가며 조절한다. 그래서 두 버전의 Pod가 잠시 공존한다.
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-2
spec:
selector:
matchLabels:
type: app
replicas: 2
strategy:
type: RollingUpdate
minReadySeconds: 10
template:
metadata:
labels:
type: app
spec:
containers:
- name: container
image: tmkube/app:v1
| 항목 | 설명 |
|---|---|
strategy.type: RollingUpdate | 하나씩 교체하는 방식. 생략해도 기본값 |
minReadySeconds | Pod가 Ready 상태가 된 후 다음 Pod로 넘어가기까지 대기할 시간(초) |
minReadySeconds가 필요한 이유가 있다. Pod가 Ready 상태가 되었더라도 애플리케이션이 완전히 안정화되지 않았을 수 있다. 이 값을 두면 각 단계마다 잠시 기다리므로, 문제가 있는 버전이 한꺼번에 배포되는 것을 막을 수 있다.
v1과 v2가 동시에 서비스되는 구간이 있으므로, 두 버전이 공존해도 문제가 없어야 한다.
이런 경우에는 ReCreate나 Blue/Green을 고려해야 한다.
Controller를 하나 더 만들어서 새 버전을 통째로 준비해 두고, Service의 연결 대상만 바꾸는 방식이다.
Deployment가 직접 지원하는 기능이 아니라, Controller 두 개와 Service의 Label을 이용해 직접 구현한다.
① v1 Controller와 Pod만 존재 (Blue)
┌──────────────┐
│ Controller │
│ (v1) │
└──┬────────┬──┘
│ │
┌────▼────┐ ┌─▼───────┐
│ Pod │ │ Pod │
│ ver: v1 │ │ ver: v1 │
└────┬────┘ └────┬────┘
│ │
└─────┬─────┘
│
┌─────▼──────┐
│ Service │
│ ver: v1 │ ← selector가 v1을 가리킴
└────────────┘
자원 사용량: ██ ██ (2개)
② v2 Controller를 새로 생성 (Green)
┌──────────────┐ ┌──────────────┐
│ Controller │ │ Controller │
│ (v1) │ │ (v2) │ ← 새로 생성
└──┬────────┬──┘ └──┬────────┬──┘
│ │ │ │
┌────▼────┐ ┌─▼───────┐ ┌────▼────┐ ┌─▼───────┐
│ Pod │ │ Pod │ │ Pod │ │ Pod │
│ ver: v1 │ │ ver: v1 │ │ ver: v2 │ │ ver: v2 │
└────┬────┘ └────┬────┘ └─────────┘ └─────────┘
│ │
└─────┬─────┘ 트래픽 없음 (대기 상태)
│
┌─────▼──────┐
│ Service │
│ ver: v1 │
└────────────┘
자원 사용량: ██ ██ ██ ██ (4개, 2배)
이 시점에서 v2 Pod는 떠 있지만 Service가 연결하지 않으므로 트래픽을 받지 않는다. 이 상태에서 v2를 충분히 검증할 수 있다.
③ Service의 Label만 v2로 변경
┌──────────────┐ ┌──────────────┐
│ Controller │ │ Controller │
│ (v1) │ │ (v2) │
└──┬────────┬──┘ └──┬────────┬──┘
│ │ │ │
┌────▼────┐ ┌─▼───────┐ ┌────▼────┐ ┌─▼───────┐
│ Pod │ │ Pod │ │ Pod │ │ Pod │
│ ver: v1 │ │ ver: v1 │ │ ver: v2 │ │ ver: v2 │
└─────────┘ └─────────┘ └────┬────┘ └────┬────┘
│ │
트래픽 없음 └─────┬─────┘
│
┌─────▼──────┐
│ Service │
│ ver: v2 │ ← selector 변경
└────────────┘
Service의 selector를 ver: v1에서 ver: v2로 수정하는 것만으로 순간적으로 전환된다. Pod를 만들거나 지우는 과정이 없으므로 다운타임이 없다.
④ v1 Controller 삭제
┌──────────────┐
│ Controller │
│ (v2) │
└──┬────────┬──┘
│ │
┌────▼────┐ ┌─▼───────┐
│ Pod │ │ Pod │
│ ver: v2 │ │ ver: v2 │
└────┬────┘ └────┬────┘
└─────┬─────┘
┌─────▼──────┐
│ Service │
│ ver: v2 │
└────────────┘
자원 사용량: ██ ██ (2개로 복귀)
사용량
│
4 │ ████ ████
│
2 │ ██ ██ ██ ██
│
└───────────────────────────▶ 시간
초기 배포 중 완료
(2배 사용)
| 구분 | 내용 |
|---|---|
| 장점 — 다운타임 없음 | Label 변경만으로 전환되므로 순간적으로 바뀐다 |
| 장점 — 롤백이 쉬움 | 문제가 생기면 Service의 Label을 다시 v1로 되돌리면 끝이다 |
| 장점 — 사전 검증 가능 | v2가 트래픽을 받기 전에 충분히 테스트할 수 있다 |
| 단점 — 자원 2배 | 두 버전의 Pod가 모두 떠 있어야 하므로 자원이 두 배로 필요하다 |
롤백의 차이가 특히 중요하다. Rolling Update는 롤백할 때도 Pod를 하나씩 다시 교체해야 하지만, Blue/Green은 Label 하나만 되돌리면 즉시 복구된다. 그래서 실패했을 때의 영향이 가장 적은 방식이다.
새 버전을 일부 트래픽에만 먼저 노출해서 검증한 뒤, 문제가 없으면 전체로 확대하는 방식이다.
이름은 탄광에서 유독가스를 미리 감지하던 카나리아 새에서 유래했다.
가장 단순한 구현이다. Service 하나가 v1과 v2 Pod를 모두 선택하도록 하고, Pod 개수로 트래픽 비율을 조절한다.
┌──────────────┐ ┌──────────────┐
│ Controller │ │ Controller │
│ (v1) │ │ (v2) │
└──┬────────┬──┘ └──────┬───────┘
│ │ │
┌────▼────┐ ┌─▼───────┐ ┌─────▼─────┐
│ Pod │ │ Pod │ │ Pod │
│ ty: app │ │ ty: app │ │ ty: app │
│ ver: v1 │ │ ver: v1 │ │ ver: v2 │
└────┬────┘ └────┬────┘ └─────┬─────┘
│ │ │
└───────────┼───────────────┘
│
┌─────▼──────┐
│ Service │
│ ty: app │ ← ver를 조건에 넣지 않음
└────────────┘
트래픽 비율: v1 67% / v2 33%
Service의 selector가 ty: app만 보므로 v1, v2 Pod를 모두 선택한다. v2 Pod가 1개, v1 Pod가 2개이므로 트래픽의 약 1/3이 v2로 간다.
문제가 없으면 v2 Pod를 늘리고 v1 Pod를 줄여가며 비율을 조정한다.
URL 경로에 따라 서비스를 나누는 방식이다. Ingress Controller를 활용한다.
┌──────────────┐ ┌──────────────┐
│ Controller │ │ Controller │
│ (v1) │ │ (v2) │
└──┬────────┬──┘ └──────┬───────┘
│ │ │
┌────▼────┐ ┌─▼───────┐ ┌─────▼─────┐
│ Pod │ │ Pod │ │ Pod │
│ ver: v1 │ │ ver: v1 │ │ ver: v2 │
└────┬────┘ └────┬────┘ └─────┬─────┘
└─────┬─────┘ │
│ │
┌─────▼──────┐ ┌─────▼──────┐
│ Service │ │ Service │
│ ver: v1 │ │ ver: v2 │
└─────┬──────┘ └─────┬──────┘
│ │
/app /v2/app
│ │
└──────────┬──────────┘
│
┌───────────▼────────────┐
│ Ingress Controller │
└────────────────────────┘
| 요청 경로 | 연결되는 버전 |
|---|---|
/app | v1 |
/v2/app | v2 |
특정 사용자에게만 /v2/app 경로를 안내해 새 버전을 먼저 써보게 할 수 있다. 검증이 끝나면 Ingress 설정을 바꿔 /app 요청이 v2로 가도록 전환한다.
검증 전: /app ──▶ v1
/v2/app ──▶ v2
전환 후: /app ──▶ v2
사용량
│
3 │ ███ ███ ███
│
2 │ ██ ██ ██ ██
│
└───────────────────────────▶ 시간
초기 검증 중 완료
v2 Pod를 조금만 띄우므로 Blue/Green처럼 2배가 필요하지는 않다.
| 구분 | 내용 |
|---|---|
| 장점 — 다운타임 없음 | 기존 v1이 계속 서비스하는 상태에서 v2를 추가한다 |
| 장점 — 위험 최소화 | 문제가 생겨도 영향받는 사용자가 일부로 제한된다 |
| 장점 — 실제 환경 검증 | 테스트 환경이 아닌 실제 트래픽으로 새 버전을 확인할 수 있다 |
| 단점 — 구성이 복잡 | Controller, Service, Ingress를 직접 구성하고 관리해야 한다 |
| 단점 — 모니터링 필요 | v2에 문제가 있는지 판단할 지표와 관찰 체계가 갖춰져 있어야 의미가 있다 |
ReCreate Rolling Update Blue/Green Canary
──────── ────────────── ────────── ──────
v1 v1 v1 v1 v1 v1 v2 v2 v1 v1 v2
↓ ↓ ↘ ↙ ↓ ↓
(없음) ⚠ v1 v2 Service 전환 Service
↓ ↓ ↓ ↓
v2 v2 v2 v2 v2 v2 비율 조정
Downtime 발생 Zero Downtime Zero Downtime Zero Downtime
자원 추가 없음 자원 일부 증가 자원 2배 자원 일부 증가
| 구분 | ReCreate | Rolling Update | Blue/Green | Canary |
|---|---|---|---|---|
| 다운타임 | 발생 | 없음 | 없음 | 없음 |
| 배포 중 자원 | 감소 (0까지) | 일부 증가 | 2배 | 일부 증가 |
| 두 버전 공존 | 없음 | 있음 | 없음 (전환 순간만) | 있음 (의도적) |
| 롤백 속도 | 느림 (재배포) | 느림 (재교체) | 즉시 (Label 변경) | 빠름 (v2 제거) |
| Deployment 지원 | Recreate | RollingUpdate | 미지원 (직접 구성) | 미지원 (직접 구성) |
| 구성 난이도 | 매우 쉬움 | 쉬움 | 보통 | 어려움 |
선택 기준
ReplicaSet은 "Pod를 몇 개 유지할지"를 정하고, 스케줄러가 자원 여유를 보고 배치할 Node를 고른다. 그래서 Pod가 특정 Node에 몰리거나, 어떤 Node에는 하나도 배치되지 않을 수 있다.
DaemonSet은 다르다. Node의 자원 상태와 상관없이 모든 Node에 Pod를 하나씩 생성한다.
[ ReplicaSet ] [ DaemonSet ]
────────────── ─────────────
┌────────────┐ ┌────────────┐
│ ReplicaSet │ │ DaemonSet │
└─┬────┬─────┘ └─┬───┬───┬──┘
│ │ │ │ │
┌─▼────▼──────┐ ┌─▼───────────┐
│ Node1 │ │ Node1 │
│ Pod Pod │ │ Pod │
│ ▓▓ ▓▓ ▓▓ │ 자원 여유 많음 │ │
└─────────────┘ └─────────────┘
┌─────────────┐ ┌─────────────┐
│ Node2 │ │ Node2 │
│ Pod │ │ Pod │
│ ▓▓ ▓▓ │ │ │
└─────────────┘ └─────────────┘
┌─────────────┐ ┌─────────────┐
│ Node3 │ │ Node3 │
│ (없음) │ 자원 부족 │ Pod │
│ ▓ │ │ │
└─────────────┘ └─────────────┘
자원 여유에 따라 배치 Node마다 무조건 1개
Node가 새로 추가되면 DaemonSet이 그 Node에도 자동으로 Pod를 하나 생성한다. Node가 제거되면 그 Pod도 함께 사라진다.
Node마다 하나씩 떠 있어야 의미가 있는 작업에 사용한다.
| 용도 | 대표 도구 | 하는 일 |
|---|---|---|
| Performance | Prometheus (Node Exporter) | Node의 CPU, 메모리, 디스크 사용량을 수집 |
| Logging | Fluentd | Node에 쌓인 로그를 수집해 중앙 서버로 전송 |
| Storage | GlusterFS | Node의 디스크를 묶어 분산 스토리지로 제공 |
공통점은 Node 하나하나의 정보에 접근해야 한다는 것이다. 로그를 수집하려면 모든 Node에 수집기가 있어야 하고, 한 Node라도 빠지면 그 Node의 로그는 유실된다.
Volume에서 다룬 hostPath가 DaemonSet과 함께 쓰이는 이유도 여기에 있다. 모든 Node에 Pod를 하나씩 띄우고, 각 Pod가 자기 Node의 /var/log를 마운트해 로그를 읽는 구조다.
모든 Node가 아니라 조건에 맞는 Node에만 배치할 수도 있다.
┌──────────────────────────────┐
│ DaemonSet │
│ selector template │
│ type:app Pod │──▶ nodeSelector: os: centos
└───┬──────────┬───────────┬───┘
│ │ │
▼ ▼ ✗
┌─────────┐┌─────────┐┌─────────┐
│ Node1 ││ Node2 ││ Node3 │
│ Pod ││ Pod ││ (없음) │
│os:centos││os:centos││os:ubuntu│
└─────────┘└─────────┘└─────────┘
os: centos Label이 붙은 Node에만 Pod가 생성되고, os: ubuntu인 Node3에는 생성되지 않는다.
DaemonSet은 Node마다 Pod가 하나씩이므로, Node의 포트를 Pod에 직접 연결할 수 있다.
┌─────────────────┐ ┌─────────────────┐
│ Node1 │ │ Node2 │
│ ┌─────────┐ │ │ ┌─────────┐ │
│ │ Pod │ │ │ │ Pod │ │
│ │ 8080 │ │ │ │ 8080 │ │
│ └────┬────┘ │ │ └────┬────┘ │
│ │ │ │ │ │
│ [ 18080 ] │ │ [ 18080 ] │
└────────┬────────┘ └────────┬────────┘
│ │
Node IP : 18080 로 직접 접근
| 항목 | 설명 |
|---|---|
containerPort | 컨테이너가 사용하는 포트 |
hostPort | 그 컨테이너를 연결할 Node의 포트 |
NodePort와 비슷해 보이지만 차이가 있다. NodePort는 Service를 거쳐 어느 Node로 들어와도 Pod가 있는 Node로 전달되지만, hostPort는 그 Node의 Pod에 직접 연결된다. 각 Node의 상태를 개별적으로 확인해야 하는 모니터링 도구에 적합하다.
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: daemonset-1
spec:
selector:
matchLabels:
type: app
template:
metadata:
labels:
type: app
spec:
nodeSelector:
os: centos
containers:
- name: container
image: tmkube/app
ports:
- containerPort: 8080
hostPort: 18080
| 항목 | 설명 |
|---|---|
replicas 없음 | DaemonSet에는 replicas가 없다. 개수는 Node 수로 결정되기 때문 |
nodeSelector | 조건에 맞는 Node에만 Pod를 배치 |
hostPort | Node의 포트를 Pod에 직접 연결 |
replicas가 없다는 점이 다른 Controller와 가장 뚜렷한 차이다.
세 오브젝트 모두 Pod를 만들지만, Pod가 종료되었을 때의 동작이 다르다.
┌──────────────┐
│ Pod │ Node1 장애 시 → Pod 사라짐, 복구 없음
└──────────────┘
┌──────────────┐ ┌─────────────────────┐
│ ReplicaSet │ Node1 장애 시 → │ Node2에 Recreate │
└──────────────┘ │ 계속 Running 유지 │
└─────────────────────┘
┌──────────────┐ ┌─────────────────────┐
│ Job │ Node1 장애 시 → │ Node2에 Recreate │
└──────────────┘ │ 작업 끝나면 Finish │
└─────────────────────┘
| 구분 | Pod | ReplicaSet | Job |
|---|---|---|---|
| Node 장애 시 | 복구되지 않음 | 다른 Node에 재생성 | 다른 Node에 재생성 |
| Pod 종료 후 | 종료된 상태로 남음 | 계속 Restart (서비스 유지) | Finish (작업 완료로 처리) |
| 목적 | 일회성 실행 | 계속 떠 있어야 하는 서비스 | 끝이 있는 작업 |
핵심은 ReplicaSet은 Pod가 끝나는 것을 장애로 보고 다시 띄우지만, Job은 정상 종료로 인정한다는 점이다.
웹 서버가 꺼지면 문제이므로 다시 띄워야 하지만, 백업 작업이 끝난 것은 성공이므로 다시 띄우면 안 된다. 이 차이 때문에 Job이 따로 존재한다.
| 용도 | 설명 |
|---|---|
| Backup | 데이터베이스를 백업 파일로 내보내는 작업 |
| Checking | 데이터 정합성 검사, 업데이트 확인 |
| Messaging | 대량 메일이나 알림 발송 |
작업이 끝나면 Pod가 종료되고 자원이 반납되므로, 항상 띄워두는 것보다 자원을 효율적으로 쓸 수 있다.
┌─────────────────────────────────┐
│ Job │
│ completions : 6 │
│ ┌────┐ ┌────┐ ┌────┐ │
│ │Pod │ │Pod │ │Pod │ │
│ └────┘ └────┘ └────┘ │
│ ┌────┐ ┌────┐ ┌────┐ │
│ │Pod │ │Pod │ │Pod │ │
│ └────┘ └────┘ └────┘ │
└─────────────────────────────────┘
Pod 6개가 순차적으로 실행되고 완료
completions: 6은 성공적으로 완료되어야 하는 Pod의 수를 뜻한다. 6개가 모두 성공해야 Job이 완료 상태가 된다.
parallelism: 2
┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐ ┌────┐
│Pod │ │Pod │ → │Pod │ │Pod │ → │Pod │ │Pod │
└────┘ └────┘ └────┘ └────┘ └────┘ └────┘
1회차 (2개) 2회차 (2개) 3회차 (2개)
합계 6개 = completions
기본값은 1이라 하나씩 순차 실행되지만, parallelism: 2를 주면 2개씩 동시에 실행된다. 6개를 처리하는 데 걸리는 시간이 줄어든다.
┌────┐ ┌ ─ ─┐
│Job │ ────────────────────────────▶ Job
└────┘ └ ─ ─┘
시작 30초 경과 강제 종료
작업이 지정한 시간 안에 끝나지 않으면 강제로 종료한다. 무한 루프에 빠지거나 응답 없는 외부 시스템을 기다리며 Pod가 영원히 남는 상황을 막는다.
apiVersion: batch/v1
kind: Job
metadata:
name: job-1
spec:
completions: 6
parallelism: 2
activeDeadlineSeconds: 30
template:
spec:
restartPolicy: Never
containers:
- name: container
image: tmkube/init
| 항목 | 설명 |
|---|---|
apiVersion: batch/v1 | Job과 CronJob은 apps/v1이 아니라 batch/v1 을 사용한다 |
completions | 성공해야 하는 Pod의 총 개수 |
parallelism | 동시에 실행할 Pod의 개수 |
activeDeadlineSeconds | 작업의 제한 시간(초). 초과하면 강제 종료 |
restartPolicy: Never | Pod가 실패해도 컨테이너를 재시작하지 않는다 |
selector 없음 | Job은 selector를 지정하지 않는다. 쿠버네티스가 자동으로 부여한다 |
restartPolicy는 Job에서 반드시 지정해야 하는 값이다.
| 값 | 동작 |
|---|---|
Never | 실패 시 컨테이너를 재시작하지 않고 새 Pod를 만든다 |
OnFailure | 실패 시 같은 Pod 안에서 컨테이너를 재시작한다 |
Always는 Job에서 사용할 수 없다. 작업이 끝나도 계속 재시작하게 되어 Job의 목적과 맞지 않기 때문이다.
CronJob은 Job을 정해진 일정에 따라 반복 생성하는 Controller다. Job을 직접 만드는 것이 아니라, Job을 만드는 Controller라는 점이 핵심이다.
┌─────────────────────────────────────────┐
│ CronJob │
│ schedule : */1 * * * * │
│ jobTemplate : Job │
└────┬──────────────┬──────────────┬──────┘
│ │ │
1 Min 2 Min 3 Min
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Job │ │ Job │ │ Job │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Pod │ │ Pod │ │ Pod │
└─────────┘ └─────────┘ └─────────┘
1분마다 Job이 하나씩 생성되고, 각 Job이 Pod를 만들어 작업을 수행한다.
리눅스의 cron 표현식을 그대로 사용한다.
* * * * *
│ │ │ │ │
│ │ │ │ └── 요일 (0~6, 0=일요일)
│ │ │ └──── 월 (1~12)
│ │ └────── 일 (1~31)
│ └──────── 시 (0~23)
└────────── 분 (0~59)
| 표현식 | 의미 |
|---|---|
*/1 * * * * | 1분마다 |
0 * * * * | 매시 정각 |
0 2 * * * | 매일 새벽 2시 |
0 3 * * 0 | 매주 일요일 새벽 3시 |
정해진 시간이 되었는데 이전 Job이 아직 끝나지 않은 경우를 어떻게 처리할지 정한다.
Allow Forbid Replace
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│1min 2min 3min │ │1min 2min 3min │ │1min 2min 3min │
│ │ │ skip │ │ │
│ Job Job Job │ │ Job ⚠ Job │ │ Job Job Job │
│ │ │ │ │ │ │ │ │ │ │ ✗ │ │ │
│ Pod Pod Pod │ │ Pod ────▶ Pod │ │ Pod Pod Pod │
└───────────────┘ └───────────────┘ └───────────────┘
이전 Job과 상관없이 이전 Job이 실행 중이면 이전 Job을 삭제하고
무조건 새로 생성 이번 차례는 건너뜀 새 Job으로 교체
| 값 | 동작 | 사용하는 경우 |
|---|---|---|
Allow (기본값) | 이전 Job과 무관하게 새 Job을 생성한다. 여러 Job이 동시에 실행될 수 있다 | 작업들이 서로 영향을 주지 않는 경우 |
Forbid | 이전 Job이 실행 중이면 이번 실행을 건너뛴다 | 같은 작업이 중복되면 안 되는 경우 (예: DB 백업) |
Replace | 이전 Job을 삭제하고 새 Job으로 교체한다 | 최신 결과만 중요한 경우 (예: 캐시 갱신) |
┌─────────────────────────────────┐
│ CronJob │◀── Suspend (일시 중지)
│ schedule : */1 * * * * │
│ jobTemplate : Job │◀── Manual Trigger (수동 실행)
└─────────────────────────────────┘
| 기능 | 설명 |
|---|---|
| Suspend | 스케줄을 일시 중지한다. CronJob은 남아 있지만 Job을 만들지 않는다 |
| Manual Trigger | 일정과 무관하게 지금 즉시 Job을 실행한다. 테스트나 긴급 작업에 사용 |
# 일시 중지
kubectl patch cronjob cron-job -p '{"spec":{"suspend":true}}'
# 수동 실행
kubectl create job --from=cronjob/cron-job manual-job-1
apiVersion: batch/v1
kind: CronJob
metadata:
name: cron-job
spec:
schedule: "*/1 * * * *"
concurrencyPolicy: Allow
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
containers:
- name: container
image: tmkube/app
| 항목 | 설명 |
|---|---|
schedule | Job을 생성할 일정 (cron 표현식) |
concurrencyPolicy | 이전 Job이 실행 중일 때의 처리 방식 |
jobTemplate | 생성할 Job의 내용 |
구조가 한 겹 더 깊다는 점에 주의해야 한다.
CronJob
└── jobTemplate ← Job의 정의
└── template ← Pod의 정의
└── containers
CronJob이 Job을 만들고, Job이 Pod를 만들기 때문에 템플릿이 두 번 중첩된다.
| 항목 | 내용 |
|---|---|
| 목적 | 끝이 있는 일시적인 작업을 수행한다 |
| apiVersion | 둘 다 batch/v1을 사용한다 |
| Pod 종료 처리 | 작업이 끝나면 Pod를 Finish로 처리하고 재시작하지 않는다 |
| 자원 효율 | 작업이 끝나면 자원을 반납한다 |
| restartPolicy | Never 또는 OnFailure만 사용 가능하다 |
| 사용 사례 | Backup, Checking, Messaging |
Job CronJob
──────────── ──────────────────
┌──────────┐ ┌──────────┐
│ Job │ │ CronJob │
└────┬─────┘ └────┬─────┘
│ │ 일정마다
▼ ┌────┼────┬────┐
┌──────────┐ ▼ ▼ ▼ ▼
│ Pod │ ┌────┐┌────┐┌────┐
└──────────┘ │Job ││Job ││Job │
└─┬──┘└─┬──┘└─┬──┘
한 번 실행하고 끝 ▼ ▼ ▼
Pod Pod Pod
일정에 따라 반복
| 구분 | Job | CronJob |
|---|---|---|
| 실행 시점 | 생성하는 즉시 1회 실행 | 일정에 따라 반복 실행 |
| 만드는 대상 | Pod를 직접 생성 | Job을 생성 (Job이 Pod를 생성) |
| 반복 여부 | 반복하지 않음 | 계속 반복 |
| 고유 설정 | completions, parallelism, activeDeadlineSeconds | schedule, concurrencyPolicy, jobTemplate |
| 템플릿 깊이 | template (Pod) | jobTemplate → template (Job → Pod) |
| 사용 예 | 일회성 데이터 마이그레이션 | 매일 새벽 2시 DB 백업 |
관계로 정리하면 이렇다.
CronJob ──생성──▶ Job ──생성──▶ Pod
(일정 관리) (작업 관리) (실제 실행)
CronJob은 Job을 대체하는 것이 아니라 Job을 반복 실행해 주는 상위 개념이다. 한 번만 실행할 작업이면 Job을, 주기적으로 실행할 작업이면 CronJob을 사용한다.