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

これは、Python 開発をコンテナー化する方法に関する ブログ投稿シリーズの 第 2 部です。 パート 1 では、Python サービスをコンテナー化する方法とそのベスト プラクティスを既に示しました。 このパートでは、他のコンポーネントを設定してコンテナ化されたPythonサービスに接続する方法について説明します。 プロジェクト ファイルとデータを整理する優れた方法と、Docker Compose を使用してプロジェクト全体の構成を管理する方法を示します。 また、コンテナー化された開発プロセスを高速化するための Compose ファイルを作成するためのベスト プラクティスについても説明します。

Docker Composeを使用したプロジェクト構成の管理

例として、マイクロサービス アーキテクチャに従って機能を 3 層に分けるアプリケーションを取り上げましょう。 これは、マルチサービス アプリケーションではかなり一般的なアーキテクチャです。 サンプル アプリケーションは、次のもので構成されています。

  • UI 層 – nginx サービスで実行される
  • ロジック 層 – 私たちが焦点を当てているPythonコンポーネント
  • データ 層 – MySQLデータベースを使用して、ロジック層に必要なデータを保存します
コンテナ化された python 開発 2 1

アプリケーションを階層に分割する理由は、プロジェクト全体を作り直すことなく、新しい階層を簡単に変更または追加できるためです。

プロジェクト ファイルを構造化する良い方法は、各サービスのファイルと構成を分離することです。 これは、プロジェクト内のサービスごとに専用のディレクトリを用意することで簡単に実行できます。 これは、コンポーネントを明確に表示し、各サービスを簡単にコンテナー化する場合に非常に便利です。 また、他のサービスファイルを誤って変更する可能性があることを心配することなく、サービス固有のファイルを操作するのに役立ちます。

サンプルアプリケーションには、次のディレクトリがあります。

Project
├─── web
└─── app
└─── db

Python コンポーネントをコンテナー化する方法については、このブログ投稿シリーズの最初の部分で既に説明しました。 同じことが他のプロジェクトコンポーネントにも当てはまりますが、ここで説明する構造を実装するサンプルに簡単にアクセスできるため、詳細は省略します。 awesome-compose リポジトリによって提供される nginx-flask-mysql の例はそれらの1つです。 

これは、Dockerfile が配置された更新されたプロジェクト構造です。 Webコンポーネントとdbコンポーネントに同様の設定があるとします。

Project
├─── web
├─── app
│ ├─── Dockerfile
│ ├─── requirements.txt
│ └─── src
│ └─── server.py
└─── db

これで、コンテナー化されたすべてのプロジェクト コンポーネントに対してコンテナーを手動で開始できるようになりました。 ただし、それらを通信させるには、ネットワークの作成を手動で処理し、コンテナをそれにアタッチする必要があります。 これはかなり複雑で、頻繁に行う必要がある場合は貴重な開発時間がかかります。

ここで、Docker Composeは、ローカル環境でコンテナを調整し、サービスをスピンアップおよび停止するための非常に簡単な方法を提供します。このために必要なのは、プロジェクトのサービスの構成を含むComposeファイルを作成することだけです。 それができたら、1つのコマンドでプロジェクトを実行できます。

ファイルの作成

Composeファイルの構造と、それを使用してプロジェクトサービスを管理する方法を見てみましょう。

以下は、プロジェクトのサンプルファイルです。 ご覧のとおり、サービスのリストを定義します。 dbセクションでは、適用する特定の構成がないため、基本イメージを直接指定します。 一方、私たちのWebおよびアプリサービスは、Dockerファイルからイメージを構築します。 サービスイメージを取得できる場所に応じて、ビルドフィールドまたはイメージフィールドを設定できます。 ビルド フィールドには、内部に Dockerfile を含むパスが必要です。

docker-compose.yaml

version: "3.7"
services:
  db:
    image: mysql:8.0.19
    command: '--default-authentication-plugin=mysql_native_password'
    restart: always
    environment:
      - MYSQL_DATABASE=example
      - MYSQL_ROOT_PASSWORD=password

  app:
    build: app
    restart: always

  web:
    build: web
    restart: always
    ports:
      - 80:80

データベースを初期化するには、環境変数をDB名とパスワードで渡すことができますが、Webサービスの場合は、プロジェクトのWebインターフェイスにアクセスできるようにコンテナポートをローカルホストにマップします。

Docker Composeを使用してプロジェクトをデプロイする方法を見てみましょう。 

ここで行う必要があるのは、プロジェクトのルートディレクトリにdocker-compose.yamlを配置してから、docker-composeを使用してデプロイするためのコマンドを発行することだけです。

Project
├─── docker-compose.yaml
├─── web
├─── app
└─── db

Docker Composeは、Docker Hubからmysqlイメージをプルしてdbコンテナを起動し、Webおよびアプリサービスの場合は、イメージをローカルでビルドしてから、それらからコンテナを実行します。また、デフォルトのネットワークを作成し、その中にすべてのコンテナを配置して、相互に到達できるようにします。

