[Kubernetes] 크론잡

Kubernetes 2025. 10. 7. 00:04

CronJob 은 리눅스 크론탭처럼 스케쥴된 일정 시각마다 Job 을 실행시키는 리소스이다. Job 의 또다른 형태로 보이지만, 실제로는 Deployment 와 ReplicaSet 의 관계처럼 CronJob 이 Job 을 관리하고, Job 이 Pod 를 관리하는 관계이다.

 

1. 크론잡 생성

apiVersion: batch/v1
kind: CronJob
metadata:
  name: sample-cronjob
spec:
  schedule: "*/1 * * * *"
  concurrencyPolicy: Allow
  startingDeadlineSeconds: 30
  successfulJobsHistoryLimit: 5
  failedJobsHistoryLimit: 3
  suspend: false
  jobTemplate:
    spec:
      completions: 1
      parallelism: 1
      backoffLimit: 1
      template:
        spec:
          containers:
            - name: sleep-container
              image: centos:7
              command:
                - "sh"
                - "-c"
              args:
                # 약 50% 확률로 성공하는 명령어
                - "sleep 40; date +'%N' | cut -c 9 | egrep '[1|3|5|7|9]'"
          restartPolicy: Never

 

위의 매니페스트를 기반으로 50% 확률로 성공하는 Job 을 실행하는 CronJob 을 생성하고, 아래 커맨드로 CronJob, Job, Pod 를 조회해보자

 

$ kubectl get cronjobs.batch
$ 크론잡 생성 직후에는 아직 다음 시작 1분에 도달하지 않아서 크롭잡만 존재하고, ACTIVE 잡이 없다.
NAME             SCHEDULE      TIMEZONE   SUSPEND   ACTIVE   LAST SCHEDULE   AGE
sample-cronjob   */1 * * * *   <none>     False     0        <none>          5s
$ kubectl get jobs
No resources found in default namespace.

# 몇초가 지난 후 재실행하면 ACTIVE 상태로 변경되며, Job 도 조회된다.
$ kubectl get cronjobs.batch
NAME             SCHEDULE      TIMEZONE   SUSPEND   ACTIVE   LAST SCHEDULE   AGE
sample-cronjob   */1 * * * *   <none>     False     1        40s             52s
$ kubectl get jobs
NAME                      STATUS    COMPLETIONS   DURATION   AGE
sample-cronjob-29329291   Running   0/1           30s        30s

# 50퍼센트 확률로 실패했다.
$ kubectl get pods
NAME                            READY   STATUS   RESTARTS   AGE
sample-cronjob-29329291-wg8rp   0/1     Error    0          48s

# 몇분간 방치한 후 매분 생성된 Job 과 Pod 들을 조회한다.
$ kubectl get jobs
NAME                      STATUS     COMPLETIONS   DURATION   AGE
sample-cronjob-29329300   Failed     0/1           11m        11m
sample-cronjob-29329301   Failed     0/1           10m        10m
sample-cronjob-29329305   Complete   1/1           92s        6m39s
sample-cronjob-29329306   Complete   1/1           93s        5m39s
sample-cronjob-29329307   Complete   1/1           93s        4m39s
sample-cronjob-29329308   Complete   1/1           43s        3m39s
sample-cronjob-29329309   Failed     0/1           2m39s      2m39s
sample-cronjob-29329310   Complete   1/1           43s        99s
sample-cronjob-29329311   Running    0/1           39s        39s

$ kubectl get pods
# backoffLimit: 1 이므로, 실패한 경우 한번 더 Pod 를 생성하여 시도한다.
NAME                            READY   STATUS      RESTARTS   AGE
sample-cronjob-29329300-4gdrc   0/1     Error       0          11m
sample-cronjob-29329300-8ts96   0/1     Error       0          10m
sample-cronjob-29329301-6wsxr   0/1     Error       0          10m
sample-cronjob-29329301-ww9jj   0/1     Error       0          9m56s
sample-cronjob-29329306-96zcl   0/1     Completed   0          4m56s
sample-cronjob-29329306-p7nmp   0/1     Error       0          5m46s
sample-cronjob-29329307-45pnz   0/1     Error       0          4m46s
sample-cronjob-29329307-pr9ls   0/1     Completed   0          3m56s
sample-cronjob-29329308-cvc4k   0/1     Completed   0          3m46s
sample-cronjob-29329309-6nrc9   0/1     Error       0          2m46s
sample-cronjob-29329309-6qhs6   0/1     Error       0          116s
sample-cronjob-29329310-fwj22   0/1     Completed   0          106s
sample-cronjob-29329311-rlmls   0/1     Completed   0          46s

 

2. 크론잡 일시정지

spec.suspend: true 로 설정하여 적용하면 스케쥴링 대상에서 제외된다. 매니페스트를 변경 후 kubectl apply 를 하거나, 아래처럼 kubectl patch 를 하여 적용한다. 다시 suspend: false 로 적용하면 일시정지가 풀린다.

$ kubectl patch cronjob sample-cronjob -p '{"spec":{"suspend":true}}'

# SUSPEND: True 로 변경되며, 스케쥴링에서 제외된 상태이다.
$ kubectl get cronjobs
NAME             SCHEDULE      TIMEZONE   SUSPEND   ACTIVE   LAST SCHEDULE   AGE
sample-cronjob   */1 * * * *   <none>     True      1        52s             45m

 

3. 크론잡을 스케쥴링 이외의 시점에 실행

CronJob 으로부터 Job 을 생성하여 1회 자율적으로 실행할 수 있다.

$ kubectl create job job-from-cronjob --from cronjob/sample-cronjob

 

4. 이전 잡이 종료되지 않았을 경우 동시성 제어

