🌊

StrapiをGitHub ActionsでCloud RunにCDする話

2020/12/08に公開

こんにちは。株式会社dottの清水です。
今日でStrapi Advent Calender 2020も8日目となりました!

今日はStrapiをCloud Runで動かしながら、GitHub Actionsを使ってCI/CDというか、レビュー用環境を構築する話です。
Strapiで動かす場合を想定して書いていきますが、基本的にはどんなサービスでも同じような流れで行けると思います。

なぜGitHub ActionsでPull Requestに対して環境を作るか

まず本題の前に、なぜGitHub Actionsでデプロイをしているかという説明をしていきたいと思います。基本的にはGitHub Actionsではなくても、Cloud Buildさえ設定すればGitHubのリポジトリに反応してレビュー用の環境を作ることができます。GCPのGitHub Appでも、最近ではPRナンバーがちゃんと取れるようになっているので(以前は取れなくて断念した)、PRに対してコメントを残すということなんかもより簡単になっています。

ただdottでは以下のような理由から、わざわざGitHub ActionsでCloud Build経由でデプロイをする方法を選択してます。やはり大きいのは、我々dottではGitHubのIssueベースでプロダクトの開発を行っているからということはあると思います。

  1. PRに対するGitHub Actionsの他のチェックの結果によってデプロイする・しないを変えたい
  2. GitHubへのコメントが楽(もちろんCloud Buildでもできるけど)

GitHub Actionsのworkflowでは、シーケンシャルにjobを進めて行くことができるので「直前のチェックが失敗した場合はデプロイを行わない」という設定が容易に行えます。

jobs:
  auto-test: 
    .........
  deploy-strapi:
    needs: auto-test  # ここに直前のjobの名前を指定するだけ
    runs-on: ubuntu-18.04

GitHubへのコメントをつける動作も、このようにとても簡単です。

      - name: Comment to GitHub
        uses: actions/github-script@v3
        with:
          script: |
            const { issue: { number: issue_number }, repo: { owner, repo }, runId  } = context;
            github.issues.createComment({
              issue_number,
              owner,
              repo,
              body: `Review environment was deployed on...
            **Strapi**:https://pr-strapi-${{ github.event.number }}-<Cloud RunのID>-an.a.run.app
              `
            });

Cloud RunでStrapiを動かす

環境変数の設定について

Cloud Runで動かすためにコンテナー化したStrapiに環境変数を渡すためには、方法としては大きく2つの方法があります。
各環境ごとに、環境変数によって接続するDBの情報を変えたりするためには、このような情報が参考になります。
Strapiの環境設定
StrapiをCloud Runで運用している話

方法1. GCPコンソール上で環境変数を設定する

Cloud RunをデプロイするGUIでは、環境変数を設定することができます。gcloudコマンドでやらない場合は、他にもCloud SQLと接続させたりということはこのような画面で設定します。
デフォルトの構成のStrapiではデータベースの接続情報なんかは、この環境変数で運用すると幸せになれるでしょう。
ただGUIではコマンドでデプロイする際にここら辺の設定を上書きしてしまって、何もしないと空に戻るのでその点は注意です。

方法2. gcloudのGUIで環境変数を設定する

前述の記事のこのセクションにもありますが、上の画面と同様のことをコマンドライン上で行う場合が以下のようになります。

gcloud run deploy --image gcr.io/<GCPプロジェクトのID>/<任意のコンテナイメージのタグ> <任意のCloud Runのサービス名> \
--region asia-northeast1 --platform managed --allow-unauthenticated \
--cpu 1 --memory 512Mi \
--set-env-vars "HOST=0.0.0.0" \
--set-env-vars "NODE_ENV=<developなどの環境名>" \
--set-env-vars "DATABASE_NAME=<DB名>" \
--set-env-vars "DATABASE_USERNAME=<DBユーザー名>" \
--set-env-vars "DATABASE_PASSWORD=<DBパスワード>" \
--set-env-vars "INSTANCE_CONNECTION_NAME=<ここはproject-id:asia-northeast1:db-nameみたいな物がCloud SQLのコンソール画面にあるのでそれを入れる>" \
--add-cloudsql-instances <ここはproject-id:asia-northeast1:db-nameみたいな物がCloud SQLのコンソール画面にあるのでそれを入れる>

追加したい環境変数がある場合は、上記のコマンドに --set-env-vars "ORENO_ENVIRONMENT=<DBユーザー名>" \ を追加すればOKです。

ようやく本題のCI/CD

今回は、GitHub ActionsのPRに合わせてコンテナイメージをビルドして、PR専用の環境にデプロイしてPRコメントとしてそのURLをポストするという流れです。
案件の性質次第だと思いますが、今回の方法ではCloud SQLのデータベースはPR環境ごとには分けませんでした。StrapiはSQLiteでも動かせるので、CIと合わせてデータセットなどを登録してしまって、それぞれ独自の環境で動かすなんていうのもいいかもしれませんね(ステートが保持できないのでCloud Runでなければ!)。
あとはGitHubの世界のことはGitHubの中でやりたいし、GCPの世界のことはGCPのプロジェクトの中でやりたいですよね。そこを切り分けるポイントとして、どちらも併用しています(その分お金はかかるけど)。

CDの概要

