- 第11回スコープ・学習目標・今ここマップ
- ワークロードリソースの使い分け — Job / CronJob / DaemonSet はどこで使うか
- Job 基礎 — completions・parallelism・backoffLimit・ttlSecondsAfterFinished
- restartPolicy の使い分け — Never と OnFailure は何が違うか
- やってみよう①:DB マイグレーション Job と CSV エクスポート Job を実行する
- CronJob 基礎 — schedule・timeZone・concurrencyPolicy・suspend
- やってみよう②:CronJob(1 分間隔)を作成して suspend と concurrencyPolicy を実機確認する
- DaemonSet 基礎 — 全 Node に 1 Pod を配置するデーモン型ワークロード
- やってみよう③:kindnet と kube-proxy を観察して自前 DaemonSet を apply する
- CKAD 試験頻出パターン — kubectl dry-run と explain の活用
- 現場ヒヤリハット — restartPolicy: Always で Job が起動しない / CronJob の timeZone 設定漏れで夜間バッチが昼間に動く
- 第11回完了後の模擬アプリ状態と第3部まとめ・第12回への橋渡し
- 理解度チェック・第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 Pod | default ns で 1/1 Running(envFrom 注入版・automountServiceAccountToken: false) | Lead 実機観察 |
| fanclub-backend Service | ClusterIP 10.96.150.60:80 稼働中 | Lead 実機観察 |
| fanclub-db StatefulSet | fanclub-db-0 Pod Running(PostgreSQL 18.3) | Lead 実機観察 |
| fanclub-db Service / fanclub-db-headless Service | ClusterIP 10.96.131.167:5432 / clusterIP: None | Lead 実機観察 |
| postgres-data-fanclub-db-0 PVC | Bound(1Gi・standard SC) | Lead 実機観察 |
| ConfigMap | fanclub-config(4 キー: DB_HOST / DB_PORT / DB_NAME / JAVA_OPTS)+ fanclub-db-init + kube-root-ca.crt | SP_vol1-pre-19 |
| Secret | fanclub-secret(Opaque・2 キー: DB_USER / DB_PASSWORD) | SP_vol1-pre-19 |
| ServiceAccount | default(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: NeverとOnFailureの違いを実機で確認できる - CronJob の cron 式(5 フィールド)・
timeZone・concurrencyPolicy(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-secret の DB_PASSWORD を secretKeyRef で 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 × parallelism | DB マイグレーション・バッチ集計・1 回限り処理 |
| CronJob | cron スケジュール | Job が完了 | Never / OnFailure のみ | Job として管理 | 日次レポート・定期クリーンアップ・バックアップ |
| DaemonSet | Node 追加時に自動配置 | 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 つ挙げます。
- 「失敗しても最大 N 回までリトライする 1 回限りの処理」→ Job +
backoffLimit: N。N を指定しないとデフォルト 6 回まで再試行される - 「毎日 / 毎時 / 1 分ごとなど cron 式で定期実行」→ CronJob +
schedule+timeZone。timeZone省略は UTC 動作のためヒヤリハット頻発(後述 H2-11) - 「Linux Node にだけ 1 Pod ずつデーモンを配置」→ DaemonSet +
nodeSelector: kubernetes.io/os: linux。replicasを書くのはアンチパターン
上記 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 の状態 | 挙動 |
|---|---|---|
| Running | Active | Pod が実行中 |
| Succeeded(exit 0) | Complete | Pod が正常終了。completions に達すれば Job 完了 |
| Failed(exit 非 0) | Active(リトライ中) | Pod が異常終了。backoffLimit 未満なら再試行 |
| Failed(リトライ尽きた) | Failed | backoffLimit 超過で Job 全体が失敗 |
Job が完了しても Pod は自動削除されません(デフォルト動作)。これは kubectl logs で実行結果を確認できるようにするための設計です。完了済み Pod を蓄積させたくない場合は ttlSecondsAfterFinished を設定して時間経過で自動削除するか、kubectl delete job で手動削除します。
主要フィールド一覧
Job の spec 配下で押さえておくべきフィールドを整理します。
| フィールド | デフォルト | 意味 |
|---|---|---|
completions | 1 | 正常完了させる Pod の総数。3 なら 3 Pod が成功するまで Job 継続 |
parallelism | 1 | 同時実行する Pod の最大数。3 なら最大 3 Pod が並列実行 |
backoffLimit | 6 | Pod 失敗時の再試行上限。超えると Job が Failed 状態になる |
activeDeadlineSeconds | 無制限 | Job 全体のタイムアウト秒数。超過すると Pod 強制終了 + Failed |
ttlSecondsAfterFinished | 無制限 | 完了後に Job + Pod を自動削除するまでの秒数(K8s v1.23 GA) |
template.spec.restartPolicy | — | Never または OnFailure のみ。Always は不可 |
本番運用では backoffLimit をデフォルトの 6 に任せるのは推奨されません。失敗 Pod が最大 7 個(初回 + 6 回リトライ)残るため、Pod の異常終了を引き起こす設定ミスがあるとクラスタ内に Failed Pod が大量に蓄積します。
ワークロードの性質に合わせて backoffLimit: 2 〜 backoffLimit: 3 程度に明示する設計が一般的です。同様に ttlSecondsAfterFinished もデフォルトでは Job リソースが永続的に残るため、本番では必ず明示する運用ルールを設けます。
completions と parallelism の組み合わせパターン
completions と parallelism の組み合わせで 3 つの実行パターンが表現できます。
| パターン | completions | parallelism | 挙動 |
|---|---|---|---|
| ① 単発 Job | 1 | 1 | 1 Pod を起動して成功すれば完了。本回の演習①で採用 |
| ② 順次実行 | N | 1 | N 個の Pod を順番に 1 個ずつ実行 |
| ③ 並列実行 | N | M (≤ 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 でファイル出力し、command や env を追記して完成させる流れが定石です。試験では時間制限がきついため、可能な限りコマンド生成 → 編集の流れに統一します。
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 数の変化 | 使い所 |
|---|---|---|---|---|
Always | Deployment / StatefulSet / DaemonSet(必須) | 同じ Pod のコンテナを無制限に再起動 | 変わらない | 常時稼働サービス |
OnFailure | Job / CronJob のみ | 同じ Pod のコンテナを再起動(backoffLimit まで) | 変わらない | 軽量タスク・再起動コスト小 |
Never | Job / 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 分です。
演習の全体フローは以下のとおりです。
- 前提状態の確認(
kubectl get pods,svc,configmap,secret) fanclub-db-migrate-job.yaml作成(ALTER TABLE でscoreカラム追加)kubectl applyでマイグレーション Job を実行kubectl get jobsで COMPLETIONS 1/1 を確認kubectl logs job/fanclub-db-migrateでマイグレーション結果を確認fanclub-db-export-job.yaml作成(COPY で members テーブルを CSV 出力)kubectl applyで CSV エクスポート Job を実行kubectl logs job/fanclub-db-exportで CSV データを確認failing-job-demo-job.yaml作成(backoffLimit: 2+exit 1)kubectl applyで失敗 Job を実行kubectl get jobs+kubectl get podsでFailed状態と Pod 3 個生成を確認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-config・fanclub-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_PASSWORD を secretKeyRef で PGPASSWORD 環境変数として注入し、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 が成功すれば完了する単発 JobbackoffLimit: 3:失敗時のリトライ上限。デフォルト 6 を本番想定で絞っているttlSecondsAfterFinished: 600:完了から 10 分後に Job + Pod を自動削除restartPolicy: Never:副作用ある DDL なので、失敗時は新しい Pod で最初からやり直すimage: postgres:18:ep9 で kind ノード内に既に存在する PostgreSQL 公式イメージ。psql クライアントが含まれるcommand:psql -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-secretのDB_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 の主要フィールド一覧
| フィールド | デフォルト | 意味 |
|---|---|---|
schedule | — | 5 フィールド cron 式(必須) |
timeZone | UTC | スケジュール基準のタイムゾーン(K8s v1.27 GA)。IANA 形式(例: Asia/Tokyo) |
concurrencyPolicy | Allow | 前 Job 実行中の挙動。Allow / Forbid / Replace の 3 値 |
suspend | false | true で新規 Job 作成を停止(実行中 Job は影響なし) |
successfulJobsHistoryLimit | 3 | 成功 Job の保持件数 |
failedJobsHistoryLimit | 1 | 失敗 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 が生成するスケルトンの restartPolicy は OnFailure になります。本シリーズの方針に合わせて Never に変更し、timeZone や concurrencyPolicy を追記する流れになります。

やってみよう②:CronJob(1 分間隔)を作成して suspend と concurrencyPolicy を実機確認する
1 分間隔で members テーブルの行数を出力する CronJob を作成し、Job 履歴の積み上がり・suspend による停止・concurrencyPolicy の Replace 動作を実機で確認します。所要時間目安は約 20 分です。
演習の全体フローは以下のとおりです。
fanclub-member-count-cronjob.yaml作成(timeZone: Asia/Tokyo・concurrencyPolicy: Allow)kubectl applyで CronJob を作成kubectl get cronjobで SCHEDULE / SUSPEND / ACTIVE / LAST SCHEDULE 列を確認- 3 分待機 →
kubectl get jobsで 3 件の Job が生成されたことを確認 kubectl logsで行数出力を確認successfulJobsHistoryLimit: 3による古い Job の自動削除を確認kubectl patchでsuspend: trueに切り替えkubectl get cronjobで SUSPEND 列がTrueになり、Job が新規作成されないことを確認concurrencyPolicyをReplaceに変更suspend: falseに戻して動作観察- 演習終了後、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 patch で spec.suspend を true に切り替えます。
実行コマンド:
$ 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 の効果を視覚化したい場合は command を sh -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 の違いを整理します。
| 項目 | Deployment | DaemonSet |
|---|---|---|
| Pod 数の管理 | replicas: N で N 個に保つ | Node 数 = Pod 数(自動) |
| Pod の配置先 | scheduler が判断(複数 Pod が同 Node に集中することもある) | 各 Node に必ず 1 Pod |
| Node 追加時 | Pod は既存 Node で再配分されない | 新 Node に自動で Pod が追加される |
| Node 削除時 | 該当 Pod は他 Node で再起動 | 該当 Pod も自動削除(再配置なし) |
| 用途 | ステートレス Web サービス | インフラ常駐処理 |
| restartPolicy | Always のみ | 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 分です。
演習の全体フローは以下のとおりです。
- kube-system namespace の DaemonSet 一覧を観察
- kindnet DaemonSet の詳細を
kubectl describeで観察(nodeSelector / tolerations / DESIRED / READY) kubectl describe nodeで kind ノードの taint を確認node-logger-daemonset.yamlを作成(busybox + 5 秒ごとに hostname を echo)kubectl applyで自前 DaemonSet を起動kubectl get daemonset / kubectl get podsで 1 Pod 配置を確認kubectl logsでデーモン動作を確認- 演習終了後の状態確認(任意削除可)
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 の nodeSelector と tolerations を観察します。
実行コマンド:
$ 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 と同じ apiGroupspec.replicas不在:DaemonSet は Node 数 = Pod 数のためreplicasを書かないselector.matchLabels.app: node-loggerとtemplate.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 を素早く作成するためのコマンド・テクニックを整理します。
スケルトン生成コマンド一覧
| リソース | スケルトン生成コマンド |
|---|---|
| Job | kubectl create job <name> --image=<image> --dry-run=client -o yaml |
| CronJob | kubectl 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 を急いで作る場合の手順:
kubectl create deployment node-logger --image=busybox:1.36 --dry-run=client -o yaml > ds.yamlds.yamlを編集し、kind: Deploymentをkind: DaemonSetに変更spec.replicas:行を削除spec.strategy:セクションを削除(DaemonSet ではupdateStrategyという別フィールドのため)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 試験で出題される典型問題のテンプレートを示します。
- 「失敗した場合のリトライを最大 2 回に制限する Job を作成」→
spec.backoffLimit: 2+restartPolicy: Never - 「JST 09:00 に実行する CronJob を作成」→
schedule: "0 9 * * *"+timeZone: "Asia/Tokyo"。timeZone漏れは即減点 - 「linux Node に 1 Pod ずつ配置する DaemonSet を作成」→
nodeSelector: kubernetes.io/os: linux。replicasを書いたら不正解
これらのテンプレートを暗記しておけば、試験本番で要件文を読んだ瞬間に YAML 構造を組み立てられます。
現場ヒヤリハット — restartPolicy: Always で Job が起動しない / CronJob の timeZone 設定漏れで夜間バッチが昼間に動く
本セクションでは、Job / CronJob で現場頻発する 2 件のトラブルシュート事例を扱います。両者とも本番運用の初期段階で誰もが一度は踏む地雷で、対策を本番ガードレールとして組み込んでおく価値があります。
ヒヤリハット 1:Job の restartPolicy: Always でバリデーションエラー
シナリオ:チーム A の新人エンジニアが「fanclub-api の Deployment YAML をコピーして、kind を Job に変えて DB マイグレーション Job を作る」というアプローチで作業を始めた。
kind: Deployment を kind: Job に変更し、spec.replicas: 1 を spec.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 テンプレートでは restartPolicy に Always を指定できない仕様(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 パイプラインで
kubevalやkubeconformによるバリデーションを実行し、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 Pod | Running(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 PVC | Bound | 変更なし |
| members テーブル | 2 行残存 + score カラム追加 | 本回マイグレーション Job で score カラム追加 |
| fanclub-config ConfigMap | 残存 | 変更なし(ep10 から継続) |
| fanclub-secret Secret | 残存 | 変更なし(Job からも参照) |
| fanclub-backend-sa ServiceAccount | 残存 | 変更なし(ep10 から継続) |
Job fanclub-db-migrate | Succeeded(ttl 600 で自動削除予定) | 本回新規 |
Job fanclub-db-export | Succeeded(ttl 600 で自動削除予定) | 本回新規 |
CronJob fanclub-member-count | suspend: true(停止中) | 本回新規 |
DaemonSet node-logger | 1 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 問)
問 1:completions: 3、parallelism: 1 の Job は、3 つの Pod を同時に並列起動する。
問 2:Job の Pod テンプレートに restartPolicy: Always を設定すると、kubectl apply 時にバリデーションエラーが返る。
問 3:ttlSecondsAfterFinished: 600 を設定した Job は、完了から 10 分後に Job リソースと完了済み Pod が自動削除される。
問 4:CronJob の schedule: "0 9 * * *" は timeZone を省略すると UTC 基準で動作するため、JST では 18 時に実行される。
問 5:concurrencyPolicy: 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 も増やす |
| 問 2 | ○ | Job の Pod テンプレートで restartPolicy に許される値は Never または OnFailure のみ。Always を指定すると The Job is invalid: spec.template.spec.restartPolicy: Unsupported value: "Always": supported values: "OnFailure", "Never" エラーが返る |
| 問 3 | ○ | ttlSecondsAfterFinished は K8s v1.23 で GA。完了(Succeeded / Failed)から指定秒数経過後に Job + 関連 Pod を自動削除する。本番では Job の蓄積を防ぐため必須設定 |
| 問 4 | ○ | timeZone 省略時は UTC 基準。0 9 * * * は UTC 09:00 を指し、JST(UTC+9)では 18:00 に実行される。timeZone: "Asia/Tokyo" を明示することで JST 09:00 動作になる |
| 問 5 | ○ | Forbid は「前 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)を網羅した。restartPolicyはNeverまたはOnFailureのみで、Alwaysはバリデーションエラーになる仕様を確認した。fanclub-api の DB マイグレーション Job + CSV エクスポート Job をfanclub-secretからsecretKeyRefでPGPASSWORDを注入する設計で実装し、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
- 第1回 コンテナ技術概念 + Docker 環境準備
- 第2回 Docker 基本操作
- 第3回 Dockerfile + マルチステージビルド + JDK 25 / Payara Micro イメージビルド
- 第4回 コンテナレジストリ + イメージタグ戦略 + Trivy スキャン
第2部:Kubernetes 基礎
第3部:アプリリソース
- 第7回 Pod + Multi-container パターン
- 第8回 Service とネットワーキング
- 第9回 ストレージ(PVC + StatefulSet)+ PostgreSQL DB 追加
- 第10回 ConfigMap + Secret + ServiceAccount 基礎
- 第11回 Job + CronJob + DaemonSet ← 今ここ
第4部:ワークロード戦略
- 第12回 Deployment + 3 Probe + Rolling Update + Probe デバッグ実践
- 第13回 Deployment 戦略補完(Blue/Green + Canary + Recreate)
- 第14回 ResourceQuota + LimitRange + Multi-tenant Namespace
