Makefile で環境構築を確実に一瞬で終わらせる話

2024/04/01に公開

はじめに

ラブグラフ 開発チーム インターン の こるく です。
私がラブグラフに Join してまず感動したのが、コマンド一発で完了する超お手軽な環境構築でした。
普通プロジェクトに Join するときは面倒な環境構築をする必要がありますが、ラブグラフではそれが全くありませんでした
ということで今回は、それを実現している Make と Docker を使って、開発テストCI本番すべての環境で、ランタイムの環境と環境変数の設定をすべてコードベース ( IaC というやつ? ) でラクに共有して開発体験を爆アゲしようと思います。

この構成が目指すところ

✅ 環境で悩むことをなくして開発体験を爆アゲする
環境構築をコマンド一発でできるようにする
✅ ついでにテストもコマンド一発でできるようにする
環境変数をホストマシンのシェルから排除し、コードの一部としてリポジトリ内で管理する
環境変数をセットすることなく CI ( GitHub Actions ) と本番環境でも開発環境と全く同じ環境を用意する

ココがつらいよ環境構築

環境変数はつらいよ

エンジニアなら絶対に避けて通れない環境変数ですが、これが結構開発体験を悪くしています。
というのも、環境変数はその名の通り環境ごとにセットの方法が異なる上に、Git でのバージョン管理が難しいからです。
例えばローカル環境ではシェルの設定ファイルに環境変数を定義しますが、GitHub Actions では GitHub の Web UI から設定します。
環境ごとにこのような差が生まれるせいで、シンプルに面倒ですし、おま環の原因になりがちです。
今回はこれを .env ファイルを複数作ることで解決したいと思います。

CI もつらいよ

ある程度プロジェクトの規模が大きくなってくると、テストを行う必要が出てきます。
しかも、そのテストをするために DB が必要になったりします。
これがかなり厄介です。
今回はこれを Make と Docker Compose を使うことで解決します。

結論 ( Django の例 )

ディレクトリ構造

.
├── Makefile
├── docker
│   ├── postgres
│   │   ├── Dockerfile
│   │   └── initdb
│   │       └── 01.sql
│   └── python
│       └── Dockerfile
└── compose.yml

Makefile

ifneq (,$(wildcard ./.env.production))
	include .env.production
	export
else

ifneq (,$(wildcard ./.env.local))
	include .env.local
	export
endif

.PHONY: build
build:
	docker compose build

.PHONY: up
up:
	docker compose up -d --build
	$(MAKE) online_migrate
	$(MAKE) logs

.PHONY: down
down:
	docker compose down

.PHONY: logs
logs:
	docker compose logs -f

.PHONY: online_migrate
online_migrate:
	docker compose run --rm django bash -c "python manage.py migrate"

.PHONY: flake8
flake8:
	docker compose run --rm api bash -c "python run_command.py flake8 ./"

.PHONY: mypy
mypy:
	docker compose run --rm api bash -c "python run_command.py mypy ./"

.PHONY: black
black:
	docker compose run --rm api bash -c "python run_command.py black ./"

.PHONY: black_check
black_check:
	docker compose run --rm api bash -c "python run_command.py black ./ --check"

.PHONY: isort
isort:
	docker compose run --rm api bash -c "python run_command.py isort ./"

.PHONY: isort_check
isort_check:
	docker compose run --rm api bash -c "python run_command.py isort ./ --check-only"

.PHONY: pytest_html
pytest_html:
	docker compose run --rm api bash -c "python run_command.py pytest -v ./test/ --cov=./src/ --cov-report=html --html=report.html"

.PHONY: pytest_xml
pytest_xml:
	docker compose run --rm api bash -c "python run_command.py pytest -v ./test/ --cov=./src/ --cov-report=xml"

.PHONY: pytest_ci
pytest_ci:
	docker compose run --rm api bash -c "python run_command.py pytest -v ./test/ --cov --junitxml=pytest.xml --cov-report=term-missing:skip-covered | tee pytest-coverage.txt"

.github/workflows/python.yml

name: Python CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

env:
  DOCKER_BUILDKIT: 1
  COMPOSE_DOCKER_CLI_BUILD: 1
  IMAGE_CACHE_DIR: /tmp/cache/docker-image
  IMAGE_CACHE_KEY: cache-image

jobs:
  build:
    name: Build docker image
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        id: checkout
        uses: actions/checkout@v2

      - uses: satackey/action-docker-layer-caching@v0.0.11
        continue-on-error: true

      - name: Docker build
        run: make build

  flake8:
    name: Run flake8
    runs-on: ubuntu-latest
    needs: build

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - uses: satackey/action-docker-layer-caching@v0.0.11
        continue-on-error: true

      - name: Run flake8
        run: |
          make flake8

  mypy:
    name: Run mypy
    runs-on: ubuntu-latest
    needs: build

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - uses: satackey/action-docker-layer-caching@v0.0.11
        continue-on-error: true

      - name: Run mypy
        run: |
          make mypy

  pytest:
    name: Run pytest
    runs-on: ubuntu-latest
    needs: build

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - uses: satackey/action-docker-layer-caching@v0.0.11
        continue-on-error: true

      - name: Run pytest
        run: |
          make pytest_ci

compose.yml

  services:
  postgres:
    build:
      context: .
      dockerfile: ./docker/postgres/Dockerfile
    container_name: "back-postgres"
    networks:
      back-nw:
    ports:
      - "5432:5432"
    volumes:
      - ./docker/postgres/initdb:/docker-entrypoint-initdb.d
      - postgres-data:/var/lib/postgresql/data
    restart: unless-stopped
    tty: true

  django:
    build:
      context: .
      dockerfile: ./docker/python/Dockerfile
    container_name: "back-django"
    networks:
      back-nw:
    ports:
      - "8000:8000"
    volumes:
      # .venv をマウントしないようにするために Volume として分離させる
      - django-venv:/deploy/.venv
      - ./:/deploy
    working_dir: /deploy
    command: >
      bash -c "
      python manage.py migrate &&
      python manage.py runserver 0.0.0.0:8080
      "
    restart: unless-stopped
    tty: true

networks:
  back-nw:
    driver: bridge
    ipam:
      driver: default

volumes:
  postgres-data:
    driver: local
  django-venv:
    driver: local

使い方

ローカルでも本番でも CI でも全く同じコマンドを使えるような構成になっています。

起動 / 停止 / ログ閲覧

すべてのサービスを 起動 / 停止 / ログ閲覧 します。

make up # 起動
make down # 停止
make logs # ログ閲覧

Lint / Test

make up コマンドでサービスが起動されていれば、どの環境でも以下のワンライナーで全て完了します。

make flake8 # flake8
make mypy # mypy
make pytest # pytest

その他

実行したいコマンドを Makefile に追加して、コマンドを記述すると便利です。

あとがき

なんと今回は こるく@ラブグラフ として 2 回目の記事です。
私は飽き性なのでアウトプットを習慣にすることができなかったのですが、ラブグラフの開発チームでは通称Zenn 記事タイムという時間があり、なんとか 2 回目のアウトプットに成功しました。
ラブグラフの開発チームでは勉強会や輪読会など、エンジニアの自己成長の場が用意されていてとても楽しく働けています!
( そう書けと言われたわけじゃないです!!!本心です!!! )
ラブグラフの技術ブログの裏話も併せてどうぞ!
https://zenn.dev/lovegraph/articles/4d5556c1c5228d

ラブグラフのエンジニアブログ

Discussion