Dockerを使ってPythonアプリケーションをコンテナ化するためのベストプラクティス
Daniel Campos Olivares
2021年11月11日
0 分で読めますPythonのDockerコンテナに関するブログを数多く読んだところ、その大半は、フレームワーク(Django、Flask、Falconなど)に依存しない形でPythonアプリケーションをコンテナ化する例を紹介していました。たとえば、次のようなものです。
このDockerfileを使えば、Python Flaskアプリケーションをビルドして実行できます。
簡単な2つの手順で、問題なく動きますよね?
この例はシンプルで、デモや入門チュートリアルには役立ちますが、重要な懸念事項の多くに対処できていません。そこでこの記事では、そうした懸念に対処し、Dockerを使ってPythonアプリケーションをコンテナ化する際の6つのベストプラクティスを紹介します。具体的には、次の点を解説します。
コンテナ化したPythonアプリケーションには、明示的かつ再現性の高いDockerベースイメージのタグを使う
依存関係とソースコードを分離する
本番環境ではPython WSGIを使う
コンテナは最小限の権限で実行し、rootでは決して実行しない
アプリケーションの異常な状態に対処する
Python Dockerアプリケーションイメージのセキュリティ脆弱性を検出して修正する
1. コンテナ化したPythonアプリケーションには、明示的かつ再現性の高いDockerベースイメージのタグを使う
Docker化したPythonアプリケーションのベースイメージにpythonを使うのは理にかなっているように思えますが、どのバージョンのPythonが使われるのかという問題が残ります。
この記事の執筆時点では、前述のDockerfileのベースイメージはPython 3.10を含むイメージを指しています。なぜでしょうか。特定のタグを指定していないため、デフォルトでそのベースイメージの:latestが使われるからです。Docker Hubの公式イメージページを見ると、これは3.10に当たります。
コンテナ化するPythonのバージョンを制御するには、Dockerfileで必ずバージョン情報を指定する必要があります。
Python 3.10を使いたいので、Dockerfileに
:3.10タグを追加すればいいんですよね?
うーん……そう簡単ではありません。
Dockerのベースイメージタグ:3.10は、Python 3.10がインストールされた完全なオペレーティングシステムで、おそらく使うことのないライブラリが大量に含まれています。ソフトウェアが大量に含まれていることで、こうしたライブラリに存在する脆弱性によって攻撃対象領域が広がるという副作用もあります。
容量の大きなPython Dockerイメージを使うと、含まれるすべてのライブラリのバージョンを最新に保ち、メンテナンスするのが難しくなります。
Snyk Advisorを使ってPythonのベースイメージを調べると、Dockerベースイメージpython:3.10には、重大度が高い問題が12件、中程度が27件、低い問題が132件あることがわかります。つまり、Python Dockerイメージには、何も追加していない状態で最低でも171件のセキュリティ脆弱性が含まれることになります。
また、実質的にオペレーティングシステム全体を導入することになるため、Pythonアプリケーションサーバーのベースイメージはかなり大きくなり、ビルドが遅くなるうえ、より多くのディスク容量が必要になります。ベースイメージを選ぶ際の基本的なルールはいくつかありますが、特に重要なのは次の2つです。
Python Dockerイメージを選ぶ際のベストプラクティス
要件を満たす最小のベースイメージを選び、その上に構築しましょう。イメージが小さいほど脆弱性が少なく、リソース消費を抑えられ、不要なパッケージも少なくなります。
名前付きタグを使うだけでは、常に同じベースイメージを使えるとは限りません。それを保証する唯一の方法は、イメージダイジェストを使うことです。
この知識を踏まえて、もう一度Snyk Advisorを確認し、推奨される代替タグを見てみましょう。このツールでは、ベースイメージの脆弱性やサイズを一覧できるため、選択の際に大いに役立ちます。
アプリケーションでPython 3.10を使いたいので、そのバージョンのタグを探します。

