Certified Kubernetes Application Developer (CKAD) #4 コンテナイメージ: Dockerfile、マルチステージ、試験で直接ビルド

#3 Multi-container パターン で 1 つの Pod の中に複数のコンテナを配置する設計を扱ったとすれば、この記事はそのコンテナの材料である イメージを自分で作る タスクへ下りていきます。CKAD はマニフェストだけを書く試験ではありません。一部のタスクは、与えられたソースから イメージを自分でビルドしてレジストリにプッシュした後、そのイメージを参照する Pod を起動するまでの流れを一度に要求します。

そこでこの記事は、Dockerfile の基本命令からマルチステージビルド、そして試験環境でよく使われる podmanbuildah のビルド手順と imagePullPolicy の落とし穴まで、イメージを作って参照する一連のサイクル を実技の観点で整理します。Docker に慣れているならほとんどそのまま通用しますが、試験環境の違いを押さえておくのが要点です。

なぜ CKAD でイメージを自分でビルドするのか #

CKAD の最初のドメインは Application Design and Build です。名前のとおりアプリケーションをビルドする能力を含むため、試験には「与えられたディレクトリの Dockerfile でイメージをビルドしてタグ myapp:1.0 を付け、ローカルレジストリにプッシュした後、そのイメージで Pod を実行せよ」のようなタスクが出ることがあります。

このタスクが厄介な理由は、1 つのタスクの中に ビルドツール・タグ付け・プッシュ・マニフェスト がすべて混ざっていて、どこか一段でも食い違えば Pod が ErrImagePull または ImagePullBackOff に落ちるためです。そのため、ビルドが一度成功したかどうかだけでなく、イメージがクラスタで正常に引かれるかまで確認する習慣が必要です。Docker 入門シリーズ を見ているなら Dockerfile 自体は慣れているはずですが、ここでは試験で得点に直結する部分だけ圧縮して扱います。

Dockerfile の基本命令 #

イメージは Dockerfile というテキストファイルの命令を上から下へ実行して作られます。各命令は 1 つの レイヤー を作り、これらのレイヤーが積み重なって最終イメージになります。

FROM node:20-alpine        # ベースイメージ (常に最初の命令)
WORKDIR /app               # 以降の命令の基準パス
COPY package*.json ./      # 依存ファイルだけ先にコピー (キャッシュ最適化)
RUN npm ci --omit=dev      # ビルド時点のシェル命令を実行
COPY . .                   # 残りのソースをコピー
EXPOSE 3000                # 公開ポートの文書化 (実際に開放するわけではない)
CMD ["node", "server.js"]  # 起動時のデフォルト命令 (上書き可能)

RUN はビルド時点でシェル命令を実行し、CMDENTRYPOINT はコンテナが起動するときに何を実行するかを決めます。この 2 つの区別が要点です。

CMD と ENTRYPOINT の違い #

この 2 つの違いは、試験で command・args のマッピングを問う問題につながるため、正確に把握しておく必要があります。

  • ENTRYPOINT はコンテナを 何で実行するか を固定します。docker run または Pod の args がこの後ろに引数として付きます。
  • CMDデフォルト引数またはデフォルト命令 です。実行時に引数を与えると丸ごと置き換わります。

推奨パターンは、ENTRYPOINT で実行ファイルを固定し CMD でデフォルト引数を与える組み合わせです。

ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]

こうしておくとデフォルトの実行は python app.py --port 8080 で、実行時に --port 9090 を渡すと python app.py --port 9090 になります。

レイヤーキャッシュ: 命令の順序がビルド速度を分ける #

ビルドは変更されていないレイヤーをキャッシュから再利用します。あるレイヤーが変わると その下のすべてのレイヤーは再ビルド されます。そのため上の例のように、頻繁に変わるソース (COPY . .) よりほとんど変わらない依存ファイル (COPY package*.json) を先にコピーしてインストールしておくと、ソースだけ直したときに依存インストールのレイヤーを再利用してビルドが速くなります。

試験でビルド時間が長くなればその分だけ時間を失うため、キャッシュがよく効く順序を知っておくのが実戦で有利です。

マルチステージビルドでイメージを軽量化 #

