Docker コンテナは現在、Web アプリケーションのパッケージングの標準となっており、すべての依存関係をクリーンで標準化された方法で含めることができます。しかし、これは偽りの安心感をもたらします。一見すると Dockerfile は決定論的に見えます(同じ Dockerfile から同じ Docker コンテナが作成される)が、実際には無数のミラー、パッケージレジストリ、リポジトリに依存しており、それらはいつでも変更される可能性があります。依存関係をコンテナ内にきれいにパッケージ化したように見えても、その依存関係リストはいつでも変更され、アプリケーションが壊れる可能性があります。
典型的な Dockerfile
典型的な Dockerfile を見てみましょう(ここでは Python アプリケーションをインストールしていますが、他のプログラミング言語でも同様です):
# このDockerfileをUbuntu 20.04ベースにする
FROM ubuntu:20.04
# パッケージインストール時にプロンプトを表示しない
ARG DEBIAN_FRONTEND=noninteractive
# OSレベルのパッケージをインストール
RUN apt update && apt install -y curl software-properties-common
# 新しいPythonバージョンが必要なため、Deadsnakes PPAから取得
RUN add-apt-repository -y ppa:deadsnakes/ppa && \
apt update && \
apt install -y python3.9 python3.9-distutils
# pip(Pythonパッケージマネージャー)をインストール
RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \
python3.9 get-pip.py
# pipを通じてPythonパッケージをインストール
RUN pip3 install onnx==1.14.0
この Dockerfile を一行ずつ確認してみましょう:
ここでは Docker レジストリ(多くの場合 Docker Hub)から 20.04 タグの ubuntu ベースイメージをプルしています。Docker Hub のイメージとタグはイミュータブルではなく、上書き(ここでは新しい Ubuntu 20.04 バージョンがリリースされるたびに)または削除される可能性があります。どのベースイメージが返されるかは、ビルドのタイミングとビルドキャッシュの内容によって異なります。これにより、問題のデバッグが非常に困難になります(本番環境とは異なるベースイメージで実行することになります)。コンテナを再ビルドすると、ベース OS バージョンが突然変わったり(例:ビルドキャッシュが空の場合)、ベースイメージが削除されてコンテナがまったくビルドできなくなったりする可能性があります。
次に Ubuntu パッケージレジストリからいくつかのパッケージをインストールしています。Ubuntu パッケージレジストリは常に更新されているため、これらのパッケージの最新バージョンがインストールされます。また、レジストリは古いバージョンを積極的に削除するため、特定のバージョンにピン留めすることも不可能です。つまり、コンテナのコアな依存関係が突然更新されたり、利用できなくなったりする可能性があります。上記と同様に、インストールされるパッケージはコンテナをビルドしたタイミング(とビルドキャッシュの内容)によって異なり、いつでもビルドが壊れる可能性があります。
次に Ubuntu パッケージレジストリにないパッケージ(Python 3.9)が必要なため、deadsnakes PPA(代替パッケージリポジトリ)を追加し、そこからパッケージをインストールしています。これは前のステップとまったく同じ問題を抱えていますが、さらに PPA への依存関係が追加されます。PPA はいつでもオフラインになったり、動作しなくなったりする可能性があります。
次にパッケージリポジトリにない追加の依存関係が必要です。get-pip.py ファイルをダウンロードして実行し、pip パッケージマネージャーをインストールしています。この URL のコンテンツは変更される可能性があります(そして実際に変わります!)。URL が別のスクリプトに解決されたり、URL 自体が削除されたりする可能性があります。また、スクリプト自体が追加のリソースをダウンロードするための HTTP コールを行っている可能性が高いです。したがって、コンテナにインストールされる pip のバージョンも、コンテナのビルドタイミングによって異なり、更新がプッシュされるたびにアプリケーションが壊れる可能性があります。
最後に Python パッケージをインストールしています。このパッケージを正確な 1.14.0 リリースにピン留めしているように見えます。しかし、それは正確ではありません。なぜなら onnx 自体がピン留めされていない依存関係に依存しているからです(例:protobuf>=3.20.2 と指定されています)。onnx のサブ依存関係の一つが更新されると(例:破壊的変更を含む protobuf 5 がリリースされる)、コンテナを再ビルドした際にアプリケーションが壊れます。pip はこれらのサブ依存関係の最新バージョンを取得するため、コンテナのビルドタイミング(とビルドキャッシュ)によってインストールされる Python パッケージが大きく異なる可能性があるという問題が再び発生します。
一つの Dockerfile に潜む多くの問題点
見てきたように、この非常にシンプルな Dockerfile は 5 つのサービス(Docker Hub、Ubuntu パッケージレジストリ、deadsnakes PPA、pypa.io、PyPI レジストリ)に依存しており、これらはすべて不安定です。どの依存関係も、いつでも更新、変更、削除される可能性があり、アプリケーションを壊す恐れがあります。
さらに、コンテナの内容はビルドしたタイミングとビルドキャッシュの内容に依存します。これは問題です。なぜなら、同じ Dockerfile から大きく異なるコンテナが生成される可能性があり、依存関係の更新によって発生する問題のデバッグが非常に困難になるからです(自分のマシンでは動くのに、同僚のマシンでは動かない、という状況)。
これらが合わさると、膨大なメンテナンス負担になります。特に、すべてが事後対応になるという点が問題です。誰かがコードベースに一見小さな変更を加えると、ビルドサーバーがコンテナをゼロからビルドし、まったく無関係なエラーでビルドが壊れます。今すぐアプリケーションを更新して新しい依存関係に対応しなければならなくなります(ビルドが壊れているので!)。
そこで、StableBuild!
この問題に対処するために、StableBuild を作りました。StableBuild は、コンテナのビルドを信頼性が高く再現可能にすることを目的としたミラーとパッケージレジストリのセットです。本質的には、StableBuild を使用して任意の依存関係を固定できるため、同じ Dockerfile から同じ Docker コンテナが生成されます。現在、Docker ベースイメージ、Ubuntu・Debian・Alpine の完全なパッケージレジストリ、最も人気のある PPA、PyPI Python パッケージレジストリに対応しており、さらに多くのもの(インターネット上の任意のファイルの固定など)に積極的に取り組んでいます。
興味が出てきましたか?上記の Dockerfile を再現可能にする方法は、はじめてのコンテナのピン留め で学ぶことができます。🎉