일반적으로 크론잡은 작업 완료까지 소요되는 시간을 고려하여 충분한 시간 간격으로 스케쥴링을 해야한다. 이전에 수행된 잡이 다음 수행시점 이전에 종료되었다면 다음 수행시점에는 새로운 잡을 생성하여 실행하지만, 만약에 이전 잡이 아직 실행중이라면 새로운 잡 실행 방식에 대해서 아래와 같이 spec.concurrencyPolicy 필드에 지정할 수 있다.

spec.concurrencyPolicy 값 설명
Allow 새로운 잡을 생성하여 이전 잡과 동시에 실행(기본값)
Forbid 새로운 잡을 생성하지 않음(동시실행 제한)
Replace 이전 잡을 삭제하고 새로운 잡을 생성하여 실행

 

5. 잡 실행 시작 기한 설정

크론잡으로 스케쥴링한 시각에는 kubernetes master 가 잡을 생성한다. 그러므로 해당 시각에 마스터 노드의 시스템 리소스가 부족하거나 일시적으로 다운되어 시작시각에 정확히 시작하지 못하면, 얼마나 지연 시작을 허용하지를 spec.startingDeadlineSeconds 필드에 지정할 수 있다. 예를 들어 매시 정각에 실행되는 크론잡인 경우(schedule: "0 * * * *") 정각부터 5분이 경과되기 전까지 시작을 허용하고자 한다면, spec.startingDeadlineSeconds: 300 값을 지정해주면 된다. 미지정시 아무리 지연되더라도 잡을 실행하게 되어있다.

 

6. 크론잡 이력 저장

크론잡이 생성한 잡은 종료된 후에도 삭제되지 않고 남아있는데, 남길 갯수를 지정할 수 있다.

필드 설명
spec.successfulJobsHistoryLimit 성공한 잡 저장 개수(기본값 3)
spec.failedJobsHistoryLimit 실패한 잡 저장 개수(기본값 3)

 

잡이 남아있으므로, 잡이 관리하는 파드도 Completed(정상종료) 또는 Error(비정상종료) 상태로 남아있게 된다.(1번 항목에서 조회한 Pod 참고) 0으로 지정하면 잡은 종료 즉시 삭제된다. 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

Job 리소스는 실행이 완료되어도 그 Job 과 Pod 가 자동으로 삭제되지 않는다. 이를 정리하는 옵션이 바로 ttlSecondsAfterFinished 이다. ttlSecondsAfterFinished 를 원하는 만큼 초 단위로 지정하면, Job 실행이 완료되 이후 해당 초가 지나면 Job 과 Pod 가 삭제된다.

apiVersion: batch/v1
kind: Job
metadata:
  name: job-ttl
spec:
  ttlSecondsAfterFinished: 30
  completions: 1
  parallelism: 1
  backoffLimit: 10
  template:
    spec:
      containers:
        - name: sleep-container
          image: centos:7
          command: ["sleep"]
          args: ["60"]
      restartPolicy: Never

 

위의 매니페스트로 Job 을 실행하면 60초 동안 Pod 하나가 실행되고 완료된 후, 30초 후에 Job 과 Pod 가 삭제되는 것을 볼 수 있다.

$ kubectl apply -f sample-job-ttl.yaml

$ kubectl get jobs
NAME                  STATUS     COMPLETIONS   DURATION   AGE
job-ttl               Running    0/1           4s         4s

# 30초 동안 실행되고 종료되는 Pod
$ kubectl get pods
NAME                        READY   STATUS      RESTARTS   AGE
job-ttl-kcv8n               1/1     Running     0          7s

# Job 의 상태가 변경되는 것을 출력
$ kubectl get job job-ttl --watch --output-watch-events
EVENT      NAME      STATUS    COMPLETIONS   DURATION   AGE
ADDED      job-ttl   Running   0/1           36s        36s
MODIFIED   job-ttl   Running   0/1           62s        62s
MODIFIED   job-ttl   Running   0/1           63s        63s
MODIFIED   job-ttl   Complete   1/1           63s        63s
MODIFIED   job-ttl   Complete   1/1           63s        93s
DELETED    job-ttl   Complete   1/1           63s        93s # Job 이 삭제된

# Pod 삭제 확인
$ kubectl get pods
No resources found in default namespace.

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

Job 매니페스트에는 아래 세가지 주요 필드를 설정할 수 있다.

필드명 의미 비고
completions 성공 횟수 실행 중 변경 불가능
parallelism 병렬성 동시에 실행하고자 하는 잡 갯수. 실행 중 변경 가능
backoffLimit 실패 허용 횟수 0 지정시 실패를 허용하지 않음. 실행 중 변경 가능

 

이를 기반으로 전형적인 네 가지 워크로드를 Job 으로 실행해보자

 

1. 1회만 실행하는  태스크

completions 1
parallelism 1
backoffLimit 0

 

성공 유무와 관계없이(backoffLimit: 0) 1회만 실행된다.

 

2. N 개를 병렬로 M 번 성공시까지 실행하는 태스크

completions M
parallelism N
backoffLimit P

 

실패는 P번 까지 허용하며, 동시에 N개의 파드를 기동하여 잡을 실행하며, 성공 횟수가 M 번이 될 때 까지 실행한다. 주의할 점은, 남은 성공횟수가 N 이하인 경우 남은 성공횟수 만큼만의 파드만 기동한다는 점이다.

예를들어, completions: 5, parallelism: 3 을 지정하면 아래와 같은 순서로 진행된다.

  • 3개의 파드가 동시 실행된다.(병렬성 3)
  • 3개의 파드가 성공적으로 실행 완료된다.(목표 성공횟수 성공횟수 2/5)
  • 남은 성공 횟수가 2 이므로, 3개가 아닌 2개의 파드가 동시 실행된다.(즉, 동시 실행되는 파드의 수는 남은 성공횟수보다 클 수 없다.)

