Docker Compose を使用してPythonモジュールを作成

Docker Compose を使用して大きなシステムの中のPythonのモジュールの1つを作成する必要ができたので、勉強 and 備忘録のために本記事を作成いたしました。

# 目的
* 大きなシステムをチーム開発する際に、システムをモジュール分割し、互いに疎な開発を進めたい
* Docker Compose を使用することでモジュールごとにコンテナを立ち上げ、システムを開発したい
* 本記事は、そんな数あるモジュール + コンテナのうちの一つの開発環境を整えた際の備忘録

# Dockerとは
## Dockerの目的
チームでシステム開発を行う際に問題となるのは、開発環境の違いである。例えば、AさんはWindows、BさんはMacOSで開発を行っている場合に、AさんがWindows環境で開発したシステムをBさんのMacOSで動かすのは手間がかかるケースが多い。Dockerは、Dockerコンテナと呼ばれるアプリケーションの実行環境を各環境に構築し、コンテナ内でアプリケーションを実行することで、このような環境の違いに影響されることなく、アプリを開発、実行することができる。

## Dockerコンテナを立てる手順
コンテナは以下の手順で構築することができる。

1. Dockerfileを作成（自身で作成 or 公開されているものを使用）
1. Dockerfile から Dockerイメージを作成
```docker build -t my-app-image .```
1. Dockerイメージを使用してコンテナを起動
```docker run my-app-image```

それぞれについて軽く解説。詳しい使い方までは触れない。

### Dockerfile
Dockerfile とは、Dockerイメージを構成するテキストデータであり、Dockerイメージを作成するための設計図である。以下のように、ベースとなるイメージ、依存関係、作業ディレクトリ、コンテナ起動時の実行コマンドなどを記載。

```dockerfile
# Pythonベースのイメージを使用
FROM python:3.8

# 作業ディレクトリを設定
WORKDIR /app

# 依存関係をインストールするためのファイルをコピー
COPY requirements.txt .

# 依存関係をインストール
RUN pip install -r requirements.txt

# アプリケーションのソースコードをコピー
COPY . .

# コンテナ起動時に実行するコマンド
CMD ["python", "app.py"]

```

### Dockerイメージ
* 役割：実際のソフトウェアが動作するために必要なファイルや設定をパッケージ化したもの。**Dockerfile（設計図）に従って作成された完成品** と考えられる。「実行可能なソフトウェアのパッケージ」
* 状態：イメージは実行されていない「スナップショット」のようなもので、読み取り専用
* 内容：OS、アプリケーション、ライブラリ、依存関係などを含んでおり、これをもとにコンテナが動作

### Dockerコンテナ
* 役割：Dockerコンテナは、Dockerイメージを元に実行される動的な実行環境
* 状態：コンテナは実行中のプロセスで、読み書き可能です。実行中にファイルシステムに変更が加えられたり、コンテナ内のデータを変更が可能。コンテナの変更はコンテナ内に保存されるが、イメージには反映されない

### それぞれの比較（By GPT-4o）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2718862/b0fd6243-d636-6d7d-924c-8146a3a6c016.png)


# Docker Compose
巨大なシステムを構築する際は、モジュールごとに機能を開発することが多い。例えばチャットボットのアプリは大雑把に、以下のように機能分割することができる。

* テキストの入出力やフロントエンドを担当するモジュール
* 入力されたテキストから出力テキストをバックエンドで出力するモジュール

このようなモジュールは必要なライブラリも異なるし、複数人で開発している際は担当を分けることもある。このような場合に、それぞれ異なるDockerイメージを作成して開発を進めることになる。

実際にチャットボットアプリを起動する際に、全てのモジュールのコンテナを起動して、相互で通信を行うことになるが、このような**コンテナの同時立ち上げやコンテナ同士の振る舞いなどを定義・実行**するのがDocker Composeの役割である。

## 使い方の流れ
1. モジュールごとにDockerfileを作成
1. docker-compose.yml といったyml形式のファイルを用意し、モジュールごとの設定を記述
1. ```docker compose up -d``` を実行して全てのコンテナを一斉起動

