メインコンテンツへスキップ
  1. Blogs/

Gitlab CI/CDでPythonのDevOps 1 -開発環境の作成-

·4405 文字·
Blog Gitlab Python
hiroki
著者
hiroki
クラウドを作るお仕事をしてます。
目次
gitlab-cicd-python - 関連記事
1: << この記事 >>

はじめに
#

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
最近ではryeuvも人気なのでこちら検討をしても良いかも知れません。ただ今回はコンテナを開発環境のメインで使いPythonのversion管理は不要になるのでpoetryで必要十分です。

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からも利用できるようになります。

alt text

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.pyPipfile等を利用していましたが、最近ではPythonのライブラリのほとんどがpyproject.tomlを利用しています。 分かりやすい上に、参考例も多いため今回はpyproject.tomlに設定項目をまとめていく形にします。

Gunosyの技術blog -その設定、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になるため採用しています。

Ruffはpandasfastapi等でも利用されています。

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について少し紹介しています。

Pythonのlinter(Ruff)のTIPS
·758 文字
Memo Python

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
small/mediumテストを分ける想定ですが、分けない場合はsmallフォルダは不要です

これも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を実施する方法を紹介します

gitlab-cicd-python - 関連記事
1: << この記事 >>

Related

Gitlab CI/CDでseleniumを利用する方法
·2155 文字
Blog Gitlab Python
pyvmomiで自動化8 -情報をまとめて取得-
·2484 文字
Blog VMware VSphere Pyvmomi Python
pyvmomiで自動化7 -HoLを使った検証環境の用意-
·1189 文字
Blog VMware VSphere Pyvmomi Python