또한, parallelism 을 completions 이상으로 설정하더라도 마찬가지로 남은 성공횟수가 completions 가 되어버리기 때문에 결국에는 completions 갯수만큼의 파드가 기동된다.

 

3. N 개를 병렬로 실행하는 작업큐

위에서 말한 Task 잡은 일정 성공 횟수에 도달할 때 까지 파드 종료 -> 시작을 반복하며 진행한다. 하지만 성공 횟수가 정해지지 않고 메시지 큐에 쌓인 모든 Topic 을 처리할 때 까지 N 개의 병렬성을 가지고 N 개의 파드를 계속 유지하면서 작업을 처리해야 하는 경우가 있다. 이때는 아래처럼 설정한다.

completions 지정하지 않음
parallelism N
backoffLimit P

 

이렇게 Job 을 실행하면 N 개의 파드가 병렬로 실행된다. 그 중 하나라도 정상 종료되면 전체 Job 이 종료되게 되며 더이상의 추가 파드 실행은 없다. 당연히 이미 실행중인 파드는 자체적으로 작업이 종료될 때 까지 유지된다.

 

작업큐의 잡을 이용하려면 대상 작업을 쌓기 위한 메시지 큐를 따로 두어야 한다. Job 이 관리하는 파드 내의 애플리케이션에서는 현재 진행중인 작업이 종료되기 전에 그 메시지 큐로부터 반복하여 Topic 을 가져오는 처리를 구현해두어야 한다. Topic 이 존재하면 현재 작업이 종료되어도 파드를 성공 종료처리하지 않고, 바로 이어서 해당 작업을 진행한다. 이를 반복하다가 메시지 큐가 빈 것을 확인하면 그때서야 파드를 정상 종료 처리하도록 구현하여야 전체 병렬 작업들을 마무리할 수 있게 된다.

이렇게 메시지 큐가 빈 이후에 다시 새로운 Topic 이 등록되는 경우를 위해서는 나중에 따로 설명할 Cron Job 을 이용하여 Job 을 정기적으로 실행하여 병렬 작업큐가 다시 기동될 수 있도록 구현해둔다.

메시지 큐에 쌓인 메시지 양에 따라 Job 의 parallelism 을 동적으로 변경할 수 있도록 작성하여 오토 스케일링하는 것도 가능하다.

 

작업큐는 completions 를 지정하지 않는다고 하였는데, 이렇게 작성하면 COMPLETIONS 가 태스크와 다르게 표시된다.

# COMPLETIONS 표시 형식 확인
$ kubectl get jobs multi-workqueue-job
NAME                  STATUS    COMPLETIONS   DURATION   AGE
multi-workqueue-job   Running   0/1 of 3      6s         6s

# 실행중인 파드 확인
$ kubectl get pods
NAME                        READY   STATUS    RESTARTS   AGE
multi-workqueue-job-6hdcg   1/1     Running   0          11s
multi-workqueue-job-jgxbt   1/1     Running   0          11s
multi-workqueue-job-tl4s2   1/1     Running   0          11s

# 파드 실행 완료 확인
$ kubectl get pods
NAME                        READY   STATUS      RESTARTS   AGE
multi-workqueue-job-6hdcg   0/1     Completed   0          21m
multi-workqueue-job-jgxbt   0/1     Completed   0          21m
multi-workqueue-job-tl4s2   0/1     Completed   0          21m

# COMPLETIONS 표시 형식 확인
$ kubectl get jobs multi-workqueue-job
NAME                  STATUS     COMPLETIONS   DURATION   AGE
multi-workqueue-job   Complete   3/1 of 3      33s        21m

 

4. 1개씩 실행하는 작업큐

병렬 작업큐에서 parallelism 만 1로 변경한 것으로, 병렬성이 없는 작업큐라고 보면 된다.

completions 지정하지 않음
parallelism N
backoffLimit P

 

parallelism 은 Job 실행중에 변경할 수 있으므로, N개의 병렬 작업큐로 전환할 수 있다. kubectl patch 를 사용하거나, 매니페스트 수정 후 apply 를 진행하면 된다.(이력관리를 위해서 apply 권장)

# 확인을 위해 parallelism: 1 로 변경
$ kubectl patch job multi-workqueue-job -p '{"spec": {"parallelism": 1}}'
job.batch/multi-workqueue-job patched

$ kubectl get jobs
NAME                  STATUS     COMPLETIONS   DURATION   AGE
multi-workqueue-job   Complete   3/1           33s        79m

$ kubectl patch job multi-workqueue-job -p '{"spec": {"parallelism": 2}}'
job.batch/multi-workqueue-job patched

$ kubectl get jobs
NAME                  STATUS     COMPLETIONS   DURATION   AGE
multi-workqueue-job   Complete   3/1 of 2      33s        80m

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

[Kubernetes] 잡

Kubernetes 2025. 10. 4. 13:54

Job은 일정 횟수의 배치작업 성공을 보장하기 위해 사용하는 리소스이다. 좀 더 자세히 말하면, n개의 파드에서 각각 컨테이너를 띄워 일련의 프로세스를 병렬로 수행하고, 프로세스가 종료되면 정상종료/비정상종료 여부에 따라서 성공 횟수를 집계하고, 원하는 성공 횟수가 될 때 까지 반복하는 것이다.

 

1. 잡 생성

apiVersion: batch/v1
kind: Job
metadata:
  name: sample-job
spec:
  completions: 1
  parallelism: 1
  backoffLimit: 10
  template:
    spec:
      containers:
        - name: sleep-container
          image: centos:7
          command: ["sleep"]
          args: ["60"]
      restartPolicy: Never

 

위의 매니페스트로 잡을 생성한다.

 

2. 잡 동작

아래 커맨드를 이용하여 job 이 관리하는 pod 를 관찰하면, 60 초 후 pod 가 정상 종료되면 job dml completions 가 1/1 로 변경되는 것을 볼 수 있다.

