こんにちは。新規IAMプロダクトでフロントエンドアーキテクトを担当している@shisama_です。
この記事では、開発スピードを上げるためにGitHub Actionsを使ってプルリクエストごとにレビュー用の検証環境をGoogle Cloud Run(以下、Cloud Run)に構築する仕組みについて紹介します。
今回紹介する内容のサンプルのリポジトリはこちらになります。
この仕組みは業務委託でお手伝いいただいていた@chimame_rtさんが考案し設計してくれました 👏
はじめに
現在サイボウズでは新規のIAMプロダクトの開発に取り組んでいます。 2023年内にプロトタイプの開発を終えて製品開発に着手したいという感じでやっております。 詳しくは @ymmt2005が書いた記事を読んで頂けると幸いです。
フロントエンドチームが抱えていた課題
まず、今回紹介する仕組みがなぜ必要になったのかを説明します。
IAMプロダクトのフロントエンドチームは次のロールで構成されており、日本とアメリカにメンバーがいます。
- デザイナー(日本、PM兼務)
- フロントエンドエンジニア(日本/アメリカ)
- フルスタックエンジニア(日本)
デザイナーがページや機能ごとに要件とデザインを考えて、 バックログに書き起こしたバックログをエンジニアが実装するという流れで開発しています。
プルリクエストのレビューはエンジニアがコードをレビューし、デザイナーがUIや動作をレビューしています。 各エンジニアのプルリクエストがマージされるとアプリケーションが検証用の環境へデプロイされるようになっています。
しかし、このやり方だとデザイナーが自分のローカルマシンでアプリケーションを起動して動作確認するか、エンジニアと一緒に動作確認するかしないといけませんでした。
チームメンバーが増えてきて、アメリカとの時差の関係もあり、デザイナーとエンジニアが一緒に作業する時間調整が難しくなってきました。
そこでプルリクエストごとにレビュー環境をクラウド上にサクッと立ち上げてデザイナーが動作確認やUIの確認ができるようにしたいという要望が出てきました。
プルリクエストごとに Cloud Run に検証環境を構築する
デザイナーと他のエンジニアと話して次のような要件を満たすようにレビュー用の環境を整えることにしました。
- プルリクエストごとに個別の環境を用意する
- 認証によるアクセス制限
- 任意のタイミングでデプロイできるようにする
複数人で開発しているため、1つの環境を共有してしまうとデータの更新や他の人の修正の影響により動作確認がうまくできない可能性があります。 そのため、プルリクエストごとにレビュー用の環境を用意することにしました。
プルリクエストごとに任意のタイミングでアプリケーションをCloud Runへデプロイする仕組みを作りました。
このプロジェクトでは GCP を利用していたので、Cloud Run を選択しました。
デプロイの仕組みの概要は次の図のとおりです。
ざっくりと次のフローになっています。
Step 1. プルリクエストにラベルを適用してGitHub Actionsを起動(上図の2)
Step 2. Next.jsのビルド
Step 3. Google Cloudの認証
Step 4. Dockerイメージのビルドとプッシュ(上図の3、4)
Step 5. Next.jsのアプリケーションをCloud Runへデプロイ(上図の5)
Step 6. デプロイに成功したサービスのURLをプルリクエストにコメントする
上記のStep.2から順番に説明していきます。
GitHub Actionsの全体像は以下のリンク先をご確認ください。
Step 1. プルリクエストにラベルを適用して GitHub Actions を起動
デプロイは手動でやるのではなく、GitHub Actionsで自動化しています。
プルリクエストに deploy
というラベルを適用すれば、GitHub Actionsがレビュー環境のサービスを立ち上げてURLをコメントしてくれるようになっています。
プルリクエストを作ったタイミングやコミットをpushしたタイミングではない理由は、開発環境の改善やリファクタリングのためのプルリクエストの場合はデザイナーによるUIの確認は不要だからです。
また、作業途中の内容をpushしても検証環境にデプロイする意味はないため、任意のタイミングでデプロイできるようにしています。
GitHub Actionsのworkflowsのyamlファイルには以下のように記述することでラベルを付与したときに起動できます。
name: deploy verification environment on: pull_request: types: labeled
また、ラベルの名前がdeploy
のときだけに限定するには次のように設定します。
jobs: deploy-verification-environment: if: ${{ github.event.label.name == 'deploy' }} # 以下省略
deploy
ラベルを付け直すとその時点で最新のコミットのコードをビルドしてデプロイするようにしています。
また、同じプルリクエスト内でラベルの付け直しをすることで複数回アプリケーションのデプロイをできます。
そのため、プルリクエスト内で修正して再レビューをするときもdeploy
ラベルを付けることで修正版のアプリケーションの動作確認を行うことができます。
Step 2. Next.js のビルド
私たちはフロントエンドのフレームワークにNext.jsを利用しています。
Next.jsをビルドしますが、実行効率を上げるためにnode_modules
はあらかじめキャッシュされているものを使います。
キャッシュはactions/cacheを使ってキャッシュします。package-lock.jsonのハッシュ値を含んだ文字列をキャッシュのキーにしているため、package-lock.jsonに変更があったときはキャッシュがヒットせずnpm ci
を実行してダウンロードしたnode_modules
以下のファイルをキャッシュします。
package-lock.jsonに変更がない場合はキャッシュがヒットするので、npm ci
は実行せずにキャッシュを利用します。
GitHub Actionsでは以下のように定義しています。
- uses: actions/cache@v3 id: node_modules_cache_id env: cache-name: cache-node-modules with: path: '**/node_modules' key: ${{ runner.os }}-setup-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} - if: ${{ steps.node_modules_cache_id.outputs.cache-hit != 'true' }} run: npm ci shell: bash
次に、キャッシュされているnode_modules
内のnpmパッケージを利用してNext.jsのアプリケーションをビルドします。
Next.jsを含む現在のバンドラーの仕組みだとコンテナ内でビルドせずにGitHub Actions側(ホスト側)でビルドした結果をコンテナに取り込むことが大切です。 なぜなら、コンテナ内にビルドのキャッシュを含めてビルドしないと差分キャッシュが効かないため、パフォーマンスが悪くなります。ただ、ビルドキャッシュをコンテナにコピーするのは必要ないものを取り込むことになるのでホスト側にキャッシュは保持しておきます。
CI上でのNext.jsのビルドのキャッシュについてはこちらのページをご参照ください。
Step 3. Google Cloud の認証
Google Container Registry(以下、GCR)やCloud Runを利用するには、Google Cloudへの認証が必要です。
GitHub Actionsでは以下のように定義しています。
- id: "auth" uses: "google-github-actions/auth@v0" with: credentials_json: "${{ secrets.GCP_SERVICE_ACCOUNT_KEY }}" - name: Configure docker to use the gcloud cli run: gcloud auth configure-docker --quiet
google-github-actions/authというGitHub Actionsを利用しています。
https://github.com/google-github-actions/auth
gcloud auth configure-docker
は gcloud
でDockerを扱うための認証を行っています。
secrets.GCP_SERVICE_ACCOUNT_KEY
はGitHubのSecretsに格納されているGoogle Cloudのサービスアカウントのキーを参照しています。
サービスアカウントは書き込み権限とCloud Runを操作する権限を持っています。
詳しくは以下のドキュメントをお読みください。
https://cloud.google.com/sdk/gcloud/reference/auth/configure-docker
Step 4. Docker イメージのビルドとプッシュ
次に、Next.jsのにビルド結果のファイルをデプロイするためのDockerイメージをビルドしていきます。
ビルドしたDockerイメージはGCRへプッシュします。
GitHub Actionsでは以下のように定義しています。
- name: Build a docker image run: docker build --cache-from ${{ env.IMAGE }},${{ env.CACHE_IMAGE }} -t ${{ env.IMAGE }} -f ./Dockerfile . - name: Tag a container image run: docker tag $(docker images --filter 'label=build-stage=installer' -q | head -n 1) ${{ env.CACHE_IMAGE }} - name: Push the docker image run: | docker push ${{ env.IMAGE }} & docker push ${{ env.CACHE_IMAGE }} & wait
ビルド時に利用するイメージキャッシュ( installer
ステージ)も併せてプッシュしています。
このイメージはすべてのプルリクエストで cache
というタグの同じイメージを共有しています。
プロトタイプの段階では、このイメージが頻繁に変わることないと見込んで、同じタグ名を使っています。
現在はまだプロトタイプということもあり、Renovateなどで自動でアップデートする設定なども入れておらず、手動で行っています。
ただ、プロトタイプより後のフェーズになると、セキュリティパッチは可能な限り素早く取り込みたいので、ライブラリのアップデート頻度も変わる可能性があります。 そうなると、このキャッシュの方法も変える必要があるかもしれません。
また、現在はGCRを利用していますが、Artifact Registryへの移行も検討しています。
ビルド時の引数について
-t ${{ env.IMAGE }}
- ビルドしたイメージにタグ付けをします
asia.gcr.io/${{ secrets.GCP_PROJECT_ID }}/frontend-pr-verification-environment:${{ github.head_ref }}
で構成される文字列です${{ github.head_ref }}
はプルリクエストのブランチ名に置き換わります${{ secrets.GCP_PROJECT_ID }}
はGCPのプロジェクトIDに置き換わります
--cache-from ${{ env.IMAGE }},${{ env.CACHE_IMAGE }}
- 前回のビルドイメージおよびnpmキャッシュイメージをビルド時に利用する、という指定です
Dockerfile
キャッシュをヒットさせやすくするためにマルチステージビルドを利用しているため、Dockerfile
の中身に続いて仕組みを解説していきます。
該当のDockerfileは以下のリンク先をご確認ください。
installerステージ
installer
はnpmパッケージをインストールし、node_modules
に配置するためのステージです。この後のステージや、次回以降のビルドのキャッシュとして利用します。
runnerステージ
runner
は名前の通りNext.jsの実行を含むステージです。 すでにインストールされたnpmパッケージを installer
ステージからコピーして利用します。
COPY --from=installer --chown=nextjs:nodejs /app/node_modules ./node_modules COPY --from=installer --chown=nextjs:nodejs /app/package.json ./package.json
同様に、Next.jsのビルド結果やNext.jsの設定をホスト側からコピーします。
COPY --chown=nextjs:nodejs ./.next ./.next COPY --chown=nextjs:nodejs ./next.config.js ./
次のステップでGCRへPushしたイメージを使ってCloud Runへデプロイします。
Step 5. Next.js のアプリケーションを Cloud Run へデプロイ
Cloud Runへデプロイします。
- name: Deploy to Cloud Run id: deploy uses: google-github-actions/deploy-cloudrun@v0 with: service: frontend-pr-${{ github.event.pull_request.number }} image: ${{ env.IMAGE }} region: ${{ env.GCP_REGION }} flags: "--allow-unauthenticated --memory=512Mi --port=3000"
デプロイにはdeploy-run
というGitHub Actionsを利用します。
いくつかのパラメータを設定しなければいけません。それぞれ次の値を設定します。
service
: サービス名を設定します。frontend-pr-<プルリクエストの番号>
を指定します。image
: イメージの場所を指定します。PushしたGCRのURLを指定します。region
: デプロイするリージョンを指定します。flags
:gloud
コマンドのフラグを設定します。詳細は後述。
flags
には--allow-unauthenticated --memory=512Mi --port=3000
を設定します。
--allow-unauthenticated
: サービスを公開しないとアクセスできないため、公開しています。(参考)--memory=512Mi
: メモリを確保しています。--port=3000
: ポート番号3000で起動するようにしています。
ここまででプルリクエストのブランチからCloud RunへNext.jsのアプリケーションのデプロイが完了します。 この時点でサービスへのURLが発行されるので、アクセスは可能になります。
Step 6. デプロイに成功したサービスの URL をプルリクエストにコメント
すでにCloud Runへのデプロイは完了し、デプロイしたアプリケーションへはアクセス可能になっています。
しかし、そのアクセス先のURLはGCPのコンソールを見に行かないといけません。
それだと手間ですし、非エンジニアには難しいかもしれません。
そこで、デプロイが完了したら自動でURLが共有されるようにしています。
URLの共有は該当のプルリクエストのコメントで行われます。
workflowの該当箇所は次のリンク先になります。
- name: Comment uses: peter-evans/create-or-update-comment@v1.4.5 id: report-comment with: issue-number: ${{ github.event.pull_request.number }} body: "${{ steps.deploy.outputs.url }}"
プルリクエストへのコメント投稿にはpeter-evans/create-or-update-commentを利用しています。
https://github.com/peter-evans/create-or-update-comment
このGitHub Actionsにはパラメータを設定しなければいけません。それぞれの設定値は次のとおりです。
issue-number
: プルリクエストの番号を指定しますbody
: デプロイしたアプリケーションのURL
これにより、次のようにGitHub ActionsのbotがURLをコメントしてくれます。
これでデザイナーもバックログのIssueに紐づくプルリクエストからすぐに検証環境へアクセスすることが可能になります。
プルリクエストを閉じたら Cloud Run のサービスを削除する
Cloud Run上に立ち上げたサービスはそのままにしていくと溜まっていきます。 そこで、そのプルリクエストのために構築した検証環境のCloud Runのサービスを削除するGitHub Actionsのワークフローも作成しました。
GitHub Actionsのworkflowの全体像は以下のリンク先をご確認ください。
全体の流れは以下のとおりです。
- プルリクエストをマージまたはクローズしたらGitHub Actionsを起動
- Google Cloudの認証
- デプロイした検証環境のサービス名を取得
- 検証環境のサービスを削除
- GCRからイメージを削除
Step 1. プルリクエストをマージまたはクローズしたら GitHub Actions を起動
閉じたプルリクエストに関してはレビューが完了しているとみなして、サービスを削除しても問題ありません。 そのため、プルリクエストが閉じられたときにGitHub Actionsが実行されるように設定しました。
on: pull_request: types: - closed
Step 2. Google Cloud の認証
まずはGoogle Cloudの認証ですが、この処理についてはデプロイ時の「Step 4. Google Cloud の認証」と同じため解説は割愛します。
Step 3. 検証環境がデプロイされているか確認
次にそのプルリクエストがデプロイされているかどうかを検証します。 検証のためにサービスが存在するかを名前でフィルタリングします。
GitHub Actionsの定義は以下のとおりです。
- name: check if Cloud Run is deployed id: check run: echo "service=$(gcloud run services list --filter metadata.name=frontend-pr-${{ github.event.pull_request.number }} --format 'value(metadata.name)')" >> $GITHUB_OUTPUT
以下のコマンドでfrontend-pr-<プルリクエストの番号>
のサービス名を取得しています。
gcloud run services list --filter metadata.name=frontend-pr-${{ github.event.pull_request.number }}
次のように取得したサービス名がfrontend-pr-
からはじまるか検証します。もし上記のgcloud
コマンドでサービス名が取得できていなかった場合は後続の処理は行われません。
if: ${{ startsWith(steps.check.outputs.service, 'frontend-pr-') }}
Step 4. 検証環境のサービスを削除
次にCloud Runのサービスを削除します。
workflowの該当箇所は次のリンク先になります。
- name: destroy Cloud Run service if: ${{ startsWith(steps.check.outputs.service, 'frontend-pr-') }} run: gcloud run services delete frontend-pr-${{ github.event.pull_request.number }} --region ${{ env.GCP_REGION }} --quiet
gcloud run services delete
はCloud Run上のサービスを削除するコマンドです。サービス名とリージョンを指定しています。
詳しくは公式のドキュメントをお読みください。
https://cloud.google.com/sdk/gcloud/reference/run/services/delete
Step 5. GCR からイメージを削除
最後にGCRから検証環境用にビルドしてPushしたDockerイメージを削除します。
workflowの該当箇所は次のリンク先になります。
- name: delete Google Cloud Registry if: ${{ startsWith(steps.check.outputs.service, 'frontend-pr-') }} run: | for d in $(gcloud container images list-tags ${{ env.IMAGE }} --filter='-tags:*' --format="get(digest)"); do gcloud container images delete ${{ env.IMAGE }}@$d --quiet; done gcloud container images delete ${{ env.IMAGE }}:${{ github.head_ref }} --quiet
for d in ... do
のブロックで、タグ付けされていないイメージの削除を行っています。
これは同じプルリクエストで複数回デプロイされたときに残っているタグが付いていない古いイメージを削除するためです。PushするDockerイメージはブランチ名でタグ付けされるようにしていますが、同じプルリクエスト内に複数回このワークフローを実行すると、最新のイメージにタグが付与され、同じ名前でタグ付けされていた古いイメージからはタグが外されます。
そのため、古いイメージはタグがないまま残ってしまうので、それらを削除するためのコマンドです。
gcloud container images delete ${{ env.IMAGE }}:${{ github.head_ref }} --quiet
はプルリクエストに紐づく検証環境のサービスのために作成したイメージを削除しています。
github.head_ref
にはブランチ名が入ります。
これでCloud Runからサービスを削除し、GCRからもイメージを削除できました。
まとめ
GitHub Actionsを使ってプルリクエストごとに検証環境をCloud Runにデプロイする方法について説明しました。
この仕組みを用意したことで、非エンジニアでも簡単にプルリクエストの変更箇所の動作確認をできるようになりました。
これによりレビューを効率的に行えるようになり開発スピードが加速したと感じています。
とても便利なので、ぜひご活用ください。
今回は紹介しませんでしたがアクセス制限をかけたい場合は、GCP側またはNext.jsのmiddlewareなどを利用して認証を行うようにしてください。
最後に
サイボウズではチームワークあふれる社会を創るための仲間を募集しています。 今回紹介したような開発体験の向上を好きなメンバーやフロントエンドエンジニアを募集しています。 よろしくお願いします!