Cloud RunとIdentity-Aware ProxyとGitHub ActionsでPull RequestごとのDeployment Previewを実現する

マンガ投稿チームでWebアプリケーションエンジニアをしているid:stefafafanです。この記事では、最近私がチーム向けに整備したDeployment Preview環境の事例を紹介します。

Deployment Previewとはどのようなものか?

Pull Requestベースの開発をする際の「Pull Request専用の確認環境」のことを、私のチームでは「Deployment Preview」と呼んでいます。

Netlify、Cloudflare Pages、Vercelをはじめとして、さまざまなサービスがこの機能を提供しています。それぞれ「Deploy Previews」や「Preview Deployments」など表記はいろいろとありそうです。

チームとして求める要件

Deployment Previewを実現するには、Vercelのような既存のサービスを活用するか、クラウドサービスを活用して自分たちで組み立てるかということになります。

ここで私のチームで開発している「マンガノ」の技術スタックは以下の通りですが、この構成のサービスが問題なく動く確認環境であることが必須要件になります。

  • Dockerでコンテナ化されたWebアプリケーション
  • フロントエンドはNext.js
  • バックエンドはGraphQL(Golang)
  • データベースはMySQL
  • ユーザ認証はFirebase Authentication

またチームのエンジニアとしては、

  • メンテナンスコストを抑えたシンプルな構成にしたい
  • Deployment Preview環境に限らず、Staging環境などにも展開しやすくしたい
  • 特定の機能に依存した構成は避けたい

という気持ちもありました。その上で、

  • 開発環境なのでインターネットに全公開するのではなく、何かしらの認証もかけて関係者しか閲覧できないようにしたい

という要件もありました。

これらの要件を踏まえた結果、今回はGoogle Cloudを活用した構成を考えることに決めました。*1

実現したDeployment Previewの全体像

さっそく完成した構成図を紹介します。

もともとNext.js側にもGraphQL側にもDockerfileがあるので、個別にCloud Runにデプロイする形で構成しています。またGoogle認証をかけるため、Cloud Load BalancingIdentity-Aware Proxyを適用させています。

Deployment Previewの構成図

これでPull Requestを開くと、ブランチ名に対応する確認環境が次のようなURLで見られるようになっています。

https://branch-name.frontend.example.com

それでは、この構成を実現するのに必要だった主な作業を紹介します。

1. DockerイメージをビルドしてArtifact RegistryにpushしてCloud Runで動かすまで

コンテナ化されている場合は難しいことはあまりなくて、公式ドキュメントを参考に練習して雰囲気を理解しました。

Deploying to Cloud Run using Cloud Build | Cloud Build Documentation | Google Cloud

判断ポイントは1つあって、それは「Cloud Buildを使う」か「GitHub Actionsでやる」かです。Google Cloudではリポジトリにcloudbuild.yamlファイルを用意し、そこにbuild・push・deployのステップを記述するという方法が上記のドキュメントでも紹介されていますが*2、同様のことはGitHub ActionsのWorkflow内のStepでもできます。

私のチームのエンジニアはGitHub Actionsの方が書き慣れている上、最近はWorkload Identity Federation*3も活用できるようだったので、こちらを選びました。

Googleは公式でGitHub Actionsをいくつか提供しており、最終的に以下の3つを活用しました。

GitHub Actionsでどのように実現したか

Google Cloudとの認証とgcloudコマンドの準備は、以下のようなコードをちょっと書くだけで終わりです。以後のStepでは、gcloudがそのまま使えます。

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: "read"
      id-token: "write"
    steps:
      - name: Checkout
        uses: "actions/checkout@v3"

      - name: Authenticate Google Cloud
        uses: "google-github-actions/auth@v0"
        with:
          workload_identity_provider: "..."
          service_account: "..."

      - name: Setup Cloud SDK
        uses: "google-github-actions/setup-gcloud@v0"

イメージのビルドとデプロイはこのように記述します。

env:
  REPOSITORY: my-repository
  PROJECT: my-deploy-preview
  LOCATION: asia-northeast1
  SERVICE: my-service
  CLOUD_SQL_INSTANCE: my-sql-instance

