はじめに
こんにちは
2023年1月に入社し、システム開発グループでエンジニアをしてる春名です。
私の所属しているシステム開発グループでは、開発初期の環境構築をより効率的に行うための活動に取り組んでいます。 今回はそのうちの一つである、Pythonでコンテナ開発をする環境を構築した内容をご紹介します。
なぜコンテナ開発環境かと言いますと、単にAWSのECSやGoogle CloudのCloud Runを使ってデリバリーする案件が多いからです。 より使用頻度の高い開発環境を整備し、テンプレート化しておくことで開発の効率化に活用しています。
目次
- はじめに
- 目次
- 今回作成する環境
- Poetryによるプロジェクトの作成
- 依存関係の追加
- 必要なライブラリの追加
- Ruffの設定
- mypyの設定
- Blackの設定
- Hadolintの設定
- FastAPIでAPIを実装
- APIのテストを実装
- Makefileでエイリアスを作成
- Dockefileの実装
- GitHub Actions (CI)の設定
- 環境構築完了
- おわりに
- 採用情報
今回作成する環境
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に以下の記載があるように、include
やexclude
以外は特に設定する必要ないと思います。
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が動作します。
ここでは手順は省略しますが、実行すると以下のような感じになります。
環境構築完了
お疲れ様でした、これで環境構築完了です🎉
コード整形を自動で行いつつ各種チェックツールでコーディングルールを守りながらコーディングができます。
Hadolintも導入しているので、ベストプラクティスに沿ったDockefileを書きながらコンテナ開発も行えます。
PRを作成したときにはGitHub Actionsを使ったCIテストが実行されて品質が維持できるので、
これでPythonでコンテナ開発をする際の基本的な要素は一通りテンプレート化できたのではないでしょうか。
おわりに
環境構築するの楽しいのですが、毎回リンターやフォーマッターの設定をするのは時間がかかり大変です。
テンプレートの利用が進んでいけば、構築を担当した人によって使うツールや設定が違って戸惑うようなことも防げますし、もっと充実させて効率的に開発を進めていきたいですね。
採用情報
ABEJAでは一緒に働く仲間を募集しています!ご興味がある方は、是非、採用ページをご確認下さい。