🎃

Remote Containers + Poetryの俺最強テンプレート

2022/09/24に公開

tl;dr

Remote Containers + Poetryの俺最強テンプレートを作りました。

対象読者

以下の基本的な使い方がわかる人。

  • VS Code Remote Remote Container
  • Poetry
  • Dockerfile
  • Docker Compose

あるいはこれらの技術を理解したい人。(この記事を駆け抜けると、これらの知識が得られます)

目指すこと

  • ローカルの開発環境を汚さない
  • Remote Containersを意識せずに、アプリを起動することができる
  • 開発用のコンテナの起動をできるだけ早くする
  • moduleとしてpythonスクリプトを実行する

ファイル構成

最小限のファイル構成は以下になります。

.
├── .devcontainer
│   ├── devcontainer.json
│   └── docker-compose.yml
├── app
│   └── Dockerfile
└── docker-compose.yml

個別ファイルの解説

.devcontaier/devcontainer.json

VS Code Remote - Remote Containersの設定ファイルです。読み込むdocker-compose.ymlファイルの順序と、起動するサービスを指定します。リファレンスは以下にあります。

https://containers.dev/implementors/json_reference/

{
    "name": "poetry",
    "dockerComposeFile": ["../docker-compose.yml", "docker-compose.yml"],
    "service": "app",
    "workspaceFolder": "/workspace",
    // 省略
}

Remote Containersのコマンド(Ctrl-shift-pで選択)で、Add Development Container Configuration Fileを実行した結果から作成します。

"dockerComposeFile": ["../docker-compose.yml", "docker-compose.yml"],

によって、<project root>/docker-compose.ymlを先に読み込み、.devcontaier/docker-compose.ymlの値で上書きします。このとき、docker composeが実行されるディレクトリは、<project root> となります。したがって、.devcoontaier/docker-compose.ymlにおける相対パスの記述は、<project root> を基準にする必要があります。

"service"には、適当なサービス名を入れておきます。

<project root>/docker-compose.yml

Remote Containersに関係なく使用するdocker-compose.ymlファイルです。

version: "3.9"

services:
   app:
    container_name: app
    build:
      args:
        - PYTHON_VERSION=3.10
      context: app
      dockerfile: Dockerfile
      target: run
  • app:のように、devcontainer.jsonで指定したサービス名を記載します。
  • build:以下では、コンテナイメージのビルドに必要な情報を記載します。
    • PYTHON_VERSION=3.10として、Pythonのバージョンをビルド時に渡せるようにします。
    • Dockerfileappディレクトリ以下にあるので、context: appとしています。
    • target: runで、実行時に用いるステージを指定します。

「ステージ❓」となった方は以下を参照してください。開発に用いるイメージと実行に用いるイメージの定義を同じDockerfileに記載するために、マルチステージビルドを採用しています。
https://docs.docker.jp/develop/develop-images/multistage-build.html

リファレンスも見ておきましょう
https://docs.docker.jp/compose/compose-file/compose-file-v3.html

.devcoontaier/docker-compose.yml

Remote Containersを使用する際に、プロジェクトルートのdocker-compose.ymlの定義を上書きするためのファイルです。<project root>/docker-compose.ymlにキーが存在する値はこのファイルの値で上書きされて、存在しない値は単に追加されます。

version: "3.9"
services:
  app:
    container_name: app-dev
    build:
      args:
       - PYTHON_BASE_IMAGE=mcr.microsoft.com/vscode/devcontainers/python
      target: ${TARGET_STAGE:-dev}
    command: sleep infinity
    volumes:
      - .:/workspace:cached
  • app:のように、devcontainer.jsonで指定したサービス名を記載します。
  • container_name: app-devのように、実行時のコンテナ名と開発時のコンテナ名を分けておく方が良いです。これらの名前が衝突すると、実行方法をかえるたびにいちいちコンテナを削除する必要があります。
  • build:以下では、開発時に用いるコンテナイメージのビルドに必要な情報を記載します。
    • PYTHON_IMAGE_BASE=mcr.microsoft.com/vscode/devcontainers/pythonとして、開発時に用いるベースイメージを指定します。
    • target: ${TARGET_STAGE:-dev}では、環境変数TARGET_STAGEをビルド対象のステージに設定します。この環境変数が設定されていない場合には、デフォルトでdevステージが実行されます。
  • command: sleep infinityとして、コンテナが終了しないようにします。
  • volumesの定義は、Remote Containersが生成するファイルと同じです。好みに応じて、./appとしてもよいでしょう。前述したとおり、プロジェクトルートからの相対パスで記載する必要があります。