...
    steps:
     ...

      - name: Configure docker
        run: gcloud auth configure-docker ${{ env.LOCATION }}-docker.pkg.dev

      - name: Build image
        run: |
          docker build -t ${{ env.LOCATION }}-docker.pkg.dev/${{ env.PROJECT }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:${{ github.sha }} -f ./Dockerfile .

      # gcloud auth configure-docker の後であればArtifact Registryへ docker push することができる
      - name: Publish image to Google Artifact Registry
        run: |
          docker push ${{ env.LOCATION }}-docker.pkg.dev/${{ env.PROJECT }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:${{ github.sha }}
      
      - name: Deploy to Cloud Run
        uses: google-github-actions/deploy-cloudrun@v0
        with:
          service: ${{ env.SERVICE }}
          image: ${{ env.LOCATION }}-docker.pkg.dev/${{ env.PROJECT }}/${{ env.REPOSITORY }}/${{ env.SERVICE }}:${{ github.sha }}
          region: ${{ env.LOCATION }}
          # 別のStepで抽出したgit branch名をrevision tagとして指定する
          tag: ${{ steps.extract_branch.outputs.branch }} 
          # 他のflagがあれば個別に指定できる。ここでCloud SQLとの接続とかも指定する
          flags: --add-cloudsql-instances=${{ env.PROJECT }}:${{ env.LOCATION }}:${{ env.CLOUD_SQL_INSTANCE }}
          # 環境変数が指定できる
          env_vars: |
            ...
          # Google Secrets Managerの値も指定できるので安心
          secrets: |
            MYSQL_USER=MYSQL_USER:latest
            MYSQL_PASSWORD=MYSQL_PASSWORD:latest
            ...

全体的にシンプルに書ける上に、デプロイ時の環境変数やSecrets Managerの値も簡単に指定できてとても便利です。マンガノではGraphQLが動いているイメージとNext.jsが動いているイメージをデプロイしたいので、それぞれのイメージに対して上記の記述を書いています。

2. ロードバランサーと証明書の準備、またServerless NEGによる振り分け

以上でCloud Run上でアプリケーションは動かせますが、Google認証をかけるには追加でコンポーネントをいくつか用意する必要があります。最終的に、ロードバランサー周りのより詳細なコンポーネントの図はこうなります。

Load BalancerとCertificate ManagerとIAPとServerless NEGの構成を表した図*4

今回の要件を実現するには以下の3点が必要になります。

  1. Certificate Managerを活用してワイルドカード証明書を取得する
  2. Serverless Network Endpoint Groupを用意し、URL MaskでCloud Runのリビジョンタグとの対応づけをする
  3. Identity-Aware Proxyを有効化し、ロードバランサー経由のリクエストのみ許可する

Certificate Managerでワイルドカード証明書を取得

Identity-Aware ProxyをかけるにはSSL化されている必要があるので、証明書を取得しないといけません。また、マンガノのDeployment Previewはブランチ名をサブドメインに登録する形(branch-a.frontend.example.comみたいなイメージ)を想定しているので、*.frontend.example.comのワイルドカード証明書を取得できると要件にマッチします。

2022年6月現在、Google CloudのCloud Load BalancingのWeb UI経由で証明書をセットしようにもワイルドカード証明書は作れません。代わりに最近発表されたCertificate Managerというサービスを使うと実現できるので今回はこちらを利用しました。*5

cloud.google.com

具体的な用意の仕方はクラスメソッドさんのこちらの記事によくまとまっています。

dev.classmethod.jp

Serverless NEGを用意してURL MaskでCloud Runのリビジョンタグと対応づける

ここでサブドメインbranch-a.service-a.example.comが、Cloud Runの特定サービスの特定リビジョンタグに向くようにする必要があります。つまり、Cloud Runにあるservice-aサービスのbranch-aリビジョンタグに向いてほしいということです。

Google Cloudでこういう振り分けをしてくれる機能にはLoad BalancerのURL Mapと、Serverless Network Endpoint Group(NEG)のURL Maskがあります。

URL Mapはホストやパスを元に指定したCloud RunサービスやCloud Storageバケットに振り分ける機能ですが、単体ではCloud Runのリビジョンタグに向けられません。例えばdevelop・staging・productionの3環境があり、対応する3つのCloud Runサービスに振り分けるのならURL Mapで十分ですが、今回はブランチごとに環境がほしいのでCloud Runサービスをあらかじめ大量に立てておくなどが必要になり少し面倒です。

URL maps overview | Load Balancing | Google Cloud

一方、Serverless NEGのURL Maskは今回のユースケースに合致している機能で、バックエンドのサービスごとに利用できるプレイスホルダーがいくつかあり、Cloud Runに向ける場合は<service><tag>が使えます。

なので今回のケースでは<tag>.<service>.example.comを設定したServerless NEGを1つ用意するだけで、branch-a.service-a.example.comのアクセスをCloud Runのservice-aサービスのbranch-aリビジョンタグに向けることが実現できます。

Set up a global external HTTP(S) load balancer (classic) with Cloud Run, App Engine, or Cloud Functions | Load Balancing | Google Cloud

Identity-Aware Proxyを有効化してロードバランサー経由のリクエストのみ許可

ここまでできればIdentity-Aware Proxyの設定はあまり難しいところはなく、ドキュメントに沿って準備すると完成します。

gcloud run deploy | Google Cloud CLI Documentation

一点あるとしたら、ロードバランサー経由でサービスを閲覧できるようにするには、Cloud Run側のIngressやAuthenticationの設定をいじる必要があるということです。

IAPを有効化したあと、Ingress側を「Allow internal traffic and traffic from Cloud Load Balancing」に、そして Authentication を「Allow unauthenticated invocations」にすると、IAP側の認証に任せてサービスを公開できます。Ingressの設定により、ロードバランサーを通さずCloud RunのURLに直接アクセスしても見れないという状態になります。

Cloud RunのTriggerの設定画面

このIngressやAuthenticationの設定は、GitHub Actions経由でCloud Runにデプロイする際にオプションとして指定ができます。--ingress=internal-and-cloud-load-balancing--allow-unauthenticatedのオプションですね。詳しくはgcloud run deployのドキュメントを参照してください。

gcloud run deploy | Google Cloud CLI Documentation

認証のかかったDeployment Previewとして必要最低限の整備はここまでで完成です。

3. Firebase Authenticationの対応とTerraformによるIaC化

上記以外にも個人ブログに少し書いた話題が2つあるので、ここで軽く紹介します。

まず上記の要件として説明したように、マンガノのアプリケーションはユーザ認証にFirebase Authenticationを利用していますが、今回のようにドメインがブランチごとに変わるDeployment Previewに関しては動かない部分があったためGitHub Actionsで対応しました、という話を次の記事に書いています。

blog.stenyan.jp

これはGoogle Cloudに限った話ではないので、Deployment Previewのような仕組みを用意したのにFirebase Authenticationの承認済みドメインの扱いで困っている方は参考にしてもらえたら嬉しいです。

もう1つの記事は、Infrastructure as Codeになっていない部分をTerraformに落とし込みましたという話です。実はgcloudにTerraform向けにエクスポートするための便利な機能が備わっているため、その紹介をしています。

blog.stenyan.jp

今回のDeployment Previewのよい点と振り返り

完成した環境を見直して、よかったと思える点をまとめます。

  • GitHub ActionsからイメージをCloud Runにデプロイするだけで認証のかかった環境が用意されるということが、それなりにシンプルな構成で実現できた
    • 用意したCloud Load Balancingなどの設定も変更せずそのまま利用できている
  • デプロイに失敗したり異常が発生した場合は、GitHub Workflowのログから確認できるので使いやすい
  • 普段からGoogle Workspaceを利用しているので、Google認証で見られる点も開発者としての体験がよい
  • 全てのブランチで動作するため、mainブランチの様子も自然とこの環境で確認できる
    • Pull Requestごとの環境を作ったら、ついでにリリース前の確認環境も兼ねることができた

これから調整したい箇所

一方、ひとまず利用できる状態になったところでこの記事を書いているので、これから調整したい点もまとめておきます。

  • Artifact Registryに溜まっている古いイメージは消したい

Cloud Storageにあるようなライフサイクルの機能はないため、GitHub Actionsなどで定期的に古いイメージを消したりしようかと考えています。幸いgcloudのコマンドで一定期間古いイメージをフィルターして一覧できそうなので、簡単なコマンドで対応できそうです。

  • Workflowやデプロイ周りの高速化

全ブランチでGitHub Actions経由のデプロイが行われるようになった結果、デプロイ時間を改善したいという気持ちがエンジニアメンバー間で強まったので、対応していきたいです。エンジニアが毎日目にしているGitHub Actions側でビルドもデプロイもしていることによってこのあたりがより可視化されたので、この点よかったと思っています。

終わりに

最初に紹介したように私はSREではなく、普段はGoやTypeScriptなどアプリケーションコードを書く仕事がメインですが、今回Google Cloudに入門して大きくハマることなくDeployment Previewの仕組みをチーム向けに用意することができました。

構成図も理解しやすいシンプルな構成になったので、今後メンテナンスするときもアプリケーションコードを書いているエンジニアだけで簡単に対応できます。手作りなところは少ないので、仮に他の環境で動かしたくなってもあまり困らないのも良いですね。みなさんにもぜひ参考にしてもらえたら嬉しいです。

*1:Vercelも検討しましたが、バックエンドやデータベースのことも考慮した上で、すでにGoogleのFirebase Authenticationも利用していることもあってGoogleのサービスでまとめるという判断をしました。

*2:現在非推奨のContainer Registryが例として紹介されているのでご注意ください。Artifact Registryを使いましょう。

*3:GitHub Repository SecretsにService Accountのキーを登録せずに済んで嬉しい!

*4:この図はGoogle CloudのCertificate ManagerのドキュメントServerless network endpoint group (NEG) のドキュメントに載ってる図を参考に描いています。

*5:2022年6月時点ではCertificate ManagerはまだGA前の機能なので注意。