Python 初心者が始める AI 時代の Docker を使ったアプリ開発
最近の AI 技術の進歩は目覚ましく、OpenAI による ChatGPT などの API の提供は沢山のアプリケーション開発者にとって Python を使った開発を始めるきっかけになったのではないでしょうか。筆者もそのうちの 1 人です。
効率的かつ再現性の高い開発環境は必要不可欠であり、それを実現するためのツールの一つが Docker です。Docker はアプリケーションを容易に再現可能なコンテナ内で動作させることができ、開発から本番環境まで一貫した環境を提供します。これで「僕の環境では動いているんですけどねぇ...」といった問題[1]を避けることができます。
本記事では、AI 時代に立ち向かうべく筆者が Python 初心者なりに取り組んだアプリケーション開発の方法の一つを解説します。
ディレクトリ構成
このような形を目指していきました。
.
├── .devcontainer
│ ├── devcontainer.json
│ ├── docker-compose.yml
│ └── postCreateCommand.sh
├── .github
│ └── workflows
│ └── test.yml
├── src # ビジネスロジックや main.py が含まれるディレクトリ
├── scripts # ビジネスロジックを用いたマイグレーション用のスクリプトなどのディレクトリ
├── tests # テストコードのディレクトリ
├── Dockerfile
└── docker-compose.yml
Dockerfile: 開発から本番環境まで一貫した環境構築
Docker を用いることで、Python の開発環境と本番環境を統一し、一貫性を確保することができます。その鍵となるのが Dockerfile です。Dockerfile の中にはアプリケーションをどう構築し、どのように実行するかが記述されています。開発者はこれを使用して、開発環境と本番環境を一致させ、問題の再現性と解決の効率化を図ることができます。
以下に示す Dockerfile は、Python 3.11 をベースに、Poetry[2] を用いたパッケージ管理を行い、その上でアプリケーションを実行するためのものです。この Dockerfile では、マルチステージビルドという技術を使用して開発(AS dev
)と本番(AS run
)の 2 つのステージが定義されています。
これにより、異なる目的のための異なるイメージを作成しますが、パッケージの管理や共通した設定を行う、ベースのステージ(AS poetry
)を用意することで開発環境と本番環境ともに Python のアプリケーションを動かすのに必要最低限なセットアップができます。
ARG PYTHON_BASE_IMAGE='python'
# Poetry をインストールするステージを示しています。
# Poetry は Python の依存関係管理とパッケージングを行うツールです。
FROM ${PYTHON_BASE_IMAGE}:3.11 AS poetry
# Python が pyc ファイルを作成するのを防ぎます。
# これは一時的なキャッシュファイルで、Docker イメージ内に不要なファイルを作成するのを避けるためです。
ENV PYTHONDONTWRITEBYTECODE=1
# 標準出力や標準エラーをバッファリングするのを防ぐことで、バッファリングによりログが出力されない状態でアプリケーションがクラッシュするのを避けます。
ENV PYTHONUNBUFFERED=1
RUN \
\
apt-get update \
&& apt-get install -y --no-install-recommends build-essential
ENV POETRY_HOME="/opt/poetry"
ENV PATH="$POETRY_HOME/bin:$PATH"
RUN curl -sSL https://install.python-poetry.org | python3 - \
&& poetry config virtualenvs.create false \
&& mkdir -p /cache/poetry \
&& poetry config cache-dir /cache/poetry
# Docker のキャッシュを活用するため、依存関係のダウンロードを別のステップで行います。
# 次回のビルドを速くするために、/cache/poetry へのキャッシュマウントを利用します。
# このレイヤーに pyproject.toml や poetry.lock をコピーする必要がないように、バインドマウントを活用します。
RUN \
poetry install --no-interaction --no-root --without dev
# 開発環境用のステージを示しています。
FROM poetry AS dev
WORKDIR /workspace
# src 以下にアプリケーションコードを記述しています。
# ビジネスロジックをテストやスクリプトから参照(import)するために PYTHONPATH 環境変数を指定します。
ENV PYTHONPATH="/workspace/src:$PYTHONPATH"
# オプション。
# DB として PostgreSQL を使っていたため、ここでは追加で postgresql-client をインストールしています。
RUN \
\
apt-get update \
&& apt-get install -y --no-install-recommends postgresql-client
RUN \
poetry config virtualenvs.create false && \
poetry install --no-interaction --no-root
# 本番環境用のステージです。
FROM poetry AS run
WORKDIR /app
# 特権のないユーザーを作成し、そのユーザーとしてアプリケーションを実行します。
# これは Docker のベストプラクティスで、アプリケーションがシステム全体に対する潜在的な攻撃から守るための重要なステップです。
# https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#user
ARG UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
appuser
# 特権のないユーザーへ切り替え、そのユーザーとしてアプリケーションを実行します。
USER appuser
COPY . .
ENTRYPOINT ["python3", "./main.py"]
`--mount` オプションについて
--mount
オプションは、Dockerfile 内で RUN
コマンド実行時にファイルまたはディレクトリを一時的にコンテナにマウントするためのものです。--mount
オプションは、BuildKit ビルダーでのみ利用可能で、Docker 18.09 以降のバージョンで使用できます。ビルドキャッシュ、シークレット、SSH エージェントなど、ビルドプロセス中に必要となる一時的なデータのマウントに役立ちます。
この Dockerfile では、--mount
オプションが 2 つのタイプで使用されています。
-
type=cache
: ビルド間でのキャッシュ共有を実現します。apt-get update
およびapt-get install
コマンドでダウンロードされたパッケージがキャッシュとして保持され、次回のビルド時に再ダウンロードの必要をなくします。これはビルド時間を短縮し、ネットワーク帯域を節約します。 -
type=bind
: ビルド時にホストのファイルまたはディレクトリをコンテナにマウントします。ここでは、pyproject.toml
とpoetry.lock
がソースとして指定され、これらはコンテナ内で poetry による依存関係のインストールに利用されます。このマウントタイプは、ビルドコンテキストを送信するのではなく、直接ホストのファイルをビルドに使用するため、ビルド速度の向上やデータ転送量の削減に役立ちます。
Dockerfile と同じディレクトリの階層に docker-compose.yml を定義します。docker-compose up
といったコマンドを利用することで上記の run
ステージを実行することができます。
version: "3.9"
services:
# これは例なので必要に応じてコメントアウト外したり、追加してください。
#
# db:
# image: supabase/postgres:15.1.0.33
# restart: "no"
# ports:
# - 54322:5432
# healthcheck:
# test: pg_isready -U postgres -h localhost
# interval: 2s
# timeout: 2s
# retries: 10
# environment:
# POSTGRES_HOST: /var/run/postgresql
# POSTGRES_PASSWORD: ${DB_PASS}
# volumes:
# - ./db/migrations:/docker-entrypoint-initdb.d/migrations
ai-app:
# 必要あればコメントアウト
# depends_on:
# - db
build:
context: .
dockerfile: Dockerfile
target: run # to specify the multi-stage build target
ports:
- 8080:8080
Visual Studio Code (VSCode) と Devcontainer
VSCode では、Devcontainer と呼ばれる機能を用いることで、コンテナ内に完全に閉じ込められた開発環境を簡単にセットアップすることができます。Devcontainer は、開発環境をコードで定義し、チーム内の全ての開発者が一貫した環境で作業できるようにすることができます。
Devcontainer の設定は、一つの .devcontainer
ディレクトリ内に格納されるdevcontainer.json
という設定ファイルによって行います。この設定ファイルには、使用するコンテナイメージ、バインドマウント、環境変数、起動時に実行するコマンドなど、コンテナ内の開発環境の詳細を定義します。
また、VSCode の拡張機能を活用することで、Python の開発を更に効率化することが可能です。例えば、Python の拡張機能は、Linting、IntelliSense(コード補完)、コードナビゲーション、コードフォーマット、リファクタリング、ユニットテスト、デバッギングといった機能を提供します。最近流行りの Ruff と呼ばれる Rust 製の Linter にも対応しており専用の拡張機能を通じて利用可能です。[3]
一部変更してますが以下が実際に利用している devcontainer.json になります。先ほど紹介した拡張機能以外に Better TOML と Docker 拡張機能もインストールしています。
{
"name": "poetry",
"dockerComposeFile": [
"../docker-compose.yml",
"docker-compose.yml"
],
"service": "ai-app",
"workspaceFolder": "/workspace",
"postCreateCommand": "bash .devcontainer/postCreateCommand.sh",
"userEnvProbe": "loginInteractiveShell",
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"charliermarsh.ruff",
"bungcip.better-toml",
"ms-azuretools.vscode-docker"
]
}
}
}
そして同じディレクトリに docker-compose.yml
を追加します。この YAML では前節で紹介した docker-compose.yml
の次の動作を主に上書きします。
- devcontainer として利用するコンテナイメージに
mcr.microsoft.com/vscode/devcontainers/python
を利用します。 - target でデフォルトで
dev
ステージを利用します。- ただし
TARGET_STAGE
環境変数を利用することでrun
ステージも利用できます。
- ただし
version: "3.9"
services:
app:
container_name: ai-app-dev
build:
args:
- PYTHON_BASE_IMAGE=mcr.microsoft.com/vscode/devcontainers/python
target: ${TARGET_STAGE:-dev} # override to use dev stage
command: sleep infinity
volumes:
- .:/workspace:cached
Docker コンテナを利用した本番環境へのデプロイ
筆者は本番環境として GCP の Cloud Run を利用しています。Cloud Build を利用してコンテナイメージをビルドし、Container Registry へアップロードして Cloud Run へデプロイしています。
Cloud Run 以外にも、いくつかのクラウドサービスがコンテナイメージをアプリケーションとして利用できる環境を提供しています。
- AWS Fargate: AWS のサーバレスコンピュートエンジンで、コンテナを実行するためのものです。これにより、アプリケーションのスケーリングや管理を自動化できます。
- Azure Container Instances (ACI): Microsoft Azure が提供するサービスで、こちらも同様にサーバーを管理することなく、コンテナデプロイを実現します。
運用の仕方としてソースコードは GitHub 上で管理し、Git のタグを push すると Cloud Build が実行されるように設定しています。その時行われる内容は下記の YAML に記述された内容をもとに実行されます。
steps:
- name: docker:20.10.22
id: "build"
entrypoint: sh
env:
- "DOCKER_BUILDKIT=1"
args:
- -c
- |
docker build \
-t gcr.io/$PROJECT_ID/${_IMAGE_NAME}:$COMMIT_SHA \
-t gcr.io/$PROJECT_ID/${_IMAGE_NAME}:latest \
. \
-f Dockerfile \
--target run \
--cache-from gcr.io/$PROJECT_ID/${_IMAGE_NAME}:latest
timeout: 600s
- name: docker:20.10.22
id: push
args:
- push
- --all-tags
- gcr.io/$PROJECT_ID/${_IMAGE_NAME}
waitFor:
- build
- name: gcr.io/google.com/cloudsdktool/google-cloud-cli:396.0.0-alpine
entrypoint: gcloud
args:
- run
- deploy
- ${_RUN_SERVICE_NAME}
- --image
- gcr.io/$PROJECT_ID/${_IMAGE_NAME}:$COMMIT_SHA
- --region
- ${_REGION}
- --platform
- managed
waitFor:
- push
substitutions:
_REGION: asia-northeast1
_IMAGE_NAME: ai-app
_RUN_SERVICE_NAME: ai-app
images:
- gcr.io/$PROJECT_ID/${_IMAGE_NAME}:$COMMIT_SHA
timeout: 1200s
GitHub Actions を用いた CI 環境
GitHub Actions 上では全くコンテナ要素が出てきません。
actions/setup-python を利用して Python 環境を構築し、abatilo/actions-poetry を利用して Poetry をセットアップしています。
CI として実行するのは Ruff を使った Linting と Pytest を使ったテストの実行のみです。
name: Test
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: Set up PYTHONPATH
run: |
echo "PYTHONPATH=$PWD/src:$PYTHONPATH" >> $GITHUB_ENV
- name: Setup Poetry
uses: abatilo/actions-poetry@v2
with:
poetry-version: latest
- name: Install dependencies
run: |
poetry --version
poetry config virtualenvs.in-project true
poetry install --no-interaction --no-root
- name: Lint with ruff
run: |
poetry run ruff check src tests
- name: Test with pytest
run: |
poetry run pytest
最後に
これまで、Docker を使用して Python を使ったウェブアプリケーションの開発について筆者が取り組んだことを詳しく解説してきました。ディレクトリ構成、VSCode と Devcontainer を利用した、一貫した開発環境の構築の使用、本番環境への Docker コンテナのデプロイ、そして GitHub Actions を用いた CI 環境について記述しました。誰かの参考になれば嬉しいです。(今後は Jupyter Notebook の環境を整えていきたい...!!!)
筆者が働いている NOT A HOTEL では仲間を積極的に募集しています。是非カジュアル面談からでもお気軽にご連絡ください!
Discussion