前述の脆弱性に加え、このイメージのベースサイズは約350 MBで、Debian 11をベースとしており、427個のパッケージがインストールされていることがわかります。この小さなPythonアプリケーションには、少し過剰だと言えるでしょう。
一方、Python用Dockerベースイメージには:3.10-slimもあります。重大度が高い問題は1件、中程度は1件、低い問題は35件です。ベースサイズは46.2 MBで、こちらもDebian 11をベースとしたオペレーティングシステムであり、インストール済みパッケージは106個です。デフォルトのイメージの代わりにこのDockerベースイメージをPythonアプリケーションサーバーに選ぶだけで、セキュリティ脆弱性、ディスク上のサイズ、インストール済みライブラリの数を減らしつつ、Pythonアプリケーションに必要な:3.10の要件も満たせます。
これで決まりですね!タグに:3.10-slimを追加すれば準備完了です!
あと一歩です!最初のルール、つまり要件を満たす小さなDockerベースイメージを選ぶことはできましたが、2つ目の課題が残っています。Pythonアプリケーションサーバーをビルドするたびに、まったく同じDockerベースイメージが使われるようにする必要があります。
その方法はいくつかあります。
Docker HubからDockerベースイメージのダイジェストを取得する。
docker pull python:3.10-slimを実行してDockerイメージをコンピューターにダウンロードすると、イメージダイジェストが表示されます。
すでにPython Dockerイメージがコンピューターにある場合は、docker images --digests | grep pythonコマンドを使って、ディスク上にあるイメージのダイジェストを確認できます。
ベースイメージのダイジェストがわかったら、前述のDockerfileに追加します。
Dockerfile
この方法なら、PythonアプリケーションのDockerイメージを再ビルドするたびに、同じ基盤のオペレーティングシステムとライブラリのバージョンが使われます。これにより、再現性の高いビルドを実現できます。
2. 依存関係とソースコードを分離する
この2つ目のベストプラクティスは、依存関係のあるプロジェクトを含むあらゆるDockerイメージでよくあるエラーを防ぎます。まず、避けるべき方法を見てみましょう。
プロジェクトフォルダー内のすべてをイメージのコンテキストにコピーする。
依存関係をインストールする。
アプリケーションを実行する。
確かに動きますが、改善の余地は大いにあります。まず、ローカルでプロジェクトを開発するとき、依存関係が変わった場合にだけインストールしますよね?コードをほんの少し変更するたびに、Dockerイメージに依存関係をダウンロードしてインストールさせるのはやめましょう。
このベストプラクティスでは、Dockerイメージのレイヤーを最適化します。Dockerビルドでキャッシュシステムを活用するには、Dockerfileを書く際に、変更される可能性の高いものから順にレイヤーを並べることが重要です。
これまで使ってきたDockerfileを見てみましょう。Pythonアプリケーションのイメージをビルドするたびに、Dockerは各レイヤーを調べて、次のように判断します。何か変更されたのか、それとも以前のものをそのまま使えるのか?
現在のDockerfileでは、プロジェクトフォルダーに変更があるとCOPY命令が再実行され、その後のビルドレイヤーもすべて再実行されます。これは合理的とは言えず、最適化や高速化の余地が大いにあります。
次のDockerfileを使って改善しましょう。
この新しいDockerfileでは、次回Dockerがレイヤーを再利用できるか確認したとき、requirements.txtファイルに変更がなければ、COPY命令までスキップします。これなら数秒で完了します。この小さな変更によって、ビルドプロセスを大幅に高速化できます。コードを少し変更するたびに、ビルドの間に何分も待つ必要はもうありません。
依存関係のすべてがwheelsとしてパッケージ化されているとは限らない点に注意してください。その場合、イメージにコンパイラーをインストールする必要があります。
でも、アプリケーションの実行には可能な限り小さなイメージを使うべきだと言っていましたよね!
その通りです。そこで、Dockerのもう1つの優れた機能、マルチステージビルドを紹介します。
マルチステージビルド
マルチステージビルドでは、必要な依存関係のコンパイルに多くのツールを含むDockerイメージを使い、その後、生成された成果物のうち必要なものだけを、実際に使うPython Dockerイメージにコピーします。
Node.jsベースのアプリケーションを例にすると、次のようになります。
注:これはマルチステージビルドの機能を紹介するための簡単な例です。Node.jsアプリケーションの適切なコンテナ化に関するベストプラクティスを知りたい方は、Liran Tal(見覚えのある名前ですね……)とYoni Goldbergによるこちらの記事をご覧ください。
Node.jsのマルチステージビルドは比較的簡単です。node_modulesフォルダーがプロジェクト本体と同じフォルダー内にあるためです。しかし、Pythonアプリケーションではそうはいきません。
単にpip installを実行すると、あちこちにさまざまなものがインストールされ、マルチステージビルドを実行できなくなります。これを解決する方法は2つあります。
pip install --userを使うvirtualenvを使う
pip install --userを使えば、すべてのパッケージが~/.localディレクトリーにインストールされるため、あるステージから別のステージへのコピーが簡単にできそうです。しかし、別の問題が生じます。依存関係のコンパイルに使ったイメージのシステムレベルの依存関係がすべて、最終的なDockerベースイメージに追加されてしまうのです。これは避けたいところです(できる限り小さなDockerベースイメージにするというベストプラクティスを思い出してください)。
最初の方法は使えないため、2つ目のvirtualenvを使う方法を見ていきましょう。この方法を使うと、次のようなDockerfileになります。
これで、必要な依存関係をすべて揃えつつ、コンパイルに必要な追加パッケージを含めずに済みます。
Pythonアプリケーションのコンテナ化でマルチステージビルドを使う際の既知の問題
現在、前段のステージがキャッシュされないという既知の問題があります。最も簡単な解決方法は、BuildKitを使い、ビルドプロセスに引数BUILDKIT_INLINE_CACHE=1を追加することです。
最初のビルドは通常どおり行いますが、2回目以降は次のコマンドを使います。
DockerにビルドプロセスでBuildKitを使わせるには、環境変数DOCKER_BUILDKIT=1が必要です。また、Dockerソフトウェアの設定ファイル/etc/docker/daemon.jsonに次の設定を追加して有効にすることもできます(この場合、環境変数は不要です)。
3. 本番環境ではPython WSGIを使う
本番環境向けに構築したPythonアプリケーションでデバッグモードを有効にしたままにするのは、絶対に避けるべきことであり、セキュリティインシデントを招きかねません。残念ながら、コンテナ化されたPython Flaskアプリケーションや、その他のWSGI対応Pythonアプリケーションフレームワークに関する多くのブログ記事で、よく見かける誤りです。
デバッガーを使うと、ブラウザーから任意のPythonコードを実行できるため、セキュリティ上の非常に大きなリスクが生じます。ある程度は防ぐことができますが、脆弱性が残ることに変わりはありません。セキュリティのため、もう一度強調します。本番環境では、開発サーバーやデバッガーを実行しないでください!これは、コンテナ化したPythonアプリケーションにも当てはまります。
WSGIサーバーとWebサーバー/プロキシを設定するより、Python Flaskアプリケーションをデバッグモードでデプロイするほうが簡単なのはわかります。しかし、アプリケーションがハッキングされた理由を説明する羽目になるまでは、という話です。
では、どうすればよいのでしょうか?まず、使いたいWSGIサーバーの実装を選びましょう。Pythonアプリケーションで一般的に使われるのは、主に次の4つです。
Green Unicorn(Gunicorn) — RubyのUnicornプロジェクトから移植された、prefork型のワーカーモデルです。
uWSGI — 汎用性とパフォーマンスに優れ、リソース消費を抑えたWSGIサーバーの実装です。
mod_wsgi — Python WSGI仕様に対応するあらゆるWebアプリケーションをホストできるApacheモジュール。
CherryPy — Pythonらしいオブジェクト指向のHTTPフレームワークで、WSGIサーバーとしても機能します。
この記事では例としてGunicornを使用しますが、それぞれのドキュメントや情報もぜひご覧になり、ご自身のニーズに最適なものをお選びください。この記事では、ユースケースによって異なるため、設定については扱いません。
コンテナ化にあたって必要なのは、次の作業だけです。
requirements.txtにgunicornの依存関係を追加するPythonアプリケーションコンテナのエントリーポイントを変更する(変更には
CMD命令を使用します)。
この新しいDockerfileを使ってPythonアプリケーションを再ビルドしたら、Flaskアプリケーションがリクエストを処理できる状態かどうかを実行してテストできます。
注: 本番環境向けのデプロイでは、Gunicornが公開するポートをホストに直接バインドしないことをおすすめします。代わりに、同じネットワーク内にリバースプロキシサーバーをデプロイし、すべてのHTTPリクエストを処理して静的ファイルを配信することをおすすめします。
4. コンテナ化したPythonアプリケーションは、可能な限り最小限の権限で実行する(rootでの実行は絶対に避ける)
最小権限の原則は、Unixの黎明期から長く用いられているセキュリティ対策です。コンテナ化したPythonアプリケーションを実行する際も、常にこの原則に従いましょう。
公式のpython Dockerイメージには、デフォルトで権限を持つユーザーが含まれていません。そのため、最小権限のユーザーでプロセスを実行できるよう、ユーザーを作成する必要があります。
そのために、実際にgunicornプロセスを実行する最終イメージ(マルチステージビルドの第2ステージ)のDockerfileに、次のgroupaddコマンドを追加します。
ただし問題があります。前述の変更を行うと、Dockerが実行するシステムプロセスの所有者はユーザーpythonになります……。コピーしたファイルやWORKDIRディレクトリの所有者はどうなるでしょうか?Dockerは、デフォルトでWORKDIRディレクトリが存在しない場合に作成しますが、その所有者はシステムユーザーのrootになります。そのため、ディレクトリへの書き込みを伴う操作を行うと、アプリケーションが致命的なエラーを起こす可能性があります。また、所有権の動作を変更しなければ、ユーザーを変更済みであっても、コピーしたファイルの所有者はデフォルトでrootになります。
修正しましょう。
上記のようにCOPY命令を更新すると、ファイルの所有権に起因するWORKDIRディレクトリでの予期しない動作を防ぎ、すべてのファイルがプロセスを実行するユーザーと同じユーザーに所有されるようにできます。
5. コンテナ化したPythonアプリケーションの異常状態に対処する
アプリケーションをデプロイする際は、未処理のイベントや問題によってアプリケーションが異常な状態になる可能性を考慮する必要があります。この状態では、1)アプリケーションが動作しなくなっても、2)プロセスは終了しません。この場合、コンテナには通知が届かず、実行中のPythonアプリケーションサーバーがHTTPリクエストに応答しなくなります。
これを防ぐには、ヘルスチェック用のエンドポイントを実装します。コンテナ化したPythonアプリケーションの健全性を確認するには、アプリケーションがユーザーのリクエストを引き続き正常に処理できることを確認するため、monitoringまたはhealthのHTTPエンドポイントを設けることをおすすめします。FlaskなどのPython用Webアプリケーションフレームワークでは、簡単に実装できます(以下のFlaskの例を参照)。DockerのHEALTHCHECK命令と組み合わせることで、アプリケーションの健全性を適切に監視できます。
以下は、/health HTTPエンドポイントを追加するPython Flaskアプリケーションのコード例です。
このエンドポイントをアプリケーションに実装したら、DockerfileにHEALTHCHECK命令を追加するだけです。
Kubernetesにデプロイする場合、DockerfileのHEALTHCHECKディレクティブは無視される点に注意してください。代わりに、YAMLにKubernetesのliveness、readiness、startupプローブを設定する必要があります。
そのため、上記のHEALTHCHECK命令をKubernetesへのデプロイで使用する場合は、次のように対応します。
この設定は、PodまたはデプロイメントのYAMLファイルにあるcontainer specの部分にネストされます。alwaysまたはunless_stoppedの再起動ポリシーを設定すると、Pythonアプリケーションコンテナが異常な状態になった場合に必ず再起動されます。
6. Python Dockerアプリケーションイメージのセキュリティ脆弱性を見つけて修正する
すでに見てきたように、Dockerのベースイメージが大きいと、保守やセキュリティ修正の適用、最新状態の維持が必要なソフトウェアスタックが増えるなど、さまざまな問題が生じます。
さまざまなベースイメージのサイズや脆弱性の指標を確認するために、Snyk Advisorを使う方法も紹介しました。しかし、Advisorはほんの入り口にすぎません。Snykは無料の開発者向けセキュリティプラットフォームです。PythonコードやPythonの依存関係(requirements.txt内のものなど)、アプリケーションを実行するPythonコンテナイメージ、さらにそれらをオーケストレーションするTerraformやKubernetesの設定まで、あらゆるものをテストできます。
Snykの優れた点は、検出した脆弱性に対する修正案を提示することです。セキュリティ脆弱性の存在を知らせるだけでなく、修正用のPRを自動で作成したり、修復方法を提案したりします。しかも、既存のツール(IDE、CLI、Dockerなど)やワークフロー(Git、CI/CDなど)内で、すべてを実行できます。
例:Snykでコンテナ化したPythonアプリをスキャンする
このサンプルPython Flaskアプリケーションのビルドにpython:3.8のPython Dockerイメージを使用した場合、Snyk CLIでどのようにスキャンできるか見てみましょう。
Snyk CLIをインストールすると、Pythonプロジェクトの依存関係やPythonコードなどをスキャンできます。まず、Snyk CLIをインストールしましょう。Node.js環境がある場合は、次のようにnpmパッケージマネージャーを使ってインストールできます。
macOSまたはLinuxでHomebrewを使用している場合は、次のようにインストールできます。
その他のインストール方法については、Snyk CLIのインストール方法をご覧ください。
次に、脆弱性データベースを照会するための有効なAPIトークンを取得するため、CLIで認証します。
ここまで完了したら、python:3.8ベースイメージを使ってPythonアプリケーションのDockerイメージをローカルにビルドします。
続いて、次のコマンドを実行してSnykでスキャンしましょう。
実行すると、次の結果が表示されます(出力が長いため、意図的に短縮しています)。
このPython 3.8オペレーティングシステムの内容には、オープンソースライブラリとして427個の依存関係が含まれています。ベースイメージにpython:3.8を選んだため、このPython Flaskアプリケーションには合計171件のセキュリティ脆弱性が持ち込まれています。
ここで「どうすれば修正できるのだろう?」と疑問に思うかもしれません。幸い、Snykは攻撃対象領域を縮小するために、アップグレードまたは完全に切り替えられる別のベースイメージを提案します。
ベースイメージの推奨内容を視覚的にわかりやすく示したスクリーンショットをご覧ください。

