FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

シークレットを使わずにCloud Runで動くアプリケーションをGitHub Actionsでデプロイする

こんにちは、小林です。
2024年1月にFLINTERSが設立10周年を迎えるということで、記念の全社員ブログリレー10日目を担当します。

安心安全なCI/CD

多くのCI/CDツールにはセキュアな情報を保存しパイプライン内で参照するための、シークレットなどと呼ばれるような機能があると思います。
アクセスキーなど静的なクレデンシャルを保存することが多いと思いますが、可能であれば使わずに済ませたいところです。
そこでシークレットを使わずにGitHub ActionsでCloud Runアプリケーションのデプロイを行う、安心安全なCI/CDを作ってみたいと思います。

お題

  • Cloud Runで動作するアプリケーションをデプロイしたい
  • DBとして使用しているCloud SQLも同じCI/CDでマイグレーションしたい
  • 上記をリポジトリのシークレットにセキュアな情報を設定することなく実現したい
備考

アプリケーションの実装は何で書かれていても今回の趣旨とは関係ないのですが、これから紹介するGitHub ActionsのYAMLの内容は私のチームで実際に使われているものを参考にしているため、GitHub Actionsが動作するリポジトリは

  • Scala及びsbtで作成されたアプリケーション
  • マイグレーションにはflywayのsbt pluginを使用

という構成になっていることとします。
その関係で趣旨とは外れたYAMLの記載も少々出てきてしまいますが、適宜補足します。

どうやるか

ざっくり以下の手段で実現します。

  • OpenID Connect(OIDC)を使う
  • Cloud SQL Auth Proxyを使ったIAM認証を行う

正直目新しい情報は何もなく恐縮なのですが、最低限自分の備忘録として役に立つのでヨシの精神でやっていきます。

とりあえず

チェックアウトとjdkの準備までをしておきましょう。
mainブランチにマージされたら動くやつです。

name: Deploy

on:
  push:
    branches:
      - 'main'

permissions:
  id-token: write
  contents: read

jobs:
  staging-deploy:
  runs-on: ubuntu-latest
  timeout-minutes: 10

  steps:
    - name: Checkout
      uses: actions/checkout@v3

    - name: Setup JDK
      uses: actions/setup-java@v3
      with:
        distribution: temurin
        java-version: 17
        cache: sbt

ここで重要なのは以下になります。

permissions:
  id-token: write
  contents: read

この後登場するOpenID Connect(OIDC)を使用した認証に必要な設定となります。

サービスアカウントで認証する

resource "google_iam_workload_identity_pool" "github_actions" {
  workload_identity_pool_id = "github-actions"
}

resource "google_iam_workload_identity_pool_provider" "github_actions_oidc" {
  workload_identity_pool_id          = google_iam_workload_identity_pool.github_actions.workload_identity_pool_id
  workload_identity_pool_provider_id = "github-actions-oidc"
  attribute_mapping = {
    "google.subject"       = "assertion.sub"
    "attribute.actor"      = "assertion.actor"
    "attribute.aud"        = "assertion.aud"
    "attribute.repository" = "assertion.repository"
  }
  oidc {
    issuer_uri = "https://token.actions.githubusercontent.com"
  }
}

locals {
  service_account_name = "hoge@hogeproject.iam.gserviceaccount.com"
  repository           = "hoge/hoge-backend"
}

resource "google_service_account_iam_member" "workload_identity_sa" {
  service_account_id = local.service_account_name
  role               = "roles/iam.workloadIdentityUser"
  member             = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github_actions.name}/attribute.repository/${local.repository}"
}

output "github_actions_provider_name" {
  value = google_iam_workload_identity_pool_provider.github_actions_oidc.name
}

突然ですがterraformです。
このようにWorkload Identity連携を利用することで、OIDC認証によってサービスアカウントの権限を行使できます。
上記のservice_account_nameが権限を行使したいサービスアカウント、repositoryがCI/CDを実行するGitHubのリポジトリとなります。

これらが適用された前提で引き続きYAMLを見ていきます。
先程のYAMLの続きになります。

    - id: auth-google
      name: Auth Google Cloud
      uses: google-github-actions/auth@v1
      with:
        token_format: access_token
        workload_identity_provider: projects/111111111111/locations/global/workloadIdentityPools/github-actions/providers/github-actions-oidc
        service_account: hoge@hogeproject.iam.gserviceaccount.com

