きっかけ
開発時、feature ブランチの Pull Request (以下、PR)ごとに実行環境が準備されると便利だよねというところから、PR ごとに開発環境を構築される仕組みを作ることになりました。
使用技術スタック
- Github Actions
- Cloud Run
実装例 (バックエンド)
git-flow にて feature/hotfix のブランチ上で、環境を用意したいという要望があり、
git-flow に基づいて実装をしてきます。
※ git-flow は Atlassianのブログ がわかりやすいかと思います。
git-flow の図
- feature ブランチにて PR を作成する
- 環境を作成したい PR に対して
PReview
というラベルを Github を付与する - Github Actions の Job が実行される
- PR ごとの環境とエンドポイントの URL が発行される
Github Actionsのバックエンド job実装例
on:
pull_request:
types: [synchronize, labeled]
branches:
- "develop"
- "master"
env:
PROJECT_ID: site-staging
GCP_SA_KEY: ${{ secrets.STG_GCLOUD_SERVICE_KEY }}
MYSQL_USER: XXX
SERVICE: site-api
REGION: asia-northeast1
IMAGE: asia-docker.pkg.dev/site-staging/site-api/api:${{ github.sha }}
jobs:
deploy-pr-staging-environment:
if: contains(github.event.pull_request.labels.*.name, 'PReview')
runs-on: ubuntu-18.04
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Cloud SDK
uses: google-github-actions/setup-gcloud@v0.2.0
with:
project_id: ${{ env.PROJECT_ID }}
service_account_key: ${{ env.GCP_SA_KEY }}
export_default_credentials: true
- name: Exec Cloud SQL Proxy
uses: mattes/gce-cloudsql-proxy-action@v1
with:
creds: ${{ env.GCP_SA_KEY }}
instance: site-staging:asia-northeast1:site-database
- name: Copy site database
run: |
sudo apt-get --allow-releaseinfo-change update
sudo apt install -y default-mysql-client
mysql -u ${{ env.MYSQL_USER }} -h 127.0.0.1 --port=5432 -e "DROP DATABASE IF EXISTS site_${{ github.event.pull_request.number }};"
mysqldump -u ${{ env.MYSQL_USER }} -h 127.0.0.1 --port=5432 --set-gtid-purged=OFF site > from_db.dump.sql
mysqladmin -u ${{ env.MYSQL_USER }} -h 127.0.0.1 --port=5432 create site_${{ github.event.pull_request.number }}
mysql -u ${{ env.MYSQL_USER }} -h 127.0.0.1 --port=5432 site_${{ github.event.pull_request.number }} < from_db.dump.sql
- name: Authorize Docker push
run: gcloud auth configure-docker asia-docker.pkg.dev --quiet
- name: Build and Push Container
run: |-
docker build . -t ${{ env.IMAGE }}
docker push ${{ env.IMAGE }}
- name: Deploy to Cloud Run
id: deploy
uses: google-github-actions/deploy-cloudrun@v0.10.0
with:
service: ${{ env.SERVICE }}
image: ${{ env.IMAGE }}
region: ${{ env.REGION }}
tag: pr-${{ github.event.pull_request.number }}
env_vars: MYSQL_DATABASE=site-${{ github.event.pull_request.number }}
no_traffic: true
- name: Find Comment
uses: peter-evans/find-comment@v1
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: github-actions[bot]
body-includes: "Preview"
- name: Create Preview URL
id: preview-url
run: echo "::set-output name=value::https://pr-${{ github.event.pull_request.number }}---site-api-*******-an.a.run.app"
- name: Get datetime for now
id: datetime
run: echo "::set-output name=value::$(date)"
env:
TZ: Asia/Tokyo
- name: Create or update comment
uses: peter-evans/create-or-update-comment@v1
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
Visit the :eyes: **Preview** :eyes: for this PR (updated for commit ${{ github.event.pull_request.head.sha }}):
<${{ steps.preview-url.outputs.value }}>
<sub>(:fire: updated at ${{ steps.datetime.outputs.value }})</sub>
edit-mode: replace
トリガー条件
on:
pull_request:
types: [synchronize, labeled]
branches:
- "develop"
- "master"
- PRに新しくpushされた際、またラベルが付与された際に、実行されるように設定が加えられています
- また、develop への PR、 master への PR はそれぞれ、feature ブランチ、hotfix ブランチからの PR を想定して、2つのブランチに絞った設定になっています
実行条件
jobs:
deploy-pr-staging-environment:
if: contains(github.event.pull_request.labels.*.name, 'PReview')
runs-on: ubuntu-18.04
...
こちらの設定にて、PReviewが付与されたときだけにトリガーされるようになっています。
if: contains(github.event.pull_request.labels.*.name, 'PReview')
データベースの作成
- name: Copy site database
run: |
sudo apt-get --allow-releaseinfo-change update
sudo apt install -y default-mysql-client
mysql -u ${{ env.MYSQL_USER }} -h 127.0.0.1 --port=5432 -e "DROP DATABASE IF EXISTS site_${{ github.event.pull_request.number }};" # 2回目動かす際に失敗するためにDBがあったら削除するように設定を追加している
mysqldump -u ${{ env.MYSQL_USER }} -h 127.0.0.1 --port=5432 --set-gtid-purged=OFF site > from_db.dump.sql
mysqladmin -u ${{ env.MYSQL_USER }} -h 127.0.0.1 --port=5432 create site_${{ github.event.pull_request.number }}
mysql -u ${{ env.MYSQL_USER }} -h 127.0.0.1 --port=5432 site_${{ github.event.pull_request.number }} < from_db.dump.sql
site_${{ github.event.pull_request.number }}
で DB を作成し、既存 DB からダンプして、インポートする形で DBを作成しています。
ビルド、プッシュ、デプロイ
- name: Build and Push Container
run: |-
docker build . -t ${{ env.IMAGE }}
docker push ${{ env.IMAGE }}
- name: Deploy to Cloud Run
id: deploy
uses: google-github-actions/deploy-cloudrun@v0.10.0
with:
service: ${{ env.SERVICE }}
image: ${{ env.IMAGE }}
region: ${{ env.REGION }}
tag: pr-${{ github.event.pull_request.number }}
# 先程作成したデータベース名を環境変数で指定している
env_vars: MYSQL_DATABASE=site-${{ github.event.pull_request.number }}
no_traffic: true
ここではDockerでビルドし、Cloud Runでデプロイする形を取っています。
Action は google-github-actions/deploy-cloudrun@v0.10.0
を利用して作成し、tag を PR ナンバーを利用して作成しています。
こちらは PR ごとの URL 作成に必要となってきます。
既にURL用のコメントが発行されているかを確認する
- name: Find Comment
uses: peter-evans/find-comment@v1
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: github-actions[bot]
body-includes: "Preview"
後述する Preview という Body が含まれていたら、新しくコメントするようにし、既にあればコメントを更新するようにできます。
PR 用のコメントを残すようにする
- name: Create Preview URL
id: preview-url
run: echo "::set-output name=value::https://pr-${{ github.event.pull_request.number }}---site-api-*******-an.a.run.app"
- name: Get datetime for now
id: datetime
run: echo "::set-output name=value::$(date)"
env:
TZ: Asia/Tokyo
- name: Create or update comment
uses: peter-evans/create-or-update-comment@v1
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
Visit the :eyes: **Preview** :eyes: for this PR (updated for commit ${{ github.event.pull_request.head.sha }}):
<${{ steps.preview-url.outputs.value }}>
<sub>(:fire: updated at ${{ steps.datetime.outputs.value }})</sub>
edit-mode: replace
- 1 つ目のステップで、PR の URL を output、2 つ目のステップで、日付の output が取得します。
- 最後のステップで PR に URL をコメントして残します。
次のURLが発行されます。
Cloud Run 上ではリビジョンが新しく作成され、トラフィックは既存の staging に向かないように 0% になっています。
PR Close 時の参考 (一部抜粋)
name: PR Staging Delete
on:
pull_request:
types: [closed]
branches:
- "develop"
- "master"
...
jobs:
delete-pr-staging-environment:
if: contains(github.event.pull_request.labels.*.name, 'PReview')
...
- name: Delete Database
run: |
sudo apt-get --allow-releaseinfo-change update
sudo apt install -y default-mysql-client
mysql -u ${{ env.MYSQL_USER }} -h 127.0.0.1 --port=5432 -e "DROP DATABASE site_${{ github.event.pull_request.number }};"
- name: Delete revision with tag
run: >
gcloud run services update-traffic ${{ env.SERVICE }}
--region ${{ env.REGION }}
--remove-tags pr-${{ github.event.pull_request.number }}
- name: Find Comment
uses: peter-evans/find-comment@v1
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: github-actions[bot]
body-includes: "Preview"
- name: Create Preview URL
id: preview-url
run: echo "::set-output name=value::https://pr-${{ github.event.pull_request.number }}---site-api-*******-an.a.run.app"
- name: Get datetime for now
id: datetime
run: echo "::set-output name=value::$(date)"
env:
TZ: Asia/Tokyo
- name: Create or update comment
uses: peter-evans/create-or-update-comment@v1
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
Visit the :eyes: **Preview** :eyes: for this PR (updated for commit ${{ github.event.pull_request.head.sha }}):
~<${{ steps.preview-url.outputs.value }}>~
<sub>(:warning: deleted at ${{ steps.datetime.outputs.value }})</sub>
edit-mode: replace
基本的には PR が Close 時に反対のことをしているだけです。
コメントは Close 時に以下のようにコメントアウトされます。
実装例 (フロントエンド) ※参考までに
こちらに関しては変更点を環境変数として上書きする必要があり、また複数 Workflow Template を利用しているので、最初に URL を取得する形の実装をしています。
jobs:
get-preview-url:
if: contains(github.event.pull_request.labels.*.name, 'PReview')
runs-on: ubuntu-latest
outputs:
app_url: ${{ steps.preview-url.outputs.app_url }}
api_url: ${{ steps.set-api-url.outputs.result }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Extract PR request API URL
uses: actions/github-script@v6
id: set-api-url
if: contains(toJSON(github.event.pull_request.body) , 'backend:')
with:
script: |
const description = context.payload.pull_request.body
const result = description.split('\r\n').find(str => str.startsWith('backend:')).split(':')[1].trim()
return result
result-encoding: string
- name: Create Preview URL
id: preview-url
run: |
echo "::set-output name=app_url::https://pr-${{ github.event.pull_request.number }}---site-app-*******-an.a.run.app"
...
バックエンドの環境変数を書き換える際に使用
- name: Extract PR request API URL
uses: actions/github-script@v6
id: set-api-url
if: contains(toJSON(github.event.pull_request.body) , 'backend:')
with:
script: |
const description = context.payload.pull_request.body
const result = description.split('\r\n').find(str => str.startsWith('backend:')).split(':')[1].trim()
return result
result-encoding: string
PR 作成時に backend:pr-XXX——api-hogehoge.a.run.app という PR のコメントを追記するとそれを見て、参照バックエンドビルド時の環境変数が書き換わるようになります。
環境変数の書き換え (一部抜粋)
- name: Set PR Staging URL
run: |-
if [ -n "${{ inputs.api_url }}" ]; then
sed -i 's|API_ROOT=.*|API_ROOT=${{ inputs.api_url }}/api/v1/|g' ./packages/site-app/.env.stg
fi
sed -i 's|SITE_APP_DOMAIN=.*|SITE_APP_DOMAIN=${{ inputs.app_url }}|g' ./packages/site-app/.env.stg
cat ./packages/site-app/.env.stg
- name: Build and Push Container
run: |-
docker build -t ${{ env.IMAGE }} -f ${{ inputs.dockerfile_name }} --build-arg ENV=stg .
docker push ${{ env.IMAGE }}
ビルド前に環境変数が埋め込まれる実装になっているため、ビルド前に環境変数のファイルを先程取得した URL の値に sed
で書き換えるようにしました。
導入時の課題
実際に導入をしてみると下記のような課題がありました。
- 各ホストのプレビューの URL が DB の ID がサブドメインとして付与されるため、各ホストごとのにプレビュー環境を用意するのが難しい形になっている
- タグより前に
.
がついた URL が発行されると証明書の ASN にマッチしなくなる*.preview.test.site
が使用不可能 **.**.preview.test.site
の証明書は発行できないので各ホストに合わせて証明書を当てることができない- Cloud Run はドメインが完全一致する必要があるので、プレフィックスに何が当たるかわからないものは使用できない
- 独自ドメインでプロキシ転送する必要があるが、アプリケーションの実装も変更しない限り実現は難しそう
⇒ 結論: 動的な値が2箇所入ると厳しい
PreviewURL
https://97938b8a21dc443a848b5ecbb3ec8e5e.preview.test.site
各ドメインごとの URL (下記のようなドメインは不可能)
https://97938b8a21dc443a848b5ecbb3ec8e5e.preview.test.site/pr-XXX—site-app-*******-an.a.run.app
補記
- Workflow Templates はデフォルトブランチに merge されると、直接利用しないにもかかわらず Actions の一覧に出てきて厄介 (視認性が落ちるため)
- Workflow Templates は
.github/workflows
以下に作成する必要があり、template だけ下の階層にディレクトリを作成しても動作しないので同列になって見にくい (命名規則で解消する必要あり) - Github Actions は実行が逐一表示されないときがあるので、その点は CircleCI の方が利便性が高い
参考文献
https://zenn.dev/matken/articles/preview-deploy-on-cloud-run