🐳

Python 初心者が始める AI 時代の Docker を使ったアプリ開発

2023/05/31に公開

最近の 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 \
  --mount=type=cache,target=/var/lib/apt/lists \
  --mount=type=cache,target=/var/cache/apt/archives \
  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 --mount=type=cache,target=/cache/poetry \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    --mount=type=bind,source=poetry.lock,target=poetry.lock \
    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 \
  --mount=type=cache,target=/var/lib/apt/lists \
  --mount=type=cache,target=/var/cache/apt/archives \
  apt-get update \
  && apt-get install -y --no-install-recommends postgresql-client


RUN --mount=type=cache,target=/cache/poetry \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    --mount=type=bind,source=poetry.lock,target=poetry.lock \
    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 つのタイプで使用されています。

  1. type=cache: ビルド間でのキャッシュ共有を実現します。apt-get updateおよびapt-get installコマンドでダウンロードされたパッケージがキャッシュとして保持され、次回のビルド時に再ダウンロードの必要をなくします。これはビルド時間を短縮し、ネットワーク帯域を節約します。
  2. type=bind: ビルド時にホストのファイルまたはディレクトリをコンテナにマウントします。ここでは、pyproject.tomlpoetry.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 TOMLDocker 拡張機能もインストールしています。

{
  "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 では仲間を積極的に募集しています。是非カジュアル面談からでもお気軽にご連絡ください!

https://notahotel.com/careers

脚注
  1. pip, Poetry, Virtualenv, Homebrew, pyenv など Python の開発環境を整える手段が沢山あります。それぞれのマシンでセットアップ方法が異なることが理由で問題が起き得ます。 ↩︎

  2. 最近流行りつつある Rye も考えましたが、まだ実験段階ということもあり Poetry を選択しています。一応 Rye を使った Dockerfile はこちらでまとめています。 ↩︎

  3. Python 拡張のドキュメントを読むと分かりますが、Jupyter 拡張だったり、いくつかの拡張機能を通すことで利用できる機能が増えるらしいです。 ↩︎

NOT A HOTEL

Discussion