つまりチーム開発において、Docker Composeで起動できるPythonモジュールを作成する際は、以下の手順が必要になる。

1. Pythonモジュールを作成
1. モジュールが実行可能なDockerイメージの設計図であるDockerfileを作成（ここで requirements.lock などを読み込んでライブラリ等を揃える）
1. docker-compose.yml ファイルに上記のDockerfileの起動を記載
1. ```docker compose up -d ``` コマンドで作成したモジュールが起動することを確認

# 実際に環境構築
これらの基礎知識を踏まえて実際にDockerを使用したPythonモジュールの環境構築に取り組んでみる。ディレクトリ構造は以下のものを想定。
```
project-root/
│
├── app/
│   ├── app.py
│   └── __init__.py  # (必要に応じて)
│
├── requirements.lock
├── Dockerfile
└── docker-compose.yml

```
`app.py`は以下の通り。
```python
# app.py
import numpy as np
from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    return "Hello, World from Docker!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

```
`requirements.lock`は以下の通り。
```text
numpy
flask
```


## Dockerfileの作成
以下のようにdockerfileを作成

```dockerfile
# ベースイメージを指定
FROM python:3.12

# 作業ディレクトリを設定
WORKDIR /app

# requirements.txtをコンテナにコピー
COPY requirements.lock .

# 依存関係をインストール
RUN pip install --no-cache-dir -r requirements.lock

# アプリケーションのソースコードをコピー
COPY app/ ./app

# コンテナ起動時の実行コマンド
CMD ["python", "app/app.py"]
```

それぞれが意味していることを説明していく。
## ベースイメージの指定
```Dockerfile
FROM python:3.12
```
* これは、Dockerイメージのベースイメージを指定するための行である。このようにしてPythonのベースイメージを指定することで、PythonインタープリタやPythonを動かすために必要な基本的なライブラリが含まれている
* ベースイメージを指定することで新しいコンテナをこのイメージ上に作り、Pythonアプリをその上で動作させることができる

## 作業ディレクトリの設定
```Dockerfile
WORKDIR /app
```
* 作成したばかりのコンテナはなんのファイルやディレクトリも含まないまっさらなファイル空間である
* WORKDIR {作業ディレクトリ} とすることで作業ディレクトリを指定することができる。Docker を起動しているホストマシンの空間とコンテナ内の空間は異なるので、仮にホストマシン上に app というディレクトリが存在しても関係ない
* もしホストとコンテナのディレクトリを関連付けたい場合はコンテナの方からホストマシンの対象ディレクトリをマウントする必要がある

## requirements.lockをコンテナにコピー
```dockerfile 
COPY requirements.lock .
```
* ホストマシン（Dockerコマンドを走らせているローカル環境）のカレントディレクトリにある `requirements.lock`をコンテナの作業ディレクトリ `/app`にコピーするコマンド
* ホストマシン内のファイルは何もしない状態では使用できず、このようにしてコンテナにコピーをすることで初めて使用可能

## 依存関係のインストール
```Dockerfile
RUN pip install --no-cache-dir -r requirements.lock
```
* `RUN`はDockerイメージのビルド中にコンテナ内でコマンドを実行するツール
* `pip install --no-cache-dir -r requirements.lock`で`requirements.lock`に記載されているライブラリをインストールするコマンド
* `--no-cache-dir`を使用することでダウンロードしたパッケージのキャッシュを保存してサイズが増加するのを防止

## アプリケーションのソースコードコピー
```dockerfile
COPY app/ ./app
```
* ホストマシン上のディレクトリ`app/`の中の全てのディレクトリをコンテナの作業ディレクトリ`/app`の中に`/app/app`としてコピー
* このコマンドを使用することによってアプリのソースコードがコピーされる
* 逆にこのコマンドを使用しないとソースコードがない空のコンテナになってしまう

