ABEJA Tech Blog

中の人の興味のある情報を発信していきます

開発品質とDeveloper eXperienceを高めるコンテナ開発環境のご紹介 (Python)

はじめに

こんにちは
2023年1月に入社し、システム開発グループでエンジニアをしてる春名です。

私の所属しているシステム開発グループでは、開発初期の環境構築をより効率的に行うための活動に取り組んでいます。 今回はそのうちの一つである、Pythonでコンテナ開発をする環境を構築した内容をご紹介します。

なぜコンテナ開発環境かと言いますと、単にAWSのECSやGoogle CloudのCloud Runを使ってデリバリーする案件が多いからです。 より使用頻度の高い開発環境を整備し、テンプレート化しておくことで開発の効率化に活用しています。

目次

今回作成する環境

FastAPIを利用してAPIを提供するバックエンド環境を作成する場合を例にして、コンテナ・イメージの作成やGitHub Actions上でのCIなどを実装していきます。
その上で以下のツールを導入し、Pythonを使った開発を開始する際のテンプレートとなるような「開発品質」と「Developer eXperience=開発者体験」を高める開発環境を構築します。

パッケージマネージャ

  • Poetry - Python packaging and dependency management made easy

リンター、フォーマッター

  • Ruff - An extremely fast Python linter, written in Rust.
  • Black - The uncompromising Python code formatter
  • mypy - Optional static typing for Python
  • Hadolint  - Dockerfile linter, validate inline bash, written in Haskell

テスト

  • pytest - The pytest framework makes it easy to write small tests, yet scales to support complex functional testing

なお、使用している環境は以下の通りです。

  • MacBook Pro 2022 (M2)
  • Python 3.8

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

Poetryのインストール

公式が用意しているインストーラーを使用してインストールします。
pipでインストールすると特定の仮想環境にPoetryがインストールされてしまい、誤ってアップグレードされたりアンインストールされる可能性があるため、インストーラーを使ってPoetry独自の新しい仮想環境にインストールします。

curl -sSL https://install.python-poetry.org | python3 -

プロジェクトの作成

poetry newコマンドで作成します。
ここではfastapi-backendというディレクトリ名でbackendというパッケージ名のプロジェクトを作成します。

poetry new fastapi-backend --name backend

これでベースとなる環境が作られました。

$ tree fastapi-backend/
fastapi-backend/
├── README.md
├── backend
│   └── __init__.py
├── pyproject.toml
└── tests
    └── __init__.py

3 directories, 4 files

プロジェクトのディレクトリに移動して、gitを初期化してから設定を進めましょう。

cd fastapi-backend
git init .

poetryの設定

公式のマニュアルを参考にして適宜設定してください。

.gitignoreの作成

こちらも適宜設定してください。
github/gitignoreにあるPython.gitignoreが参考になります。

依存関係の追加

必要なライブラリを追加していきます。

FastAPI

ここではオプションも含めて全てインストールするためfastapi[all]を指定しています。

poetry add "fastapi[all]"

Ruff / Black / mypy / pytest

開発用の依存関係として追加します。
--devオプションはPoetry 1.2.0以降でdeprecatedになっているので--groupとして指定します。

poetry add ruff black mypy pytest --group dev

必要なライブラリの追加

Hadolint

公式のインストール手順に従ってインストールします。

brew install hadolint

Ruffの設定

RuffはRustで実装された高速な動作が特徴のLinterです。
Flake8、isort、pydocstyleなど様々ルールが実装されていて、これらのツールをまとめて置き換えるような使い方が可能となっているため、 複数のツールをインストールして個別に設定する必要がなくなります。とてもありがたい。

Ruffについては多くの方が記事を書いてくださっているので、詳細はそちらを参考にしてください。

Ruffの設定はpyptoject.tomlに記述していきます。

pyptoject.toml

[tool.ruff]
target-version = "py38"
line-length = 100
select = [
  "E", # pycodestyle errors
  "W", # pycodestyle warnings
  "F", # pyflakes
  "B", # flake8-bugbear
  "I", # isort
]

ignore = [
  "E501", # line too long, handled by black
  "B008", # do not perform function calls in argument defaults
]

unfixable = [
  "F401", # module imported but unused
  "F841", # local variable is assigned to but never used
]