ビルドに必要なツール (コンパイラ、パッケージマネージャ) は実行時点では不要です。それでも 1 段だけでビルドすると、これらのツールが最終イメージにそのまま残ってイメージが重くなります。マルチステージビルド はビルド段と実行段を分離し、実行段には成果物だけをコピーします。

Go の例: ビルドステージ + distroless ランタイム #

# build stage: Go コンパイラでバイナリを生成
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server

# runtime stage: シェルすらない最小イメージ
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

要点は FROM ... AS builder で段に名前を付け、最後の段で COPY --from=builder によって 前段の成果物だけ を取り込む部分です。Go コンパイラが入っていた最初の段は最終イメージに含まれないため、数百 MB のイメージが数十 MB に減ります。Node なら、ビルド段で npm run build でバンドルを作った後、ランタイム段の node:20-alpinedistnode_modules だけをコピーする形で同じパターンを適用します。ビルドツールとソースが最終イメージに残らないため、イメージサイズだけでなくセキュリティ (攻撃表面) でも有利です。

ビルド・タグ・プッシュ #

ここからは作った Dockerfile でイメージをビルドしてレジストリに上げる手順です。試験環境によってはビルドツールが docker ではなく podman または buildah の場合があります。 どのコマンドでも同じタスクができる必要があるため、両方を整理します。

podman でビルド・タグ・プッシュ #

podman は docker とのコマンド互換性が高く、ほぼそのまま移して使えます。ビルド → タグ → プッシュの順序は次のとおりです。

podman build -t myapp:1.0 .                                       # ビルド
podman tag myapp:1.0 registry.example.com/team/myapp:1.0          # レジストリパスのタグ
podman push registry.example.com/team/myapp:1.0                   # プッシュ
podman images                                                     # ローカルイメージの確認

buildah・docker の併記 #

buildah はイメージビルドに特化したツールで、Dockerfile のビルドは buildah bud で行います。docker がインストールされた環境なら docker 命令もそのまま通用します。

# buildah (bud = build-using-dockerfile)
buildah bud -t myapp:1.0 .
buildah push myapp:1.0 docker://registry.example.com/team/myapp:1.0

# docker
docker build -t myapp:1.0 .
docker tag myapp:1.0 registry.example.com/team/myapp:1.0
docker push registry.example.com/team/myapp:1.0

3 つのツールすべてで build -t <名前:タグ> .push の流れは同じです。試験では問題文が指定したツールとタグを正確に守ることが採点の前提です。

イメージ参照と imagePullPolicy #

ビルドしたイメージを Pod で参照する方式と、クラスタがそのイメージをいつ引き直すかを決める imagePullPolicy を押さえます。

タグと digest #

イメージは 名前:タグ (例: myapp:1.0) または 名前@digest (例: myapp@sha256:abc123...) で参照します。タグは同じ名前に別の内容を再びプッシュできるため可変ですが、digest はイメージ内容のハッシュなので一度指定すれば常に同じイメージを指します。

imagePullPolicy の 3 つの値 #

動作
Always毎回レジストリから引き直す
IfNotPresentノードにイメージがないときだけ引く
Never絶対に引かない。ノードにあらかじめ存在している必要がある

デフォルト値はタグによって変わります。タグが :latest であるかタグを省略するとデフォルトは Always、それ以外の具体的なタグなら IfNotPresent です。

latest タグの落とし穴 #

:latest は常に最新を指す特別なタグではなく、名前がただ latest であるだけの普通のタグ です。それでもデフォルトのプル方針が Always になるため、ノードごとに別の時点のイメージを引くことがあり、どのバージョンが起動しているか追跡しづらくなります。そのため実務でも試験でも、コンテナに image: ...myapp:1.0 のように 具体的なバージョンタグを明記 し、imagePullPolicy: IfNotPresent を併記するのが安全です。ローカルで先ほどビルドしてノードにすでに存在するイメージなら、Never または IfNotPresent にしておいて、存在しないレジストリから引こうとして失敗する状況を防げます。

プライベートレジストリ: imagePullSecrets #

