本記事には広告(アフィリエイトリンク)が含まれます。

Job・CronJob・DaemonSet【CKAD第11回】

広告
広告

第11回スコープ・学習目標・今ここマップ

動作確認バージョン: kind v0.31.0 / kindest/node:v1.35.0 / kubectl v1.35.0 (Kustomize v5.7.1) / postgres:18 / busybox:1.36 / fanclub-backend:0.1.0 / Docker CE 29.4.3 / containerd 2.2.3 / AlmaLinux 10.1(kernel 6.12.0-124.55.3.el10_1)(2026-05-10 時点・k8s-ops 実機検証済・SP_vol1-pre-19 起点)

本回は Kubernetes 実践教科書 第1巻(CKAD 対応・全 19 回)の第11回です。第3部「アプリリソース」第5回(最終回)として、Job(バッチ処理)・CronJob(定期実行)・DaemonSet(全 Node 配置)の 3 機構をまとめて扱います。

CKAD ドメイン D1「Application Design and Build」(出題比率 20 %)の中核 Competency「適切なワークロードリソースの選択と使用(Deployment・StatefulSet・DaemonSet・Job・CronJob)」を完結させる重要回です。

第10回からの継承状態確認(SP_vol1-pre-19 状態):

項目状態出典
kind クラスタkind-control-plane Ready(v1.35.0・11h old)Lead 実機観察
fanclub-backend Poddefault ns で 1/1 Running(envFrom 注入版・automountServiceAccountToken: falseLead 実機観察
fanclub-backend ServiceClusterIP 10.96.150.60:80 稼働中Lead 実機観察
fanclub-db StatefulSetfanclub-db-0 Pod Running(PostgreSQL 18.3)Lead 実機観察
fanclub-db Service / fanclub-db-headless ServiceClusterIP 10.96.131.167:5432 / clusterIP: NoneLead 実機観察
postgres-data-fanclub-db-0 PVCBound(1Gi・standard SC)Lead 実機観察
ConfigMapfanclub-config(4 キー: DB_HOST / DB_PORT / DB_NAME / JAVA_OPTS)+ fanclub-db-init + kube-root-ca.crtSP_vol1-pre-19
Secretfanclub-secret(Opaque・2 キー: DB_USER / DB_PASSWORD)SP_vol1-pre-19
ServiceAccountdefault(auto)+ fanclub-backend-sa(ep10 で新規作成)SP_vol1-pre-19
members テーブル永続データ 2 行残存(山田太郎・鈴木花子)Lead 実機観察
~/fanclub-manifests/ディレクトリ作成済(ep7 から継続・ep10 までのマニフェスト群格納済)ep10 完了済

今ここマップ(第1巻 19 回中の現在位置):

第1部 コンテナとDocker
    第1回: コンテナ技術概念 + Docker環境準備  [完了]
    第2回: Docker基本操作  [完了]
    第3回: Dockerfile + マルチステージビルド + JDK 25/Payara Micro  [完了]
    第4回: コンテナレジストリ + イメージタグ戦略 + Trivy スキャン  [完了]

第2部 Kubernetes基礎
    第5回: K8s全体像 + kind で軽量K8s  [完了]
    第6回: kubectl基本操作 + Observability・Debug  [完了]

第3部 アプリリソース
    第7回: Pod + Multi-containerパターン  [完了]
    第8回: Service とネットワーキング  [完了]
    第9回: ストレージ(PVC + StatefulSet)+ PostgreSQL DB追加  [完了]
    第10回: ConfigMap + Secret + ServiceAccount 基礎  [完了]
  ★ 第11回: Job + CronJob + DaemonSet  ← 今ここ

第4部 ワークロード戦略(第12〜14回)
第5部 セキュリティ基礎(第15〜16回)
第6部 パッケージ管理 + HTTPS公開(第17〜19回)

第11回を終えると、以下を習得した状態になります。

  • Job の completions / parallelism / backoffLimit / ttlSecondsAfterFinished を説明し、restartPolicy: NeverOnFailure の違いを実機で確認できる
  • CronJob の cron 式(5 フィールド)・timeZoneconcurrencyPolicy(Allow / Forbid / Replace)・suspend を説明し、1 分間隔の CronJob を作成・一時停止できる
  • DaemonSet の「全 Node に 1 Pod」の原則と主な用途(ログエージェント・監視・CNI プラグイン)を説明し、既存 DaemonSet(kindnet / kube-proxy)の構成を観察できる
  • fanclub-api の DB マイグレーション Job と CSV エクスポート Job を作成し、kubectl logs で実行結果を確認できる
  • Job・CronJob・DaemonSet・Deployment・StatefulSet の使い分けの判断基準を説明できる(CKAD D1 ワークロードリソース選択問題対策)

模擬アプリ進捗(第11回):第10回までで設定外部化(ConfigMap / Secret)と最小権限の SA まで完成しました。本回ではアプリ本体の常駐 Pod とは別軸の「ワンショット実行のバッチ処理」「定期実行スケジューラ」「全 Node に常駐するデーモン」の 3 機構を追加します。

fanclub-api の DB スキーマ変更を Job で実行する運用パターンを体験し、fanclub-secretDB_PASSWORDsecretKeyRef で Job 側に注入する設計を採用します。第10回で確立した ConfigMap / Secret は、Job / CronJob からも同じ仕組みで参照できます。

第11回完了後の模擬アプリ状態:fanclub-backend Pod / Service・fanclub-db StatefulSet / Service・PVC・fanclub-config ConfigMap・fanclub-secret Secret・fanclub-backend-sa ServiceAccount は ep10 から継続稼働。

新規追加として、Job fanclub-db-migrate(Succeeded・ttlSecondsAfterFinished: 600)/ Job fanclub-db-export(Succeeded・同上)/ CronJob fanclub-member-count(演習②後に suspend: true)/ DaemonSet node-logger(default ns・1 Pod Running)/ members テーブルに score カラムが追加された状態になります。

ワークロードリソースの使い分け — Job / CronJob / DaemonSet はどこで使うか

Job / CronJob / DaemonSet の各論に入る前に、Kubernetes が提供するワークロードリソース全体を整理します。CKAD 試験では「次のうち、毎日 02:00 に実行されるバックアップ処理を実装するために最も適切なリソースはどれか」のような選択問題が頻出します。各リソースの起動タイミング・終了タイミング・典型用途を一覧で押さえることが第一歩です。

ワークロードリソース 6 種類の比較表

本シリーズで扱う代表的なワークロードリソースを比較します。本回で新たに学ぶのは表中の Job / CronJob / DaemonSet の 3 行です。

リソース起動終了restartPolicyレプリカ典型用途
Pod(直接)即時削除されるまでAlways / OnFailure / Neverデバッグ・一時検証
Deployment宣言的ローリングAlways のみreplicas: N で指定ステートレス Web サービス
StatefulSet順序付き起動順序付き停止Always のみreplicas: N(安定 ID)DB・KVS 等のステートフルアプリ
Job即時Pod 完了で終了Never / OnFailure のみcompletions × parallelismDB マイグレーション・バッチ集計・1 回限り処理
CronJobcron スケジュールJob が完了Never / OnFailure のみJob として管理日次レポート・定期クリーンアップ・バックアップ
DaemonSetNode 追加時に自動配置Node 削除時に自動削除Always のみNode 数 = Pod 数ログ収集・ノード監視・CNI・kube-proxy

表中の特に重要な差分は次の 3 点です。

  • restartPolicy の制約が逆向き:Deployment / StatefulSet / DaemonSet は Always のみ有効。Job / CronJob は Never または OnFailure のみ有効。Always を Job に書くと kubectl apply 時にバリデーションエラーになる(ヒヤリハット 1 で詳述)
  • レプリカの考え方が異なる:Deployment / StatefulSet は replicas: N で台数を指定する。Job は completions(成功させる Pod 数)と parallelism(並列数)の組み合わせ。DaemonSet は明示的な台数指定がなく、Node 数がそのまま Pod 数になる
  • 終了の概念が異なる:Deployment / StatefulSet / DaemonSet は基本的に「終わらない」前提。Job / CronJob は Pod の正常終了をゴールにする。CronJob は Job を生成するだけで自身は永続的に動く

6 種類のワークロードリソースを起動タイミング・終了条件・典型用途・本シリーズ登場回の 4 軸で並べたマトリクスを以下に示します。本回で扱う Job / CronJob / DaemonSet の 3 行(★ 今回)に注目してください。

ワークロードリソース 6 種類の使い分けマトリクス
================================================================

 リソース        起動タイミング        終了条件                典型用途                登場回
 --------------- --------------------- ----------------------- ----------------------- ------------
 Pod(直接)     即時(kubectl apply) 手動削除                デバッグ・一時検証      第7回
 Deployment      宣言的(replicas)    手動削除 / scale=0      ステートレス Web        第12回
 StatefulSet     順序付き起動          順序付き停止            DB・KVS 等ステートフル  第9回
 Job        ★今回 即時実行             completions 達成で終了  DB マイグレーション     第11回 ← 今ここ
 CronJob    ★今回 cron スケジュール    Job が完了              日次レポート・定期実行  第11回 ← 今ここ
 DaemonSet  ★今回 Node 追加時に自動    Node 削除時に自動       ログ・監視・CNI         第11回 ← 今ここ

================================================================
[CKAD 判断フロー]
  「一回限りのタスク実行」          → Job
  「スケジュールに沿って定期実行」  → CronJob
  「全 Node で同じ処理を常時実行」  → DaemonSet
  「ステートレス Web を N 個常駐」  → Deployment
  「安定した識別子が必要な DB 等」  → StatefulSet
  「一時デバッグ・即席検証」        → Pod(直接)
================================================================

使い分けの判断フロー

CKAD 試験では「要件文 → 適切なリソース」を瞬時に判断する必要があります。次の判断フローを暗記しておくと選択問題に強くなります。

要件: 「一回限りのタスクを実行したい」
       例: DB マイグレーション・データ移行・バッチ集計
  → Job

要件: 「スケジュールに沿って定期実行したい」
       例: 毎日 02:00 のバックアップ・週次レポート生成
  → CronJob

要件: 「全 Node で同じ処理を常時実行したい」
       例: ログ収集 Agent・ノード監視・CNI・kube-proxy
  → DaemonSet

要件: 「ステートレスな Web サービスを N 個常駐させたい」
       例: フロントエンド・REST API バックエンド
  → Deployment

要件: 「安定した識別子と永続ストレージが必要なステートフルアプリ」
       例: PostgreSQL・MySQL・Cassandra
  → StatefulSet

この 5 行を覚えておけば、CKAD 試験のワークロードリソース選択問題は機械的に解けます。要件のキーワードと判断結果の対応関係を体に染み込ませることが対策の核です。

CKAD 試験頻出パターン 3 選

過去問・公式 Curriculum・各種模擬試験で繰り返し出題される典型パターンを 3 つ挙げます。

  1. 「失敗しても最大 N 回までリトライする 1 回限りの処理」→ Job + backoffLimit: N。N を指定しないとデフォルト 6 回まで再試行される
  2. 「毎日 / 毎時 / 1 分ごとなど cron 式で定期実行」→ CronJob + schedule + timeZonetimeZone 省略は UTC 動作のためヒヤリハット頻発(後述 H2-11)
  3. 「Linux Node にだけ 1 Pod ずつデーモンを配置」→ DaemonSet + nodeSelector: kubernetes.io/os: linuxreplicas を書くのはアンチパターン

上記 3 パターンの YAML は本回 H2-3 / H2-6 / H2-8 で全量を掲載します。試験では「DaemonSet を replicas: 3 で書け」のような誤った要件は出ませんが、「DaemonSet と Deployment の違いは何か」を聞かれる場面は頻出します。Node 追加時の挙動の差まで答えられるようにしておくのが定石です。

Job 基礎 — completions・parallelism・backoffLimit・ttlSecondsAfterFinished

Job はバッチ処理を実行するワークロードリソースです。指定した数の Pod が正常終了(exit code 0)するまでリトライを繰り返し、すべて完了すると Job 自体が Complete 状態になります。本セクションでは Job の主要フィールドを体系的に整理し、CKAD 試験で頻出する kubectl create job --dry-run によるスケルトン生成テクニックを示します。

Job の概念 — Pod を管理するコントローラー

Job は単独で Pod を実行するわけではなく、Job コントローラーが Pod を生成・監視します。Pod の状態と Job の状態は次のように対応します。

Pod の状態Job の状態挙動
RunningActivePod が実行中
Succeeded(exit 0)CompletePod が正常終了。completions に達すれば Job 完了
Failed(exit 非 0)Active(リトライ中)Pod が異常終了。backoffLimit 未満なら再試行
Failed(リトライ尽きた)FailedbackoffLimit 超過で Job 全体が失敗

Job が完了しても Pod は自動削除されません(デフォルト動作)。これは kubectl logs で実行結果を確認できるようにするための設計です。完了済み Pod を蓄積させたくない場合は ttlSecondsAfterFinished を設定して時間経過で自動削除するか、kubectl delete job で手動削除します。

主要フィールド一覧

Job の spec 配下で押さえておくべきフィールドを整理します。

フィールドデフォルト意味
completions1正常完了させる Pod の総数。3 なら 3 Pod が成功するまで Job 継続
parallelism1同時実行する Pod の最大数。3 なら最大 3 Pod が並列実行
backoffLimit6Pod 失敗時の再試行上限。超えると Job が Failed 状態になる
activeDeadlineSeconds無制限Job 全体のタイムアウト秒数。超過すると Pod 強制終了 + Failed
ttlSecondsAfterFinished無制限完了後に Job + Pod を自動削除するまでの秒数(K8s v1.23 GA)
template.spec.restartPolicyNever または OnFailure のみ。Always は不可

本番運用では backoffLimit をデフォルトの 6 に任せるのは推奨されません。失敗 Pod が最大 7 個(初回 + 6 回リトライ)残るため、Pod の異常終了を引き起こす設定ミスがあるとクラスタ内に Failed Pod が大量に蓄積します。

ワークロードの性質に合わせて backoffLimit: 2backoffLimit: 3 程度に明示する設計が一般的です。同様に ttlSecondsAfterFinished もデフォルトでは Job リソースが永続的に残るため、本番では必ず明示する運用ルールを設けます。

completions と parallelism の組み合わせパターン

completionsparallelism の組み合わせで 3 つの実行パターンが表現できます。

パターンcompletionsparallelism挙動
① 単発 Job111 Pod を起動して成功すれば完了。本回の演習①で採用
② 順次実行N1N 個の Pod を順番に 1 個ずつ実行
③ 並列実行NM (≤ N)N 個の成功を目指しつつ、最大 M 個まで並列

本回で扱う「DB マイグレーションを 1 回実行する」「members テーブルを CSV エクスポートする」はパターン①の単発 Job です。並列処理(パターン③)は CKAD では概念理解までが範囲で、実機演習は省きます。

kubectl explain でフィールド仕様を確認する

CKAD 試験では kubectl explain を使ってフィールド仕様を確認できます。Job の場合は次のように使います。

実行コマンド:

$ kubectl explain job.spec

実行結果(抜粋):

KIND:       Job
VERSION:    batch/v1

FIELD: spec <JobSpec>

DESCRIPTION:
    Specification of the desired behavior of a job. More info:
    https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status

FIELDS:
  activeDeadlineSeconds	<integer>
    Specifies the duration in seconds relative to the startTime that the job may
    be continuously active before the system tries to terminate it; value must
    be positive integer.

  backoffLimit	<integer>
    Specifies the number of retries before marking this job failed. Defaults to
    6.

  completions	<integer>
    Specifies the desired number of successfully finished pods the job should be
    run with.

  parallelism	<integer>
    Specifies the maximum desired number of pods the job should run at any given
    time.

  template	<PodTemplateSpec> -required-
    Describes the pod that will be created when executing a job.

  ttlSecondsAfterFinished	<integer>
    ttlSecondsAfterFinished limits the lifetime of a Job that has finished
    execution (either Complete or Failed).

特定フィールドのみ調べたい場合は kubectl explain job.spec.completions のように一段下のフィールドまで指定できます。試験中はこのコマンドで仕様を確認すれば、暗記に頼らずに正解できます。

スケルトン生成テクニック — kubectl create job –dry-run

Job の YAML を一から書くより、kubectl create job でスケルトンを生成して編集する方が高速です。CKAD 試験で頻用するテクニックです。

実行コマンド:

$ kubectl create job fanclub-db-migrate --image=postgres:18 --dry-run=client -o yaml

実行結果:

apiVersion: batch/v1
kind: Job
metadata:
  creationTimestamp: null
  name: fanclub-db-migrate
spec:
  template:
    metadata:
      creationTimestamp: null
    spec:
      containers:
      - image: postgres:18
        name: fanclub-db-migrate
        resources: {}
      restartPolicy: Never
status: {}

このスケルトンを > ~/fanclub-manifests/fanclub-db-migrate-job.yaml でファイル出力し、commandenv を追記して完成させる流れが定石です。試験では時間制限がきついため、可能な限りコマンド生成 → 編集の流れに統一します。

restartPolicy の使い分け — Never と OnFailure は何が違うか

Job の spec.template.spec.restartPolicy には Never または OnFailure のいずれかを必ず指定します。Pod のデフォルト(および Deployment / StatefulSet / DaemonSet で必須となる)Always は Job では使用不可です。

本セクションでは Never と OnFailure の挙動差を整理し、ヒヤリハット 1 への布石を置きます。

restartPolicy 3 値の比較表

Kubernetes の restartPolicy は 3 値です。リソース種別との対応関係を整理します。

restartPolicy対応リソース失敗時の挙動Pod 数の変化使い所
AlwaysDeployment / StatefulSet / DaemonSet(必須)同じ Pod のコンテナを無制限に再起動変わらない常時稼働サービス
OnFailureJob / CronJob のみ同じ Pod のコンテナを再起動(backoffLimit まで)変わらない軽量タスク・再起動コスト小
NeverJob / CronJob のみ新しい Pod を作成して再試行(backoffLimit まで)失敗のたびに増える副作用回避・冪等タスク

Job に restartPolicy: Always を指定すると kubectl apply 時にバリデーションエラーが返ります。Deployment 用 YAML をコピー流用したときに頻発するミスで、ヒヤリハット 1(H2-11)で実機エラー出力を扱います。

Never と OnFailure の使い分け基準

Never と OnFailure はどちらを選ぶかで挙動が大きく変わります。実務での選定基準を整理します。

  • Never を選ぶケース:DB マイグレーション・外部 API への副作用ある操作・冪等でない処理。失敗のたびに新しい Pod が作られるため、コンテナ初期化処理(init や DB 接続セットアップ)から毎回やり直したい場合に向く。本回の演習①ではすべて Never を採用
  • OnFailure を選ぶケース:軽量な計算処理・冪等な集計・状態を持たないバッチ。Pod の起動コストを節約したい場合に向く。同じ Pod でコンテナのみ再起動するため、起動オーバーヘッドが少ない

判断に迷ったら Never を選ぶのが安全です。OnFailure は Pod 内のローカル状態(例: /tmp に書き込んだ中間ファイル)が前回の試行から残るため、副作用ある処理では予期しない挙動を起こします。Never で「失敗したら最初からやり直す」設計の方が事故を起こしづらく、本シリーズ全体でも Never を採用方針とします。

backoffLimit 超過 Job のデモ用 YAML

演習①の最後で、意図的に失敗する Job を実行して backoffLimit 超過時の Job 状態を実機確認します。デモ用 YAML を先に提示します。

apiVersion: batch/v1
kind: Job
metadata:
  name: failing-job-demo
  namespace: default
spec:
  backoffLimit: 2
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: fail
          image: busybox:1.36
          imagePullPolicy: IfNotPresent
          command:
            - sh
            - -c
            - "exit 1"

backoffLimit: 2 の場合、初回 + リトライ 2 回 = 計 3 回失敗で Job が Failed 状態になります。restartPolicy: Never なので失敗のたびに新しい Pod が生成され、最終的に Failed 状態の Pod が 3 個残ります。演習①の Step ⑩ 〜 ⑫ で実機確認します。

やってみよう①:DB マイグレーション Job と CSV エクスポート Job を実行する

fanclub-api の DB に対して、(1) スキーマ変更を Job で実行する DDL マイグレーション、(2) members テーブルを CSV エクスポートする Job、(3) backoffLimit 超過の挙動を確認する失敗 Job、の 3 本を順に実行します。所要時間目安は約 30 分です。

演習の全体フローは以下のとおりです。

  1. 前提状態の確認(kubectl get pods,svc,configmap,secret
  2. fanclub-db-migrate-job.yaml 作成(ALTER TABLE で score カラム追加)
  3. kubectl apply でマイグレーション Job を実行
  4. kubectl get jobs で COMPLETIONS 1/1 を確認
  5. kubectl logs job/fanclub-db-migrate でマイグレーション結果を確認
  6. fanclub-db-export-job.yaml 作成(COPY で members テーブルを CSV 出力)
  7. kubectl apply で CSV エクスポート Job を実行
  8. kubectl logs job/fanclub-db-export で CSV データを確認
  9. failing-job-demo-job.yaml 作成(backoffLimit: 2 + exit 1
  10. kubectl apply で失敗 Job を実行
  11. kubectl get jobs + kubectl get podsFailed 状態と Pod 3 個生成を確認
  12. kubectl delete job failing-job-demo でクリーンアップ

Step 1: 前提状態の確認

k8s-ops の作業端末で ~/fanclub-manifests/ に移動し、現在のクラスタ状態を確認します。

実行コマンド:

$ cd ~/fanclub-manifests/
$ kubectl get pods,svc,configmap,secret -n default

実行結果(fanclub-backend Pod・fanclub-db-0 Pod・各種 Service・fanclub-configfanclub-secret が表示される。Job はゼロ件):

NAME                    READY   STATUS    RESTARTS   AGE
pod/fanclub-backend     1/1     Running   0          11h
pod/fanclub-db-0        1/1     Running   0          11h

NAME                          TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
service/fanclub-backend       ClusterIP   10.96.150.60    <none>        80/TCP     11h
service/fanclub-db            ClusterIP   10.96.131.167   <none>        5432/TCP   11h
service/fanclub-db-headless   ClusterIP   None            <none>        5432/TCP   11h
service/kubernetes            ClusterIP   10.96.0.1       <none>        443/TCP    11h

NAME                         DATA   AGE
configmap/fanclub-config     4      11h
configmap/fanclub-db-init    1      11h
configmap/kube-root-ca.crt   1      11h

NAME                    TYPE     DATA   AGE
secret/fanclub-secret   Opaque   2      11h

fanclub-backend / fanclub-db-0 の両 Pod が Running、fanclub-config ConfigMap と fanclub-secret Secret が存在することを確認します。

Step 2: マイグレーション Job YAML を作成

members テーブルに score カラムを追加する DDL を実行する Job を作成します。fanclub-secret から DB_PASSWORDsecretKeyRefPGPASSWORD 環境変数として注入し、psql -c でワンライナー DDL を実行する設計です。

実行コマンド:

$ vi ~/fanclub-manifests/fanclub-db-migrate-job.yaml

ファイル内容:

apiVersion: batch/v1
kind: Job
metadata:
  name: fanclub-db-migrate
  namespace: default
  labels:
    app: fanclub-api
    job-type: migration
spec:
  completions: 1
  parallelism: 1
  backoffLimit: 3
  ttlSecondsAfterFinished: 600
  template:
    metadata:
      labels:
        app: fanclub-api
        job-type: migration
    spec:
      restartPolicy: Never
      containers:
        - name: db-migrate
          image: postgres:18
          imagePullPolicy: IfNotPresent
          command:
            - psql
            - -h
            - fanclub-db
            - -U
            - appuser
            - -d
            - fanclubdb
            - -c
            - "ALTER TABLE members ADD COLUMN IF NOT EXISTS score INTEGER DEFAULT 0;"
          env:
            - name: PGPASSWORD
              valueFrom:
                secretKeyRef:
                  name: fanclub-secret
                  key: DB_PASSWORD

各フィールドの設計意図を整理します。

  • completions: 1 + parallelism: 1:1 Pod が成功すれば完了する単発 Job
  • backoffLimit: 3:失敗時のリトライ上限。デフォルト 6 を本番想定で絞っている
  • ttlSecondsAfterFinished: 600:完了から 10 分後に Job + Pod を自動削除
  • restartPolicy: Never:副作用ある DDL なので、失敗時は新しい Pod で最初からやり直す
  • image: postgres:18:ep9 で kind ノード内に既に存在する PostgreSQL 公式イメージ。psql クライアントが含まれる
  • commandpsql -h fanclub-db -U appuser -d fanclubdb -c "DDL"。接続先の Service 名は ep9 の fanclub-db を使用
  • ALTER TABLE ... ADD COLUMN IF NOT EXISTS:冪等な DDL。Job を再実行しても 2 回目以降はスキーマ変更なしで成功する
  • PGPASSWORD 環境変数:fanclub-secretDB_PASSWORD キーを secretKeyRef で参照

Step 3: マイグレーション Job を apply

実行コマンド:

$ kubectl apply -f ~/fanclub-manifests/fanclub-db-migrate-job.yaml

実行結果:

job.batch/fanclub-db-migrate created

Step 4: Job の COMPLETIONS を確認

Job の状態を確認します。

実行コマンド:

$ kubectl get jobs -n default

実行結果(COMPLETIONS が 1/1 になれば成功):

NAME                 STATUS     COMPLETIONS   DURATION   AGE
fanclub-db-migrate   Complete   1/1           10s        20s

Job の Pod も合わせて確認します。Job の Pod は job-name=fanclub-db-migrate ラベルを自動付与されるため、ラベルセレクタで絞り込めます。

実行コマンド:

$ kubectl get pods -l job-name=fanclub-db-migrate -n default

実行結果(Pod が Completed 状態):

NAME                        READY   STATUS      RESTARTS   AGE
fanclub-db-migrate-kf2t8   0/1     Completed   0          20s

Step 5: マイグレーション結果を確認

kubectl logs でマイグレーション Job の実行結果を確認します。Job リソースに対して kubectl logs を実行すると、紐づく Pod のログを取得できます。

実行コマンド:

$ kubectl logs job/fanclub-db-migrate

実行結果(PostgreSQL の ALTER TABLE 応答):

ALTER TABLE

ALTER TABLE の 1 行が表示されれば DDL が正常に適用されています。members テーブルに score カラムが追加され、デフォルト値 0 で既存 2 行に値が埋まった状態です。念のため kubectl exec で DB に直接接続してスキーマを確認します。

実行コマンド:

$ kubectl exec fanclub-db-0 -- psql -U appuser -d fanclubdb -c "\d members"
$ kubectl exec fanclub-db-0 -- psql -U appuser -d fanclubdb -c "SELECT id, name, plan, score FROM members ORDER BY id;"

実行結果:

                                        Table "public.members"
   Column   |            Type             | Collation | Nullable |               Default
------------+-----------------------------+-----------+----------+-------------------------------------
 id         | integer                     |           | not null | nextval('members_id_seq'::regclass)
 name       | character varying(100)      |           | not null |
 email      | character varying(255)      |           | not null |
 plan       | character varying(50)       |           | not null |
 created_at | timestamp without time zone |           |          | CURRENT_TIMESTAMP
 score      | integer                     |           |          | 0
Indexes:
    "members_pkey" PRIMARY KEY, btree (id)
    "members_email_key" UNIQUE CONSTRAINT, btree (email)

 id |   name   |   plan   | score
----+----------+----------+-------
  1 | 山田太郎 | premium  |     0
  2 | 鈴木花子 | standard |     0
(2 rows)

score カラムが末尾に追加され、デフォルト値 0 で既存 2 行に適用されていることが確認できます。

Step 6: CSV エクスポート Job YAML を作成

続いて members テーブルを CSV 形式でエクスポートする Job を作成します。COPY ... TO STDOUT WITH CSV HEADER を使うと、psql の標準出力に CSV データが流れるため、kubectl logs で結果を確認できます。emptyDir に書き出す方式は Pod 削除時に消えるため不採用としました。

実行コマンド:

$ vi ~/fanclub-manifests/fanclub-db-export-job.yaml

ファイル内容:

apiVersion: batch/v1
kind: Job
metadata:
  name: fanclub-db-export
  namespace: default
  labels:
    app: fanclub-api
    job-type: export
spec:
  completions: 1
  parallelism: 1
  backoffLimit: 3
  ttlSecondsAfterFinished: 600
  template:
    metadata:
      labels:
        app: fanclub-api
        job-type: export
    spec:
      restartPolicy: Never
      containers:
        - name: db-export
          image: postgres:18
          imagePullPolicy: IfNotPresent
          command:
            - psql
            - -h
            - fanclub-db
            - -U
            - appuser
            - -d
            - fanclubdb
            - -c
            - "COPY members TO STDOUT WITH CSV HEADER;"
          env:
            - name: PGPASSWORD
              valueFrom:
                secretKeyRef:
                  name: fanclub-secret
                  key: DB_PASSWORD

マイグレーション Job との差分は command の SQL のみです。COPY members TO STDOUT WITH CSV HEADER はテーブル全行をヘッダー付き CSV として標準出力に書き込みます。Step 5 で追加した score カラムも一緒に出力されるため、マイグレーションの効果を視覚的に確認できます。

Step 7: CSV エクスポート Job を apply

実行コマンド:

$ kubectl apply -f ~/fanclub-manifests/fanclub-db-export-job.yaml
$ kubectl get jobs -n default

実行結果:

job.batch/fanclub-db-export created

NAME                 STATUS     COMPLETIONS   DURATION   AGE
fanclub-db-migrate   Complete   1/1           10s        2m
fanclub-db-export    Complete   1/1           4s         10s

Step 8: CSV データを確認

実行コマンド:

$ kubectl logs job/fanclub-db-export

実行結果(ヘッダー行 + データ 2 行の CSV):

id,name,email,plan,created_at,score
1,山田太郎,yamada@example.com,premium,2026-05-10 05:42:17.132549,0
2,鈴木花子,suzuki@example.com,standard,2026-05-10 05:42:17.132549,0

CSV の 1 行目はカラム名のヘッダー、2 行目以降がデータです。マイグレーション Job で追加した score カラムが末尾に付き、両行とも値 0(デフォルト値)で出力されているのが確認できます。エクスポート結果をファイルに残したい場合は、kubectl logs job/fanclub-db-export > members-export.csv でリダイレクトします。

Step 9: 失敗 Job のデモ用 YAML を作成

最後に backoffLimit 超過で Job が Failed になる挙動を確認します。exit 1 を実行して必ず失敗する Job です。

実行コマンド:

$ vi ~/fanclub-manifests/failing-job-demo-job.yaml

ファイル内容:

apiVersion: batch/v1
kind: Job
metadata:
  name: failing-job-demo
  namespace: default
spec:
  backoffLimit: 2
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: fail
          image: busybox:1.36
          imagePullPolicy: IfNotPresent
          command:
            - sh
            - -c
            - "exit 1"

Step 10〜11: 失敗 Job を実行して状態を確認

実行コマンド:

$ kubectl apply -f ~/fanclub-manifests/failing-job-demo-job.yaml
$ sleep 30
$ kubectl get jobs failing-job-demo -n default
$ kubectl get pods -l job-name=failing-job-demo -n default

実行結果:

job.batch/failing-job-demo created

NAME               STATUS   COMPLETIONS   DURATION   AGE
failing-job-demo   Failed   0/1           30s        91s

NAME                         READY   STATUS   RESTARTS   AGE
failing-job-demo-knf7t       0/1     Error    0          91s
failing-job-demo-6kczd       0/1     Error    0          80s
failing-job-demo-stfd7       0/1     Error    0          60s

backoffLimit: 2 で Pod が 3 個生成されたのは、初回失敗 + リトライ 2 回の合計が 3 回だからです。Job の STATUS 列が Failed になり、kubectl describe job failing-job-demo の Events セクションで BackoffLimitExceeded を確認できます。

実行コマンド:

$ kubectl describe job failing-job-demo

実行結果(Events 抜粋):

Events:
  Type     Reason                Age   From            Message
  ----     ------                ----  ----            -------
  Normal   SuccessfulCreate      91s   job-controller  Created pod: failing-job-demo-knf7t
  Normal   SuccessfulCreate      80s   job-controller  Created pod: failing-job-demo-6kczd
  Normal   SuccessfulCreate      60s   job-controller  Created pod: failing-job-demo-stfd7
  Warning  BackoffLimitExceeded  57s   job-controller  Job has reached the specified backoff limit

3 つの Pod 名(knf7t・6kczd・stfd7)が順番に生成され、最後に BackoffLimitExceeded イベントが出力されているのが実機で確認できます。

Step 12: 失敗 Job をクリーンアップ

失敗 Job は ttlSecondsAfterFinished を設定していないため自動削除されません。手動で削除します。

実行コマンド:

$ kubectl delete job failing-job-demo -n default

実行結果:

job.batch "failing-job-demo" deleted

Job を削除すると関連 Pod も連鎖削除されます。マイグレーション Job と CSV エクスポート Job は ttlSecondsAfterFinished: 600 を設定済みのため、放置すれば 10 分後に自動削除されます。これで Job の演習が完了しました。

CronJob 基礎 — schedule・timeZone・concurrencyPolicy・suspend

CronJob は cron スケジュールに基づいて Job を定期的に作成するワークロードリソースです。Linux の cron デーモンと同じ構文の 5 フィールドスケジュールを使い、定刻になると Job を生成して紐づく Pod を起動します。本セクションでは CronJob の主要フィールドを体系的に整理し、本回のヒヤリハット 2 で扱う timeZone 問題への布石を置きます。

cron 式 5 フィールドの構造

CronJob の schedule フィールドは、Linux cron と同じ 5 フィールドの記法を使います。

┌──────────── 分(0-59)
│  ┌─────────── 時(0-23)
│  │  ┌────────── 日(1-31)
│  │  │  ┌───────── 月(1-12)
│  │  │  │  ┌──────── 曜日(0-7・0と7が日曜日)
│  │  │  │  │
*/1 *  *  *  *   = 1 分ごと
 0  2  *  *  *   = 毎日 02:00
 0  9  *  * 1-5  = 平日(月〜金)09:00
 0 */6 *  *  *   = 6 時間ごと(00:00, 06:00, 12:00, 18:00)

記号の意味は次のとおりです。

  • *:そのフィールドの全値にマッチ(例: 分フィールドの * は 0-59 すべて)
  • */N:N ごとにマッチ(例: 分フィールドの */5 は 0,5,10,…,55)
  • A-B:A から B までの範囲(例: 曜日 1-5 は月〜金)
  • A,B,C:複数値の列挙(例: 月 1,4,7,10 は四半期初月)

CKAD 試験ではよく */1 * * * *(1 分ごと)を見ますが、これは「分が */1 = 1 ごとにマッチ」する記法です。* * * * * でも同じ意味になります(厳密には毎分マッチ)。schedule 値はクォートで囲む必要があります(YAML パーサで * が anchor 等と誤解されないため)。

CronJob の主要フィールド一覧

フィールドデフォルト意味
schedule5 フィールド cron 式(必須)
timeZoneUTCスケジュール基準のタイムゾーン(K8s v1.27 GA)。IANA 形式(例: Asia/Tokyo
concurrencyPolicyAllow前 Job 実行中の挙動。Allow / Forbid / Replace の 3 値
suspendfalsetrue で新規 Job 作成を停止(実行中 Job は影響なし)
successfulJobsHistoryLimit3成功 Job の保持件数
failedJobsHistoryLimit1失敗 Job の保持件数
startingDeadlineSeconds無制限スケジュール遅延の許容秒数。後述
jobTemplate生成する Job の spec(Job リソースの spec と同じ構造)

startingDeadlineSeconds は実務でも見落とされがちな項目です。CronJob コントローラーが何らかの事情で停止し、その間にスケジュールティックを見逃した場合、復旧後に「見逃した分の Job をまとめて起動する」挙動が発生します。

startingDeadlineSeconds: 200 のように指定すると「ティックから 200 秒以内なら Job を起動するが、それを超えていたらスキップする」というガードレールになります。本番では大量の Job が同時起動してリソースを枯渇させる事故を防ぐため、明示的に設定する設計が定石です。

timeZone — UTC デフォルトの落とし穴

timeZone フィールドは Kubernetes v1.27 で GA となった比較的新しい機能です。省略するとスケジュールは UTC 基準で動作します。日本(JST = UTC+9)で「朝 9 時にバックアップを実行したい」と schedule: "0 9 * * *" を書いてしまうと、実際は UTC 9:00(JST 18:00)に実行されることになります。

これは本番でレポートの遅延・夜間バッチの誤実行を引き起こす典型ヒヤリハットで、H2-11 の事例で詳細を扱います。

JST 基準で運用するなら、CronJob には次のようにタイムゾーンを明示します。

spec:
  schedule: "0 9 * * *"
  timeZone: "Asia/Tokyo"

IANA タイムゾーンデータベースの形式(Asia/Tokyo / America/Los_Angeles / UTC 等)で指定します。文字列は " で囲みます。

concurrencyPolicy 3 モード比較

concurrencyPolicy は「前のスケジュールの Job がまだ実行中のとき、次のスケジュールが到来したらどうするか」を制御します。3 値あります。

concurrencyPolicy前 Job が実行中のとき典型シナリオ
Allow(デフォルト)新しい Job を起動する(並行実行)タスクが独立していて並行可能な場合
Forbid新しい Job を起動しない(スキップ)前 Job の完了を必ず待つ必要がある場合
Replace実行中の Job を削除して新しい Job を起動常に最新の結果だけが必要な場合

Allow は最もシンプルですが、Job の処理時間がスケジュール間隔より長い場合に Job が無限に積み上がるリスクがあります。本番では Forbid または Replace を選ぶのが定石です。

Forbid は前 Job が遅延した場合にスケジュールを 1 回スキップしてしまう一方、Replace は強制的に最新ティックの Job だけを残すため、後続データに依存しないバッチ(最新サマリーの集計等)に向きます。

suspend — 一時停止フラグ

suspend: true を設定すると CronJob が一時停止状態になり、新しい Job が作成されなくなります。kubectl patch cronjob <name> -p '{"spec":{"suspend":true}}' で動的に切り替えられます。

注意点として、suspend は新規 Job の生成を停止するだけで、すでに実行中の Job には影響しません。実行中 Job を強制終了したい場合は kubectl delete job で個別に削除します。

本番では「メンテナンス時間中は定期バックアップを止めたい」「障害調査中は監視 Job を一時停止したい」といったユースケースで頻繁に使います。CronJob を削除する代わりに suspend: true にしておけば、再開時に suspend: false に戻すだけで再稼働できるため、運用上有用です。

Job 履歴の保持件数 — successfulJobsHistoryLimit / failedJobsHistoryLimit

CronJob は生成した Job をすべて残すわけではなく、デフォルトで成功 Job 3 件・失敗 Job 1 件を保持します。それを超えると古い Job から自動削除されます。本番では失敗 Job のログを長く保持したいケースが多いため、failedJobsHistoryLimit: 5 程度に増やす運用が一般的です。

スケルトン生成テクニック — kubectl create cronjob –dry-run

Job と同様に CronJob もコマンドでスケルトンを生成できます。

実行コマンド:

$ kubectl create cronjob fanclub-member-count --image=postgres:18 --schedule="*/1 * * * *" --dry-run=client -o yaml

実行結果:

apiVersion: batch/v1
kind: CronJob
metadata:
  creationTimestamp: null
  name: fanclub-member-count
spec:
  jobTemplate:
    metadata:
      creationTimestamp: null
      name: fanclub-member-count
    spec:
      template:
        metadata:
          creationTimestamp: null
        spec:
          containers:
          - image: postgres:18
            name: fanclub-member-count
            resources: {}
          restartPolicy: OnFailure
  schedule: '*/1 * * * *'
status: {}

注意:kubectl create cronjob が生成するスケルトンの restartPolicyOnFailure になります。本シリーズの方針に合わせて Never に変更し、timeZoneconcurrencyPolicy を追記する流れになります。

Job・CronJob・DaemonSet の動作モデルを 3 パネルで比較した図。左パネルの Job は completions・parallelism・backoffLimit を持ち、Pod-1 から Pod-3 が 1 つずつ順番に完了して Job Succeeded に至る逐次実行サイクルを示す。中パネルの CronJob はスケジュール到達ごとに Job を生成・実行・完了削除する繰り返しサイクルを示し、successfulJobsHistoryLimit で古い Job が自動削除されることを明記。右パネルの DaemonSet は Node 追加イベントで Node 枠内に Pod が自動配置されるネスト構造で Node 数イコール Pod 数の原則を示す。下部の全幅バンドが 3 機構の失敗時挙動の差を整理している。

やってみよう②:CronJob(1 分間隔)を作成して suspend と concurrencyPolicy を実機確認する

1 分間隔で members テーブルの行数を出力する CronJob を作成し、Job 履歴の積み上がり・suspend による停止・concurrencyPolicy の Replace 動作を実機で確認します。所要時間目安は約 20 分です。

演習の全体フローは以下のとおりです。

  1. fanclub-member-count-cronjob.yaml 作成(timeZone: Asia/Tokyo・concurrencyPolicy: Allow)
  2. kubectl apply で CronJob を作成
  3. kubectl get cronjob で SCHEDULE / SUSPEND / ACTIVE / LAST SCHEDULE 列を確認
  4. 3 分待機 → kubectl get jobs で 3 件の Job が生成されたことを確認
  5. kubectl logs で行数出力を確認
  6. successfulJobsHistoryLimit: 3 による古い Job の自動削除を確認
  7. kubectl patchsuspend: true に切り替え
  8. kubectl get cronjob で SUSPEND 列が True になり、Job が新規作成されないことを確認
  9. concurrencyPolicyReplace に変更
  10. suspend: false に戻して動作観察
  11. 演習終了後、CronJob を再度 suspend: true に戻して残置(ep11 完了状態)

Step 1: CronJob YAML を作成

1 分ごとに members テーブルの行数を SELECT する CronJob を定義します。

実行コマンド:

$ vi ~/fanclub-manifests/fanclub-member-count-cronjob.yaml

ファイル内容:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: fanclub-member-count
  namespace: default
  labels:
    app: fanclub-api
    job-type: member-count
spec:
  schedule: "*/1 * * * *"
  timeZone: "Asia/Tokyo"
  concurrencyPolicy: Allow
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
  startingDeadlineSeconds: 200
  jobTemplate:
    spec:
      backoffLimit: 2
      ttlSecondsAfterFinished: 600
      template:
        metadata:
          labels:
            app: fanclub-api
            job-type: member-count
        spec:
          restartPolicy: Never
          containers:
            - name: count-members
              image: postgres:18
              imagePullPolicy: IfNotPresent
              command:
                - psql
                - -h
                - fanclub-db
                - -U
                - appuser
                - -d
                - fanclubdb
                - -c
                - "SELECT COUNT(*) AS member_count FROM members;"
              env:
                - name: PGPASSWORD
                  valueFrom:
                    secretKeyRef:
                      name: fanclub-secret
                      key: DB_PASSWORD

YAML の入れ子構造に注意が必要です。CronJob の Pod 定義は spec.jobTemplate.spec.template.spec という 4 段階のネストになります。Job が spec.template.spec の 2 段階だったのに比べて深く、kubectl create cronjob --dry-run でスケルトン生成して使い回す方が安全です。

主要フィールドの設計意図:

  • schedule: "*/1 * * * *":1 分ごとに Job を生成(教材用に短い間隔)
  • timeZone: "Asia/Tokyo":JST 基準。1 分間隔ではタイムゾーン差は実害がないが、明示するルールで統一
  • concurrencyPolicy: Allow:演習で挙動を観察するため最初は Allow を採用。後で Replace に変更する
  • startingDeadlineSeconds: 200:CronJob コントローラー復旧時の大量 Job 起動を防ぐ
  • jobTemplate.spec.backoffLimit: 2:個々の Job の失敗時リトライ上限
  • jobTemplate.spec.ttlSecondsAfterFinished: 600:個々の Job の完了後 10 分自動削除(successfulJobsHistoryLimit と二重ガード)

Step 2: CronJob を apply

実行コマンド:

$ kubectl apply -f ~/fanclub-manifests/fanclub-member-count-cronjob.yaml
$ kubectl get cronjob -n default

実行結果:

cronjob.batch/fanclub-member-count created

NAME                   SCHEDULE      TIMEZONE     SUSPEND   ACTIVE   LAST SCHEDULE   AGE
fanclub-member-count   */1 * * * *   Asia/Tokyo   False     0        <none>          0s

CronJob 作成直後は LAST SCHEDULE 列が <none> です。CronJob は次のスケジュールティックまで Job を作成しないため、1 分間隔の場合は最長 1 分待つ必要があります。すぐに kubectl get jobs しても何も表示されなくても焦らずに待機します。

Step 3: 3 分待機して Job 履歴を確認

3 分待機して Job がいくつ生成されたか確認します。

実行コマンド:

$ sleep 180
$ kubectl get jobs -n default
$ kubectl get cronjob fanclub-member-count -n default

実行結果(CronJob から生成された Job が 3 件表示・LAST SCHEDULE が更新される):

NAME                            STATUS     COMPLETIONS   DURATION   AGE
fanclub-member-count-29640193   Complete   1/1           3s         2m46s
fanclub-member-count-29640194   Complete   1/1           3s         106s
fanclub-member-count-29640195   Complete   1/1           3s         46s

NAME                   SCHEDULE      TIMEZONE     SUSPEND   ACTIVE   LAST SCHEDULE   AGE
fanclub-member-count   */1 * * * *   Asia/Tokyo   False     0        46s             3m22s

Job 名は fanclub-member-count-<hash> または fanclub-member-count-<timestamp> 形式で自動付与されます。CronJob からは Job が生成され、Job からは Pod が生成される 3 階層構造です。

Step 4: Job のログで行数を確認

最新の Job のログを確認します。Job 名が動的なため、ラベルセレクタを使うと便利です。

実行コマンド:

$ kubectl logs -l job-type=member-count --tail=10 -n default

実行結果(行数 2 が出力される):

 member_count
--------------
            2
(1 row)

Step 5: successfulJobsHistoryLimit の動作を観察

5 分以上待機すると、successfulJobsHistoryLimit: 3 により Job 履歴が 3 件に保たれる挙動を観察できます。

実行コマンド:

$ sleep 180
$ kubectl get jobs -l job-type=member-count -n default

実行結果(5〜6 件積み上がっているように見えても、Complete 状態の Job は最大 3 件に保たれる):

NAME                            STATUS     COMPLETIONS   DURATION   AGE
fanclub-member-count-29640193   Complete   1/1           3s         5m46s
fanclub-member-count-29640194   Complete   1/1           3s         4m46s
fanclub-member-count-29640195   Complete   1/1           3s         3m46s

ttlSecondsAfterFinished: 600(10 分)と successfulJobsHistoryLimit: 3(3 件)はそれぞれ独立に動作し、どちらかの条件に引っかかった時点で Job が削除されます。1 分間隔で実行する CronJob では successfulJobsHistoryLimit: 3 が先に発動し、Job 履歴が常に 3 件に保たれます。

Step 6: suspend で一時停止

CronJob を一時停止します。kubectl patchspec.suspendtrue に切り替えます。

実行コマンド:

$ kubectl patch cronjob fanclub-member-count -n default -p '{"spec":{"suspend":true}}'
$ kubectl get cronjob fanclub-member-count -n default

実行結果(SUSPEND 列が True に変わる):

cronjob.batch/fanclub-member-count patched

NAME                   SCHEDULE      TIMEZONE     SUSPEND   ACTIVE   LAST SCHEDULE   AGE
fanclub-member-count   */1 * * * *   Asia/Tokyo   True      0        51s             3m27s

2〜3 分待機しても新しい Job が作成されないことを確認します。

実行コマンド:

$ sleep 180
$ kubectl get jobs -l job-type=member-count -n default

実行結果(Job 件数が増えない・むしろ ttl で減る):

NAME                            STATUS     COMPLETIONS   DURATION   AGE
fanclub-member-count-29640193   Complete   1/1           3s         6m46s
fanclub-member-count-29640194   Complete   1/1           3s         5m46s
fanclub-member-count-29640195   Complete   1/1           3s         4m46s

suspend が新規 Job 生成のみを止めることを確認します。実行中の Job がもしあれば(1 分間隔の軽量 SELECT では実質的にゼロですが)、それは強制終了されず最後まで完了します。「suspend = 完全停止」と誤解してすでに走っている処理を止めようとすると、設計が破綻します。

Step 7: concurrencyPolicy を Replace に変更して再開

concurrencyPolicy を Replace に変更し、suspend を解除して動作を観察します。kubectl edit でも変更できますが、ここでは kubectl patch でまとめて操作します。

実行コマンド:

$ kubectl patch cronjob fanclub-member-count -n default -p '{"spec":{"concurrencyPolicy":"Replace","suspend":false}}'
$ kubectl get cronjob fanclub-member-count -n default

実行結果(SUSPEND 列が False に戻り、CONCURRENCY POLICY は default 表示の場合 kubectl get cronjob -o yaml で確認):

cronjob.batch/fanclub-member-count patched

NAME                   SCHEDULE      TIMEZONE     SUSPEND   ACTIVE   LAST SCHEDULE   AGE
fanclub-member-count   */1 * * * *   Asia/Tokyo   False     0        51s             3m27s

本回の SELECT クエリは数秒で完了するため、1 分間隔のスケジュールでは concurrencyPolicy の差は観測しづらいです。

Replace の効果を視覚化したい場合は commandsh -c "sleep 90; psql ..." のように 90 秒スリープを入れて Job を遅延させると、次のティックで前 Job が削除される動作が観察できます。本演習では仕様確認にとどめます。

Step 8: 演習終了後の状態を整える

ep11 完了状態として CronJob を suspend: true で残します。Job 履歴は ttl で順次消えていきます。

実行コマンド:

$ kubectl patch cronjob fanclub-member-count -n default -p '{"spec":{"suspend":true}}'
$ kubectl get cronjob fanclub-member-count -n default

実行結果:

cronjob.batch/fanclub-member-count patched

NAME                   SCHEDULE      TIMEZONE     SUSPEND   ACTIVE   LAST SCHEDULE   AGE
fanclub-member-count   */1 * * * *   Asia/Tokyo   True      0        46s             6m10s

CronJob を完全に削除したい場合は kubectl delete cronjob fanclub-member-count ですが、ep12 以降では言及しないため、suspend 状態で残置するのが教材上の推奨設定です。

DaemonSet 基礎 — 全 Node に 1 Pod を配置するデーモン型ワークロード

DaemonSet はクラスタの全 Node(または条件一致 Node)に 1 Pod ずつ自動配置するワークロードリソースです。Node が追加されると自動で Pod が配置され、Node が削除されると Pod も削除されます。本セクションでは DaemonSet の概念・主な用途・Deployment との違いを整理し、kind 環境特有の挙動(シングルノード)を確認します。

DaemonSet の主な用途 5 選

DaemonSet は「全 Node で同じインフラ処理を動かしたい」場合に使います。代表的な用途は次の 5 つです。

  • ログ収集 Agent:Fluent Bit / Fluentd 等。各 Node の /var/log/containers/ を読んで集約サーバーに転送する
  • ノード監視 Agent:Prometheus Node Exporter 等。各 Node のメトリクス(CPU・メモリ・ディスク・ネットワーク)を expose する
  • CNI プラグイン:Calico / Cilium / kindnet 等。各 Node に Pod ネットワークを設定する
  • kube-proxy:各 Node で Service の iptables / IPVS ルールを管理する
  • ストレージプラグイン:CSI Node Plugin 等。各 Node でボリュームマウント処理を担当する

共通点は「Node ごとに 1 つ走らせる必要がある」処理であることです。Node が増えれば自動で Pod を増やし、Node が減れば自動で Pod を減らす、という弾力性が DaemonSet の価値です。

Deployment との違い

「Node 数だけ Pod が必要」という要件を Deployment + replicas: N で実装したくなる初学者は多いですが、これはアンチパターンです。Deployment と DaemonSet の違いを整理します。

項目DeploymentDaemonSet
Pod 数の管理replicas: N で N 個に保つNode 数 = Pod 数(自動)
Pod の配置先scheduler が判断(複数 Pod が同 Node に集中することもある)各 Node に必ず 1 Pod
Node 追加時Pod は既存 Node で再配分されない新 Node に自動で Pod が追加される
Node 削除時該当 Pod は他 Node で再起動該当 Pod も自動削除(再配置なし)
用途ステートレス Web サービスインフラ常駐処理
restartPolicyAlways のみAlways のみ

「Deployment + replicas: 3」と「DaemonSet(3 Node クラスタ)」は一見同じに見えますが、Node が 4 台に増えたとき Deployment は依然として 3 Pod のまま(4 番目の Node にはログ収集 Agent が動かない)、DaemonSet は自動で 4 Pod に増える、という挙動差があります。

本番ではこの差が「新規 Node にログが出ない」「監視データが歯抜けになる」事故を生みます。Node ごと処理を担う設計は DaemonSet を選ぶのが鉄則です。

nodeSelector と tolerations による配置制御

DaemonSet は「全 Node に 1 Pod」がデフォルトですが、特定 Node のみに配置することもできます。

  • nodeSelector:指定ラベルを持つ Node にのみ配置(例: kubernetes.io/os: linux で Linux Node 限定)
  • tolerations:Node の taint(汚染)を許容する設定。Control Plane Node 等の特殊 Node にも配置したい場合に使う

kind のシングルノード環境では Node が 1 台しかないため nodeSelector の効果は限定的ですが、本番マルチノード環境では「GPU 搭載 Node のみ」「特定リージョンの Node のみ」といった用途で頻繁に使います。

tolerations は kind 環境では特に重要です。kind のシングルノードは Control Plane Node を兼ねており、node-role.kubernetes.io/control-plane: NoSchedule taint が設定されている場合があります。

DaemonSet が自動で持つ toleration(node.kubernetes.io/unschedulable 等)には control-plane taint が含まれないため、明示的な toleration を追加しないと Pod がスケジュールされません。

updateStrategy — RollingUpdate と OnDelete

DaemonSet の Pod 更新方式は 2 種類あります。

updateStrategy挙動使い所
RollingUpdate(デフォルト)maxUnavailable で段階的に Pod を更新無停止更新したい場合
OnDelete手動で Pod を削除するまで更新しない更新タイミングを慎重に制御したい場合

本番では RollingUpdate + maxUnavailable: 1 がデフォルトであり、ログ収集 Agent や監視 Agent をローリング更新するのが定石です。OnDelete は CNI プラグインのような「全 Node 同時更新がリスキーで手動制御したい」処理で選びます。

DaemonSet には kubectl create コマンドがない

Job / CronJob は kubectl create job / kubectl create cronjob でスケルトン生成できますが、DaemonSet には対応する kubectl create サブコマンドが存在しません。CKAD 試験で DaemonSet を作る場合は、以下のいずれかの方針で対応します。

  • YAML を手書きする(apiVersion: apps/v1 / kind: DaemonSet)
  • Deployment のスケルトンを kubectl create deployment ... --dry-run=client -o yaml で出して、kind: DaemonSet に変更し replicas: 行を削除する
  • kubectl explain daemonset.spec でフィールドを確認しながら書く

第 2 案が試験では速いです。Deployment と DaemonSet は spec 構造がほぼ同じで、違いは replicas: の有無と strategy / updateStrategy のフィールド名くらいです。

やってみよう③:kindnet と kube-proxy を観察して自前 DaemonSet を apply する

kube-system namespace の既存 DaemonSet(kindnet と kube-proxy)を観察して DaemonSet の実態をつかみ、その後 default namespace に busybox:1.36 を使った自前 DaemonSet(node-logger)を apply して動作を確認します。所要時間目安は約 20 分です。

演習の全体フローは以下のとおりです。

  1. kube-system namespace の DaemonSet 一覧を観察
  2. kindnet DaemonSet の詳細を kubectl describe で観察(nodeSelector / tolerations / DESIRED / READY)
  3. kubectl describe node で kind ノードの taint を確認
  4. node-logger-daemonset.yaml を作成(busybox + 5 秒ごとに hostname を echo)
  5. kubectl apply で自前 DaemonSet を起動
  6. kubectl get daemonset / kubectl get pods で 1 Pod 配置を確認
  7. kubectl logs でデーモン動作を確認
  8. 演習終了後の状態確認(任意削除可)

Step 1: kube-system の DaemonSet を観察

kind クラスタには既に kindnet(CNI)と kube-proxy(Service ルーティング)が DaemonSet として動作しています。

実行コマンド:

$ kubectl get daemonset -n kube-system

実行結果(DESIRED / CURRENT / READY 列を確認・kind シングルノードのため 1):

NAMESPACE     NAME         DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
kube-system   kindnet      1         1         1       1            1           kubernetes.io/os=linux   11h
kube-system   kube-proxy   1         1         1       1            1           kubernetes.io/os=linux   11h

列の意味は次のとおりです。

  • DESIRED:配置すべき Pod 数(条件一致 Node 数)
  • CURRENT:現在存在する Pod 数
  • READY:Ready 状態の Pod 数
  • UP-TO-DATE:最新の DaemonSet 定義に追従している Pod 数
  • AVAILABLE:minReadySeconds を満たし利用可能な Pod 数
  • NODE SELECTOR:配置対象 Node の絞り込みラベル

kind はシングルノード(Node 1 台)のため、両 DaemonSet とも DESIRED=1 です。本番マルチノード環境では Node 数だけこの値が増えます。

Step 2: kindnet の詳細を describe で観察

kindnet DaemonSet の nodeSelectortolerations を観察します。

実行コマンド:

$ kubectl describe daemonset kindnet -n kube-system

実行結果(抜粋・Node-Selector と Tolerations セクションに注目):

Name:           kindnet
Namespace:      kube-system
Selector:       app=kindnet
Node-Selector:  kubernetes.io/os=linux
Labels:         app=kindnet
                k8s-app=kindnet
                tier=node
Annotations:    deprecated.daemonset.template.generation: 1
Desired Number of Nodes Scheduled: 1
Current Number of Nodes Scheduled: 1
Number of Nodes Scheduled with Up-to-date Pods: 1
Number of Nodes Scheduled with Available Pods: 1
Number of Nodes Misscheduled: 0
Pods Status:  1 Running / 0 Waiting / 0 Succeeded / 0 Failed
Pod Template:
  Labels:           app=kindnet
                    k8s-app=kindnet
                    tier=node
  Service Account:  kindnet
  Containers:
   kindnet-cni:
    Image:      docker.io/kindest/kindnetd:v20251212-v0.29.0-alpha-105-g20ccfc88
    Port:       <none>
    Host Port:  <none>
    Limits:
      cpu:     100m
      memory:  50Mi
    Requests:
      cpu:     100m
      memory:  50Mi
    Environment:
      HOST_IP:                  (v1:status.hostIP)
      POD_IP:                   (v1:status.podIP)
      POD_SUBNET:              10.244.0.0/16
      CONTROL_PLANE_ENDPOINT:  kind-control-plane:6443
    Mounts:
      /etc/cni/net.d from cni-cfg (rw)
      /lib/modules from lib-modules (ro)
      /run/xtables.lock from xtables-lock (rw)
      /var/run/nri from nri-plugin (rw)

kindnet の Tolerations 行は典型的に次のような構成になります(実機出力の読み方を annotated 形式で説明)。

Tolerations:
  :NoSchedule op=Exists                                    # すべての NoSchedule を許容(CNI のため必須)
  :NoExecute op=Exists                                     # すべての NoExecute を許容
  node.kubernetes.io/disk-pressure:NoSchedule op=Exists    # ディスク逼迫 Node にも配置
  node.kubernetes.io/memory-pressure:NoSchedule op=Exists  # メモリ逼迫 Node にも配置
  node.kubernetes.io/not-ready:NoExecute op=Exists         # NotReady Node から退去しない
  node.kubernetes.io/pid-pressure:NoSchedule op=Exists     # PID 逼迫 Node にも配置
  node.kubernetes.io/unreachable:NoExecute op=Exists       # 到達不能 Node から退去しない
  node.kubernetes.io/unschedulable:NoSchedule op=Exists    # cordon 済み Node にも配置

各行は key:effect op=operator の形式で、Node に該当 taint があっても DaemonSet Pod を配置・継続実行することを意味します。CNI プラグインは「Node が壊れていても自分は動かないと他の Pod もネットワークに繋がらない」役割なので、ほぼすべての taint を許容する設定になっています。

本番の自前 DaemonSet ではここまで広範な toleration は不要で、必要な taint だけに絞ります。

Step 3: kind ノードの taint を確認

自前 DaemonSet を作る前に、kind シングルノードに設定されている taint を確認します。

実行コマンド:

$ kubectl describe node kind-control-plane | grep -A 3 Taints

実行結果(Taints 行に control-plane taint があるか・ないかで自前 DaemonSet の toleration 必要性が変わる):

Taints:             <none>
Unschedulable:      false
Lease:
  HolderIdentity:  kind-control-plane

kind v0.31.0 のシングルノード設定では、デフォルトで control-plane taint が設定されない場合と設定される場合があります。

Step 4 で作成する node-logger DaemonSet は安全側に倒して control-plane taint への toleration を入れておきます。taint がない環境でも余分な toleration は害になりません。

Step 4: 自前 DaemonSet YAML を作成

busybox を使って 5 秒ごとに hostname を echo するシンプルな DaemonSet を作成します。

実行コマンド:

$ vi ~/fanclub-manifests/node-logger-daemonset.yaml

ファイル内容:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: node-logger
  namespace: default
  labels:
    app: node-logger
spec:
  selector:
    matchLabels:
      app: node-logger
  template:
    metadata:
      labels:
        app: node-logger
    spec:
      tolerations:
        - key: "node-role.kubernetes.io/control-plane"
          operator: "Exists"
          effect: "NoSchedule"
      nodeSelector:
        kubernetes.io/os: linux
      containers:
        - name: logger
          image: busybox:1.36
          imagePullPolicy: IfNotPresent
          command:
            - sh
            - -c
            - 'while true; do echo "[$(date +%Y-%m-%dT%H:%M:%S%z)] node=$NODE_NAME pod=$(hostname)"; sleep 5; done'
          env:
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
          resources:
            requests:
              memory: "16Mi"
              cpu: "10m"
            limits:
              memory: "32Mi"
              cpu: "50m"

主要フィールドの設計意図:

  • apiVersion: apps/v1 + kind: DaemonSet:Deployment と同じ apiGroup
  • spec.replicas 不在:DaemonSet は Node 数 = Pod 数のため replicas を書かない
  • selector.matchLabels.app: node-loggertemplate.metadata.labels.app: node-logger:両者が一致する必要がある
  • tolerations:control-plane taint への toleration(kind シングルノード対応)
  • nodeSelector: kubernetes.io/os: linux:Linux Node 限定(kindnet と同じ条件)
  • resources.requests/limits:軽量なため最小限。本番でも DaemonSet は Node 全体に同居するため、リソース要求を最小化する設計が定石

Step 5: 自前 DaemonSet を apply

実行コマンド:

$ kubectl apply -f ~/fanclub-manifests/node-logger-daemonset.yaml
$ kubectl get daemonset node-logger -n default
$ kubectl get pods -l app=node-logger -n default

実行結果:

daemonset.apps/node-logger created

NAME          DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
node-logger   1         1         1       1            1           <none>          15s

NAME                READY   STATUS    RESTARTS   AGE   IP            NODE                 NOMINATED NODE   READINESS GATES
node-logger-v9hbf   1/1     Running   0          15s   10.244.0.38   kind-control-plane   <none>           <none>

kind シングルノードのため DESIRED=1 になります。本番マルチノード環境(Node 数 N 台)では DESIRED=N になります。

Step 6: デーモン動作を確認

kubectl logs で 5 秒ごとに ISO 8601 タイムスタンプ + ノード名 + Pod 名が出力されることを確認します。NODE_NAME 環境変数は Pod の spec.nodeName を Downward API(fieldRef)で注入したものです。

実行コマンド:

$ kubectl logs -l app=node-logger --tail=5 -n default

実行結果:

[2026-05-10T11:15:52+0000] node=kind-control-plane pod=node-logger-v9hbf
[2026-05-10T11:15:57+0000] node=kind-control-plane pod=node-logger-v9hbf
[2026-05-10T11:16:02+0000] node=kind-control-plane pod=node-logger-v9hbf
[2026-05-10T11:16:07+0000] node=kind-control-plane pod=node-logger-v9hbf
[2026-05-10T11:16:12+0000] node=kind-control-plane pod=node-logger-v9hbf

Pod が走り続けているため kubectl logs -f を使えばリアルタイムで追えます。停止には Ctrl+C で抜けます。

Step 7: 演習終了後の状態

node-logger DaemonSet はそのまま残しても問題ありません(軽量で実害なし)。リソースを節約したい場合は削除します。

実行コマンド(任意・削除する場合のみ):

$ kubectl delete -f ~/fanclub-manifests/node-logger-daemonset.yaml

本シリーズでは ep11 完了状態として node-logger を残置する想定です。ep12 で Deployment を扱う際の比較対象としても観察可能なため、削除せず残しておくのを推奨します。

CKAD 試験頻出パターン — kubectl dry-run と explain の活用

CKAD 試験は Performance-based Test(実機操作試験)で、制限時間内に複数のタスクを完遂する必要があります。Job / CronJob / DaemonSet を素早く作成するためのコマンド・テクニックを整理します。

スケルトン生成コマンド一覧

リソーススケルトン生成コマンド
Jobkubectl create job <name> --image=<image> --dry-run=client -o yaml
CronJobkubectl create cronjob <name> --image=<image> --schedule="*/1 * * * *" --dry-run=client -o yaml
DaemonSet該当コマンドなし(Deployment スケルトンから kind を変更)
Deployment(参考)kubectl create deployment <name> --image=<image> --dry-run=client -o yaml

DaemonSet を急いで作る場合の手順:

  1. kubectl create deployment node-logger --image=busybox:1.36 --dry-run=client -o yaml > ds.yaml
  2. ds.yaml を編集し、kind: Deploymentkind: DaemonSet に変更
  3. spec.replicas: 行を削除
  4. spec.strategy: セクションを削除(DaemonSet では updateStrategy という別フィールドのため)
  5. kubectl apply -f ds.yaml

kubectl explain でフィールドを確認

試験中に「completions と parallelism どっちが並列数だっけ?」と迷った場合は kubectl explain を使います。

$ kubectl explain job.spec.completions
$ kubectl explain job.spec.parallelism
$ kubectl explain cronjob.spec.concurrencyPolicy
$ kubectl explain daemonset.spec.updateStrategy

該当フィールドの 1 行説明と詳細が表示されます。CKAD 試験中は kubernetes.io 公式ドキュメントの参照も許可されていますが、kubectl explain の方がブラウザ切り替えが不要で速い場面が多いです。

alias k=kubectl と –dry-run の組み合わせ

CKAD 試験前提として alias k=kubectl を最初に設定するのが定番です。本シリーズの第6回でも触れたとおり、k 1 文字で kubectl を打てるとタイピング時間が大幅に削減されます。k create job ... --dry-run=client -o yaml はフルで打つよりタイピング量を大きく減らせます。

CKAD 頻出問題テンプレート 3 選

本回の 3 機構について、CKAD 試験で出題される典型問題のテンプレートを示します。

  1. 「失敗した場合のリトライを最大 2 回に制限する Job を作成」spec.backoffLimit: 2 + restartPolicy: Never
  2. 「JST 09:00 に実行する CronJob を作成」schedule: "0 9 * * *" + timeZone: "Asia/Tokyo"timeZone 漏れは即減点
  3. 「linux Node に 1 Pod ずつ配置する DaemonSet を作成」nodeSelector: kubernetes.io/os: linuxreplicas を書いたら不正解

これらのテンプレートを暗記しておけば、試験本番で要件文を読んだ瞬間に YAML 構造を組み立てられます。

現場ヒヤリハット — restartPolicy: Always で Job が起動しない / CronJob の timeZone 設定漏れで夜間バッチが昼間に動く

本セクションでは、Job / CronJob で現場頻発する 2 件のトラブルシュート事例を扱います。両者とも本番運用の初期段階で誰もが一度は踏む地雷で、対策を本番ガードレールとして組み込んでおく価値があります。

ヒヤリハット 1:Job の restartPolicy: Always でバリデーションエラー

シナリオ:チーム A の新人エンジニアが「fanclub-api の Deployment YAML をコピーして、kind を Job に変えて DB マイグレーション Job を作る」というアプローチで作業を始めた。

kind: Deploymentkind: Job に変更し、spec.replicas: 1spec.completions: 1 に変更したものの、spec.template.spec.restartPolicy: Always はそのまま残してしまった。kubectl apply -f wrong-job.yaml を実行したところエラーが返った。

実行コマンド(問題のある YAML を apply):

$ kubectl apply -f wrong-restartpolicy-job.yaml

実行結果(バリデーションエラー):

The Job "wrong-job" is invalid: spec.template.spec.restartPolicy: Unsupported value: "Always": supported values: "OnFailure", "Never"

根本原因:Job の Pod テンプレートでは restartPolicyAlways を指定できない仕様(API バリデーションでブロック)。Deployment / StatefulSet / DaemonSet ではデフォルト Always 必須なのに対し、Job / CronJob では Never または OnFailure のみ許可されるため、設定流用時にミスマッチが起きる。

解決策restartPolicy を以下のいずれかに変更する。

  • restartPolicy: Never:副作用ある DDL や外部 API 呼び出しを含むバッチ処理。失敗時は新しい Pod で最初からやり直したい
  • restartPolicy: OnFailure:軽量な計算処理。Pod 起動コストを節約したい

本番ガードレール

  • Job / CronJob 用の YAML は Deployment / StatefulSet / DaemonSet からコピー流用しない。kubectl create job --dry-run=client -o yaml でゼロから生成する習慣をつける
  • マニフェストの CI/CD パイプラインで kubevalkubeconform によるバリデーションを実行し、apply 前に restartPolicy 不正を検出する
  • チーム内のコードレビューで「Job/CronJob の YAML を見たら必ず restartPolicy を確認」をチェックリスト化する

このバリデーションは kubectl apply 時点で必ず弾かれるため本番影響はゼロですが、新人がデバッグに時間を取られる典型ケースです。エラーメッセージを正しく読めば supported values: "OnFailure", "Never" と書かれているため、メッセージを丁寧に読む習慣が解決の最短ルートです。

ヒヤリハット 2:CronJob の timeZone 設定漏れで夜間バッチが UTC 動作・JST 9 時間ズレ

シナリオ:チーム B はオンプレ環境から K8s に移行した際、cron デーモンで動かしていた「毎朝 9 時の日次レポート生成」を CronJob 化した。schedule: "0 9 * * *" と書いて apply し、初日は問題なく動作したように見えた。

しかし数日後、業務担当者から「朝 9 時にレポートが届かず、夕方 18 時に届いている」とエスカレが入った。CronJob の YAML を確認したところ、timeZone フィールドが指定されていなかった。

実行コマンド(問題のある CronJob を確認):

$ kubectl get cronjob daily-report -o yaml | grep -E "schedule|timeZone"

実行結果(timeZone がない):

  schedule: 0 9 * * *

CronJob の最終実行時刻を確認します。

実行コマンド:

$ kubectl get cronjob daily-report

実行結果(LAST SCHEDULE が JST 18:00 台になっている):

NAME           SCHEDULE    TIMEZONE   SUSPEND   ACTIVE   LAST SCHEDULE   AGE
daily-report   0 9 * * *   <none>     False     0        18:00:14        3d

根本原因timeZone フィールドを省略した CronJob は UTC 基準で動作する。schedule: "0 9 * * *" は UTC 09:00 を指し、JST(UTC+9)では 18:00 に実行される。

Kubernetes v1.27 で timeZone が GA となるまでは「ノードのローカルタイムゾーンに依存」というあいまいな挙動だったが、現在は明確に UTC 基準に統一されている。

解決策timeZone を明示して再 apply する。

spec:
  schedule: "0 9 * * *"
  timeZone: "Asia/Tokyo"

再 apply 後は LAST SCHEDULE が JST 09:00 台で更新されるようになります。kubectl get cronjob の TIMEZONE 列が Asia/Tokyo と表示されることも確認します。

本番ガードレール

  • すべての CronJob で timeZone を必須項目とするチームコーディング規約を設ける。「timeZone がない CronJob は CI でレビュー差し戻し」をルール化する
  • OPA Gatekeeper や Kyverno の policy で「CronJob は timeZone を持たなければならない」を enforce する(第3巻 CKS で扱う)
  • テスト環境では 1 日待たずに動作確認するため、運用本番投入前に schedule: "*/1 * * * *" でローカル時刻基準を確認するスモークテストを実施する
  • 業務時間ベースでスケジュールするバッチは、移行プロジェクトの初期から「ローカルタイムゾーンか UTC か」を要件として明文化する。曖昧なまま実装すると本事故が起きる

本シリーズの CronJob はすべて timeZone: "Asia/Tokyo" を明示する方針です。教育上「TZ は必ず書く」を体に刷り込む狙いです。

実機発見・補足:timeZone はスケジュール解釈にのみ使われる

timeZone: "Asia/Tokyo" を設定した CronJob でも、コンテナ内のシェルコマンド(date 等)が出力するタイムスタンプはコンテナ OS のタイムゾーン(デフォルト UTC)で表示されます。実機で CronJob のログを確認すると、スケジュールは JST で動作していても、ログの時刻表記は +0000(UTC)になります。

$ kubectl logs job/fanclub-member-count-29640195
[2026-05-10T11:15:00+0000] member count: 2

タイムスタンプの +0000 はコンテナ内 OS の TZ(UTC)を反映したものです。timeZone: "Asia/Tokyo" が制御するのは「スケジューラが Job を起動するタイミング」のみです。コンテナ内のログに JST タイムスタンプを出したい場合は、コンテナイメージ側で TZ=Asia/Tokyo 環境変数を設定するか、tzdata パッケージを含むベースイメージを使う必要があります。

本番でログ収集基盤がタイムスタンプを UTC で統一している場合は問題になりませんが、ログを目視で確認する際に「スケジュール時刻と 9 時間ズレている」と混乱しないよう注意します。

第11回完了後の模擬アプリ状態と第3部まとめ・第12回への橋渡し

本回の演習 3 本がすべて完了したあとの fanclub-api 構成を整理し、第3部(ep7〜ep11)の総括と第12回への接続を確認します。

第11回完了後のクラスタ状態

リソース状態本回の変更
fanclub-backend PodRunning(envFrom 版・SA 設定済)変更なし(ep10 から継続)
fanclub-backend Service (ClusterIP)残存変更なし
fanclub-db StatefulSet(fanclub-db-0)Running変更なし
fanclub-db Service / fanclub-db-headless Service残存変更なし
postgres-data-fanclub-db-0 PVCBound変更なし
members テーブル2 行残存 + score カラム追加本回マイグレーション Job で score カラム追加
fanclub-config ConfigMap残存変更なし(ep10 から継続)
fanclub-secret Secret残存変更なし(Job からも参照)
fanclub-backend-sa ServiceAccount残存変更なし(ep10 から継続)
Job fanclub-db-migrateSucceeded(ttl 600 で自動削除予定)本回新規
Job fanclub-db-exportSucceeded(ttl 600 で自動削除予定)本回新規
CronJob fanclub-member-countsuspend: true(停止中)本回新規
DaemonSet node-logger1 Pod Running本回新規

fanclub-api の構成は次のように進化しました。

第10回完了時:
  [Backend Pod (envFrom + SA)] → [Backend Service]
       ↑ envFrom        ↑ SA
  [ConfigMap]        [SA: fanclub-backend-sa]
  [Secret]
                ↓ envFrom 経由で DB 接続情報
  [DB StatefulSet] ← [DB Service / Headless Service] ← [PVC]

第11回完了時:
  [Backend Pod (envFrom + SA)] → [Backend Service]
       ↑ envFrom        ↑ SA
  [ConfigMap]        [SA: fanclub-backend-sa]
  [Secret] ──┐
             │ secretKeyRef で参照
             ▼
  [Job: fanclub-db-migrate (DDL)]   [Job: fanclub-db-export (CSV)]
  [CronJob: fanclub-member-count (suspend)]
                ↓ DDL を発行
  [DB StatefulSet (members + score 列)] ← [DB Service / Headless Service] ← [PVC]

  [DaemonSet: node-logger] ← Node 上で 5 秒ごとに hostname echo

常駐サービス(Backend / DB)に加えて、バッチ処理(Job)・定期実行(CronJob)・全 Node デーモン(DaemonSet)の機構が揃いました。本番運用に必要なワークロードリソースの主要 5 種(Pod・StatefulSet・Job・CronJob・DaemonSet)を実機で扱った状態です。

第3部「アプリリソース」(ep7〜ep11)の総括

本回で第3部(全 5 回)が完結しました。各回の到達物を一覧で整理します。

第7回: Pod(基本ユニット)+ マルチコンテナパターン(Init / Sidecar / Ephemeral)
        → fanclub-backend Pod を起動

第8回: Service(Pod の安定的な接続口)+ CoreDNS
        → fanclub-backend Service で外部からアクセス可能に

第9回: PVC + StatefulSet(永続化 + 安定 ID)
        → fanclub-db (PostgreSQL) を追加して 3 層構成完成

第10回: ConfigMap + Secret + ServiceAccount(設定外部化 + 認証アイデンティティ)
        → DB 接続情報を ConfigMap / Secret に分離し envFrom で注入

第11回: Job + CronJob + DaemonSet(バッチ・定期実行・デーモン常駐)
        → DB マイグレーション Job + 定期 SELECT CronJob + 全 Node デーモン

CKAD ドメイン D1(Application Design and Build・出題比率 20 %)の中核 Competency「適切なワークロードリソースの選択と使用」は、ep7(Pod / マルチコンテナ)+ ep9(StatefulSet)+ ep11(Job / CronJob / DaemonSet)の 3 回で完全網羅しました。

Deployment は第4部 ep12 で扱い、これで全ワークロードリソースが揃います。

第12回への橋渡し

本回でバッチ系・デーモン系のワークロードが完成しました。次回からは第4部「ワークロード戦略」に入り、まずアプリ本体の常駐サービスを Deployment 化して 3 種の Probe を設計します。

  • Deployment 化:fanclub-backend Pod を Deployment に移行する。replicas: でレプリカ数管理・strategy: でローリングアップデートを設計する
  • 3 種の Probe:startupProbe(起動完了判定)・livenessProbe(生存判定)・readinessProbe(疎通判定)の設計と実装。Payara Micro の起動時間(約 6 秒)に合わせた閾値設定が核心
  • Probe デバッグ実践:CrashLoopBackOff の原因調査・kubectl describe pod での Events 確認・kubectl logs --previous での前世代 Pod ログ取得

第12回は CKAD ドメイン D2(Application Deployment・出題比率 20 %)+ D3(Application Observability and Maintenance・出題比率 15 %)の中核回です。これで CKAD の主要ドメインが ep11 までで D1(完了)・ep12 で D2/D3 着手と、計画的に網羅していきます。

理解度チェック・第11回まとめ・次回予告・シリーズ一覧

理解度チェック(○×形式・8 問)

問 1completions: 3parallelism: 1 の Job は、3 つの Pod を同時に並列起動する。

問 2:Job の Pod テンプレートに restartPolicy: Always を設定すると、kubectl apply 時にバリデーションエラーが返る。

問 3ttlSecondsAfterFinished: 600 を設定した Job は、完了から 10 分後に Job リソースと完了済み Pod が自動削除される。

問 4:CronJob の schedule: "0 9 * * *"timeZone を省略すると UTC 基準で動作するため、JST では 18 時に実行される。

問 5concurrencyPolicy: Forbid の CronJob は、前のスケジュールの Job がまだ実行中の場合、新しい Job を起動せずスキップする。

問 6:DaemonSet は spec.replicas フィールドで Pod の数を指定する。

問 7:DaemonSet は主にステートレス Web サービスのスケーリングに使用するワークロードリソースである。

問 8:CronJob の suspend: true を設定すると、現在実行中の Job が即座に強制終了される。

解答

解答解説
問 1×parallelism: 1 なので最大同時実行 Pod 数は 1。completions: 3 は 3 Pod が成功するまで継続するという意味で、同時並列ではなく順次実行になる。並列実行したい場合は parallelism も増やす
問 2Job の Pod テンプレートで restartPolicy に許される値は Never または OnFailure のみ。Always を指定すると The Job is invalid: spec.template.spec.restartPolicy: Unsupported value: "Always": supported values: "OnFailure", "Never" エラーが返る
問 3ttlSecondsAfterFinished は K8s v1.23 で GA。完了(Succeeded / Failed)から指定秒数経過後に Job + 関連 Pod を自動削除する。本番では Job の蓄積を防ぐため必須設定
問 4timeZone 省略時は UTC 基準。0 9 * * * は UTC 09:00 を指し、JST(UTC+9)では 18:00 に実行される。timeZone: "Asia/Tokyo" を明示することで JST 09:00 動作になる
問 5Forbid は「前 Job が実行中なら新規 Job を起動しない(スキップ)」。Allow は並行実行・Replace は前 Job を削除して新規起動。3 値の使い分けを押さえる
問 6×DaemonSet は replicas フィールドを持たない。Node 数 = Pod 数になる仕様で、Node が増減すれば Pod も自動で増減する。replicas: 3 と書くアンチパターンは Deployment との違いを見落とすミスの典型
問 7×DaemonSet は「全 Node に常駐するインフラ処理」用。ステートレス Web サービスのスケーリングは Deployment が適切。DaemonSet を Web サービスに使うと「Node 1 台 = Pod 1 台」固定になり、トラフィックに応じた水平スケーリングができない
問 8×suspend: true新規 Job 生成のみを停止する。すでに起動済みの Job Pod は完了まで自然に動作する。「即座に停止」と誤解して suspend を頼るとバッチが完了するまで止まらず、事故になる。実行中 Job を強制終了したい場合は kubectl delete job を使う

第11回まとめ

第11回では以下を実施しました。

  • ワークロードリソース 6 種(Pod / Deployment / StatefulSet / Job / CronJob / DaemonSet)の使い分けを比較表と判断フローで整理した。CKAD 試験頻出の選択問題に対応できる判断基準を 5 行のフローとして定式化し、本回扱う Job / CronJob / DaemonSet の 3 機構の位置付けを明確化した
  • Job の主要フィールド(completions / parallelism / backoffLimit / ttlSecondsAfterFinished)を網羅した。restartPolicyNever または OnFailure のみで、Always はバリデーションエラーになる仕様を確認した。fanclub-api の DB マイグレーション Job + CSV エクスポート Job を fanclub-secret から secretKeyRefPGPASSWORD を注入する設計で実装し、kubectl logs で結果を確認した。失敗 Job のデモで backoffLimit 超過時に Job が Failed 状態になり Pod が複数生成されることも実機で確認した
  • CronJob の cron 式 5 フィールド(分・時・日・月・曜日)と主要パターンを整理した。timeZone は K8s v1.27 GA で省略時 UTC 基準動作。concurrencyPolicy の Allow / Forbid / Replace と suspend による一時停止、successfulJobsHistoryLimit / failedJobsHistoryLimit による履歴管理、startingDeadlineSeconds による遅延ガードまで網羅した。1 分間隔の CronJob を作成して 3 分待機後の Job 履歴を観察し、suspend → Replace への切り替えと再開を実機で行った
  • DaemonSet の「全 Node に 1 Pod」原則と主な用途 5 選(ログ収集・ノード監視・CNI・kube-proxy・ストレージ)を整理した。Deployment との違い(replicas の有無 / Node 追加時の自動配置)を比較表で明確化し、nodeSelector / tolerations / updateStrategy を解説した。kube-system の kindnet と kube-proxy の Tolerations を annotated 形式で読み下し、自前 DaemonSet(node-logger)を default ns に apply して 1 Pod が配置されることを確認した
  • 現場ヒヤリハットを 2 件扱った。Job の restartPolicy: Always によるバリデーションエラー(Deployment YAML 流用ミス)と、CronJob の timeZone 設定漏れによる UTC 動作・JST 9 時間ズレ(夜間バッチが昼間に動く事故)を、根本原因と本番ガードレールまで整理した。本シリーズの CronJob はすべて timeZone: "Asia/Tokyo" を必須とする方針を確立した

次回予告

第12回 Deployment + 3 Probe + Rolling Update + Probe デバッグ実践では、fanclub-backend Pod を Deployment に移行し、startupProbe / livenessProbe / readinessProbe の 3 種を設計・実装します。

Payara Micro の起動時間(約 6 秒)に合わせた startupProbe の failureThreshold / periodSeconds の設計、CrashLoopBackOff の原因調査、kubectl logs --previous による前世代 Pod ログ取得など、現場で必須のデバッグ手順を一通り扱います。CKAD ドメイン D2(Application Deployment)+ D3(Application Observability and Maintenance)の中核回です。

シリーズ一覧

第1部:コンテナと Docker

第2部:Kubernetes 基礎

第3部:アプリリソース

第4部:ワークロード戦略

第5部:セキュリティ基礎

第6部:パッケージ管理 + HTTPS 公開

広告
kubernetes
スポンサーリンク