インフラエンジニアの羅針盤

インフラエンジニア1〜3年目のための技術ガイド

レジストリ+タグ戦略+Trivy|CKAD第4回

公開更新

新卒インフラエンジニア向け「Kubernetes 実践教科書① CKAD アプリケーション開発編」の第4回です。第3回で模擬アプリ fanclub-api の Backend イメージ fanclub-backend:0.1.0 をローカルにビルドしました。今回はこのイメージをプライベートレジストリ(k8s-registry)に push して共有できる状態にし、イメージタグ戦略latest の罠・セマンティックバージョニング)と、Trivy による脆弱性スキャンを学びます。

  • AlmaLinux 10.2
  • Docker CE 29.6.0
  • Docker Registry registry:2
  • Trivy v0.70.0
  • 確認日 2026-06-23
目次
  1. 今ここマップ(全 19 回中の現在地)
  2. この回のゴール
  3. なぜプライベートレジストリが必要か
  4. k8s-registry に Docker Registry を構築
    1. k8s-registry に Docker CE を導入
    2. registry:2 コンテナを起動する
    3. クライアントの名前解決を設定する
    4. クライアントに insecure-registries を設定する
  5. イメージタグ戦略
    1. latest の罠
    2. セマンティックバージョニング(SemVer)
    3. Git ハッシュタグという選択肢
  6. Backend イメージを push する
  7. Trivy で脆弱性スキャン
    1. Trivy を導入する
    2. イメージをスキャンする
    3. 重大度の読み方と対処の優先度
    4. Harbor という選択肢(概念紹介)
  8. やってみよう
  9. 理解度チェック
  10. まとめ
  11. 次回予告

今ここマップ(全 19 回中の現在地)

現在地は第 1 部「コンテナと Docker」の第 4 回、第 1 部の最終回です。次回からは Kubernetes に入ります。

  • 第 1 部 コンテナと Docker(第 1〜4 回)← 今ここ
  • 第 2 部 Kubernetes 基礎(第 5〜6 回)
  • 第 3 部 アプリリソース(第 7〜11 回)
  • 第 4 部 ワークロード戦略(第 12〜14 回)
  • 第 5 部 セキュリティ基礎(第 15〜16 回)
  • 第 6 部 パッケージ管理と HTTPS 公開(第 17〜19 回)

この回のゴール

  • Docker Registry を構築し、イメージを push / pull できる。
  • タグ戦略(latest の罠・SemVer・Git ハッシュ)を説明できる。
  • Trivy でイメージの脆弱性スキャンを実施し、重大度を読める。

なぜプライベートレジストリが必要か

ビルドしたイメージは、そのままではビルドしたホスト(k8s-ops)のローカルにしか存在しません。Kubernetes は複数のノードでコンテナを動かすため、各ノードが共通の場所からイメージを pull できる必要があります。その共通の置き場がレジストリです。

Docker Hub という公開レジストリもありますが、業務では自前のプライベートレジストリを使う理由があります。

  • 非公開要件:自社プロダクトのイメージを公開リポジトリに置けない。
  • レート制限:Docker Hub は匿名・無料枠で pull 回数に制限があり、CI やクラスタの一斉 pull で引っかかる。
  • ネットワーク:社内ネットワーク内に置けば pull が速く、外部依存も減る。

本シリーズでは k8s-registry という専用 VM(192.168.1.123)に Docker Registry を立て、ここに fanclub-api のイメージを集約します。以降のコマンドはホスト名 k8s-registry で参照します。この名前を解決できないと docker push / pulldial tcp: lookup k8s-registry ... no such host で失敗するため、後述の「クライアント設定」で k8s-ops の /etc/hosts に名前解決を登録します。

k8s-registry に Docker Registry を構築

k8s-registry に Docker CE を導入