target-versionでPythonバージョン、line-lengthで1行の文字数を100に指定しています。 そしてselectには有効にするルール、ignoreには無効にするルール、unfixableには自動修正を行わないルールを記述します。

全てのルールを対象にする場合は"ALL"と記述します。 個別に指定したい場合は公式のRulesを確認しながら設定しましょう。

今回の例では以下をignoreで無効にしています。

  • E501 (line-too-long)
    • blackによって自動フォーマットされるため不要(警告されない)
  • B008 (function-call-in-default-argument)
    • デフォルト引数を使った実装を使いたいので無効化

また、unfixableで以下を無効にして未使用のインポートと変数を自動削除しないようにしています。

  • F401 (unused-import)
  • F841 (unused-variable)

これは、この後ファイルを保存した時に自動でフォーマットが掛かるように設定していくので、 「後から使う予定で記述してたのに保存したら勝手に消されてる…」という悲しい事故を防ぐためです。

mypyの設定

mypyは静的な型チェックを行ってくれるツールです。
Pythonでも型定義を使った実装を行うことが通常になっているので導入しています。

pyptoject.toml

[tool.mypy]
python_version = 3.8
strict = true

Pythonのバージョンとstrictモードだけを設定しています。

strictモードを有効にすることで、オプションのチェックを全て有効にすることができます。
有効になるオプションはmypy --help--strictに記載されています。

その他の設定項目についてはconfiguration fileを参照してください。

Blackの設定

blackはコードを自動整形してくれるフォーマッターで、前述のRuffはBlackと一緒に使うように設計されています。

pyptoject.toml

[tool.black]
target-version= ['py38']
line-length = 100

Pythonのバージョンと1行当たりの文字数を設定しています。
その他のオプションはUsage and Configuration - The basicsに以下の記載があるように、includeexclude以外は特に設定する必要ないと思います。

Pro-tip: If you’re asking yourself “Do I need to configure anything?” the answer is “No”. Black is all about sensible defaults. Applying those defaults will have your code in compliance with many other Black formatted projects.

Hadolintの設定

HadolintはDockerfileのlinterで、Dockerfileのベストプラクティスに従ったチェックを行ってくれます。
Hadolintの設定は.hadolint.yamlへ記述します。

.hadolint.yaml

ignored:
  - DL3008 # Pin versions in apt get install

DL3008はaptで入れるパッケージのバージョンを全て固定する必要があり、ルールとして厳しいので無視する設定にしています。
その他のオプションについてはConfigure、指定可能なルールについては Rulesを参照してください。

FastAPIでAPIを実装

FastAPIは例として使っているだけなので、ここでは簡単なヘルスチェック用のエンドポイントのみを実装します。

backend/schemas.py

from pydantic import BaseModel


class HealthCheck(BaseModel):
    status: str

backend/main.py

from fastapi import FastAPI

from . import schemas

app = FastAPI()


@app.get("/health-check", response_model=schemas.HealthCheck)
def health_check() -> schemas.HealthCheck:
    return schemas.HealthCheck(status="ok")

GET要求されたら{status="ok"}を返すだけの簡単なエンドポイントです。
pydanticを使って応答の型を定義して型安全な実装にしています。

以下のコマンドを実行して起動し、http://localhost:8000/health-checkへアクセスすると{"status":"ok"}が確認できると思います。

poetry run uvicorn backend.main:app --host 0.0.0.0 --port 8000
$ curl http://localhost:8000/health-check
{"status":"ok"}

APIのテストを実装

先ほど実装したエンドポイントをpytestでテストするコードを実装します。
pytestはtest_で始まるファイル・関数を単体テストのコードとみなすため、testsフォルダ内にファイル・関数を追加していきます。

tests/test_api.py

from fastapi.testclient import TestClient

from backend.main import app

client = TestClient(app)


def test_health_check() -> None:
    response = client.get(
        "/health-check",
    )
    assert response.status_code == 200, response.text
    data = response.json()
    assert data["status"] == "ok"

pytestを実行してテストが通ることを確認します。

poetry run pytest

Makefileでエイリアスを作成

各コマンドをmakeコマンドで実行できるようにMakefileを作成します。

Makefile

