バイセル Tech Blog

バイセル Tech Blogは株式会社BuySell Technologiesのエンジニア達が知見・発見を共有する技術ブログです。

バイセル Tech Blog

GitHub Actions から Cloud Run ジョブで Google Cloud のプライベートネットワーク内へアクセスした

はじめに

こちらは バイセルテクノロジーズ Advent Calendar 2022 の 11 日目の記事です。前日の記事は近藤さんの「Databricks で綺麗にメダリオンアーキテクチャを構築するために実装ルールを決めた話」でした。

こんにちは。バイセルテクノロジーズ テクノロジー戦略本部 開発 2 部の小松山です。

本記事では Google Cloud のプライベートネットワーク内に、GitHub Actions から Cloud Run ジョブを使ってアクセスした事例を紹介します。

Cloud Run ジョブは2022 年 5 月にリリースされた比較的新しいサービスで、記事執筆時点ではプレビュー版として提供されています。サービスの解説記事や試してみた系の記事は散見されるものの、具体的な採用事例の紹介が少ないです。本記事では Cloud Run ジョブの採用事例として、採用に至った背景やその実装内容を紹介します。

前回以下の記事で、Cloud Run ジョブを使って比較的重めのバッチ処理を実行する事例を紹介しました。今回はバッチ処理ではなくデプロイのパイプラインの中で Cloud Run ジョブを利用する事例の紹介となります。

tech.buysell-technologies.com

Google Cloud のプライベートネットワーク内にアプリケーションをホスティングしていて、CI/CD に GitHub Actions を使っている方の参考になれば幸いです。

背景

GitHub Actions からマイグレーションをしたい

今回実現したかったのは、GitHub Actions 経由でのデータベースへのマイグレーションと API サーバへの設定の適用です。

プロジェクトでは CI/CD パイプラインは基本的に GitHub Actions を使用していました。そのため、DB マイグレーションなども同様に GitHub Actions で実現したかったという背景があります。GitHub Actions を使うことのメリットは、他の CI サービスと比べ料金が安いこと、GitHub との親和性が高くアクセスしやすいこと、社内に知見が溜まっており再利用可能なワークフローが多いことなどが挙げられますが、中でも個人的に良いと思っているポイントは OpenID Connect (OIDC) がサポートされたことです。

これにより、リポジトリのシークレットとしてクレデンシャル情報を入れなくても、ワークフロー上でサービスアカウントとして認証することができるようになりました。従来の GitHub Actions の取り回しやすさを備えつつ、よりセキュアにプロジェクト内のリソースにアクセスすることができるようになったことは大きなメリットではないでしょうか。

プロジェクトの構成

私の開発するプロダクトでのサーバサイドの構成は下図のとおりです (一部簡略化しています)。

私の所属するプロジェクトでは API サーバとして Hasura を全面的に採用しています。Hasura はオープンソースの GraphQL サーバです。PostgreSQL 等のデータソースと接続し、そのスキーマをベースに GraphQL の API を自動生成してくれます。Hasura は GraphQL 以外にも様々な API を提供しています。例えばデータベースのマイグレーションに使う Schema API や API サーバに設定を適用するための MetaData API などです。そのためデータベースのマイグレーションや設定の適用は Hasura サーバを経由して行います。

Hasura に関しての説明は本記事の趣旨と逸れるため、ここでは詳細な説明はしません。気になる方は公式ドキュメントや解説記事をご覧ください。

API サーバの前段には API Gateway があります。API Gateway は他のプロダクトからも利用するため API サーバと GCP のプロジェクトが分かれています。API サーバへのアクセスは API Gateway 経由のみに限定したいため下記の通りにアクセス制限を設けています。

  • API サーバへのアクセスを、内部トラフィックとロードバランサからのトラフィックのみに限定して Cloud Run への直アクセスを禁止
    • GraphQL API は Auth0 の発行したアクセストークンを使って API Gateway 経由でインターネットからアクセス可能
    • GraphQL API 以外は、Google Cloud のプライベートネットワーク内からしかアクセスできない
  • API Gateway と API サーバの間のロードバランサで IP 制限
    • 送信元の IP アドレスが API Gateway のものと一致しないアクセスをブロックするよう Cloud Armor のポリシーを設定

解決策の検討

GitHub Actions でマイグレーションをさせたかったのですが、前述の通り、API サーバにはアクセス制限がかかっているので GitHub Actions から到達できない点が課題でした。そこで解決策の検討を行いました。

API サーバの前の LB で GitHub Actions の IP を許可する

API サーバと API Gateway の間のロードバランサから API サーバへアクセスする方法です。GitHub Actions の IP アドレスのレンジは公開されているので Cloud Armor で設定している IP アドレスのホワイトリストにこれらを追加すればアクセスは可能になります。ですが、レンジが広すぎることや、GitHub Actions を経由すれば誰でも API Gateway を通さずに API サーバへ到達できてしまうことを考慮し、このやり方は採用しませんでした。

API Gateway を通してリクエストする