PYTHON_IMAGE_BASE=mcr.microsoft.com/vscode/devcontainers/pythonは、microsoftが提供しているRemote Containerのためのベースイメージです。最近は、以下のようにVS codeとは関係なく、コンテナ定義が提供されています。
https://github.com/devcontainers/images

python向けの定義は、こちらにあります。
https://github.com/devcontainers/images/tree/main/src/python

通常のコンテナイメージ、例えばubuntu:latest等を使うこともできるはずですが、こんな感じで バグにはまることもあるので、できるだけmicrosoftが提供しているものを利用したほうがよいでしょう。

app/Dockerfile

コンテナの定義です。ここからは、ステージごとに説明します。なお、BuildKitを前提としています。
https://docs.docker.jp/develop/develop-images/build_enhancements.html

事前定義

ステージを定義する前に、イメージのビルドに必要な引数を定義しておきます。

# syntax=docker/dockerfile:1
ARG APP_NAME="app" # アプリ名
ARG PYTHON_VERSION='3.10' # pythonのバージョン
ARG PYTHON_BASE_IMAGE='python' # 実行時のベースイメージ
ARG POETRY_CACHE_DIR='/root/.cache/poetry' # poetryのキャッシュディレクトリ

poetry ステージ

このステージでは、Poetryのインストールを行います。プロジェクトを初期化する前には、pyproject.tomlpoetry.lockが存在しないため、ファイルの非存在により後述するdevステージのビルドができません。そこで、ただPoetryを使えるようにするだけのステージを定義します。

FROM ${PYTHON_BASE_IMAGE}:${PYTHON_VERSION} AS poetry
ENV PYTHONUNBUFFERED=true
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

