コンテナ化された Python 開発 – パート 1

ローカル環境でのPythonプロジェクトの開発は、複数のプロジェクトが同時に開発されている場合、かなり困難になる可能性があります。 プロジェクトのブートストラップは、バージョンを管理し、依存関係と構成を設定する必要があるため、時間がかかる場合があります。 以前は、すべてのプロジェクト要件をローカル環境に直接インストールしてから、コードの記述に集中していました。 ただし、同じ環境で複数のプロジェクトが進行中であると、構成や依存関係の競合が発生する可能性があるため、すぐに問題になります。 さらに、チームメイトとプロジェクトを共有するときは、環境も調整する必要があります。 このためには、プロジェクト環境を簡単に共有できるように定義する必要があります。 

これを行う良い方法は、プロジェクトごとに分離開発環境を作成することです。 これは、コンテナーと Docker Compose を使用してコンテナーを管理することで簡単に実行できます。 これについては、それぞれが特定の焦点を当てた 一連のブログ投稿で説明します。

この最初の部分では、Python サービス/ツールをコンテナー化する方法とそのベスト プラクティスについて説明します。

必要条件

この ブログ投稿シリーズで説明した内容を簡単に実行するには、コンテナー化された環境をローカルで管理するために必要な最小限のツール セットをインストールする必要があります。

Python サービスをコンテナー化する

単純な Flask サービスを使用してこれを行う方法を示し、他のコンポーネントを設定せずにスタンドアロンで実行できるようにします。

server.py

from flask import Flask
server = Flask(__name__)

@server.route("/")
 def hello():
    return "Hello World!"

if __name__ == "__main__":
   server.run(host='0.0.0.0')

このプログラムを実行するには、最初に必要なすべての依存関係がインストールされていることを確認する必要があります。 依存関係を管理する 1 つの方法は、pip などのパッケージ インストーラーを使用することです。 このためには、要件.txtファイルを作成し、その中に依存関係を書き込む必要があります。 単純な server.py のこのようなファイルの例は次のとおりです。

requirements.txt

Flask==1.1.1

これで、次の構造になりました。

app
├─── requirements.txt
└─── src
     └─── server.py

ソースコード専用のディレクトリを作成して、他の構成ファイルから分離します。 これを行う理由は後で説明します。

Pythonプログラムを実行するには、あとはPythonインタプリタをインストールして実行するだけです。 

このプログラムはローカルで実行できます。 しかし、これは、異なる競合する要件を持つプロジェクト間で簡単に切り替えることができるクリーンな標準開発環境を維持するという開発をコンテナ化する目的に反します。

次に、このPythonサービスを簡単にコンテナ化する方法について説明します。

ドッカーファイル 

Python コードをコンテナーで実行する方法は、Python コードを Docker イメージとしてパックし、それに基づいてコンテナーを実行することです。 手順を以下にスケッチします。

コンテナ化されたパイソン

Docker イメージを生成するには、イメージのビルドに必要な手順を含む Dockerfile を作成する必要があります。 その後、ドッカーファイルは、ドッカーイメージを生成するドッカービルダーによって処理されます。 次に、単純な docker run コマンドを使用して、Python サービスでコンテナーを作成して実行します。

ドッカーファイルの分析

hello world Python サービス用の Docker イメージをアセンブルするための手順を含む Dockerfile の例は次のとおりです。

Dockerfile

# set base image (host OS)
FROM python:3.8

# set the working directory in the container
WORKDIR /code

# copy the dependencies file to the working directory
COPY requirements.txt .

# install dependencies
RUN pip install -r requirements.txt

# copy the content of the local src directory to the working directory
COPY src/ .

# command to run on container start
CMD [ "python", "./server.py" ]

Dockerfile からの命令またはコマンドごとに、Docker ビルダーはイメージ レイヤーを生成し、それを前のレイヤーにスタックします。 したがって、プロセスの結果として得られるDockerイメージは、異なるレイヤーの読み取り専用スタックにすぎません。

ビルドコマンドの出力では、Dockerfile命令がステップとして実行されているのも確認できます。

$ docker build -t myimage .
Sending build context to Docker daemon 6.144kB
Step 1/6 : FROM python:3.8
3.8.3-alpine: Pulling from library/python

Status: Downloaded newer image for python:3.8.3-alpine
---> 8ecf5a48c789
Step 2/6 : WORKDIR /code
---> Running in 9313cd5d834d
Removing intermediate container 9313cd5d834d
---> c852f099c2f9
Step 3/6 : COPY requirements.txt .
---> 2c375052ccd6
Step 4/6 : RUN pip install -r requirements.txt
---> Running in 3ee13f767d05

Removing intermediate container 3ee13f767d05
---> 8dd7f46dddf0
Step 5/6 : COPY ./src .
---> 6ab2d97e4aa1
Step 6/6 : CMD python server.py
---> Running in fbbbb21349be
Removing intermediate container fbbbb21349be
---> 27084556702b
Successfully built 70a92e92f3b5
Successfully tagged myimage:latest

次に、イメージがローカルイメージストアにあることを確認できます。

$ docker images
REPOSITORY    TAG       IMAGE ID        CREATED          SIZE
myimage       latest    70a92e92f3b5    8 seconds ago    991MB

開発中に、Python サービスのイメージを複数回再構築する必要がある場合があり、できるだけ時間を短縮する必要があります。 次に、これに役立つ可能性のあるいくつかのベストプラクティスを分析します。

ドッカーファイルの開発のベストプラクティス

現在、開発サイクルをスピードアップするためのベストプラクティスに焦点を当てています。 本番環境に焦点を当てたものについては、この ブログ投稿ドキュメントで 詳しく説明します。

ベースイメージ

