GitHubActionsでDjangoのCIを構築する時にはまったポイントと、解消方法

結論

開発環境で利用している Django + MySQL の docker-compose 環境を GitHubActions 上で CI できるようにした。
DB は MySQL5.7, フレームワークは Django3.07。開発環境は Python3.8 だけど、CI は 3.6, 3.7, 3.8 で、DockerCompose コマンドによりテストが実行されるようになっている。

DB 接続周りと、ユーザー権限で色々はまったのでメモしておく。
こちらが最終コード。Python の version を絞ればもっと少なくできるはず。

name: Django CI

on:
  push:
    branches: [master]
  pull_request:
    branches: [master]

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      max-parallel: 4
      matrix:
        python-version: [3.6, 3.7, 3.8]
    steps:
      - uses: actions/checkout@v2
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v2
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install Dependencies
        run: |
          docker-compose build web
          docker-compose up -d
          sleep 15s
          docker ps -a
      - name: Grant to user at container mysql
        run: |
          echo grant user to test databases directly
          docker exec -t mysql mysql -uroot -proot -e'GRANT ALL ON test_tutorial.* TO user;'
      - name: Execute tests
        run: |
          chmod +x manage.py
          docker exec django python manage.py test

ざっくり解説。

master ブランチにプッシュまたはプルリクすると GitHubActions の workflow が実行されるようになっている。
実行ファイルは.github/workflows/django.ymlで、GitHub の仮想環境上で、開発環境と同じようにdocker-composeコマンドを実行してビルド、テストができる。

DB は MySQL を使用していて、こちらも開発環境と同じようにdocker-composeコマンドで立ち上がる。
テスト実行の為にuserに権限だけ付与してある。
DB の立ち上がりに時間がかかるので、sleepコマンドで任意の秒数待たせてから実行してもらう。

なので、大人数で workflow を動かすのには向いていないし、実行全体で 2 分程度かかるので高速 CI とも言いがたいのが現状。

ちなみに Python が複数バージョンで動いて欲しいモチベーションはない。
GitHubActions でNew Workflowしたときのテンプレをそのまま貰ってきた。

前提とする構成とモチベーション

  • モチベーション
    開発環境は docker-compose で、なるべく同一環境を模擬って CI したい。
    GitHubActions 上の MySQL を使用したくない。
  • 構成
    MySQL と Django が 1 つの docker-compose コマンドで動作している。
    MySQL は GitHubAction で services を用いてセットアップして、Django だけ自前で頑張る。という風にはなっていない。

DB 接続周り

GitHubActions で用意できる コンテナ DB を使って構築するかどうかで手順が異なる。

  • 開発環境同様に、docker-compose コマンドを使いたい!
    結果として上述の yml コードになる。
    望ましい形としては.github/workflows/xxxxx.yml以外の設定ファイルを全く触らず、CI 環境が構築できる状態だと考えている。
    GitHubActions 上に MySQL コンテナを立ち上げるような構築もできるはずだが、以下に続く理由で今回は断念した。
  • localhost 実行でいいので、とりあえず CI が動いて欲しい。実際の設定値はこちらのリビジョンを参照(settings.py の一部を改変している)
    開発環境を DockerCompose で運用している場合はsettings.pyの一部に修正を行う必要がある。
    修正せずに workflow を実行すると、DB スキーマが存在しないので、django.db.utils.OperationalError: (2005, "Unknown MySQL server host 'db' (11)")というエラーと共に失敗する。
    これは、Django 側の設定であるsettings.pyDATABASES属性にあるHOSTdbと設定されているからで、python manage.py testコマンドを CI 上で実行すると Django のsettings.pyがロードされるからだと推測した。
    ちなみにこの名前空間はdocker-compose.ymlservices属性と関連している。
    (単にどちらかを localhost にすると、開発環境が動かなくなる)

    つまり、開発環境の docker 上の namespace にはdbという MySQL コンテナが存在している。
    これを維持したまま(≒settings.pyに修正を必要としないようにしたまま)CI するにはdocker-composeコマンドを CI 上で実行する必要があった。
    上述通り、直接 MySQL を使用する(localhost を使用する)場合はsettings.pyを変更する必要がある。
    変更しないとlocalhost ≒ 127.0.0.1 ≠ db(というネームスペース)であるので、動かない。
    具体的にはsettings.pyのソースを修正して、CI を実行する。

    勿論、弊害として開発環境の DB が接続できなくなり、アプリが動かなくなる。
    これでは本末転倒なので、開発環境で docker-compose を使用しているのであればこちらの選択肢はほぼないと考えていい。

    DATABASES = {
        "default": {
            "ENGINE": "django.db.backends.mysql",
            "NAME": "tutorial",
            "USER": "user",
            "PASSWORD": "user",
    -       "HOST": "db",
    +       "HOST": 127.0.0.1,
            "PORT": "3306",
            "TEST": {"NAME": "test_tutorial",},
        }
    }
    