$ kubectl get jobs
NAME         STATUS    COMPLETIONS   DURATION   AGE
sample-job   Running   0/1           4s         4s

$ kubectl get pods --watch
NAME               READY   STATUS    RESTARTS   AGE
sample-job-x5cp9   1/1     Running   0          10s
sample-job-x5cp9   0/1     Completed   0          62s # 60초 후
sample-job-x5cp9   0/1     Completed   0          63s
sample-job-x5cp9   0/1     Completed   0          64s

$ kubectl get jobs
NAME         STATUS     COMPLETIONS   DURATION   AGE
sample-job   Complete   1/1           64s        86s

 

3. restartPolicy

restartPolicy 는 job 을 수행하는 컨테이너 내의 프로세스가 job 수행을 완료하지 못하고 정지되었을 때, 파드를 새로 띄워 다시 job 을 진행할지(Never), 아니면 기존 파드를 재사용할지(OnFailure) 명시하는 필드이다.

restartPolicy Job 실행이 중간에 정지되었을 시 동작
Never Pod 를 재기동하지 않는 정책으로, Job 을 다시 수행하기 위해 새로운 Pod 를 기동한다.(기본값)
OnFailure 실패시 Pod 를 재기동하는 정책으로, Job 을 다시 수행하기 위해 기존 Pod 를 재기동한다.

 

아래와 같은 커맨드로 restartPolicy: Never 의 동작을 확인할 수 있다.

$ kubectl apply -f job-never-restart.yaml

$ kubectl get pods
NAME                      READY   STATUS      RESTARTS   AGE
job-never-restart-gfnx7   1/1     Running     0          8s

# 파드 내 프로세스 kill
$ kubectl exec -it job-never-restart-gfnx7 -- sh -c 'kill -9 `pgrep sleep`'

$ kubectl get pods
# 새로운 job-never-restart-xpgqb 파드가 기동됨
NAME                      READY   STATUS      RESTARTS   AGE
job-never-restart-gfnx7   0/1     Error       0          52s
job-never-restart-xpgqb   1/1     Running     0          3s

 

아래와 같은 커맨드로 restartPolicy: OnFailure 의 동작을 확인할 수 있다.

$ kubectl apply -f sample-job-onfailure-restart.yaml

$ kubectl get pods
NAME                          READY   STATUS    RESTARTS   AGE
job-onfailure-restart-5rhgb   1/1     Running   0          8s

# 파드 내 프로세스 kill
$ kubectl exec -it job-onfailure-restart-5rhgb -- sh -c 'kill -9 `pgrep sleep`'

$ kubectl get pods
# job-onfailure-restart-5rhgb 파드가 restart 됨(RESTARTS: 1)
NAME                          READY   STATUS    RESTARTS     AGE
job-onfailure-restart-5rhgb   1/1     Running   1 (5s ago)   59s

 

파드가 기동하는 노드나 파드 IP 주소는 변경되지 않는다.

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

docker pull 로 이미지를 docker hub 에서 받아올 때 아래와 같이 에러가 나며 실패하는 경우가 있다.

$ docker pull centos:6
Error response from daemon: no matching manifest for linux/arm64/v8 in the manifest list entries: no match for platform in manifest: not found

 

해당 태그(6)의 이미지가 없기 때문인데, 사용 가능한 태그 목록을 조회하기 위해 docker hub 웹사이트에 접속한다.

docker hub 웹사이트에서 원하는 이미지를 찾아 들어가면 Tag summary 에서 인기있는 주요 태그들을 볼 수있다.(권장하는 태그들)

 

그리고, Tag 탭을 통해 들어가면 docker hub 에 push 된 모든 태그를 볼 수 있다. 하지만 페이지네이션 때문에 한번에 모든 태그를 볼 수가 없다.

 

(Tag summary 에 노출되는 기준은 Tags 에서 확인할 수 있는 태그 목록의 newest 내림차순 10개인 것 같다.)

 

웹사이트에 방문하지 않고 다음 커맨드로도 조회할 수 있다.

$ curl -s "https://registry.hub.docker.com/v2/repositories/library/centos/tags/" | jq '.results[].name'

 

하지만, page_size 파라미터 기본값이 10이고, 최대값이 100 이기 때문에, nginx 나 ubuntu 와 같이 태그가 엄청나게 많은 경우에는 최대 100개까지밖에 조회가 안된다.(docker 의 정책이다) 따라서, 모든 태그를 조회하기 위해 아래 스크립트를 실행하면 된다. 각 fetch 의 응답필드 중 next 에 명시된 url 을 추가 호출하는 방식이다.

 

fetch-docker-hub-all-tags.sh

#!/bin/bash

# 인자 확인
if [ -z "$1" ]; then
  echo "사용법: $0 <repository-name>"
  echo "예시: $0 ubuntu"
  exit 1
fi

REPO="$1"
URL="https://registry.hub.docker.com/v2/repositories/library/${REPO}/tags?page_size=100"

while [ "$URL" != "null" ]; do
  RESP=$(curl -s "$URL")
  echo "$RESP" | jq -r '.results[].name'
  URL=$(echo "$RESP" | jq -r '.next')
done

 

이를 다음과 같이 이미지명을 파라미터로 주입하여 실행하면 모든 태그를 조회할 수 있다.

$ sh fetch-docker-hub-all-tags.sh centos
$ sh fetch-docker-hub-all-tags.sh ubuntu
$ sh fetch-docker-hub-all-tags.sh nginx

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

스테이트풀셋을 삭제한 경우에는(kubectl delete -f 든, kubectl delete sts 든) PV, PVC 는 삭제되지 않고 남아있게 되며, PVC 와 PV 의 바인딩 관계도 그대로 유지되게 된다. 때문에 다시 스테이트풀셋을 생성한 경우에 남아있던 영구 볼륨 데이터 그대로 파드가 기동되어 재사용하게 된다. 레플리카수를 scale in 하였다가 다시 scale out 한 경우도 마찬가지로 동작한다.