これだけでOIDC認証できました。
workload_identity_providerは上記terraformのoutputに定義されているgithub_actions_provider_nameの値です。
hoge@hogeproject.iam.gserviceaccount.comの権限に紐づくアクセストークンがGoogle Cloudの認証に使われる環境変数とstepのoutputに設定されます。
後でこのstepのoutputを使いたいので、idで参照できるようにid: auth-googleと記述しておきます。

Cloud Runをデプロイする

Cloud Runをデプロイするためのstepを見ています。 Artifact RegistoryにDocker Imageをプッシュし、それを参照したCloud Run Serviceをデプロイするという流れです。これらを実行するために必要な権限は認証方法に関わらないので割愛します。

まずArtifact Registoryを使用できるように認証します。

    - name: Login docker
      uses: docker/login-action@v2
      with:
        registry: asia-northeast1-docker.pkg.dev
        username: oauth2accesstoken
        password: ${{ steps.auth-google.outputs.access_token }}

先程のauth-googlestepで生成されたaccess_tokenを使用してログインします。

続いてDocker Imageをプッシュするところまでです。

    - name: Build
      run: sbt docker:stage
    
     - name: Docker build and push
       uses: docker/build-push-action@v4
       with:
        push: true
        tags: asia-northeast1-docker.pkg.dev/hoge-project/web-app/backend:latest
        context: ./target/docker/stage

私のチームのプロジェクトでsbt-native-packagerDockerPluginを利用している関係でstepや設定が増えていますが、要はDocker Imageをビルドしてプッシュしているだけなので、OIDCなどは特に関係ないです。

最後にCloud Runをデプロイします。

    - name: Deploy cloud run
      run: |
        gcloud run deploy backend \
          --region asia-northeast1 \
          --image asia-northeast1-docker.pkg.dev/hoge-project/web-app/backend:latest

Cloud SDKは環境変数から認証情報を読み取ってくれるため、特に認証方法を指定しなければ先程のアクセストークンが使用されます。

Cloud SQLへ接続する

resource "google_sql_database_instance" "db" {
  database_version = "POSTGRES_14"
  region           = "asia-northeast1"
  settings {
    ip_configuration {
      ipv4_enabled    = true
      require_ssl     = false
      private_network = "something-vpc"
    }

    database_flags {
      name  = "cloudsql.iam_authentication"
      value = "on"
    }

    ...省略
  }
}

locals {
  service_account_name = "hoge@hogeproject.iam.gserviceaccount.com"
}

resource "google_sql_user" "hoge_user" {
  name     = trimsuffix(local.service_account_name, ".gserviceaccount.com")
  instance = google_sql_database_instance.db.name
  type     = "CLOUD_IAM_SERVICE_ACCOUNT"
}

resource "google_project_iam_member" "cloud_sql_client" {
  project  = "hoge-project"
  role     = "roles/cloudsql.client"
  member   = "serviceAccount:${local.service_account_name}"
}

output "cloud_sql_connection_name" {
  value = google_sql_database_instance.db.connection_name
}

突然ですがterraformです。
Cloud SQLインスタンスと、そのインスタンスにサービスアカウントで接続するための設定を行なっています。
Cloud SQL for PostgreSQLを使っていますが、MySQLでも似たようなものだと思います。
ポイントは以下です。

  • ipv4_enabled = trueにすることでインターネット上からアクセスできるようにする
    • settings.ip_configuration.authorized_networks を設定しない限り、外部からはCloud SQL Auth Proxyを使用しないと接続できません
    • これがfalseだと結局踏み台サーバーなどが必要になってしまいます
  • database_flagsname = "cloudsql.iam_authentication", value = "on"を設定してIAM認証を有効化する
  • DBユーザーとしてサービスアカウントを登録する
    • hoge@hogeproject.iamというユーザーを作成します
  • サービスアカウントにroles/cloudsql.clientロールの権限を付与

ちなみにrequire_ssl = falseにしていますが、Cloud SQL Auth Proxyはここの設定とは関係なく常に暗号化通信を行います。