認証が必要なプライベートレジストリのイメージを使うには、資格情報を入れた docker-registry タイプの Secret を作り、Pod がそれを参照する必要があります。

k create secret docker-registry regcred \
  --docker-server=registry.example.com \
  --docker-username=ckad \
  --docker-password=<パスワード> \
  --docker-email=ckad@example.com

作った Secret は Pod の spec.imagePullSecrets に接続します。

spec:
  imagePullSecrets:
    - name: regcred
  containers:
    - name: app
      image: registry.example.com/team/myapp:1.0

この接続を抜かすと認証失敗で ImagePullBackOff が出るため、プライベートイメージを扱うタスクでは Secret の作成と imagePullSecrets の接続を 1 組として覚える必要があります。

Pod での命令の上書き (試験の定番) #

Pod マニフェストの commandargs は Dockerfile の ENTRYPOINTCMD を上書きします。このマッピングを問う問題がよく出るため、表で整理します。

Pod フィールドDockerfile の対応動作
commandENTRYPOINT実行ファイルを上書きする
argsCMD引数を上書きする

つまり command を指定するとイメージの ENTRYPOINT が無視され、args を指定すると CMD が無視されます。両方とも省略するとイメージの ENTRYPOINTCMD がそのまま使われます。

apiVersion: v1
kind: Pod
metadata:
  name: cmd-demo
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c"]              # ENTRYPOINT を上書き
      args: ["echo hello && sleep 3600"] # CMD を上書き

命令形で作るときは -- の後ろのトークンが上のマッピングに従います。

k run cmd-demo --image=busybox:1.36 $do \
  --command -- sh -c "echo hello && sleep 3600" > pod.yaml

ここで --command フラグがあると -- の後ろが command に、なければ args に入ります。この違いが ENTRYPOINT を上書きするか CMD を上書きするかを分けるため、問題文を正確に読む必要があります。

試験ポイント #

  • ビルドツールは docker ではないことがあります。 podman buildbuildah bud でも同じタスクができるよう手に馴染ませておきます。
  • タグとプッシュパスは問題文どおりに。 採点は指定された名前・タグ・レジストリパスを基準にするため、タイプミス 1 つが失点です。
  • マルチステージは ASCOPY --from ビルド段に名前を付け、最後の段で成果物だけをコピーするパターンを覚えておきます。
  • imagePullPolicy のデフォルト値。 :latest またはタグ省略は Always、具体的なタグは IfNotPresent。ローカルビルドのイメージは NeverIfNotPresent でプル失敗を防ぎます。
  • プライベートレジストリは Secret + imagePullSecrets の 1 組。 k create secret docker-registry と Pod の接続を一緒に覚えます。
  • command・args のマッピング。 commandENTRYPOINTargsCMD--command フラグの有無で何を上書きするかが分かれます。

まとめ #

この記事で押さえたこと:

  • CKAD はイメージを自分でビルド・プッシュ・実行する一連のサイクル を要求することがあります。一度のビルドだけでなく、Pod がイメージを正常に引くかまで確認します。
  • Dockerfile の基本。 FROMWORKDIRCOPYRUNEXPOSE、そして CMDENTRYPOINT の違いとレイヤーキャッシュの順序。
  • マルチステージビルド。 ビルド段と実行段を分離して distroless・alpine で軽量化し、攻撃表面を減らします。
  • ビルド・タグ・プッシュ。 podmanbuildahdocker の 3 つのツールすべてで build -tpush の流れは同じです。
  • イメージ参照。 タグ vs digest、imagePullPolicy の 3 つの値と :latest の落とし穴、プライベートレジストリの imagePullSecrets
  • command・argsENTRYPOINTCMD を上書きするマッピング。

次へ: Workloads 1 #

イメージを作って Pod で起動する単位まで来ました。ここから複数の Pod をまとめて運用するワークロードへ上がります。

#5 Workloads 1: Deployment、ReplicaSet、rolling update と rollback では、ReplicaSet でレプリカを維持する原理、Deployment でローリングアップデートを回す方法、問題が起きたときに kubectl rollout undo でロールバックする手順、そして試験によく出る「特定のリビジョンに戻す」タイプまで実際に作りながら整理します。

X