まず k8s-registry VM に SSH でログインし、第1回で k8s-ops に行ったのと同じ手順で Docker CE を導入します(リポジトリ追加 → dnf installsystemctl enable --now docker)。本シリーズの検証環境はプロキシ経由のため、Docker デーモンのプロキシ設定(systemd drop-in)も第1回と同様に行います。導入の詳細は第1回を参照してください。

registry:2 コンテナを起動する

Docker Registry は registry:2 というイメージをコンテナとして動かすだけで構築できます。データが消えないよう、ホストのディレクトリを -v でマウントして永続化します。次は root 権限(#)で k8s-registry 上で実行します。

実行コマンド:

# mkdir -p /opt/registry/data
# docker run -d --name registry --restart=always -p 5000:5000 -v /opt/registry/data:/var/lib/registry registry:2
  • -p 5000:5000:レジストリは 5000 番で待ち受けます。
  • -v /opt/registry/data:/var/lib/registry:push したイメージの実体をホスト側に保存し、コンテナを作り直しても残します。
  • --restart=always:VM 再起動後もレジストリが自動で立ち上がります。

起動後、レジストリの API が応答するか確認します。空のレジストリでは空のリポジトリ一覧が返ります。

実行コマンド:

# curl -s http://localhost:5000/v2/_catalog

実行結果(例):

{"repositories":[]}

クライアントの名前解決を設定する

push / pull は k8s-ops からホスト名 k8s-registry 宛に行うため、まず k8s-ops でこの名前を 192.168.1.123 に解決できるようにします。DNS に k8s-registry の A レコードが無い環境では、/etc/hosts に直接登録するのが確実です。これを行わないと、後段の docker pushdial tcp: lookup k8s-registry ... no such host で失敗します。

実行コマンド:

$ echo "192.168.1.123 k8s-registry" | sudo tee -a /etc/hosts

実行結果:

192.168.1.123 k8s-registry

クライアントに insecure-registries を設定する

本シリーズのレジストリは学習用途のため HTTP(TLS なし)で動かします。Docker は既定で HTTPS のレジストリしか信用しないため、HTTP のレジストリを使うクライアント(ここでは k8s-ops)に「このレジストリは HTTP でよい」と明示します。k8s-ops で /etc/docker/daemon.json に次を追記し、Docker を再起動します。

{
  "insecure-registries": ["k8s-registry:5000"]
}

実行コマンド:

$ sudo systemctl restart docker

注意点

insecure-registries はあくまで学習用です。本番では TLS 証明書を設定した HTTPS レジストリ(後述の Harbor 等)を使い、HTTP 平文での push は行いません。また k8s-registry のホスト名はプロキシを経由しないよう、Docker デーモンの NO_PROXY に含めておきます(本シリーズの初期設定で設定済み)。

イメージタグ戦略

push の前に、イメージに付けるタグの考え方を整理します。タグは「どのバージョンのイメージか」を識別する名前で、運用の質を大きく左右します。

latest の罠

latest は「タグを省略したときの既定」にすぎず、「最新を自動で指す」特別な意味はありません。latest だけで運用すると次の問題が起きます。

  • 再現性がない:同じ latest でも、いつ pull したかで中身が変わる。「昨日は動いたのに今日は動かない」の温床になる。
  • ロールバックできない:障害時に「ひとつ前の版」に戻そうにも、どれが前の版か分からない。
  • Kubernetes と相性が悪い:Pod 再作成のたびに別の中身を引く可能性があり、挙動が不安定になる。

セマンティックバージョニング(SemVer)

そこで MAJOR.MINOR.PATCH の形式(例 1.2.0)でバージョンを付けます。これをセマンティックバージョニング(SemVer)と呼びます。

  • MAJOR:後方互換性を壊す変更(API 仕様変更など)。
  • MINOR:後方互換を保った機能追加。
  • PATCH:後方互換を保ったバグ修正。

バージョン番号は一度 push したら中身を変えない(イミュータブル)のが鉄則です。0.2.0 として push したイメージの中身を後から差し替えてはいけません。修正したら必ず新しい番号(0.2.1 など)を付けます。これにより「このバージョン=この中身」が常に成立し、再現性とロールバックが保証されます。

Git ハッシュタグという選択肢

CI/CD では、ビルド元のコミットを表す Git のコミットハッシュ(例 git-9f3a1c2)をタグにする手法もよく使われます。イメージから「どのソースコードでビルドしたか」を一意に追跡でき、SemVer と併用されることも多いです。本シリーズでは分かりやすさを優先して SemVer を主に使います。3 つのタグ戦略の違いを次の図にまとめます。

latest・SemVer・Git ハッシュの3つのタグ戦略を比較し、latest は再現性が無く運用に不向き、SemVer はイミュータブルで推奨、Git ハッシュは CI/CD 向けで併用されることを示す比較図。
図2:イメージタグ戦略の比較(latest / SemVer / Git ハッシュ)

Backend イメージを push する

第3回でビルドした fanclub-backend:0.1.0 を、レジストリ運用の最初の配布版として 0.2.0 を付けて push します。本シリーズは回を追うごとにアプリを育てるため、バージョンを段階的に上げていきます(実務ではバージョンは中身の変更に合わせて上げます)。push にはまず、レジストリのアドレスを含んだ名前を docker tag で付けます。全体の流れは次の図のとおりです。

k8s-ops でローカルの fanclub-backend:0.1.0 を docker tag でレジストリ宛の名前 k8s-registry:5000/fanclub-backend:0.2.0 に付け替え、docker push で k8s-registry の Docker Registry に保存し、docker pull で取得し直す流れを示す図。
図1:tag → push → pull でイメージをレジストリ経由で共有する流れ

実行コマンド:

$ docker tag fanclub-backend:0.1.0 k8s-registry:5000/fanclub-backend:0.2.0

イメージ名の k8s-registry:5000/ という接頭辞が「どのレジストリへ push するか」を表します。続けて push します。

実行コマンド:

$ docker push k8s-registry:5000/fanclub-backend:0.2.0

実行結果(例):

The push refers to repository [k8s-registry:5000/fanclub-backend]
7945bad67bf3: Pushed
44ca211ace90: Pushed
(中略)
0.2.0: digest: sha256:7c8a91603804d48c8631e495fd9634edbe7e81cbccce73a35f11dc7a7490b84d size: 856

レジストリに登録されたか、カタログ API で確認します。プロキシ環境ではシェルの http_proxy が効いており、ホスト名 k8s-registryno_proxy の CIDR 指定(192.168.1.0/24)に一致しないためプロキシへ送られて拒否されます。レジストリへ直接アクセスするよう --noproxy k8s-registry を付けます(プロキシの無い環境では --noproxy は付けても無害です)。

実行コマンド:

$ curl -s --noproxy k8s-registry http://k8s-registry:5000/v2/_catalog

実行結果(例):

{"repositories":["fanclub-backend"]}

レジストリから本当に取得できるかを確認します。ローカルに同名イメージが残っていると、レジストリから取得したのか元からあったのかを区別できません。そこで k8s-ops のローカルイメージを一度削除してから pull し直します。

実行コマンド:

$ docker image rm k8s-registry:5000/fanclub-backend:0.2.0
$ docker pull k8s-registry:5000/fanclub-backend:0.2.0

pull が成功すれば、レジストリ経由でイメージを配布できる状態が整ったことになります。これで fanclub-backend の 0.2.0 が k8s-registry に揃いました。第5回以降に構築する kind クラスタの各ノードも、同じレジストリからこのイメージを pull して Pod を起動します。

Trivy で脆弱性スキャン

イメージには、ベースイメージや依存ライブラリに既知の脆弱性(CVE)が含まれることがあります。Trivy はイメージをスキャンして CVE を一覧化するツールで、CKAD/CKS でも頻出します。k8s-ops に導入します。

Trivy を導入する

Trivy には dnf リポジトリ(get.trivy.dev)方式もありますが、本シリーズの whitelist プロキシ環境では get.trivy.dev が未許可で取得できません。kind や Helm と同様に、whitelist 済みの GitHub Releases からバイナリを取得して配置します。sudo はシェルのプロキシ環境変数を引き継がないため、ダウンロードは一般ユーザーで行います。k8s-ops で次を実行します。

実行コマンド:

$ curl -fsSL -o /tmp/trivy.tar.gz https://github.com/aquasecurity/trivy/releases/download/v0.70.0/trivy_0.70.0_Linux-64bit.tar.gz
$ tar -xzf /tmp/trivy.tar.gz -C /tmp trivy
$ sudo install -m 0755 /tmp/trivy /usr/local/bin/trivy

導入できたらバージョンを確認します。

実行コマンド:

$ trivy --version

実行結果(例):

Version: 0.70.0

イメージをスキャンする

push した Backend イメージを、重大度 HIGHCRITICAL に絞ってスキャンします。trivy image は指定したイメージがローカルの Docker デーモンにあればそれを優先して読み込むため、直前に pull した k8s-registry:5000/fanclub-backend:0.2.0 をそのまま指定できます(ローカルに無い場合はレジストリから取得を試み、HTTP レジストリには --insecure が必要になります)。初回は脆弱性データベースのダウンロードが走るため少し時間がかかります。Trivy は既定で DB を mirror.gcr.io から取得しますが、本シリーズの whitelist 環境では未許可のため、whitelist 済みの ghcr.io--db-repository / --java-db-repository で指定します(プロキシの無い環境では両オプションを省略して既定の取得先で構いません)。

実行コマンド:

$ trivy image --severity HIGH,CRITICAL \
    --db-repository ghcr.io/aquasecurity/trivy-db:2 \
    --java-db-repository ghcr.io/aquasecurity/trivy-java-db:1 \
    k8s-registry:5000/fanclub-backend:0.2.0

出力は冒頭に各ターゲットの要約(Report Summary)が出て、続いてターゲットごとに脆弱性の詳細が並びます。結果を次の図にまとめます。

Trivy スキャン結果の図。Report Summary はベースOS(ubuntu 26.04)が脆弱性0件、opt/payara/app.war と payara-micro.jar が各2件、usr/bin/pebble が9件。主な HIGH 検出は app.war の PostgreSQL JDBC(42.7.4→42.7.7)、payara-micro.jar の JLine(3.30.9→4.2.1)、pebble(Go バイナリ)の golang.org/x/net・stdlib で、CRITICAL は0件・HIGH は計13件。
図3:Trivy スキャン結果(Report Summary と主な HIGH 検出)

ベース OS(ubuntu 26.04)は 0 件で、検出は jar(app.war の PostgreSQL JDBC・payara-micro.jar の JLine)と Go バイナリ(pebble)に集中しています。各ライブラリには複数の CVE があり、HIGH は合計 13 件(CRITICAL は 0 件)です。

重大度の読み方と対処の優先度

  • 検出はベースOSとは限らない:今回はベースイメージの OS パッケージ(ubuntu 26.04)は 0 件で、検出はすべてアプリ依存app.war 内の PostgreSQL JDBC ドライバ、payara-micro.jar 内の JLine)と、ベースイメージ同梱の Go バイナリ(pebble)に出ています。脆弱性はベースイメージだけでなく依存ライブラリからも入ることが分かります。
  • Status が fixed:修正版が提供済みです。Fixed Version 列の版へ上げれば解消します(例:PostgreSQL JDBC 42.7.442.7.7)。依存の更新は pom.xml のバージョンを上げて再ビルドします。affected(未修正)は緩和策で対応します。
  • CRITICAL / HIGH を優先:すべてを即座に消すのは現実的でないため、重大度と fixed 有無で優先順位を付けます。今回は CRITICAL なし・HIGH のみのため、まず Fixed Version のある依存から更新します。

