えらい人:今うちらさ、Git上でデータベースのテーブル構成を管理してるでしょ。
私:あーはいはい。
え:それに変更が入った時にさ、アプリの単体テスト実行したいんだよね。DB使ってるの全部。
私:はい?
え:いやほら、今までの仕組みだとJenkinsでそういうことをやっていたのよ。テーブル構成変更で悪影響でてないか確認したくてね。で、それと同じことをECS使った新しい仕組みのアプリでもやりたいんだよね。Github 使ってなんとかならないかな?
私:んーー?とりあえずやり方調べないとわからないですね。各アプリ側のCIは、Github Actions でやってるからそれを動かせばよいのかな?
え:とにかくやってみてよ。じゃ、よろしくーー。
私:りょ、了解しました。。。
現在の状況
データベースのテーブル構成のSQLファイルは、table_def
というGitHubのリポジトリで管理されている。これは、開発PC上に開発環境を作るときや、Jenkins環境でのDB初期化の時にこのリポジトリ内のSQLを使ってデータベースを作っている。
テーブル構造を更新したいときはtable_def
上で、masterからブランチ作ってやりたい更新をコミットしてプルリクエストを作る。この時に目視によるレビューをして、問題ないならmasterにマージされる。(ブランチ名はご容赦を…)
アプリ開発の要件でテーブルが増えたり定義が変わったりする。 このことをきっかけとして、発生するであろう既存アプリへの影響を検知したい。そこで、単体テストを動かして結果を知りたいというわけだ。
ECS使った新しい仕組みのアプリでは、CIを Github Actions でやってる。PRにコミット入った時にアプリ側のリポジトリの Github Actions を叩けばよいな。
あるいは、table_def
の Github Actions で全部のアプリのテストを実行してもいいかな。 けどアプリ数これから増えそうだし、全部が1個に集中すると管理が大変かもな。やめとこう。
やってみよう
別リポジトリの Github Actions ってどうやって実行すれば良いんだ?そういえばリトアニアにすごいエンジニアがいるから相談してみよう。
リトアニア在住の同僚氏: Found a solution for you
1 2 3 |
run: | curl -XPOST -u "${{ secrets.PAT_USERNAME}}:${{secrets.PAT_TOKEN}}" -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/YOURNAME/APPLICATION_NAME/dispatches --data '{"event_type": "build_application"}' |
おーなるほど。repository_dispatch
というイベントを用意して、Github Actions 内で curlでAPIを叩けばよいのだな。やってみよう!リトアニアの同僚氏ありがとーう。
<<ドタドタ・バタリ>>
ちょっと調べて構成考えてみた。まず処理全体はこんな感じになる。
table_def
でのテーブル修正PRでのコミット発生きっかけで、その時点でのSQLファイルを使ってMySQLデータベースのイメージを build して ECR に pushする。この時点での hash値でタグを付けておく。
その後、各アプリにhash値と返信に必要な情報を client_payload
で送って、 repository_dispatch
を実行する。 各アプリでは、hash値が付いたイメージでデータベースを起動して、単体テストを実行する。結果レポートをS3にアップロードしつつ、成否だけ table_def
上にあるPRにコメントとして残せば良いな。失敗だったら、テスト結果レポートのリンクも入れると。
コード例
テーブル定義リポジトリ
table_def
でのgithub Actionsはこんな感じだ。(途中のECRログインの処理は省略)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
name: notify another repo when candidate database created on: pull_request: branches: - master jobs: build_candidate_database_and_notify: env: REPOSITORY_URI: 1234dummy5678.dkr.ecr.ap-northeast-1.amazonaws.com TARGET_APPLI: appli1,appli2,appli13,appliN outputs: tag_commit_hash: ${{ steps.prepare_parameter.outputs.tag_commit_hash }} target_appli_json: ${{ steps.prepare_parameter.outputs.target_appli_json }} steps: - name: prepare_parameter id: prepare_parameter run: | set -e -x commit_hash=$(echo ${{ github.event.pull_request.head.sha }} | cut -c -7 ) echo ::set-output name=tag_commit_hash::${commit_hash} tg_json=$(jq -nc --arg t "$TARGET_APPLI" '($t|split(","))') echo ::set-output name=target_appli_json::${tg_json} - name: checkout code uses: actions/checkout@v2 # データーベースイメージをビルドし、コミットハッシュから取得したタグを付与してpushする。 - name: build and push images run: | set -e -x docker build -t $REPOSITORY_URI/database:latest . docker tag $REPOSITORY_URI/database:latest $REPOSITORY_URI/database:${{ steps.prepare_parameter.outputs.tag_commit_hash }} docker push $REPOSITORY_URI/database:latest docker push $REPOSITORY_URI/database:${{ steps.prepare_parameter.outputs.tag_commit_hash }} notify_repos: needs: build_candidate_conti_db strategy: matrix: target-appli: ${{fromJSON(needs.build_candidate_conti_db.outputs.target_appli_json)}} steps: # 各アプリのリポジトリの`repository_dispatch` なイベントを実行する。 # イメージのタグ、返信用リポジトリ名、返信用PR番号を `client_payload` で送信する。 - name: notify repository that new database candidate created run: | curl -H "Authorization: token ${{ secrets.PAT_FOR_CI }}" \ -H "Accept: application/vnd.github.everest-preview+json" \ "https://api.github.com/repos/GMORBLOGDUMMY/${{ matrix.target-appli }}/dispatches" \ -d '{"event_type": "candidate_database_created", "client_payload": { "commit_hash" : "${{ needs.build_candidate_conti_db.outputs.tag_commit_hash }}" , "repository_name":"${{ github.event.repository.name }}" , "pull_request_number":"${{ github.event.pull_request.number }}" }}' |
各アプリへ通知をする処理は繰り返しになるから matrix
を使ってみた。イメージのビルドは繰り返されたくないから別のjob
に分ける必要がある。ちょっと面倒だぜ。例えば単純に分けただけだとjob
が2個とも同時に動き出してしまう。イメージのビルドのjob
が終わった後に通知のjob
を実行したいので、yaml に以下の記述を入れる。
1 2 |
needs: build_candidate_conti_db |
さらに、ハッシュ値や通知対象アプリのリストをjob
間で受け渡すために、以下の記述も入れる。
1 2 3 4 |
outputs: tag_commit_hash: ${{ steps.prepare_parameter.outputs.tag_commit_hash }} target_appli_json: ${{ steps.prepare_parameter.outputs.target_appli_json }} |
通知対象のアプリのリストは環境変数から設定するようにした。matrix
では配列オブジェクトが必要らしいから、アプリ名をカンマ区切り文字列にして環境変数に設置し、 jq
と、fromJSON
を使って配列に変換できた。
1 2 3 4 |
# jqでカンマ区切り文字列をJSON文字列に変換 tg_json=$(jq -nc --arg t "$TARGET_APPLI" '($t|split(","))') echo ::set-output name=target_appli_json::${tg_json} |
1 2 3 4 5 |
# fromJSONでJSON文字列を配列に変換 strategy: matrix: target-appli: ${{fromJSON(needs.build_candidate_conti_db.outputs.target_appli_json)}} |
これもっと端的にできないだろうか? しかしとりあえず設定を環境変数に置くことができたからこれで良しとするか。
各アプリのリポジトリ
アプリのリポジトリでのGithub Actionsはこんな感じだ。Kotlinのアプリケーションだとこうなる。 動作のパラメータのようなものは、client_payload
で受け取る。(途中のECRログインやテストの前準備の処理は省略)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
name: test for candidate database on: repository_dispatch: types: [candidate_database_created] jobs: test_candidate_database: steps: - uses: actions/checkout@v2 # データーベースを起動してテストを実行する。環境変数でイメージのタグを設定することで起動するイメージを指定する。 - name: start backing service run: | set -x docker-compose -p ci_network -f .github/docker-compose.database.yml up -d sleep 10 gradlew test env: DATABASE_TAG: ${{ github.event.client_payload.commit_hash }} # テスト結果レポートをS3にアップロード(テスト失敗時のみ) - name: Upload report artifact to S3 if: failure() id: upload-to-s3 run: | now=$(date "+%Y%m%d-%H%M%S") echo ::set-output name=result::${now} aws s3 cp ./build/reports/ s3://GMORBLOGDUMMY/dated/${{ github.event.repository.name }}/${now}/ --recursive # テスト失敗を table_defリポジトリのPRにコメントする。 - name: notify report html url on failure if: failure() uses: marocchino/sticky-pull-request-comment@v2 with: GITHUB_TOKEN: ${{ secrets.PAT_FOR_CI }} recreate: true header: ${{ github.event.repository.name }} message: | ${{ github.event.repository.name }}でテスト失敗してます! [テストレポート](https://GMORBLOGDUMMY.s3-ap-northeast-1.amazonaws.com/dated/${{ github.event.repository.name }}/${{ steps.upload-to-s3.outputs.result }}/tests/test/index.html) repo: ${{ github.event.client_payload.repository_name }} number: ${{ github.event.client_payload.pull_request_number }} # テスト成功を table_defリポジトリPRにコメントする。 - name: notify test success if: success() uses: marocchino/sticky-pull-request-comment@v2 with: GITHUB_TOKEN: ${{ secrets.PAT_FOR_CI }} recreate: true header: ${{ github.event.repository.name }} message: | ${{ github.event.repository.name }}でテスト成功しました! repo: ${{ github.event.client_payload.repository_name }} number: ${{ github.event.client_payload.pull_request_number }} |
if: success()
と if: failure()
を使えば成功時失敗時のそれぞれの処理を設定できる。
それと、table_def
のPRへのコメントは最新の結果だけ表示されるように設定する。 実利用上は複数リポジトリからコメントを受けるので最新のコメントだけが残る形になってしまう。 (どこのリポジトリからのコメントなのかわからないので。) それだと使えないのでメッセージ送信元リポジトリの名前で識別できるようにする。
yamlとしてはこう書く。
1 2 3 |
recreate: true # 最新の結果だけ表示されるように設定 header: ${{ github.event.repository.name }} # メッセージ送信元リポジトリ名でメッセージを区別できるように設定 |
ここを参考にした。
https://github.com/marocchino/sticky-pull-request-comment#keep-more-than-one-comment
まとめ
うん。これでできた。これで ECS使った新しい仕組みのアプリでは repository_dispatch
を受け取ったら指定されたデータベースのイメージで単体テストやって結果を table_def
リポジトリに返せば良いな。テーブル構成変更PRを書いた人がアプリの単体テスト失敗に気がつけるようになるからこれは良いことだ。
各アプリのGithub Actions は既存の単体テストのworkflowをいくつか変更して別に作った形になる。うまく1本のymlファイルに纏めれたらよいけどそれは今後の課題としよう。きれいにメンテしやすく纏められるに越したことはないが、あまりに複雑だったら分けたほうが良いかもしれない。いずれにせよ、CIのコードってどれも似たりよったりでパターンになっていくだろうから、あまり気にしなくても良いかもね。