マイグレーション対象のスキーマ, テーブルへの権限付与も必要です。
以下のようなSQLを実行して権限を付与しておきます。

SET SEARCH_PATH = app;

GRANT USAGE ON SCHEMA app TO "hoge@hogeproject.iam";
GRANT ALL ON ALL TABLES IN SCHEMA app TO "hoge@hogeproject.iam";

これらが適用された前提でYAMLに戻ります。
まずCloud SQL Auth Proxyをダウンロードし、実行します。

    - name: Start Cloud SQL Proxy
      run: |
        curl "https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.6.1/cloud-sql-proxy.linux.amd64" -o cloud-sql-proxy
        chmod +x cloud-sql-proxy
        ./cloud-sql-proxy hoge-project:asia-northeast1:some-instance --auto-iam-authn &

hoge-project:asia-northeast1:some-instanceはterraformのoutputに定義されているcloud_sql_connection_nameで取得できるものです。
--auto-iam-authnオプションを指定することでIAM認証で実行します。正確に確認できていませんが、だいたいCloud SDKと同じような仕様で認証情報を読み取っていると思います。(参考)
また、バックグラウンドでコマンドが実行されるようにしておきます。

最後にマイグレーションです。

    - name: Migrate Cloud SQL
      env:
        DB_USER: hoge@hogeproject.iam
      run: sbt flywayMigrate

利用するマイグレーションツールによって接続の設定方法はいろいろあると思いますが、認証に関わるところで言うと、userはサービスアカウトに紐づくもの(上記terrformで定義したgoogle_sql_user.hoge_user)を指定、passwordは使われないのでなんでもOKです。

全体

繋げただけですがこんな感じです。 実際の運用ではマイグレーション -> Cloud Runデプロイという順序なので、解説した順番と入れ替えています。

name: Deploy

on:
  push:
    branches:
      - 'main'

permissions:
  id-token: write
  contents: read

jobs:
  staging-deploy:
  runs-on: ubuntu-latest
  timeout-minutes: 10

  steps:
    - name: Checkout
      uses: actions/checkout@v3

    - name: Setup JDK
      uses: actions/setup-java@v3
      with:
        distribution: temurin
        java-version: 17
        cache: sbt

    - id: auth-google
      name: Auth Google Cloud
      uses: google-github-actions/auth@v1
      with:
        token_format: access_token
        workload_identity_provider: projects/111111111111/locations/global/workloadIdentityPools/github-actions/providers/github-actions-oidc
        service_account: hoge@hogeproject.iam.gserviceaccount.com

    - name: Start Cloud SQL Proxy
      run: |
        curl "https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.6.1/cloud-sql-proxy.linux.amd64" -o cloud-sql-proxy
        chmod +x cloud-sql-proxy
        ./cloud-sql-proxy hoge-project:asia-northeast1:some-instance --auto-iam-authn &

    - name: Migrate Cloud SQL
      env:
        DB_USER: hoge@hogeproject.iam
      run: sbt flywayMigrate

    - name: Login docker
      uses: docker/login-action@v2
      with:
        registry: asia-northeast1-docker.pkg.dev
        username: oauth2accesstoken
        password: ${{ steps.auth-google.outputs.access_token }}

    - name: Deploy cloud run
      run: |
        gcloud run deploy backend \
          --region asia-northeast1 \
          --image asia-northeast1-docker.pkg.dev/hoge-project/web-app/backend:latest

まとめ

GitHub ActionsでOIDCを使うだけだとあまりにもありふれているので、Cloud SQLをくっつけてなんとかしようとしていたかもしれません。精進します。

とにかく、静的なクレデンシャルは漏洩時のリスクが怖いというのはもちろん、リスクを軽減するために定期的なローテーションやメンバーの異動なんかで差し替えたりする必要があるので、面倒ごとを減らすという意味でもできるだけ無くしていきたいところです。

参考

Google Cloud Platform での OpenID Connect の構成 - GitHub Docs

Workload Identity 連携  |  IAM のドキュメント  |  Google Cloud

Cloud SQL Auth Proxy を使用して接続する  |  Cloud SQL for PostgreSQL  |  Google Cloud