スキャンを CI に組み込み、CRITICAL が出たらビルドを止める運用が定石です。第3回でマルチステージビルドにより実行イメージをベース+WAR だけに絞った効果は、今回ベース OS の検出が 0 件だったことに表れています。一方で、アプリが取り込む依存ライブラリの脆弱性は別途バージョン更新で潰す必要がある、という点も実際のスキャン結果から読み取れます。

Harbor という選択肢(概念紹介)

本シリーズは学習用に軽量な registry:2 を使いますが、実務では Harbor という CNCF 製のレジストリもよく使われます。Harbor は Web UI・ユーザー認証・脆弱性スキャンの内蔵(Trivy 連携)・イメージ署名などを備えます。「レジストリ+スキャン+アクセス制御」を一体で運用したいときの選択肢として名前を覚えておくとよいでしょう。

やってみよう

  1. k8s-registry に Docker CE を導入し、registry:2 コンテナをデータ永続化付きで起動する。
  2. k8s-ops の /etc/hosts192.168.1.123 k8s-registry を追加し、/etc/docker/daemon.jsoninsecure-registries を設定して Docker を再起動する。
  3. docker tagk8s-registry:5000/fanclub-backend:0.2.0 を付け、docker push する。
  4. ローカルのイメージを削除してから docker pull し、レジストリ経由で取得できることを確認する。
  5. trivy image --severity HIGH,CRITICAL でスキャンし、CVE の重大度を確認する。