API Gateway 経由にマイグレーションのためのリクエストを通すやり方です。Hasura が提供するSchema APIMetaData API のエンドポイントのパスを網羅して転送ルールを書ききれば可能ではありますが、今後の Hasura の仕様変更に追随していく必要があるため得策ではなく、こちらも採用しませんでした。

Cloud Build プライベートプール

Cloud Build のプライベートプールを使って CI/CD パイプラインから Google Cloud のプライベートネットワーク内のリソースへアクセスするやり方です。たしかに Cloud Build を使えばやりたいことは実現できそうですが、先に述べたように GitHub Actions に寄せていきたいという背景があったため見送りました。

Compute Engine で構築した self-hosted runner

プロジェクトのプライベートネットワーク内に Compute Engine のインスタンスを立て、それを GitHub Actions の runner とするやり方です。GitHub Actions を使えてプライベートネットワーク内にアクセスできる方法のひとつですが、サーバ管理が必要になります。できればサーバレスに済ませたいところです。また、Compute Engine は稼働時間によって課金されるのである程度のコストがかかってしまいます。

Cloud Run ジョブ

Cloud Run ジョブはサーバレスなコンテナ実行環境です。処理のトリガーが HTTP リクエストではない点などで従来の「Cloud Run サービス」と異なっています。ジョブは任意のタイミングでトリガーすることができます。イメージを作ってサクッとコンテナを動かせる点、VPC コネクタを経由することで Google Cloud のプライベートネットワーク内のリソースにもアクセスできる点は Cloud Run サービスと同様です。

プライベートネットワークにアクセスできること、OIDC を使えば GitHub Actions ワークフロー上からでもジョブの呼び出しも簡単にできることから、今回やりたいことが実現できそうです。また、サーバレスであること、必要なときにジョブを動かせることも今回の要件にもマッチしそうです。

実装

最終的な構成

最終的な構成は下記のようになりました。

マイグレーションを行うジョブを作成し、GitHub Actions ワークフローから gcloud コマンドでそのジョブを実行しています。ジョブコンテナはすべての外向きの通信を VPC コネクタを経由するようにすることで API サーバへのアクセスが可能です。これによりワークフロー上から間接的にプライベートネットワーク内にアクセスできるようになりました。

各種設定

ジョブコンテナのイメージのビルドするのに使った Dockerfile は下記のとおりです。

FROM node:18.12.1-bullseye-slim

COPY ./hasura /app
RUN chmod +x /app/scripts/deploy.sh && \
  apt-get update && apt-get install -y ca-certificates && update-ca-certificates && \
  npm install --global hasura-cli@2.9.0

ENV HASURA_GRAPHQL_ENABLED_LOG_TYPES="startup, http-log, webhook-log, websocket-log, query-log"

CMD ["/app/scripts/deploy.sh"]

ジョブのコンテナは API サーバに対してマイグレーション・設定の適用だけできれば良いので、マイグレーションファイル・適用するための設定ファイルをホストマシンから COPY してきます。これらは ./hasura 配下に格納されています。マイグレーション・設定の適用のためのリクエストを API サーバ (Hasura) に対して行う必要があるため、Hasura CLI をインストールしています。 この Hasura CLI を使ってマイグレーション・設定の適用をするコマンドを deploy.sh に書いています。

#!/bin/sh -ex

cd /app

ROOT=$(pwd)

if [ ! -e $ROOT/metadata ]; then
  echo "metadata not found"
  exit 1
fi

if [ -z "$ENDPOINT" ]; then
  echo "ENDPOINT not set"
  exit 1
fi

hasura deploy --project $ROOT --endpoint $ENDPOINT --skip-update-check

また、環境ごとに異なる値・シークレットは Cloud Run ジョブ側で設定しています。ジョブのリソースは予め作っておいて、GitHub Actions からそれを実行するだけにしています。

$ gcloud beta run jobs create migrate-job \
  --image asia-northeast1-docker.pkg.dev/example-project/docker/hasura-deploy-job:latest \
  --vpc-connector migrate-job-connector \
  --vpc-egress all-traffic \
  --region asia-northeast1 \
  # Cloud Run のリビジョンに紐づく URL
  --set-env-vars ENDPOINT=https://hasura-xxxxxxxx-an.a.run.app \
  # Hasura 経由でデプロイするために必要な値
  --set-secrets HASURA_GRAPHQL_DATABASE_URL=HASURA_GRAPHQL_DATABASE_URL:latest,HASURA_GRAPHQL_ADMIN_SECRET=HASURA_GRAPHQL_ADMIN_SECRET:latest

ジョブを実行するためには、相当の権限が付与されたアカウントで認証する必要があります。こちらは前述の通りワークフロー上で OIDC を使用してサービスアカウントの認証をキーレスに行っています。認証にはWorkload Identity 連携を使用します。これにより、外部 ID プロバイダ (今回の例では GitHub) の発行した認証情報を使ってサービスアカウントの認証が可能になります。

cloud.google.com