$ kubectl apply -f sample-statefulset.yaml
$ kubectl get pv
$ kubectl get pvc

# sample-statefulset 을 수동으로 삭제한 후에도 PV, PVC 가 유지되는지 확인

$ kubectl delete statefulset sample-statefulset
$ kubectl get pv
$ kubectl get pvc

 

직접 PVC 를 삭제하면, PV 와 바인딩이 풀리면서 PV 도 자동으로 삭제되게 된다.(영구볼륨의 ClaimPolicy 설정 기본값일 때)

$ kubectl delete pvc www-statefulset-{0..2}

 

'Kubernetes' 카테고리의 다른 글

[Kubernetes] 잡 병렬 실행  (0) 2025.10.04
[Kubernetes] 잡  (0) 2025.10.04
[Kubernetes] 스테이트풀셋 업데이트 전략  (0) 2025.09.07
[Kubernetes] 스테이트풀셋  (1) 2025.09.07
[Kubernetes] 데몬셋(DaemonSet)  (0) 2025.09.02
블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

데몬셋과 동일하게 OnDelete, RollingUpdate 를 지정할 수 있지만 몇가지 차이점이 있다.

 

1. RollingUpdate 시 데몬셋과 다른 점

OnDelete 는 데몬셋의 그것과 동일하게 동작하지만, RollingUpdate 의 경우에는 좀 다르다. 스테이트풀셋은 영속성 데이터를 다루는 리소스 컨트롤러 이므로, 추가 파드를 생성해서 롤링 업데이트를 할 수 없으며, 마찬가지로 maxUnavailable 를 지정하여 업데이트 중 동시에 여러 파드를 중지시킬 수도 없다. 파드마다 ready 상태 여부를 확인하고 순차적으로 업데이트하게 된다.(spec.podManagementPolicy: Parallel 이어도 마찬가지)

2. RollingUpdate 시 파티션을 나누어 업데이트

스테이트풀셋의 경우 spec.updateStrategy.rollingUpdate.partition 필드에 RollingUpdate 를 적용할 파티션을 지정할 수 있다.(아래 참고) partition 을 설정하면, 해당 값 이상의 인덱스 값을 가진 파드들만 업데이트 대상이 된다. 수동으로 재기동한 경우에도 partition 의 영향을 받는다.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: statefulset-rollingupdate
spec:
  updateStrategy:
    type: RollingUpdate
    rollingUpdate:
      partition: 3
  serviceName: statefulset-rollingupdate
  replicas: 5
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: nginx-container
          image: nginx:1.16

 

 

위 매니페스트를 사용하여 스테이트풀셋을 만들고, 파드를 업데이트하면서 관찰해보면, 실제로 partition 에 해당하는 인덱스 이상의 파드만 업데이트 되는 것을 볼 수 있다.

$ kubectl apply -f statefulset-rollingupdate.yaml
# statefulset-rollingupdate-0 ~ 4 까지 5개의 파드가 순차적으로 생성됨

# nginx:1.16 -> nginx:1.17 로 이미지 수정 후 다시 apply 를 진행
# watch 를 이용하여 파드 상태를 관찰하면 아래와 같이 출력됨
$ kubectl get pods -l app=sample-app -o wide --watch
NAME                          READY   STATUS              RESTARTS   AGE   IP            NODE              NOMINATED NODE   READINESS GATES
statefulset-rollingupdate-4   1/1     Terminating         0          83s   10.244.2.11   desktop-worker2   <none>           <none>
statefulset-rollingupdate-4   0/1     Completed           0          83s   10.244.2.11   desktop-worker2   <none>           <none>
statefulset-rollingupdate-4   0/1     Completed           0          84s   10.244.2.11   desktop-worker2   <none>           <none>
statefulset-rollingupdate-4   0/1     Completed           0          84s   10.244.2.11   desktop-worker2   <none>           <none>
statefulset-rollingupdate-4   0/1     Pending             0          0s    <none>        <none>            <none>           <none>
statefulset-rollingupdate-4   0/1     Pending             0          0s    <none>        desktop-worker2   <none>           <none>
statefulset-rollingupdate-4   0/1     ContainerCreating   0          0s    <none>        desktop-worker2   <none>           <none>
statefulset-rollingupdate-4   1/1     Running             0          1s    10.244.2.12   desktop-worker2   <none>           <none>
statefulset-rollingupdate-3   1/1     Terminating         0          86s   10.244.2.10   desktop-worker2   <none>           <none>
statefulset-rollingupdate-3   1/1     Terminating         0          86s   10.244.2.10   desktop-worker2   <none>           <none>
statefulset-rollingupdate-3   0/1     Completed           0          86s   10.244.2.10   desktop-worker2   <none>           <none>
statefulset-rollingupdate-3   0/1     Completed           0          87s   10.244.2.10   desktop-worker2   <none>           <none>
statefulset-rollingupdate-3   0/1     Completed           0          87s   10.244.2.10   desktop-worker2   <none>           <none>
statefulset-rollingupdate-3   0/1     Pending             0          0s    <none>        <none>            <none>           <none>
statefulset-rollingupdate-3   0/1     Pending             0          0s    <none>        desktop-worker2   <none>           <none>
statefulset-rollingupdate-3   0/1     ContainerCreating   0          0s    <none>        desktop-worker2   <none>           <none>
statefulset-rollingupdate-3   1/1     Running             0          1s    10.244.2.13   desktop-worker2   <none>           <none>

 