Dockerfile の最初の命令では、アプリケーションの新しいレイヤーを追加する基本イメージを指定します。 ベースイメージの選択は、その上に構築されたレイヤーの品質に影響を与える可能性があるため、非常に重要です。 

可能であれば、一般的に頻繁に更新され、セキュリティ上の懸念が少ない公式画像を常に使用する必要があります。

基本イメージの選択は、最終的なイメージのサイズに影響を与える可能性があります。 他の考慮事項よりもサイズを優先する場合は、非常に小さいサイズでオーバーヘッドの低い基本イメージの一部を使用できます。 これらの画像は通常、 高山 分布に基づいており、それに応じてタグ付けされています。 ただし、Pythonアプリケーションの場合、公式のDocker Pythonイメージのスリムバリアントは、ほとんどの場合(例:. python:3.8-slim)。

ビルドキャッシュを活用するための命令順序の問題

イメージを頻繁にビルドする場合は、ビルダーキャッシュメカニズムを使用して、後続のビルドを高速化する必要があります。 前述のように、Dockerfile 命令は指定された順序で実行されます。 命令ごとに、ビルダーは最初にキャッシュをチェックして、再利用するイメージを探します。 レイヤーの変更が検出されると、そのレイヤーとそれ以降のすべてのレイヤーが再構築されます。

キャッシングメカニズムを効率的に使用するために、頻繁に変更されるレイヤーの命令を、変更の少ないレイヤーの後に配置する必要があります。

Dockerfile の例を確認して、命令の順序がキャッシュにどのように影響するかを理解しましょう。 興味深い行は以下のものです。

...
# copy the dependencies file to the working directory
COPY requirements.txt .

# install dependencies
RUN pip install -r requirements.txt

# copy the content of the local src directory to the working directory
COPY src/ .
...

開発中、アプリケーションの依存関係は Python コードよりも頻繁に変更されません。 このため、コード1の前のレイヤーに依存関係をインストールすることを選択します。 したがって、依存関係ファイルをコピーしてインストールしてから、ソースコードをコピーします。 これが、ソースコードをプロジェクト構造の専用ディレクトリに分離した主な理由です。

マルチステージビルド 

これは開発時にはあまり役に立たないかもしれませんが、開発が完了したらコンテナ化されたPythonアプリケーションを出荷するのに興味深いので、すぐに説明します。 

マルチステージビルドを使用する際に私たちが求めているのは、不要なファイルとソフトウェアパッケージをすべて最終的なアプリケーションイメージから取り除き、Pythonコードの実行に必要なファイルのみを配信することです。 前の例のマルチステージ Dockerfile の簡単な例は次のとおりです。

# first stage
FROM python:3.8 AS builder
COPY requirements.txt .

# install dependencies to the local user directory (eg. /root/.local)
RUN pip install --user -r requirements.txt

# second unnamed stage
FROM python:3.8-slim
WORKDIR /code

# copy only the dependencies installation from the 1st stage image
COPY --from=builder /root/.local /root/.local
COPY ./src .

# update PATH environment variable
ENV PATH=/root/.local:$PATH

CMD [ "python", "./server.py" ]

最初の 1 つだけを ビルダーとして指定する 2 段階のビルドがあることに注意してください。 FRO<NAME>M 命令に AS <NAME &gt; を追加してステージに名前を付け、必要なファイルのみを最終イメージにコピーする COPY i 命令でこの名前を使用します。

この結果、アプリケーションの最終的な画像がスリムになります。

$ docker images
REPOSITORY    TAG      IMAGE ID       CREATED         SIZE
myimage       latest   70a92e92f3b5   2 hours ago     991MB
multistage    latest   e598271edefa   6 minutes ago   197MB

この例では、 pip の –user オプションを使用して、ローカル ユーザー ディレクトリへの依存関係をインストールし、そのディレクトリを最終的なイメージにコピーします。 ただし、virtualenvやパッケージをホイールとして構築し、それらをコピーして最終イメージにインストールするなど、他のソリューションも利用できます。

コンテナーを実行する

Dockerfile を書き込み、そこからイメージをビルドしたら、Python サービスを使用してコンテナーを実行できます。

$ docker images
REPOSITORY   TAG      IMAGE ID       CREATED       SIZE
myimage      latest   70a92e92f3b5   2 hours ago   991MB
...

$ docker ps
CONTAINER ID   IMAGE   COMMAND   CREATED   STATUS   PORTS   NAMES

$ docker run -d -p 5000:5000 myimage
befb1477c1c7fc31e8e8bb8459fe05bcbdee2df417ae1d7c1d37f371b6fbf77f

これで、hello worldサーバーをコンテナ化し、localhostにマップされたポートを照会できるようになりました。

$ docker ps
CONTAINER     ID        IMAGE        COMMAND        PORTS                   ...
befb1477c1c7  myimage   "/bin/sh -c  'python ..."   0.0.0.0:5000->5000/tcp  ...

$ curl http://localhost:5000
"Hello World!"

次は何ですか?

この投稿では、開発エクスペリエンスを向上させるために Python サービスをコンテナー化する方法を示しました。 コンテナ化は、他のプラットフォームで簡単に再現できる決定論的な結果を提供するだけでなく、依存関係の競合を回避し、クリーンな標準開発環境を維持することを可能にします。 コンテナー化された開発環境は、標準環境を変更することなく簡単にデプロイできるため、管理が容易で、他の開発者と共有できます。  

このシリーズの次の投稿では、Python コンポーネントが他の外部コンポーネントに接続されているコンテナーベースのマルチサービス プロジェクトを設定する方法と、Docker Compose を使用してこれらすべてのプロジェクト コンポーネントのライフサイクルを管理する方法について説明します。

リソース

フィードバック

「コンテナ化されたPython開発–パート0」に関する1つの考え