権限

発生したエラーベースで細かく見ていく。
大きく分けてファイル権限と、DB のユーザー権限に分れる。

ファイル権限の変更

Error response from daemon: OCI runtime create failed: container_linux.go:349: starting container process caused "exec: \"./manage.py\": permission denied": unknown

実行時の結果

Docker にmanage.pyの実行権限がないので発生する。
解消するためには、単にmanage.pychmodコマンドを CI ジョブ中に実行すればよいので、冒頭のコードのExecute testschmod +x manage.py付与している。

DB のユーザー権限

django.db.utils.OperationalError: (1044, "Access denied for user 'user'@'%' to database 'test_tutorial'")

実行時の結果

docker-composeコマンドも動く。manage.pyも動く。
となると、あとはこの権限さえ倒せば動くと思われる。
開発環境であれば、docker exec -it mysql bashなどで MySQL のコンテナの中に入り、mysql -uroot -pでログインすることで権限を与えることができる。
ただし、今回は GitHubActions のジョブ中の話なので、任意のコマンドを送り込むために下記を追加した。
docker exec -t mysql mysql -uroot -proot -e'GRANT ALL ON test_tutorial.* TO user;'

若干パワープレイっぽい気もするが、現状は代替案が思い浮かばない。

その発生したエラー

同一ポートエラー

ERROR: for db Cannot start service db: driver failed programming external connectivity on endpoint mysql (ハッシュ値)): Bind for 0.0.0.0:3306 failed: port is already allocated
実行時の結果

.github/workflows/django.ymlservices属性に、mysql の port を指定していたので、GitHubActions の MySQL と、DockerCompose の MySQL のポートが競合した結果。
今回は DockerCompose を優先したいので、最終的にはservicesをまるっと削除している。

その他

the input device is not a TTY
実行時の結果

下記コマンドでユーザーに権限を与えようとして発生した。
結果、bashコマンドを実行するのではなく、-eオプションで直接 SQL 実行することで解決した。

docker exec -it mysql bash
mysql -h 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL ON test_tutorial.* TO user;"

まとめ

初めて GitHubActions を使って、ハロワまでおよそ 3 時間から 4 時間くらいはかかってしまった。
以下、構築手順を備忘録しておく

  • GitHubActions のテンプレートを使わず、xxx.ymlを作ってとりあえずプッシュ。
  • workflow が動かないので、GitHubActions の GUI から新規作成してプッシュ。
  • 無事、echoコマンドで動く確認がとれた。
  • 自前のプロジェクトに合わせるためにドキュメントを一通り確認する。
    • このとき、services が docker と関係ありそうだとか、volume あたりの確認する。
    • どこまでが予約語なのかの勘所をこの辺で触りながら確認する。
  • ファイル権限で動かなかったのでchmod追加する。
  • docker-compose が動かない(Unknown MySQL server host 'db')のにはまる
    • スリープすればよいのでは?と思いつくが記述が分からなくてぐぐる。
  • スリープしたらエラーが変わったので、接続しているっぽいけど権限が与えられないことに更にははまる。
    ここから 2 時間くらい試行錯誤が始まる。
  • MySQL を GitHubActions のコンテナを使って構築する。
    • db の namespaces が異なるので、settings.py を書き換えて確認する
    • テスト起動したので、settings.pyを変える(≒ 開発環境を変える)ことに個室しなければ、構築できる安堵感を得る。
  • DockerCompose を使用する方法に軸を戻す。
    • 外部 SQL を実行する手段を試す。
    • 分からないので直接シェルコマンドを実行する手法に行きつく。

と、かなり紆余曲折あたけどなんとか動く。までは構築できた。

あと CI の観点でフォーマッターとリンターの動作確認はしたいので、それはまた別の機会とする。

シェアする

  • このエントリーをはてなブックマークに追加

フォローする