コンテナタグがあなたを騙す理由

docker pull myapp:latest を実行して、自分が何を取得しているか正確にわかっていると思っていませんか? 実はそうではありません。そのタグは明日には別のイメージを指しているかもしれませんし、1時間前とはすでに別のイメージを指しているかもしれません。同じタグ、同じコマンドなのに、本番環境でまったく異なるソフトウェアが動いている可能性があります。

これは理論上の問題ではありません。チームが何時間も本番環境のデバッグに費やした結果、動いていると思っていたイメージが実際には動いていなかった、というケースは少なくありません。タグはあることを示しているのに、中身はまったく別物だったのです。

イメージの保存場所

タグとその問題について話す前に、コンテナイメージが実際にどこに保存されるのかを理解する必要があります。

あなたのラップトップでDockerイメージをビルドすると、そのイメージはそのマシンにしか存在しません。サーバー、ステージング環境、本番環境で実行するには、イメージを保存して共有する場所が必要です。それがレジストリです。

レジストリは、コンテナイメージ用のファイルサーバーですが、より高度な機能を備えています。バージョン管理、整合性チェック、アクセス制御を行います。docker pull nginx:latest を実行すると、Docker Hub(パブリックレジストリ)からプルしています。誰でもプルできますが、誰でもプッシュもできるため、本番環境でパブリックイメージに依存するのは避けるべきです。

ほとんどの企業は独自の内部レジストリを運用しています。一般的な選択肢としては、Harbor、Nexus、GitLab Container Registry、Amazon ECRなどがあります。内部レジストリには次の3つの利点があります。

  • 来歴(プロベナンス): すべてのイメージの出所がわかります。インターネット上の不特定のイメージはありません。
  • 速度: 内部ネットワーク経由でのイメージ転送は、パブリックインターネット経由よりもはるかに高速です。
  • 制御: イメージのプッシュとプルを誰に許可するかを決定できます。

レジストリがなければ、デプロイパイプラインはビルドしたイメージの置き場を失います。レジストリがあれば、チームが生成するすべてのイメージの信頼できる唯一の情報源(シングルソースオブトゥルース)が得られます。

タグはラベルであり、識別子ではない

レジストリ内のすべてのイメージには、1つ以上のタグがあります。タグは、特定のバージョンやバリアントをマークするためにイメージに付けるラベルです。myapp:1.0.0myapp:stagingmyapp:latest など、いたるところでタグを見かけます。

タグは便利です。人間が読める形でイメージを参照できます。長いハッシュを覚える代わりに、myapp:1.0.0 と入力すれば目的のイメージが得られます。

しかし、タグには根本的な欠陥があります。それは可変(ミュータブル) であることです。タグが指すイメージはいつでも変更できます。今日、myapp:latest はイメージハッシュ abc123 を指しています。明日、新しいビルドの後には def456 を指すようになります。タグは同じままですが、イメージは変わります。

この可変性は現実的な問題を引き起こします。ステージング環境が myapp:staging を実行しているシナリオを考えてみましょう。パイプラインが新しいイメージをビルドし、staging というタグを付けてプッシュします。これでステージングは新しいコードを実行します。しかし、誰かが手動で staging タグを別のイメージで上書きしたらどうなるでしょうか? あるいは、パイプラインが誤ったビルドにタグを付けてしまったら? ダイジェストを確認しない限り、ステージングで実際にどのイメージが実行されているかを知る方法はありません。

イミュータブルタグパターン

解決策はイミュータブルタグ(不変タグ)を使用することです。イミュータブルタグは、作成された後に決して変更されないタグです。一度タグをイメージに割り当てると、そのタグはそのイメージに永久に残ります。

イミュータブルタグには、特定のビルドを識別する一意の情報が含まれています。一般的なパターンは次のとおりです。

  • セマンティックバージョン: myapp:1.2.3
  • Gitコミットハッシュ: myapp:a1b2c3d
  • ビルドタイムスタンプ: myapp:20240515-1430
  • パイプライン実行ID: myapp:build-456

イミュータブルタグを使用すれば、どの環境でどのコードが実行されているかを正確に追跡できます。本番環境でバグが報告された場合、タグを確認し、コミットハッシュを見つければ、そのイメージを生成したコードが正確にわかります。推測や「これは最新のどのバージョンだ?」といった疑問は不要です。