PRがdevelopブランチに作成されると、そのPR番号ごとにCloud Runのサービス自体を増やして登録してしまおうという流れです。
最初はCloud Runのリビジョンを追加してURLを発行してしまおうかとも思ったんですが、バージョンというよりはやはり環境なので、サービス自体を作成することにしました。

GitHub Actions

まず一旦GitHub ActionsでLintなどのチェックを行った後で、問題がなければCloud Buildを使用してコンテナイメージをデプロイします。

# GitHub Actions用のworkflow設定
...
  deploy-strapi:
    runs-on: ubuntu-18.04

    steps:
      - uses: actions/checkout@v2

      # GCPのセットアップ いつものおまじないです。
      - name: GCP Authenticate
        uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
        with:
          version: "290.0.1"

          project_id: ${{ secrets.PROJECT_ID_DEV }}
          service_account_email: ${{ secrets.GCP_SA_EMAIL }}
          service_account_key: ${{ secrets.GCP_SA_KEY }}
          export_default_credentials: true


      # Strapi用 Cloud Run strapiディレクトリのcloud build用yamlファイルを使ってビルドします。
      - name: Deploy console-api for review
        working-directory: ./strapi
        run: gcloud builds submit --config ./cloudbuild/pull_request.yaml
             --project ${{ secrets.PROJECT_ID_DEV }}
             --substitutions=_GH_PR_NUMBER=${{ github.event.number }},_GH_SHA=${{ github.sha }}
             --timeout=30m
...

Cloud Buildに環境変数には、--substitutions=_GH_PR_NUMBER=<なんかの値>という感じで値を渡します。
PR番号ごとにサービスを分けてデプロイして、コンテナイメージのラベルも変えるためにgithub.shaを使ってしまおうという感じでやっています。
Cloud Buildのsubstitutionsで設定できる環境変数にはルールがあって、必ず_から始める必要があります。
あとデフォルトの10分だと結構タイムアウトしてしまうことがあるので、30mとかにしてます。

Cloud Build

GitHubのPRに依存する値以外は、環境ごとに以下のようなCloud Buildの設定ファイルを作って、共通の設定はsubstitutionsに書くこともできます。
(ちなみにkanikoというのはよしなにキャッシュなどをコンテナの中でさらに管理してくれる便利なやつ)

Dockerfileからgcr.io/${PROJECT_ID}/pr-${_SERVICE_NAME}:${_GH_SHA}というコンテナイメージをGoogle Container Registry上にプッシュして、Cloud Buildにそのままデプロイします。

steps:
# Using Kaniko cache https://cloud.google.com/cloud-build/docs/kaniko-cache
- name: 'gcr.io/kaniko-project/executor:latest'
  args:
  - --destination=gcr.io/${PROJECT_ID}/pr-${_SERVICE_NAME}:${_GH_SHA}
  - --cache=true
  - --cache-ttl=3h
# Deploy container image to Cloud Run
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
  entrypoint: gcloud
  args: ['run', 'deploy',
         'pr-${_SERVICE_NAME}-${_GH_PR_NUMBER}',
         '--image', 'gcr.io/${PROJECT_ID}/pr-${_SERVICE_NAME}:${_GH_SHA}',
         '--region', 'asia-northeast1',
         '--cpu', '${_CPU}',
         '--memory', '${_MEMORY}',
         '--platform', 'managed',
         '--set-env-vars', 'HOST=${_HOST}',
         '--set-env-vars', 'NODE_ENV=${_NODE_ENV}',
         '--set-env-vars', 'DATABASE_NAME=${_DATABASE_NAME}',
         '--set-env-vars', 'DATABASE_USERNAME=${_DATABASE_USERNAME}',
         '--set-env-vars', 'DATABASE_PASSWORD=${_DATABASE_PASSWORD}',
         '--set-env-vars', 'INSTANCE_CONNECTION_NAME=${_INSTANCE_CONNECTION_NAME}',
         '--add-cloudsql-instances', '${_INSTANCE_CONNECTION_NAME}',
         '--service-account', '${_SERVICE_ACCOUNT}',
         '--allow-unauthenticated']
substitutions:
  _CPU: '1'
  _MEMORY: 512Mi
  _HOST: '0.0.0.0'
  _NODE_ENV: 'develop'
  _SERVICE_NAME: <Cloud Runのサービス名>
  _DATABASE_NAME: <DB名>
  _DATABASE_USERNAME: <DBユーザ名>
  _DATABASE_PASSWORD: <DBパスワード>
  _INSTANCE_CONNECTION_NAME: <Cloud SQLプロキシ>
  _SERVICE_ACCOUNT: <Cloud Run起動時のサービスアカウント>

Dockerfileはこんな感じになっています。前出の記事にも書いてありますがシンプルです。Strapiチームありがとう。

FROM strapi/base

WORKDIR /my-path

COPY ./package.json ./
COPY ./yarn.lock ./

RUN yarn install

COPY . .

ENV NODE_ENV production

RUN yarn build

EXPOSE 1337

CMD ["yarn", "start"]

あとは最初に書いたように、GitHubのPRに対してコメントを残すWorkflowを実行すればOKです。

業務で使ってるコードをそのままあげるわけにもいかないので、ちょっと抽象的に例を上げてしまいましたが、ご質問などあればぜひコメントください!
あんまりStrapi関係ないじゃん!という叱責も受け付けておりますので、クリスマスまでアドベントカレンダーをお楽しみください!

Discussion