즉, 0번~2번 인덱스의 파드는 영향을 받지 않고, 3~4번 인덱스의 파드가 하나씩 순차적으로 Termination -> Completed 로 종료된 후, Pending -> ContainerCreating -> Running 으로 새로 실행된 것을 알 수 있다. 여기서 이번에는 partition 을 1로 변경 후 apply 하게 되면 1~2번 파드만 추가로 업데이트를 하게 된다.(기존 업데이트 완료되었던 3~4번 파드는 업데이트되지 않음)

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

데이터베이스와 같이 stateful 한 워크로드를 위한 리소스이다. 데이터를 영구적으로 저장하기 위해 사용한다.

 

1. 스테이트풀셋 생성

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: sample-statefulset
spec:
  serviceName: sample-statefulset
  replicas: 3
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: nginx-container
          image: nginx:1.16
          ports:
            - containerPort: 80
          volumeMounts:
            - name: www
              mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
    - metadata:
        name: www
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 1G

 

위 내용으로 스테이트풀셋을 생성하고, 아래 커맨드로 파드명 규칙을 확인해보면, 레플리카셋이나 데몬셋과는 다르게 파드명 suffix 로 incremental index 가 붙는것을 알 수 있다.

$ kubectl get pods -o wide -l app=sample-app

 

매니페스트에서 주요하게 보아야 할 내용은 아래와 같다.

  • spec.template.spec.containers[].volumeMounts[]: 컨테이너에서 사용할 볼륨명과 마운트 경로
  • spec.volumeClaimTemplates[]: 스테이트풀셋이 생성할 영구 볼륨 요청 템플릿

즉, 스테이트풀셋이 volumeClaimTemplates 에 정의된 볼륨 리소스를 생성하여 영구 볼륨을 확보하고, 이를 컨테이너에서 요청하는 것이다. 생성된 persistentvolumes 과 persistentvolumeclaims 는 다음 커맨드로 확인할 수 있다.

$ kubectl get pv
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                              STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
pvc-66206fda-ac99-4923-9b7c-eff0e3a3f677   1G         RWO            Delete           Bound    default/www-sample-statefulset-0   standard       <unset>                          22m
pvc-922ab66a-e5cc-447c-99d6-99d2f476345e   1G         RWO            Delete           Bound    default/www-sample-statefulset-1   standard       <unset>                          22m
pvc-9ecd3989-6435-4cbc-9e94-146866a59ddf   1G         RWO            Delete           Bound    default/www-sample-statefulset-2   standard       <unset>                          22m

$ kubectl get pvc
NAME                       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
www-sample-statefulset-0   Bound    pvc-66206fda-ac99-4923-9b7c-eff0e3a3f677   1G         RWO            standard       <unset>                 25m
www-sample-statefulset-1   Bound    pvc-922ab66a-e5cc-447c-99d6-99d2f476345e   1G         RWO            standard       <unset>                 22m
www-sample-statefulset-2   Bound    pvc-9ecd3989-6435-4cbc-9e94-146866a59ddf   1G         RWO            standard       <unset>                 22m

 

PV(persistentvolumes)와 PVC(persistentvolumeclaims) 에 대한 내용은 추후에 영구 볼륨을 따로 다룰 때 자세히 설명하고, 여기서는 간단히 정리하면 다음과 같다.

  • PV: 물리적인 스토리지. namespaced: false. 즉, cluster 전역 스토리지
  • PVC: 물리적인 스토리지를 namespace 에서 사용하기 위한 객체. namespaced: true
  • PVC 에서 PV 의 사용을 요청하기 위해서는 PVC 와 PV 의 아래 조건이 맞아야 한다.(바인딩 조건)
    • PVC 에 selector 가 설정되어 있으면 PV 중에서 selector 에 일치하는 key=value 를 찾는다.(optional)
    • spec.accessModes 가 일치하여야 한다.
    • PV 의 spec.capacity.storage 용량이 PVC spec.resources.request.storage 용량 이상이어야 한다.
    • PV 와 PVC 둘 다 storageClassName 이 명시되어 있으면 그 값이 일치하여야 한다.(둘 다 명시되어있지 않으면 빈문자열로 취급되어 일치함. 한쪽만 명시되면 불일치함)
  • PVC 는 파드에 설정되고, 파드는 PVC 를 통해서 PV 를 인식하고 사용한다.
  • PVC 한개가 여러개의 PV 에 바인딩 될 수는 없다.
  • 한번 바인딩이 되었다가 풀리면 PV 가 released 상태가 되며, 바인딩 되었던 PVC 에서는 다시 바인딩이 불가능하기 때문에, PV 를 삭제 후 다시 생성해야 한다. PV 를 조건에 맞게 생성하면 자동으로 바인딩이 이루어진다.

 

2. 스테이트풀셋 스케일링의 특이점

레플리카셋이나 데몬셋은 scale out 또는 scale in 시에 생성/삭제되는 파드가 무작위이지만, 스테이트풀셋은 아래와 같은 규칙이 있다.

  • scale out: pod 명 suffix index 가 1씩 증분하면서 생성되며, 하나의 파드가 생성 후 ready 상태가 되면 다음 파드가 생성된다.
  • scale in: pod 명의 suffix 가 큰 것부터 하나씩 삭제된다. 삭제될 때도 하나씩 순차적으로 삭제된다.

이러한 특징 때문에 0번째 파드가 항상 가장 오래 남아있게 되는데, 그 특징으로 0번째 파드를 마스터로 사용하는 이중화 애플리케이션에 적합하다.

 

3. 파드 관리 정책

위에서 스테이트풀셋의 파드는 생성/삭제시 순차적으로 진행된다고 하였는데, 이를 변경하는 파라미터가 있다. spec.podManagementPolicy 필드로, 기본값이 OrderedReady 이고, 이를 Parallel 로 설정하면 레플리카셋과 동일하게 파드 생성/삭제가 병렬로 이루어진다.

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