コンテナ化された Python 開発 2 2

これはすべて、1つのコマンドでトリガーされます。

$ docker-compose up -d
Creating network "project_default" with the default driver
Pulling db (mysql:8.0.19)…

Status: Downloaded newer image for mysql:8.0.19
Building app
Step 1/6 : FROM python:3.8
---> 7f5b6ccd03e9
Step 2/6 : WORKDIR /code
---> Using cache
---> c347603a917d
Step 3/6 : COPY requirements.txt .
---> fa9a504e43ac
Step 4/6 : RUN pip install -r requirements.txt
---> Running in f0e93a88adb1
Collecting Flask==1.1.1

Successfully tagged project_app:latest
WARNING: Image for service app was built because it did not already exist. To rebuild this image you must use docker-compose build or docker-compose up --build.
Building web
Step 1/3 : FROM nginx:1.13-alpine
1.13-alpine: Pulling from library/nginx

Status: Downloaded newer image for nginx:1.13-alpine
---> ebe2c7c61055
Step 2/3 : COPY nginx.conf /etc/nginx/nginx.conf
---> a3b2a7c8853c
Step 3/3 : COPY index.html /usr/share/nginx/html/index.html
---> 9a0713a65fd6
Successfully built 9a0713a65fd6
Successfully tagged project_web:latest

Creating project_web_1 … done
Creating project_db_1 … done
Creating project_app_1 … done

Check the running containers:

$ docker-compose ps
  Name         Command                        State  Ports
-------------------------------------------------------------------------
project_app_1  /bin/sh -c python server.py    Up
project_db_1   docker-entrypoint.sh --def ... Up     3306/tcp, 33060/tcp
project_web_1  nginx -g daemon off;           Up     0.0.0.0:80->80/tcp

To stop and remove all project containers run:

$ docker-compose down
Stopping project_db_1 ... done
Stopping project_web_1 ... done
Stopping project_app_1 ... done
Removing project_db_1 ... done
Removing project_web_1 ... done
Removing project_app_1 ... done
Removing network project-default

To rebuild images we can run a build and then an up command to update the state of the project containers:

$ docker-compose build
$ docker-compose up -d

As we can see, it is quite easy to manage the lifecycle of the project containers with docker-compose.

Best practices for writing Compose files

Let us analyse the Compose file and see how we can optimise it by following best practices for writing Compose files.

Network separation

When we have several containers we need to control how to wire them together. We need to keep in mind that, as we do not set any network in the compose file, all our containers will end in the same default network.

Containerized python development 2 3

This may not be a good thing if we want only our Python service to be able to reach the database. To address this issue, in the compose file we can actually define separate networks for each pair of components. In this case the web component won’t be able to access the DB.

Containerized python development 2 4

Docker Volumes

Every time we take down our containers, we remove them and therefore lose the data we stored in previous sessions. To avoid that and persist DB data between different containers, we can exploit named volumes. For this, we simply define a named volume in the Compose file and specify a mount point for it in the db service as shown below:

version: "3.7"
services:
  db:
    image: mysql:8.0.19
    command: '--default-authentication-plugin=mysql_native_password'
    restart: always
    volumes:
      - db-data:/var/lib/mysql

    networks:
      - backend-network
    environment:
      - MYSQL_DATABASE=example
      - MYSQL_ROOT_PASSWORD=password

  app:
    build: app
    restart: always
    networks:
      - backend-network
      - frontend-network

  web:
    build: web
    restart: always
    ports:
      - 80:80
    networks:
      - frontend-network
volumes:
  db-data:

networks:
  backend-network:
  frontend-network:

 We can explicitly remove the named volumes on docker-compose down if we want.

Docker Secrets

As we can observe in the Compose file, we set the db password in plain text. To avoid this, we can exploit docker secrets to have the password stored and share it securely with the services that need it. We can define secrets and reference them in services as below. The password is being stored locally in the project/db/password.txt file and mounted in the containers under /run/secrets/<secret-name>.

version: "3.7"
services:
  db:
    image: mysql:8.0.19
    command: '--default-authentication-plugin=mysql_native_password'
    restart: always
    secrets:
      - db-password

    volumes:
      - db-data:/var/lib/mysql
    networks:
      - backend-network
    environment:
      - MYSQL_DATABASE=example
      - MYSQL_ROOT_PASSWORD_FILE=/run/secrets/db-password

  app:
    build: app
    restart: always
    secrets:
      - db-password

    networks:
      - backend-network
      - frontend-network

  web:
    build: web
    restart: always
    ports:
      - 80:80
    networks:
      - frontend-network
volumes:
  db-data:
secrets:
  db-password:
    file: db/password.txt

networks:
  backend-network:
  frontend-network:

We have now a well defined Compose file for our project that follows best practices. An example application exercising all the aspects we discussed can be found here.

What’s next?

This blog post showed how to set up a container-based multi-service project where a Python service is wired to other services and how to deploy it locally with Docker Compose.

In the next and final part of this series, we show how to update and debug the containerized Python component.

Resources

フィードバック

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