これで、データに基づいてPythonアプリケーションのセキュリティを強化する判断ができます。Snykが推奨する代替Dockerイメージを選ぶことで、アプリケーションに含まれるソフトウェアの攻撃対象領域を大幅に縮小できます。
アプリケーションのセキュリティをさらに管理するには、リポジトリをSnyk UIに接続してソースコードとDockerfileをインポートしましょう。脆弱性を検出できるだけでなく、新たなセキュリティ問題がないか継続的に監視できます。以下は、同じDockerベースイメージのレポートをSnyk UIで表示したものです。

セキュリティ脆弱性を監視して見つけるだけでなく、修正までできたら最高ですよね!:-)
GitリポジトリをSnykに接続すると、ここに示すように、Dockerベースイメージのアップグレードを提案するプルリクエストをリポジトリに自動で作成することもできます。

興味があれば、Dockerfileのプルリクエストでコンテナセキュリティを自動化する方法を紹介した続編もご覧ください。
Pythonアプリケーションをコンテナ化するにはどうすればよいですか?
Dockerは、PythonのDockerイメージをベースにしたコンテナ化済みのPythonアプリケーションとして、再利用可能でクロスプラットフォームに対応し、すばやくデプロイできるソフトウェアを構築できる仮想化技術です。これらのアプリケーションは、Dockerfileというファイルを使い、Infrastructure as Codeとして定義します。
コンテナ化されたPythonアプリケーションをビルドして使用するには、次のコマンドを実行します。
docker build -t flask-application . docker run -p 8080:5000 flask-application
開発者向けPythonセキュリティの推奨事項
こうしたベストプラクティスは、コンテナ化したPythonアプリの作成、管理、保護に役立ちます。この記事を楽しんでいただき、アプリケーションセキュリティやセキュリティ推進に関心をお持ちの方には、次の資料もおすすめします。
Daniel Bermanによる、Snykを使ったセキュアなPython開発の始め方
便利なチートシート付き、Brian VermeerによるSnyk CLI活用の秘訣
Liran TalとYoni GolbergによるDockerのベースアプリケーションに関するNode.jsの包括的なベストプラクティス
最後に、Javaを使う方には、Brian VermeerによるJava開発者のためのDocker:セキュリティを損なわないために知っておくべき5つのことをおすすめします。
Frank FischerによるPythonセキュリティのベストプラクティス・チートシート
Pythonプロジェクトでよくあるセキュリティ問題を詳しく分析した、情報満載のレポート
開発者ファーストのコンテナセキュリティ
Snykは、コンテナイメージとKubernetesワークロードの脆弱性を検出し、自動的に修正します。