데몬셋은 쿠버네티스 클러스터 내의 모든 "노드"에 1개씩의 파드 실행을 보장하는 컨트롤러 리소스이다. 노드를 추가했을 때에도 자동으로 해당 노드에 파드가 기동된다. 아래의 용도로 주로 사용한다.

  • 파드가 출력하는 로그를 노드 단위로 수집하는 로그 프로세스를 띄울 때
  • 파드 리소스 사용 현황 및 노드 상태를 모니터링하는 프로세스를 띄울 때

즉, 모든 노드에서 반드시 동작해야 하는 프로세스를 데몬셋으로 올려 사용한다.

 

1. 데몬셋 생성

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: sample-ds
spec:
  selector:
    matchLabels:
      app: sample-app
  template:
    metadata:
      labels:
        app: sample-app
    spec:
      containers:
        - name: nginx-container
          image: nginx:1.16
          ports:
            - containerPort: 80

 

위 내용으로 데몬셋을 생성한 후, 파드를 확인해보면 각 노드에 1개씩 파드가 생성된 것을 알 수 있다.

현재 클러스터에 desktop-worker, desktop-worker2 노드가 있고, 각각의 노드에 하나씩 파드가 생성되었다.

$ kubectl get pods -o wide
NAME                                          READY   STATUS    RESTARTS   AGE     IP             NODE              NOMINATED NODE   READINESS GATES
sample-ds-b9l7s                               1/1     Running   0          3m37s   10.244.2.101   desktop-worker2   <none>           <none>
sample-ds-gjbfk                               1/1     Running   0          3m37s   10.244.1.93    desktop-worker    <none>           <none>

 

2. 데몬셋 업데이트 전략

Deployment 와 마찬가지로 spec.updateStrategy 필드에 지정하며, 아래 표의 전략에 해당하는 값을 지정한다.

전략 설명 비고
OnDelete 매니페스트 변경 후 apply 할 때 변경사항이 적용된 파드로 교체되는것이 아니라, 파드가 삭제되고 다시 생성될 때 변경사항이 적용된다. - 업데이트를 적용하려면 파드를 수동으로 삭제해주어야 함
RollingUpdate 매니페스트 변경 후 apply 할 때 변경사항이 적용된다. - 노드 단위의 rolling update 는 아니고, Deployment 와 동일하게 클러스터 단위의 rolling update 임

 

 

2.1. OnDelete

어떤 이유에서든, 파드가 정지되고 다시 생성되기 전까지는 업데이트되지 않는다. 따라서 임의의 시점에 파드를 업데이트 하려면, 데몬셋이 관리하는 파드를 kubectl delete pod 커맨드로 직접 삭제해야 한다.(데몬셋이 파드를 자동으로 새로 생성해줌)

 

위에서 생성한 데몬셋을 삭제하고, spec.updateStrategy: OnDelete 로 변경하여 다시 생성한 후, 파드 템플릿에서 nginx 버전을 1.16 -> 1.17 로 변경하여 apply 하면서 상태를 관찰해보자.

$ kubectl get daemonsets.apps -o wide --watch

# nginx:1.16 버전 확인
NAME          DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE     CONTAINERS        IMAGES       SELECTOR
ds-ondelete   2         2         2       2            2           <none>          9m42s   nginx-container   nginx:1.16   app=sample-app

# 데몬셋 매니페스트 파일의 파드 템플릿에서 컨테이너 이미지를 nginx:1.17 버전으로 수정 후
# 다른 쉘 세션을 열고 변경된 파드 템플릿을 apply 한다.

# 아래처럼 UP-TO-DATE 항목이 2 -> 0으로 바뀐 것을 볼 수 있다.
# 즉, image 는 nginx:1.17 로 변경되었지만, 파드가 최신 버전이 아니라는 뜻이다.
ds-ondelete   2         2         2       2            2           <none>          10m     nginx-container   nginx:1.17   app=sample-app
ds-ondelete   2         2         2       0            2           <none>          10m     nginx-container   nginx:1.17   app=sample-app

$ kubectl get pods -l app=sample-app -o jsonpath='{range .items[*]}{range .spec.containers[*]}{.image}{"\n"}{end}{end}'
# 또는 jq가 있다면
$ kubectl get pods -l app=sample-app -o json | jq '.items[].spec.containers[].image'
# nginx:1.16 버전이 유지되는지 확인

 

이제 nginx:1.17 버전을 적용하려면 아래 커맨드를 이용해 파드를 삭제하면, 데몬셋이 업데이트된 버전의 파드를 자동으로 생성해준다.

$ kubectl delete pod -l app=sample-app

# 파드 2개가 모두 삭제된 후, 다시 생성되어 올라온다.
# UP-TO-DATE 가 2로 변경된 것을 알 수 있다.
NAME          DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE     CONTAINERS        IMAGES       SELECTOR
ds-ondelete   2         1         0       1            0           <none>          29m     nginx-container   nginx:1.17   app=sample-app
ds-ondelete   2         2         0       2            0           <none>          29m     nginx-container   nginx:1.17   app=sample-app
ds-ondelete   2         2         2       2            2           <none>          29m     nginx-container   nginx:1.17   app=sample-app

$ kubectl get pods -l app=sample-app -o jsonpath='{range .items[*]}{range .spec.containers[*]}{.image}{"\n"}{end}{end}'
# nignx:1.17 확인

 

 

2.2. RollingUpdate

매니페스트 변경 후 apply 할 때 변경사항이 적용된다. rolling update 형태로 무중단 업데이트 배포가 이루어진다. 주의할 점은, 데몬셋에서는 하나의 노드에 동일한 파드를 2개 이상 생성할 수 없으므로, 동시에 추가 생성될 수 있는 최대 파드 수인 maxSurge 필드를 설정할 수 없다.

 

위에서 생성한 데몬셋을 삭제하고, spec.updateStrategy: RollingUpdate 로 변경하여 다시 생성한 후, 파드 템플릿에서 nginx 버전을 1.16 -> 1.17 로 변경하여 apply 하면서 상태를 관찰해보자.