理解度チェック

次の各文が正しいか(○)誤りか(×)を判断してください。解答は下にまとめています。

  1. latest タグは常に最新のイメージを自動で指す特別なタグである。
  2. SemVer では、一度 push したバージョンの中身は変えず、修正時は新しい番号を付ける。
  3. HTTP のレジストリを使うには、クライアントの insecure-registries に登録する必要がある。
  4. docker push する前に、レジストリのアドレスを含む名前を docker tag で付ける必要がある。
  5. Trivy の --severity HIGH,CRITICAL は、重大度を絞って表示するオプションである。
  6. registry:2 のデータをボリュームで永続化しないと、コンテナ削除時に push 済みイメージが消える。
  7. 本番環境でも insecure-registries(HTTP 平文)の利用が推奨される。

解答

  • 1. ×:latest は「タグ省略時の既定名」にすぎず、最新を自動追従する仕組みはない。
  • 2. ○:イミュータブルが鉄則。修正は 0.2.1 などで上げる。
  • 3. ○:Docker は既定で HTTPS を要求するため、HTTP レジストリは明示登録が要る。
  • 4. ○:k8s-registry:5000/... の名前を付けて初めて push 先が決まる。
  • 5. ○:HIGH,CRITICAL のみ表示し、ノイズを減らせる。
  • 6. ○:-v/var/lib/registry を永続化しないと実体が消える。
  • 7. ×:学習用のみ。本番は TLS の HTTPS レジストリを使う。

まとめ

本記事では、k8s-registry に Docker Registry(registry:2)を構築し、第3回のイメージを k8s-registry:5000/fanclub-backend:0.2.0 として push / pull しました。latest の罠と SemVer・イミュータブルタグの考え方を押さえ、Trivy で脆弱性スキャンも実施しました。これで「ビルドしたイメージを共有し、安全性を確認する」一連の流れがつながり、第1部「コンテナと Docker」は完了です。

次回予告

次回(第5回)からは第2部「Kubernetes 基礎」に入ります。Docker 単独では「複数ノードでの自動配置・自己修復・スケール」ができないという限界を確認し、kind で軽量な Kubernetes クラスタを手元に立ち上げます。ここから Kubernetes 本体の学習が始まります。

前の記事
次の記事