深夜2時の呼び出し:Dockerfileが嘘をつくとき
火曜日の午前2時15分、私の電話が鳴り響きました。クリティカルなマイクロサービスが本番環境でCrashLoopBackOffに陥っていました。ログは非常にシンプルで、/usr/bin/python3: not foundとだけ。私は困惑しました。450MBのUbuntuベースのイメージを50MBの「slim」バージョンに置き換えたばかりで、自分のノートPCでは完璧に動作していたからです。少なくとも、そう思っていました。
20分間にわたる必死の調査の結果、原因が判明しました。新しいベースイメージではPythonのバイナリが別のパスに移動されていたのです。私のエントリポイントスクリップは存在しない場所を指していました。私たちはしばしばDockerfileを盲信してしまいますが、実際の成果物がクラスターに投入されるまで検証することは稀です。数十ものサービスのレイヤーを手動で検査するのは不可能であり、ミスも起こりやすいものです。
これを解決するには、Container Structure Tests(CST)が必要です。これは、インフラにおける欠けていたユニットテストレイヤーだと考えてください。Googleによってオープンソース化されたこのフレームワークを使用すると、コンテナ自体に対してアサーションを実行できます。重い統合環境を立ち上げることなく、特定のファイル、コマンドの出力、メタデータをチェックできます。
なぜ現在のビルドプロセスは信用できないのか
ほとんどの開発者は、docker buildが終了コード0を返せば、イメージは正常であると考えがちです。しかし、それは危険な思い込みです。ビルドの成功は、構文が有効であったことを証明するだけであり、以下のことを保証するものではありません。
- 注入した
nginx.confに正しい644権限があるか。 nodeバイナリが実際に$PATH内にあるか。API_ENDPOINTのような必要な環境変数が設定されているか。- 大きなセキュリティの脆弱性となる
rootユーザーでイメージが実行されていないか。
CSTを使用すると、これらの要件をシンプルなYAMLファイルに記述できます。イメージが仕様を満たしていない場合、CI/CDパイプラインは即座に停止します。もう午前2時に驚かされることはありません。
フレームワークのインストール
このツールは単一の軽量なバイナリです。CIランナーを肥大化させたり、複雑な依存関係を必要としたりすることはありません。Linuxでは、以下のコマンドで15MBのバイナリをインストールできます。
# バイナリをダウンロード
curl -LO https://storage.googleapis.com/container-structure-test/latest/container-structure-test-linux-amd64
# 実行権限を付与してパスを通す
chmod +x container-structure-test-linux-amd64
sudo mv container-structure-test-linux-amd64 /usr/local/bin/container-structure-test
# 動作確認
container-structure-test version
Macユーザーは brew install container-structure-test を実行するだけです。インストールが完了したら、推測ではなく検証のステップへ進みましょう。
テストスイートの定義
CSTはYAMLを使用して期待値を定義します。私は通常、テストを「コマンド(Commands)」「ファイル(Files)」「メタデータ(Metadata)」の3つの主要な領域に分類します。標準的なPython APIの構成を見てみましょう。
1. コマンドテスト:バイナリの検証
これは私が最もよく使用するテストタイプです。環境が機能していることを確認します。ファイルが存在するだけでは不十分で、それが実行され、期待通りの出力を返す必要があります。
schemaVersion: "2.0.0"
commandTests:
- name: "Pythonのバージョンを確認"
command: "python"
args: ["--version"]
expectedOutput: ["Python 3.11.*"]
- name: "pipが利用可能であることを確認"
command: "pip"
args: ["--version"]
exitCode: 0
2. ファイルの存在と権限
これがあれば、先述の午前2時のインシデントは防げたはずです。また、機密ディレクトリが誰でも書き込み可能になっていないかなど、セキュリティポリシーの強制にも使用します。
fileExistenceTests:
- name: "アプリのエントリポイントを確認"
path: "/app/main.py"
shouldExist: true
permissions: "-rw-r--r--"
- name: "セキュリティチェック:SSHキーが存在しないこと"
path: "/root/.ssh/id_rsa"
shouldExist: false
3. メタデータの検証
このセクションでは、コンテナの「ラベル」や「環境変数」をチェックします。すべてのマイクロサービスでWORKDIRが一貫していることを確認するのに最適です。
metadataTest:
envVars:
- key: "PYTHONUNBUFFERED"
value: "1"
exposedPorts: ["8080"]
workdir: "/app"
user: "nonroot"
実行:テストの実行
config.yamlの準備ができたら、ローカルイメージに対してテストを実行します。私は通常、ビルドステップの直後にこれを実行します。十数個のテストを実行するのに5秒もかかりません。
container-structure-test test \
--image my-python-app:v1.0.2 \
--config tests.yaml
出力は簡潔です。テストが失敗した場合、CSTは出力の正規表現の不一致なのか権限エラーなのか、原因を正確に示します。このフィードバックループこそが、デバッグ時間を大幅に短縮する鍵となります。
CI/CDパイプラインへの統合
これを自動化することで、最大の価値が得られます。私のプロジェクトでは、CSTは品質ゲートとして機能します。誰かがUSERをnon-rootに設定し忘れてメタデータテストが失敗した場合、ビルドはレジストリにプッシュされる前に停止します。
以下は、このワークフローのGitHub Actionsのスニペットです。
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Dockerイメージのビルド
run: docker build -t my-app:${{ github.sha }} .
- name: Container Structure Testsの実行
uses: sudo-bot/[email protected]
with:
image: my-app:${{ github.sha }}
config: tests.yaml
- name: イメージをプッシュ
if: success()
run: docker push my-app:${{ github.sha }}
よくある落とし穴とプロのヒント
このツールは単純明快ですが、実行環境には注意が必要です。コマンドテストはコンテナの *内部* で実行されます。distrolessイメージを使用している場合、シェルが存在しません。その場合は、ほぼ全面的にfileExistenceTestsとmetadataTestに頼る必要があります。
すべてを1つの巨大なファイルに詰め込もうとしないでください。複雑なプロジェクトでは、テストをsecurity.yamlとruntime.yamlに分割しています。ツールに複数の--configフラグを渡すことができるため、プロジェクトの成長に合わせてテストスイートのメンテナンスが容易になります。
最後に
Container Structure Testsは、アプリのビジネスロジックをテストするためのものではありません。それはユニットテストの役割です。これらのテストは、デリバリービークル(配送手段)そのものを検証します。チームが1日に何度もデプロイを行うペースの速い環境では、単純なタイポによるインフラの失敗を許容する余裕はありません。
今日、20分かけてこれらのテストを設定してみてください。それはシステムの信頼性を高め、そして何より、夜にぐっすり眠れることを保証する小さな投資なのです。

