はじめに #
DevOpsを始めるに当たって、まずは手元で開発できる環境を整える必要があります。 特にPythonは色んな開発環境の構築方法があるので、ここを自由にしてしまうと各メンバーとの環境差異、CI環境との環境差異が発生します。
この結果として以下のことがよく起こると思います。
- lintのルールが各環境で異なる結果、review時にlintの差分が発生し何が本来の変更か分からない
- 開発環境、CI環境で失敗することを回避するために「xxxが無いと(しないと)動かないよ」という追加される謎の手順
これらをなるべく減らせるような開発環境の作成手法について解説します。
1. 今回のdirectory構成 #
今回解説するコードはGitlabのrepositryのbranchseries-1
で公開しています。
.
├── app
│ ├── main.py
│ └── tests
│ └── small
│ └── test_main.py
├── compose.yml
├── dockerfiles
│ ├── ci
│ │ └── python.Dockerfile
│ └── Makefile
├── Makefile
├── poetry.lock
├── pyproject.toml
└── README.md
2. Makefileの活用 #
今回 「開発とCI環境の統一」 を目指すため、開発環境とCI環境で同じコマンドでlintやtestを実行できると便利です。(開発用はxxxのコマンドでCI用だとyyyのコマンドと分けると管理が大変になるので)
そこでMakefile
に必要なコマンドを記載して、make <target>
という形で利用していきます。
そこでいくつか先に知っておくと便利なMakefileの活用法を紹介します。
helpコマンドの実装
Makefileにhelpコマンドを追加して、.DEFAULT_GOAL=help
でdefaultをhelpにしておくと分かりやすいのでおすすめです。
Makefile
.DEFAULT_GOAL=help
.PHONY: help
help: ## Show this help.
@echo "Usage: make [target]"
@echo ""
@echo "Targets:"
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z0-9_-]+:.*?## / {printf " %-20s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
.PHONY: build-ci-python
build-ci-python: ## Build the Docker image.
$ make -C dockerfiles
Usage: make [target]
Targets:
help Show this help.
build-ci-python Build the Docker image.
push-ci-python Push the Docker image to the registry.
@
を使わない
Makefile
ではコマンド前に@
をつけるとコマンド自体を出力しなくなります。通常使う時には出力が邪魔なので@
をつけることが多いですが、CI/CDのlogで実際のコマンドが確認できなくてdebugが大変なため、なるべく@
は使わないようにします。
コンテナ内、外からの実行
コンテナを開発環境として利用するので、基本的にはコンテナ内でmake <target>
を実行しますが、docker compose exec
を活用することでコンテナ外からもmakeコマンドが実行できます。
container内からlintをする例
root@v0-dev-01:~/blog/project/gitlab-ci-python-sample# docker exec -it 856c2 bash
root@856c2a9350d8:/app# make lint
poetry run ruff check app
All checks passed!
poetry run ruff check --select I --fix
All checks passed!
poetry run ruff format
2 files left unchangeds
container外からlintをする例
root@v0-dev-01:~/blog/project/gitlab-ci-python-sample# docker compose exec app make lint
poetry run ruff check app
All checks passed!
poetry run ruff check --select I --fix
All checks passed!
poetry run ruff format
2 files left unchanged
3. コンテナ開発環境の準備 #
3-1. dockerfileの作成 #
まずはPythonが使える開発環境を用意するために、dockerfiles/ci/python.Dockerfile
を用意します。
この開発環境ではdocker imageとpoetryの「2つの仮想環境を使い分け」ます。通常2重の仮想化になるので、poetryの仮想化はOFFにすることが多いですが、以下の欠点があります。
- pipのパッケージをimageに含めてしまうと、パッケージの追加ごとに毎回build + pushが必要になる。
- 「パッケージをアップデート → CIでテスト → 問題なければイメージに組み込み」という流れを取りたいため、CI用イメージにはpipパッケージを入れたくない。 従って、pipのパッケージに限りpoetryの仮想環境で保持してもらいます。
このためimage内部ではpoetry install
によるpkgのinstallはせず、poetry config virtualenvs.in-project true
によって仮想環境をproject内部に作る設定を入れるに留めます。これによってCI/CDの際にも仮想環境をキャッシュすることが可能になります。詳しくは第2回の記事を参照ください。
dockerfiles/ci/python.Dockerfile
FROM python:3.12
ENV PYTHONUNBUFFERED=1
ENV PATH="/root/.local/bin:$PATH"
RUN apt-get update && apt-get install -y \
git \
postgresql-client # Add or remove as needed
RUN curl -sSL https://install.python-poetry.org | python3 -
RUN poetry config virtualenvs.in-project true
3-2. container-registryに保存 #
上記のimageはGiltab CI/CDでもlint, test用に利用します。従ってこの段階でimageを構築して、GitLab container registryに保存しておきましょう。
またdockerfiles/Makefile
も作っておくと、以降のimage化に非常に便利です。pushする際にはGitLab container registryにログインが必要なので注意しましょう。
dockerfiles/Makefile
CONTAINER_REGISTY_URL=registry.gitlab.com/blog4894132/gitlab-ci-python-sample
CI_PYTHON_IMAGE=${CONTAINER_REGISTY_URL}/ci/python:latest
.PHONY: build-ci-python
build-ci-python: ## Build the Docker image.
# Need to change to the parent directory because Docker cannot COPY from parent directories.
cd ../ && docker build -t ${CI_PYTHON_IMAGE} -f dockerfiles/ci/python.Dockerfile .
.PHONY: push-ci-python
push-ci-python: ## Push the Docker image to the registry. (Requires login to GitLab Container Registry)
docker push ${CI_PYTHON_IMAGE}
pushできるとUIから保存されているイメージを確認することができます。これによって誰でもContainer Registryからpullして開発環境を利用でき、CI/CDからも利用できるようになります。
3-3. compose.ymlを作っておく #
基本的にvolumeをattachしてコンテナ内部で開発するので、compose.yml
と.env
を作っておきます。
またpoetry-cache:/app/.venv
と.venv
をキャッシュしておくと、コンテナを再構築した際にpipの更新が早くて便利です。
services:
app:
image: ${CONTAINER_REGISTRY_URL}/ci/python:latest
tty: true
volumes:
- ./:/app
- poetry-cache:/app/.venv
working_dir: /app
env_file:
- .env
volumes:
poetry-cache:
4. poetryを使った開発環境の準備 #
4-1. pyproject.toml
#
以前はpythonのパッケージ化や依存関係を記載する際にsetup.py
やPipfile
等を利用していましたが、最近ではPythonのライブラリのほとんどがpyproject.toml
を利用しています。
分かりやすい上に、参考例も多いため今回はpyproject.toml
に設定項目をまとめていく形にします。
4-2. poetry init #
poetry init
コマンドでpyproject.toml
を生成します。
本来package nameはproject名と同一のgitlab-ci-python-sample
にすべきですが、解説する際にややこしい + pkg化して公開もしないのでapp
に変更します。
$ poetry init
This command will guide you through creating your pyproject.toml config.
Package name [gitlab-ci-python-sample]: app ### appに変更 ###
Version [0.1.0]:
Description []:
Author [***, n to skip]: n
License []:
Compatible Python versions [^3.11]:
### 省略 ###
Generated file
[tool.poetry]
name = "app"
version = "0.1.0"
description = ""
authors = ["Your Name <[email protected]>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
4-3. 開発用pkgの追加 #
開発用のpkgは--dev
をつけてinstallします。linterはお好みですが今回はRuff
を利用します。
poetry add pytest pytest-cov ruff --dev
Ruff
は全ての設定を以下のようにpyproject.toml
に入れることができる点と、isort、linter、formatterの全てを兼ね備えており構成がsimpleになるため採用しています。
pyproject.toml
[tool.ruff]
target-version = "py312"
line-length = 88
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG001", # unused arguments in functions
"PL", # pylint
]
ignore = [
"B008", # do not perform function calls in argument defaults
"W191", # indentation contains tabs
"B904", # Allow raising exceptions without from e, for HTTPException
"PLR2004"
]
メモレベルですが以前にRuffについて少し紹介しています。
5. 開発用コンテナでpythonの実行 #
開発 + CI用コンテナと、poetryの準備ができたので実際にpythonのコードを実行してみます。
5-1. main.pyの用意 #
最初から複雑なコードを書いて、後にCI/CDに躓いても面倒なのですごく簡単なapp/main.py
を作成します。
def my_func(a: int, b: int) -> int:
return a + b
print(my_func(1, 2))
5-2 poetry installの実行 #
開発用コンテナではpoetry自体のinstallだけなので、別途poetry install
で仮想環境を作成する必要があります。
コンテナ内部でpoetry install
を実行するか、docker compose exec
を使ってコンテナ外からpoetry install
をしておきましょう。
root@01cb6fc958ea:/app# poetry install -v
Using virtualenv: /app/.venv
Installing dependencies from lock file
Finding the necessary packages for the current system
Package operations: 0 installs, 0 updates, 0 removals, 7 skipped
- Installing coverage (7.6.4): Skipped for the following reason: Already installed
- Installing pytest (8.3.3): Skipped for the following reason: Already installed
- Installing iniconfig (2.0.0): Skipped for the following reason: Already installed
- Installing pluggy (1.5.0): Skipped for the following reason: Already installed
- Installing pytest-cov (6.0.0): Skipped for the following reason: Already installed
- Installing packaging (24.1): Skipped for the following reason: Already installed
- Installing ruff (0.7.1): Skipped for the following reason: Already installed
Installing the current project: app (0.1.0)
またpoetry config virtualenvs.in-project true
によって、project内に.venv
が生成されます。この仮想環境はdocker compose
のvolume mountによってコンテナ内外で保持されるので、一応コンテナ外からの開発も可能になります。
root@01cb6fc958ea:/app# ls -al
total 72
drwxr-xr-x 6 root root 4096 Oct 30 14:27 .
drwxr-xr-x 1 root root 4096 Oct 30 14:27 ..
drwxr-xr-x 4 root root 4096 Oct 30 14:27 .venv ### poetryによって生成される ###
5-3. Makefileでcompose up #
poetry install
を毎回実施するのは大変なので、Makefile
で開発用コンテナ作成の一連の作業をコマンド化します。
Makefile
################################### Docker Compose Commands ######################################
# These command groups need to be executed from the host (outside the container) only."
.PHONY: compose-up
compose-up: ## Start the docker compose services.(Execute from outside the container)
docker compose up -d
docker compose exec app poetry install -v
.PHONY: compose-down
compose-down: ## Stop the docker compose services.(Execute from outside the container)
docker compose down
このMakefile
は、dockerfiles/Makefile
とは別で作成しましょう。
.
├── app
├── dockerfiles
│ ├── ci
│ └── Makefile # for image build
└── Makefile # for development
6. lintとtestの実行 #
CI/CDでlintとtestのチェックする前に、開発環境でlintとtestができるように整備します。
6-1. lintの実施 #
lintはよく使うコマンドなので、Makefileを作成してlintのコマンドを追加します。
Makefile
################################### Lint Commnads ######################################
# These commands should be executed from within the app container.
# If you need to run them from the host, use `docker compose exec app make <target>`."
.PHONY: lint
lint: ## Run linting.
poetry run ruff check app
poetry run ruff check --select I --fix
poetry run ruff format
6-2. testの実行 #
適当にapp/tests/small/test_main.py
を作っておきます。
from app.main import my_func
def test_my_func_positive_numbers():
assert my_func(1, 2) == 3
def test_my_func_negative_numbers():
assert my_func(-1, -2) == -3
これもMakefile
に書いておきましょう。
$ docker compose exec app make small
poetry run python -m pytest app/tests/small --cov --cov-report term
========================================================== test session starts ===========================================================
platform linux -- Python 3.12.7, pytest-8.3.3, pluggy-1.5.0
rootdir: /app
configfile: pyproject.toml
plugins: cov-6.0.0
collected 5 items
app/tests/small/test_main.py ..... [100%]
---------- coverage: platform linux, python 3.12.7-final-0 -----------
Name Stmts Miss Cover
--------------------------------------------------
app/main.py 3 0 100%
app/tests/small/test_main.py 11 0 100%
--------------------------------------------------
TOTAL 14 0 100%
=========================================================== 5 passed in 0.05s ============================================================
おわりに #
これで環境依存をかなり減らした開発環境が整備できました。
- 開発環境 → docker image + poetryで統一し、image自体もContainer Registryに保存されるので誰でも同じ環境に
- Makefile → 誰でも同じコマンドを利用することになるので、コマンド差分がなくなる。
次回はここで作成した開発環境とMakefileをそのまま活用して、Gitlab CI/CDでlintとtestを実施する方法を紹介します