.PHONY: dev run test fix ruff-fix black \
       lint ruff-check black-check mypy

dev:
  poetry run uvicorn backend.main:app --reload

run:
  poetry run uvicorn backend.main:app --host 0.0.0.0 --port 8000

test:
  poetry run pytest

fix: ruff-fix black

ruff-fix:
  poetry run ruff --fix .

black:
  poetry run black .

lint: black-check ruff-check mypy hadolint

ruff-check:
  poetry run ruff check .

black-check:
  poetry run black --check .

mypy:
  poetry run mypy . --config-file ./pyproject.toml

hadolint:
  hadolint Dockerfile

このように定義しておくことで、make lintを実行することでチェックの実行、make fixを実行することで修正とコード整形が実行できるようになります。

Dockefileの実装

実装したFastAPIが動作するコンテナを作成するためにDockerfileを作成します。
公式の説明もあるので、そちらも参考にしましょう。

Dockerfile

ARG PYTHON_VERSION="3.8"

FROM python:${PYTHON_VERSION}-slim

ARG POETRY_HOME="/opt/poetry"
ARG POETRY_VERSION="1.6.1"

SHELL ["/bin/bash", "-o", "pipefail", "-c"]

RUN apt-get update && \
    apt-get install --no-install-recommends -y make curl && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

RUN curl -sSL https://install.python-poetry.org/ | python3 - --version ${POETRY_VERSION} && \
    ln -s ${POETRY_HOME}/bin/poetry /usr/local/bin/poetry && \
    poetry config virtualenvs.create false

WORKDIR /app

COPY Makefile pyproject.toml poetry.lock ./
COPY backend/ ./backend/

RUN poetry install --no-root --no-ansi

CMD ["make", "run"]

Pythonバージョンを引数で変えられるようにし、使用するpoetryのバージョンを固定しています。
また、HadolintのDL4006に対応するためにpipefailの設定も追加しています。

作成できたらHadolintでチェックを行い、エラーがなければイメージをビルドします。

make hadolint
docker image build -t fastapi-backend .

ビルドに成功したらコンテナを起動して動作確認します。
http://localhost:8000/health-checkへアクセスして {"status":"ok"}が返ってくることが確認できればOKです。

docker container run -p 8000:8000 -it --rm fastapi-backend
$ curl http://localhost:8000/health-check
{"status":"ok"}

GitHub Actions (CI)の設定

GithubへのPRが作成時やマージされた時にテストやチェックが動くようにワークフローを設定します。

.github/workflows/ci.yml

name: CI
on:
  pull_request:
    branches:
      - main
    types: [opened, synchronize, closed]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.8", "3.11"]

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - id: setup
        uses: ./.github/workflows/composite/setup
        with:
          python-version: ${{ matrix.python-version }}

      - name: Run test
        run: make test

  code_lint:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - id: setup
        uses: ./.github/workflows/composite/setup
        with:
          python-version: ${{ matrix.python-version }}

      - name: Run ruff check
        run: make ruff-check

      - name: Run black check
        run: make black-check

  type_check:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - id: setup
        uses: ./.github/workflows/composite/setup
        with:
          python-version: ${{ matrix.python-version }}

      - name: Run mypy
        run: make mypy

  docker_lint:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Check for changes in Dockerfile
        uses: dorny/paths-filter@v2
        id: filter
        with:
          filters: |
            changed:
              - 'Dockerfile'

      - name: Lint Dockerfile
        if: steps.filter.outputs.changed == 'true'
        uses: hadolint/hadolint-action@master
        with:
          dockerfile: 'Dockerfile'

.github/workflows/composite/setup/action.yml

name: Setup
inputs:
  python-version:
    description: 'Version of python'
    required: false
    default: 3.8
  poetry-home:
    description: 'Install directory of poetry'
    required: false
    default: /opt/poetry
runs:
  using: "composite"
  steps:
    - name: Set up Python ${{ inputs.python-version }}
      uses: actions/setup-python@v4
      with:
          python-version: ${{ inputs.python-version }}

    - name: Install poetry
      run: |
          export POETRY_HOME=${{ inputs.poetry-home }}
          curl -sSL https://install.python-poetry.org/ | python - --version 1.5.1
          ln -s $POETRY_HOME/bin/poetry /usr/local/bin/poetry
          poetry config virtualenvs.create false
      shell: bash

    - name: Install dependencies
      run: |
          poetry install --no-root --no-ansi
      shell: bash