## コンテナ起動時の実行コマンド
```dockerfile
CMD ["python", "app/app.py"]
```
* `CMD`はコンテナが起動した際に実行されるデフォルトのコマンド
* このコマンドにより先ほどワーキングディレクトリにコピーした`app`ディレクトリの中の`app.py`を実行
* `CMD`はイメージのビルド時ではなく、コンテナの実行時に起動するものであることに注意

# Dockerコンテナの起動
これらの条件化でDockerコンテナを起動してみる。まずはイメージのビルドを以下のコマンドで行う。
```
docker build -t flask-app .
```
`-t flask-app .` の部分は、カレントディレクトリ`.`にある`Dockerfile`という名称のファイルを読み込み`flask-app`というタグをつけるというオプションである。

ここでイメージが起動しているかどうかは`docker images`コマンドで確認できる。ビルドが成功していれば、以下のように出力される。
```
REPOSITORY                                 TAG       IMAGE ID       CREATED        SIZE
flask-app                                  latest    09acf5d77c98   2 hours ago    1.09GB
```
続いて、Dockerイメージからコンテナを起動する。コンテナの起動は以下のコマンドで実行できる。

```
docker run -d -p 5001:5000 flask-app
```
* `-d`は「デタッチドモード（detached mode）」を意味し、コンテナがバックグラウンドで実行されるため、このコマンドを入力したターミナルが操作可能な状態なまま残る
* `-p 5001:5000`はポートマッピングを指定するオプションであり、`5001`は外部からアクセスするポート、`5000`はコンテナ内でコンテナがリッスンするポート
* ホストマシンのポート`5001`に`http://localhost:5001`のようにアクセスするとコンテナ内のアプリのポート`5000`にマッピングされ`flask-app`にアクセス可能
* このようになっている理由は、このプログラムを試していた際、ホストマシンのポート`5000`が別のプロセスで埋まっていたため

この状態で、`http://localhost:5001/`をブラウザで開いて`Hello Docker`という文字列が表示されればコンテナの起動に成功していることがわかる。


# Docker compose を使用してコンテナを起動
これまで作成してきたDockerfileをDocker composeを使用して起動する。実際の開発では、今回作成したDockerfileに加え、他のチームメンバーが開発したDockerfileもこのDocker composeで同時に起動されることになる。先ほどの`project-root`直下に以下の`docker-compose.yml`ファイルを作成。

```yml
services:
  flask-app:
    build: .
    ports:
      - "5001:5000"
    environment:
      - FLASK_ENV=development
  # app2:
  #   build: .
  #   ports:
  #     - "5002:5003"
  #   environment:
  #     - FLASK_ENV=development
```
上記は、flask-appというタグをつけて、先ほど作成した Dockerコンテナ + flaskのPythonアプリを起動するための設定ファイルである。つまり、`docker build -t flask-app .`というコマンドと`docker run -d -p 5001:5000 flask-app`という2つのコマンドを上記ファイルでは、一括で実行可能。

* `Services`：複数のコンテナ（サービス）を定義する場所
* `flask-app`：1つ目のコンテナの名称
* `build：.`：カレントディレクトリから`flask-app`のDockerfileを探索してそれに従いbuild
* `ports`：ポート番号の指定
* `app2`：今回は作成していない2つめのサービス。ポート番号を変えているので、`flask-app`と同時に別々のポートからリクエストを受け付けることが可能

このファイルを以下のコマンドにより、読み込み実行。
```
docker-compose up --build
```
`docker-compose up`コマンドは、サービスをビルドし、起動するコマンド。`--build `をつけることで、イメージの再ビルドを矯正。新しいコードの変更や、Dockerfileの更新があった場合は、必ずこのオプションをつける必要がある。

`-d` オプションを付けることでバックグラウンド実行が可能になり、`--no-cache` でキャッシュを無視して再ビルドも可能。

# 参考サイト
https://qiita.com/riku_ogawa/items/4ffc6aaf7a3df053a4ad

https://docs.docker.jp/get-started/08_using_compose.html#