RUN \
  --mount=type=cache,target=/root/.cache/pip \
  pip install -U pip poetry
  • FROM ${PYTHON_BASE_IMAGE}:${PYTHON_VERSION} AS poetryにて、ベースイメージとPythonのバージョンを切り替えられるようにします。
  • apt-getに関しては、ビルド間でキャッシュがきくように設定しています。BuildKitが使える場合、イメージサイズを小さくするために、もはやapt-get clean && rm -r /var/lib/apt/lists/*は不要です。
    • aptでライブラリのインストールが必要なライブラリを利用する場合には、ここでインストールしておきます。

https://zenn.dev/kou64yama/articles/powerful-docker-build-cache

  • 次に、pipでPoetryをインストールします。apt-getと同様に、ビルド間でキャッシュが効くように設定しています。公式では、インストールスクリプトによるインストールが推奨されていますが、pipを使うとPATHを通すめんどくささが少しだけ減ります。

dev ステージ

このステージでは、pyproject.tomlpoetry.lockを用いて、依存関係のインストールを行います。

FROM poetry AS dev
ARG APP_NAME
ARG POETRY_CACHE_DIR
WORKDIR /workspace/${APP_NAME}
COPY pyproject.toml poetry.lock ./
RUN \
  --mount=type=cache,target=${POETRY_CACHE_DIR} \
  poetry config cache-dir ${POETRY_CACHE_DIR} && \
  poetry config installer.parallel false && \
  poetry config virtualenvs.create false && \ 
  poetry install --no-interaction

3点、poetryのconfigを設定しています。

  • cache-dir ${POETRY_CACHE_DIR}
    • poetryのキャッシュディレクトリの設定です。
  • cache-dir installer.parallel false
    • パッケージのインストール時の並列実行を無効にします。筆者の環境では、この設定値をfalseにしないとインストールが失敗するという事象に悩まされました。
  • cache-dir virtualenvs.create false
    • パッケージのインストール時に仮想環境を作成しないようにします。コンテナ内で開発するので、プロジェクト間でパッケージのバージョン衝突を考慮する必要がありませんし、poetryで作成した仮想環境をVS Codeに認識させるのに一手間必要になるからです。

その他のconfigのリファレンスは、こちらにあります。
https://cocoatomo.github.io/poetry-ja/configuration/

最後に、poetry install --no-interactionにてパッケージのインストールを行います。

run ステージ

このステージでは、作成したモジュールを実行するための設定をしています。ここでは、仮想環境を使用しています。VS Codeに仮想環境を認識させる必要がないからです。

FROM poetry AS run
ARG POETRY_CACHE_DIR
WORKDIR /run
COPY pyproject.toml poetry.lock ./
RUN \
  --mount=type=cache,target=${POETRY_CACHE_DIR} \
  poetry config virtualenvs.in-project true && \
  poetry install --no-interaction
COPY . /run/${APP_NAME}
WORKDIR /run/${APP_NAME}
ENV APP_NAME=${APP_NAME}
ENTRYPOINT python -m ${APP_NAME}

COPYから先は、もう少し素直に書けないものだろうか・・・と思います。

Compose ファイルのテスト

さて、ファイルも準備したし、早速コンテナで開発!としたいところですが、このままReopen in Containerとすると、おそらく下記のようなわけのわからないエラーが出力されることになるでしょう。(この場合は簡単で、pyproject.tomlがないよ〜ということですが…)

failed to solve: rpc error: code = Unknown desc = failed to compute cache key: "/pyproject.toml" not found: not found
[10308 ms] Error: Command failed: docker compose --project-name app -f /Users/utt/work/app/docker-compose.yml -f /Users/utt/work/app/.devcontainer/docker-compose.yml build
[10309 ms]     at Mf (/Users/utt/.vscode/extensions/ms-vscode-remote.remote-containers-0.251.0/dist/spec-node/devContainersSpecCLI.js:222:419)
[10309 ms]     at process.processTicksAndRejections (node:internal/process/task_queues:96:5)
[10310 ms]     at async pF (/Users/utt/.vscode/extensions/ms-vscode-remote.remote-containers-0.251.0/dist/spec-node/devContainersSpecCLI.js:222:2358)
[10310 ms]     at async dF (/Users/utt/.vscode/extensions/ms-vscode-remote.remote-containers-0.251.0/dist/spec-node/devContainersSpecCLI.js:206:2361)
[10310 ms]     at async DF (/Users/utt/.vscode/extensions/ms-vscode-remote.remote-containers-0.251.0/dist/spec-node/devContainersSpecCLI.js:263:2177)
[10310 ms]     at async to (/Users/utt/.vscode/extensions/ms-vscode-remote.remote-containers-0.251.0/dist/spec-node/devContainersSpecCLI.js:263:3110)
[10310 ms]     at async Ak (/Users/utt/.vscode/extensions/ms-vscode-remote.remote-containers-0.251.0/dist/spec-node/devContainersSpecCLI.js:383:8108)
[10311 ms]     at async Ok (/Users/utt/.vscode/extensions/ms-vscode-remote.remote-containers-0.251.0/dist/spec-node/devContainersSpecCLI.js:383:7864)
[10319 ms] Exit code 1

正直なところ、うんざり😩です。コンテナのビルドもなんだか時間がかかります。そんな時には以下のように素のdocker composeを起動してみましょう。

utt@MacBook-Pro-2 app % docker compose -f docker-compose.yml -f .devcontainer/docker-compose.yml build
[+] Building 1.3s (13/15)                                                                                                                                                               
 => [internal] load build definition from Dockerfile                                                                                                                               0.0s
 => => transferring dockerfile: 32B                                                                                                                                                0.0s
 => [internal] load .dockerignore                                                                                                                                                  0.0s
 => => transferring context: 2B                                                                                                                                                    0.0s
 => resolve image config for docker.io/docker/dockerfile:1                                                                                                                         0.8s
 => CACHED docker-image://docker.io/docker/dockerfile:1@sha256:9ba7531bd80fb0a858632727cf7a112fbfd19b17e94c4e84ced81e24ef1a0dbc                                                    0.0s
 => [internal] load .dockerignore                                                                                                                                                  0.0s
 => [internal] load build definition from Dockerfile                                                                                                                               0.0s
 => [internal] load metadata for mcr.microsoft.com/vscode/devcontainers/python:3.10                                                                                                0.2s
 => [internal] load build context                                                                                                                                                  0.0s
 => => transferring context: 2B                                                                                                                                                    0.0s
 => CANCELED [poetry 1/3] FROM mcr.microsoft.com/vscode/devcontainers/python:3.10@sha256:77225b9dc27e3585e684d695d27d62d22b93c2a7e258c0f2adac5fbb7f3f0cd1                          0.0s
 => => resolve mcr.microsoft.com/vscode/devcontainers/python:3.10@sha256:77225b9dc27e3585e684d695d27d62d22b93c2a7e258c0f2adac5fbb7f3f0cd1                                          0.0s
 => => sha256:c5ef33b70f5dbcbc486d3dff023d6d5c27a69082544ca8b5556865b65ad8c51f 27.45kB / 27.45kB                                                                                   0.0s
 => => sha256:77225b9dc27e3585e684d695d27d62d22b93c2a7e258c0f2adac5fbb7f3f0cd1 743B / 743B                                                                                         0.0s
 => => sha256:910c979b5d0b9efff5e542e3b2871892f084aa27e8a64d2b99c1992d2a8fec66 5.34kB / 5.34kB                                                                                     0.0s
 => CACHED [poetry 2/3] 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-inst  0.0s
 => CACHED [poetry 3/3] RUN   --mount=type=cache,target=/root/.cache/pip   pip install -U pip poetry                                                                               0.0s
 => CACHED [dev 1/4] WORKDIR /workspace/app                                                                                                                                        0.0s
 => ERROR [dev 2/4] COPY pyproject.toml poetry.lock ./                                                                                                                             0.0s
------
 > [dev 2/4] COPY pyproject.toml poetry.lock ./:
------
failed to solve: rpc error: code = Unknown desc = failed to compute cache key: "/pyproject.toml" not found: not found

素のdocker composeを起動すると、VS Code Remoteの出力に紛れず、とても明確なエラーメッセージが得られます。

poetryによるプロジェクトの作成

pyproject.tomlpoetry.lockがなかったので、作成します。

まず、poetry ステージをビルドしてデーモンとして起動します。

TARGET_STAGE=poetry docker compose -f docker-compose.yml -f .devcontainer/docker-compose.yml up -d --build

起動に成功したら、コンテナの中に入ります。

TARGET_STAGE=poetry docker compose -f docker-compose.yml -f .devcontainer/docker-compose.yml exec app bash

/workspaceにプロジェクトディレクトリがマウントされているので、移動します。

cd /workspace/app

ここで、poetry initとすると、色々聞かれるので、適当に答えていきます。

root ➜ /workspace/app $ poetry init

This command will guide you through creating your pyproject.toml config.

Package name [app]:  app
Version [0.1.0]:  
Description []:  
Author [None, n to skip]:  None
License []:  
Compatible Python versions [^3.10]:  

Would you like to define your main dependencies interactively? (yes/no) [yes] no
Would you like to define your development dependencies interactively? (yes/no) [yes] no
Generated file

[tool.poetry]
name = "app"
version = "0.1.0"
description = ""
authors = ["None"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.10"


[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"


Do you confirm generation? (yes/no) [yes] yes

パッケージを追加したい場合には、poetry add <package_name>です。fastapiを入れてみます。

root ➜ /workspace/app $ poetry add fastapi
Using version ^0.85.0 for fastapi

Updating dependencies
Resolving dependencies... (1.4s)

Writing lock file

Package operations: 7 installs, 0 updates, 0 removals

  • Installing idna (3.4)
  • Installing sniffio (1.3.0)
  • Installing anyio (3.6.1)
  • Installing typing-extensions (4.3.0)
  • Installing pydantic (1.10.2)
  • Installing starlette (0.20.4)
  • Installing fastapi (0.85.0)

これで、pyproject.tomlpoetry.lockが生成されたので、コンテナから出ます。

root ➜ /workspace/app $ exit
exit

作成したコンテナは削除しておきましょう。

TARGET_STAGE=poetry docker compose -f docker-compose.yml -f .devcontainer/docker-compose.yml down

VS Codeでの開発

これで、VS Code上でPythonの開発ができるようになっています。
[Ctrl]+[Shift]+[p]のコマンドから、Remote-Containers: Rebuild Container without Cacheを選択すると、無事起動するはずです。

Pythonプロジェクトの構成

Python プロジェクトの構成は、筆者は以下のようにしています。

├── app
│   ├── Dockerfile
│   ├── app
│   │   ├── __init__.py
│   │   ├── __main__.py
│   │   ├── cli.py
│   │   └── src
│   │       └── __init__.py
│   ├── poetry.lock
│   └── pyproject.toml

パッケージの追加

devステージで直接poetry addを実行しようとすると、permission deniedのエラーが出ることがあります。
この時にはsudo suとして、rootユーザになると、poetry addを実行することができます。

VS Codeなしでの起動

これだけです!気持ちいいですね。

docker compose up --build

おわりに

初めての記事でした。書くのは大変です。でも、かなり応用の効く内容になっていると思います。先人に感謝。

Discussion