テストに関してはMatrixを使用してPython3.8とPython3.11環境で実行されるようにしています。
Pythonのセットアップ処理に関しては、Composite Actionを使用してPythonのインストールコードを切り出すことで再利用性を高める構成にしています。

その他のツールによるチェックはジョブを分割して登録しています。

これは全てのチェックを1つのジョブに入れてしまうと途中でエラーがあった場合、その後のチェックが実行されず結果が確認できないためです。
また分割したジョブはデフォルトで並列実行されるため、CI全体が早く終わるというメリットもあります。

actによる動作確認

実際にPRを作成しないとGitHub Actionsの動作確認ができないのは結構不便です。
そこで、ローカルでGitHub Actionsの動作確認ができるactを使って動作確認を行います。

インストール
brew install act

設定ファイルに使用するイメージを記載します。

~/.actrc

-P ubuntu-latest=catthehacker/ubuntu:act-latest
-P ubuntu-22.04=catthehacker/ubuntu:act-22.04
-P ubuntu-20.04=catthehacker/ubuntu:act-20.04
-P ubuntu-18.04=catthehacker/ubuntu:act-18.04
--container-architecture linux/amd64

Medium Docker Imageを使用するように設定しています。
その他の使用可能なイメージについてはRunnerを参照してください。
--container-architecture linux/amd64は使用している環境がarm64(M2)なので指定しています。

実行

-lで登録されているジョブの一覧が確認できます。

$ act pull_request -l
Stage  Job ID       Job name     Workflow name  Workflow file  Events      
0      test         test         CI             ci.yml         pull_request
0      code_lint    code_lint    CI             ci.yml         pull_request
0      type_check   type_check   CI             ci.yml         pull_request
0      docker_lint  docker_lint  CI             ci.yml         pull_request

問題なくジョブが登録されていることが確認できたら実行します。
-qは表示を少なくするオプションです。(それでも多いですが…)

$ act pull_request -q
:
:
[CI/test-2     ]   ✅  Success - Post Set up Python 3.11
[CI/test-2     ]   ✅  Success - Post ./.github/workflows/composite/setup
[CI/test-2     ] 🏁  Job succeeded
[CI/type_check ]   ✅  Success - Main Run mypy
[CI/type_check ] ⭐ Run Post ./.github/workflows/composite/setup
[CI/type_check ] ⭐ Run Post Set up Python 3.8
[CI/type_check ]   🐳  docker exec cmd=[node /var/run/act/actions/actions-setup-python@v4/dist/cache-save/index.js] user= workdir=
[CI/type_check ]   ✅  Success - Post Set up Python 3.8
[CI/type_check ]   ✅  Success - Post ./.github/workflows/composite/setup
[CI/type_check ] 🏁  Job succeeded

実行が完了するとジョブごとにJob succeededといった結果が確認できると思います。
事前確認ができたらあとは実際にGitHubへpushしてPRを作成すればCIが動作します。

ここでは手順は省略しますが、実行すると以下のような感じになります。

GitHub Actions

環境構築完了

お疲れ様でした、これで環境構築完了です🎉

コード整形を自動で行いつつ各種チェックツールでコーディングルールを守りながらコーディングができます。 Hadolintも導入しているので、ベストプラクティスに沿ったDockefileを書きながらコンテナ開発も行えます。
PRを作成したときにはGitHub Actionsを使ったCIテストが実行されて品質が維持できるので、 これでPythonでコンテナ開発をする際の基本的な要素は一通りテンプレート化できたのではないでしょうか。

おわりに

環境構築するの楽しいのですが、毎回リンターやフォーマッターの設定をするのは時間がかかり大変です。
テンプレートの利用が進んでいけば、構築を担当した人によって使うツールや設定が違って戸惑うようなことも防げますし、もっと充実させて効率的に開発を進めていきたいですね。

採用情報

ABEJAでは一緒に働く仲間を募集しています!ご興味がある方は、是非、採用ページをご確認下さい。

careers.abejainc.com