$ kubectl get daemonsets.apps -o wide --watch

# nginx:1.16 버전 확인
NAME               DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE   CONTAINERS        IMAGES       SELECTOR
ds-rollingupdate   2         2         2       2            2           <none>          1s    nginx-container   nginx:1.16   app=sample-app

# 데몬셋 매니페스트 파일의 파드 템플릿에서 컨테이너 이미지를 nginx:1.17 버전으로 수정 후
# 다른 쉘 세션을 열고 변경된 파드 템플릿을 apply 한다.

# 아래처럼 UP-TO-DATE 항목이 2 -> 0 -> 2으로 바뀐 것을 볼 수 있다.
# 파드 2개가 동시에 내려가지 않고, 한개씩 삭제된 후 재생성된 것을 알 수 있다.
ds-rollingupdate   2         2         2       2            2           <none>          28s   nginx-container   nginx:1.17   app=sample-app
ds-rollingupdate   2         2         2       0            2           <none>          28s   nginx-container   nginx:1.17   app=sample-app
ds-rollingupdate   2         2         1       1            1           <none>          28s   nginx-container   nginx:1.17   app=sample-app
ds-rollingupdate   2         2         2       1            2           <none>          29s   nginx-container   nginx:1.17   app=sample-app
ds-rollingupdate   2         2         1       2            1           <none>          29s   nginx-container   nginx:1.17   app=sample-app
ds-rollingupdate   2         2         2       2            2           <none>          30s   nginx-container   nginx:1.17   app=sample-app

# kubectl get pods -o wide --watch
# pod 도 watch 해보면, desktop-worker2 의 파드가 먼저 종료 -> 생성된 후, desktop-worker 의 파드가 종료 -> 생성된 것을 알 수 있다.
# AGE 필드를 보면 알겠지만, Terminating Completed 는 순서에 맞지 않게 이벤트가 발생했으므로 비동기적으로 수행되었음을 짐작할 수 있다.
NAME                     READY   STATUS              RESTARTS   AGE   IP             NODE              NOMINATED NODE   READINESS GATES
ds-rollingupdate-ql2lw   1/1     Terminating         0          28s   10.244.2.105   desktop-worker2   <none>           <none>
ds-rollingupdate-ql2lw   0/1     Completed           0          28s   10.244.2.105   desktop-worker2   <none>           <none>
ds-rollingupdate-kblv8   0/1     Pending             0          0s    <none>         <none>            <none>           <none>
ds-rollingupdate-kblv8   0/1     Pending             0          0s    <none>         desktop-worker2   <none>           <none>
ds-rollingupdate-kblv8   0/1     ContainerCreating   0          0s    <none>         desktop-worker2   <none>           <none>
ds-rollingupdate-ql2lw   0/1     Completed           0          28s   10.244.2.105   desktop-worker2   <none>           <none>
ds-rollingupdate-ql2lw   0/1     Completed           0          28s   10.244.2.105   desktop-worker2   <none>           <none>
ds-rollingupdate-kblv8   1/1     Running             0          1s    10.244.2.106   desktop-worker2   <none>           <none>
ds-rollingupdate-9zvrh   1/1     Terminating         0          29s   10.244.1.97    desktop-worker    <none>           <none>
ds-rollingupdate-9zvrh   0/1     Completed           0          29s   10.244.1.97    desktop-worker    <none>           <none>
ds-rollingupdate-rd65j   0/1     Pending             0          0s    <none>         <none>            <none>           <none>
ds-rollingupdate-rd65j   0/1     Pending             0          0s    <none>         desktop-worker    <none>           <none>
ds-rollingupdate-rd65j   0/1     ContainerCreating   0          0s    <none>         desktop-worker    <none>           <none>
ds-rollingupdate-rd65j   1/1     Running             0          1s    10.244.1.98    desktop-worker    <none>           <none>
ds-rollingupdate-9zvrh   0/1     Completed           0          30s   10.244.1.97    desktop-worker    <none>           <none>
ds-rollingupdate-9zvrh   0/1     Completed           0          30s   10.244.1.97    desktop-worker    <none>           <none>

 

추가로 공부해야 할 점: 데몬셋이 노드당 1개의 파드를 보장해주는 것이라면, rolling update 시에도 노드 내에서 1개의 추가 파드를 생성해서 교체해야 하는 것이 맞아보이는데, 실제로 그렇게 동작하지 않는다. 노드 단위로 항시 서비스를 유지해야 하는 프로세스라면, 무중단 배포라고 볼 수 없다는 점이 의아하다.

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,

디플로이먼트 업데이트시 몇가지 상세 설정할 수 있는 필드가 있다.

  • minReadySeconds: 파드가 ready 상태가 된 후 디플로이먼트가 파드 기동이 완료되었다고 판단하기까지의 추가 최소 시간(초)
    • 이 시간이 지난 후에야 다음 파드의 교체가 이루어진다.
  • revisionHistoryLimit: 디플로이먼트가 유지할 레플리카 수
    • 레플리카 수는 파드 템플릿의 갯수와 동일하므로, 유지할 revision 갯수와도 같은 의미이다.
    • 롤백 가능한 이력 수와도 같은 의미이다.
  • progressDeadlineSeconds: 업데이트 타임아웃(초)
    • 이 시간이 경과했는데도 업데이트가 정상적으로 완료되지 않았으면 자동으로 롤백한다.

아래는 매니페스트 예시이다.

apiVersion: apps/v1
kind: Deployment
...
spec:
  minReadySeconds: 10
  revisionHistoryLimit: 5
  progressDeadlineSeconds: 3600 # 60분
...

 

블로그 이미지

망원동똑똑이

프로그래밍 지식을 자유롭게 모아두는 곳입니다.

,