Workload Identity 連携を使用してサービスアカウントの認証をするには下記の設定が必要です。

  • Workload Identity プールの作成
  • Workload Identity プロバイダの作成
  • ワークフロー上で使用したいサービスアカウントに roles/iam.workloadIdentityUser の付与

Google 公式の GitHub OIDC Terraform モジュールを使うと上記 3 つのインフラリソースをいい感じに作ってくれて実装が楽なのでおすすめです。

module "gh_oidc" {
  source  = "terraform-google-modules/github-actions-runners/google//modules/gh-oidc"
  version = "3.1.0"

  project_id  = "example-project"
  pool_id     = "example-pool"
  provider_id = "example-gh-provider"
  sa_mapping = {
    "gh_actions_service_account" = {
      sa_name   = "projects/example-project/serviceAccounts/gh-actions@example-project.iam.gserviceaccount.com"
      attribute = "attribute.repository/buysell-technologies/example-repo"
    }
  }
}

使用しているワークフローも一部を抜粋して紹介します。

name: Apply metadata & migration

on:
  push:
    branches:
      - main

jobs:
  migrate:
    name: migrate
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    timeout-minutes: 10
    steps:
      # サービスアカウントの認証をするためのステップ
      - name: Authenticate to google cloud
        uses: google-github-actions/auth@v0
        with:
          access_token_lifetime: 600s
          project_id: example-project
          workload_identity_provider: projects/00000/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider
          service_account: gh-actions@example-project.iam.gserviceaccount.com

      - name: Set up Cloud SDK
        uses: google-github-actions/setup-gcloud@v0

      - name: Install gcloud beta
        run: gcloud components install beta --quiet

      - name: Configure docker to use the gcloud cli
        run: gcloud auth configure-docker asia-northeast1-docker.pkg.dev --quiet

      - name: Build a docker image
        run: docker build -t asia-northeast1-docker.pkg.dev/example-project/docker/migrate-job -f ./hasura/migrate-job/Dockerfile .

      - name: Push a docker image
        run: docker push asia-northeast1-docker.pkg.dev/example-project/docker/migrate-job

      # マイグレーションのジョブを実行
      - name: Trigger migrate job
        run: gcloud beta run jobs execute migrate-job --region asia-northeast1 --wait

Google が提供する google-github-actions/auth という Action を使うと以降のステップでは指定したサービスアカウントとして認証された状態になります。このサービスアカウントにジョブの実行権限を持たせることで GitHub Actions からのマイグレーションのジョブの実行が可能になります。

所感

良かったポイント

Cloud Run はコンテナをそのままデプロイできるのでランタイムの制限がないところが嬉しいです。今回のようなちょっとしたスクリプトをクラウド上で実行したい時でもコンテナ化さえすればサクッと動かせてしまうのが便利だなと思いました。

改善ポイント

マイグレーションの実行ログがワークフローのログから確認できないのがこの実装のデメリットです。gcloud beta run jobs execute ではそのコンテナのログが見られません。コンテナの実行ログを吐くようなコマンドも現時点では実装されていません (Cloud Run サービスではサービスのログを見られるコマンドがあるようです)。現時点では、ジョブの失敗等でコンテナのログを見に行くときは Cloud Logging の方を見に行くようにしています。

どうしてもワークフローにログを残したければ、gcloud logging read コマンドを使うと Cloud Logging からログを引っ張ってくることはできます。ですが GitHub Actions 上だと結構見づらいです。フォーマットや取得上限を調整することで多少マシになりますが、出力されているのは純粋な「マイグレーションの実行ログ」ではなく「ジョブの実行ログ」なのでノイズが多いです。

ワークフロー上でマイグレーションの実行ログが見られると何かと便利だと思うので、良いやり方が見つかれば改善していこうと思っています。

まとめ

本記事では Google Cloud のプライベートネットワークにアクセスするための手段として Cloud Run ジョブを採用した事例を紹介しました。本記事では Hasura 経由でマイグレーションを実装した例を紹介しましたが、ちょっとした処理のためにのプライベートネットワークの境界をまたぎたくなることは他にもあるのではないでしょうか。そういったユースケースでも Cloud Run ジョブは選択の余地があるサービスだと思いました。

ここで触れた機能以外にも Cloud Run ジョブには並行処理やタスクの分割等の機能があります。本記事の趣旨と直接関係しないので説明を省いてしまいましたがいずれも便利な機能です。こちらに関しては 2022/12/04 の「バッチ処理を Cloud Functions ではなく Cloud Run ジョブで実装した」で触れていますのでよろしければご覧ください。

技術選定の際に Cloud Run ジョブを選択肢の 1 つとして検討いただければ幸いです。

また、API サーバである Hasura に関して、マイグレーション時の工夫が本当の実装には盛り込まれているのですが、主題と逸れるので本記事では紹介しきれませんでした。機会があれば別の記事で紹介しようと思います。

バイセルテクノロジーズではエンジニアを募集しています。

herp.careers

明日の バイセルテクノロジーズ Advent Calendar 2022 は畑さんによる「Ruby on Railsで発生していたN+1を解消してパフォーマンスを改善した話」です。