チームによっては、複数のアプローチを組み合わせることもあります。リリースにはセマンティックバージョン、開発ビルドにはコミットハッシュ、自動デプロイにはタイムスタンプを使用します。重要なのは一貫性です。すべてのビルドが、再利用されることのない一意のタグを生成することです。

ダイジェスト:真実

タグは便利ですが信頼性に欠けます。実行しているイメージを絶対的に確実に知りたい場合は、ダイジェストを使用する必要があります。

すべてのコンテナイメージにはダイジェストがあります。ダイジェストはイメージコンテンツの暗号学的ハッシュです。sha256:abc123def456... のような形式です。ダイジェストはその特定のイメージに固有です。イメージの内容が1バイトでも変更されると、ダイジェストは完全に変わります。

タグとは異なり、ダイジェストは移動したり再割り当てしたりできません。ダイジェスト sha256:abc123 は、常にまったく同じイメージを指し続けます。何をしても、そのダイジェストを別のイメージにポイントすることはできません。

ダイジェストでイメージをプルすると、期待したイメージを確実に取得できます。あいまいさはなく、別のバージョンを取得する可能性もありません。

# これは安全 - 要求したものを正確に取得できる
docker pull myapp@sha256:abc123def456

パイプラインでのタグとダイジェストの併用

実際には、タグとダイジェストの両方を一緒に使用します。タグはイメージを人間が読める形にし、参照しやすくします。ダイジェストは、正しいイメージを実行しているという保証を提供します。

典型的なパイプラインの処理方法は次のとおりです。

  1. イメージをビルドする。
  2. イミュータブルタグ(コミットハッシュまたはバージョン)を付ける。
  3. イメージをレジストリにプッシュする。
  4. デプロイメントメタデータにダイジェストを記録する。
  5. ステージングから本番環境にプロモートする際に、ダイジェストが完全に一致することを確認する。

この確認手順は非常に重要です。イメージをステージングから本番環境にプロモートするときは、タグだけを確認するのではなく、ダイジェストが同一であることを確認する必要があります。これにより、誰かがステージングタグを別のイメージで上書きし、その誤ったイメージが本番環境にプロモートされるのを防ぎます。

以下は、イメージをプッシュした後にダイジェストを取得し、それをデプロイメントの固定に使用する実践的な例です。

# イミュータブルタグでイメージをビルドしてプッシュ
docker build -t myapp:build-456 .
docker push myapp:build-456

# プッシュしたイメージからダイジェストを取得
digest=$(docker inspect --format='{{index .RepoDigests 0}}' myapp:build-456 | cut -d'@' -f2)
echo "Digest: $digest"

# Kubernetesデプロイメントマニフェストでダイジェストを使用
cat <<EOF > deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp@$digest
EOF

# デプロイメントを適用
kubectl apply -f deployment.yaml

これにより、レジストリ内のタグがどうなろうと、すべてのPodがまったく同じイメージを実行することが保証されます。

多くのデプロイツールはダイジェストベースのデプロイをサポートしています。たとえばKubernetesでは、タグの代わりにダイジェストでイメージを指定できます。

spec:
  containers:
  - name: myapp
    image: myregistry.com/myapp@sha256:abc123def456

これにより、レジストリ内のタグがどうなろうと、すべてのPodがまったく同じイメージを実行することが保証されます。

実践的なチェックリスト

次のデプロイの前に、以下のチェックを実行してください。

  • すべてのビルドが一意のイミュータブルタグ(コミットハッシュ、バージョン、またはタイムスタンプ)を生成していること
  • latest タグが本番環境のデプロイで使用されていないこと
  • パイプラインがビルドしたすべてのイメージのダイジェストを記録していること
  • 環境間のイメージプロモーションがタグだけでなくダイジェストも検証していること
  • レジストリに不正なプッシュを防ぐアクセス制御が設定されていること
  • ストレージの肥大化を防ぐために、古いイメージが定期的にクリーンアップされていること

まとめ

タグは人間のためのものです。ダイジェストは機械のためのものです。タグを使って作業を楽にしつつ、ダイジェストを使ってデプロイの信頼性を高めてください。本番環境で問題が発生したとき、何が実行されているかを正確に知りたいはずです。可変タグはそれを教えてくれません。ダイジェストは、毎回、必ず教えてくれます。