🐷

GitHub Actions と Workload Identity を使って Cloud Run Functions をデプロイする

に公開

はじめに

こんにちは、クラウドエースの許です。
以前、私が執筆した記事にて、Cloud Storage にアップロードした CSV ファイルを BigQuery に自動的にインポートする方法を紹介しました。
その際、Cloud Run を使用して Cloud Functions をデプロイしましたが、デプロイの手順は手動で行っていました。

今回は、GitHub にプッシュすると、自動的に Cloud Run Functions のデプロイまで済ませる方法について紹介します。

この記事の目的

GitHub Actions のサービスアカウント認証では、サービスアカウントキーを利用する方法と Workload Identity を利用する方法があります。
サービスアカウントキーを利用すると、手軽にサービスアカウントに接続でき、設定の手間も大幅に減ります。
しかし、サービスアカウントキーは、一旦流出すると取り返しのつかないことになってしまうため、Google としても利用は推奨していません。
Cloud Run Functions を GitHub Actions でデプロイする記事は多数ありますが、サービスアカウントキーを利用しているものが多かったため、Workload Identity を利用した記事をあえて書いてみました。

また、Cloud Build でも同様のことが実現できますが、Workload Identity を利用することに主眼を置いたため、GitHub Actions でのデプロイとしました。

手順

  1. 前回記事と同様のコードを用意
  2. 下準備
  3. Workload Identity の設定
  4. GitHub Actions の設定
  5. デプロイの確認

前回記事と同様のコードを用意

まず、前回記事と同様のコードを用意します。
執筆主は Java が好みなので、Java で実装します。

package gcfv2storage;

import com.google.cloud.bigquery.*;
import com.google.cloud.functions.CloudEventsFunction;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.cloudevents.CloudEvent;

import java.util.Objects;
import java.util.logging.Logger;

public class StorageFunction implements CloudEventsFunction {
    private static final Logger logger = Logger.getLogger(StorageFunction.class.getName());

    //    各種環境情報を設定
    private static final String PROJECT_ID = {プロジェクトID};
    private static final String DATASET_ID = {データセットID};
    private static final String TABLE_ID =  {テーブルID};

    @Override
    public void accept(CloudEvent event) {
//        Cloud Run Functionsからの入力を受け付ける
        String eventName = new String(Objects.requireNonNull(event.getData()).toBytes());
        logger.info("Cloud Event data: " + eventName);

//        入力情報をGsonを使ってJson型に変換し、各種情報を取得
        JsonObject data = JsonParser.parseString(eventName).getAsJsonObject();
        String bucketName = data.get("bucket").getAsString();
        String fileName = data.get("name").getAsString();
        logger.info("Processing file: " + fileName + " from bucket: " + bucketName);

        String sourceUri = "gs://" + bucketName + "/" + fileName;

//        BigQueryの各カラムの設定を行う
//        Schema schema = Schema.of(
//                Field.of("string", LegacySQLTypeName.STRING),
//                Field.of("int", LegacySQLTypeName.INTEGER),
//                Field.of("float", LegacySQLTypeName.FLOAT),
//                Field.of("date", LegacySQLTypeName.DATE),
//                Field.of("datetime", LegacySQLTypeName.DATETIME)
//        );

        Schema schema = Schema.of(
                Field.newBuilder("string", LegacySQLTypeName.STRING).setMode(Field.Mode.NULLABLE).setDescription("string Column").build(),
                Field.newBuilder("int", LegacySQLTypeName.INTEGER).setMode(Field.Mode.NULLABLE).setDescription("int Column").build(),
                Field.newBuilder("float", LegacySQLTypeName.FLOAT).setMode(Field.Mode.NULLABLE).setDescription("float Column").build(),
                Field.newBuilder("date", LegacySQLTypeName.DATE).setMode(Field.Mode.NULLABLE).setDescription("date Column").build(),
                Field.newBuilder("datetime", LegacySQLTypeName.DATETIME).setMode(Field.Mode.NULLABLE).setDescription("datetime Column").build()
        );

//        CSVファイルをBigQueryにアップロードための各種設定と処理
        try {
            BigQuery bigQuery = BigQueryOptions.getDefaultInstance().getService();

//            先頭行をスキップ
            CsvOptions csvOptions = CsvOptions.newBuilder().setSkipLeadingRows(1).build();
            TableId tableId = TableId.of(PROJECT_ID, DATASET_ID, TABLE_ID);
            LoadJobConfiguration loadConfig =
                    LoadJobConfiguration
                            .newBuilder(tableId, sourceUri + fileName, csvOptions)
                            .setSchema(schema)
                            .build();

            // ロードジョブを作成し、完了するまで待機
            Job job = bigQuery.create(JobInfo.of(loadConfig));
            job = job.waitFor();
            if (job.isDone() && job.getStatus().getError() == null) {
                logger.info("Job completed successfully");
            }
            else {
                logger.severe("Job failed: " + job.getStatus().getError());
            }
        } catch (BigQueryException | InterruptedException e) {
            logger.severe("Error: " + e.getMessage());
        }
    }

}

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>gcfv2storage</groupId>
    <artifactId>http</artifactId>
    <version>0.0.1</version>
    <name>Cloud Storage Function for GCFv2</name>

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
    </properties>

    <dependencies>

        <!-- Google Cloud Storage client -->
        <dependency>
            <groupId>com.google.cloud</groupId>
            <artifactId>google-cloud-storage</artifactId>
            <version>2.45.0</version>
        </dependency>

        <!-- Google BigQuery client -->
        <dependency>
            <groupId>com.google.cloud</groupId>
            <artifactId>google-cloud-bigquery</artifactId>
            <version>2.44.0</version>
        </dependency>

        <!-- Apache Commons CSV for CSV parsing -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-csv</artifactId>
            <version>1.12.0</version>
        </dependency>

        <dependency>
            <groupId>com.google.cloud.functions</groupId>
            <artifactId>functions-framework-api</artifactId>
            <version>1.0.4</version>
        </dependency>
        <dependency>
            <groupId>io.cloudevents</groupId>
            <artifactId>cloudevents-api</artifactId>
            <version>2.2.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.11.0</version>
        </dependency>

    </dependencies>


</project>

下準備

今回の主題ではないため、詳しい手順は省略しますが、以下のことを行います。

  • 前回記事と同様の構成(Cloud Storage、BigQuery)を作成
  • Cloud Functions API の有効化
  • GitHub リポジトリの作成
  • Cloud Run Functions を動かすサービスアカウントの作成

Workload Identity の設定

弊社の記事を参考に、Workload Identity の設定を行います。

序盤の部分は弊社記事とほぼ同じになります。

  1. Workload Identity プールの作成
    Google Cloud コンソールを開き、「IAM と管理」→「Workload Identity 連携」を開き、「プールを作成」をクリックします。
  2. 以下のように値を設定します
    • 名前: github-actions-pool
    • 説明: (なんでもいいけど)GitHub Actions Workload Identity Pool
    • プロバイダの選択: OpenID Connect(OIDC)
    • プロバイダ名: GitHub
    • プロバイダ ID: github
    • 発行元 URL: https://token.actions.githubusercontent.com
    • JWK ファイル :空欄
    • オーディエンス: デフォルトのオーディエンス
    • プロバイダの属性:
      • Google1: google.subject
      • OIDC1: assertion.repository
  3. [保存]をクリックして、プールを作成します。
  4. 作成されたプールの詳細ページに移動
  5. [アクセスを許可] をクリック
  6. 右側に出てくる画面から、[サービス アカウントの権限借用を使用してアクセス権を付与する]を選択
  7. [サービス アカウント] に、Cloud Run Functions を動かすサービスアカウントを選択
  8. プリンシパルに以下の内容を記述
    • 属性名: subject
    • 属性値: リポジトリ所有者/リポジトリ名
      • リポジトリ所有者には、組織名やユーザー名が入る
      • リポジトリ名には、GitHub リポジトリの名前が入る
      • 例: cloud-ace/CloudRunFunctions
  9. [保存]をクリック

ここまで行えば、Workload Identity の設定は完了です。

GitHub Actions の設定

次に、GitHub Actions の設定を行います。
GitHub リポジトリのルートに .github/workflows フォルダを作成し、その中に deploy.yaml というファイルを作成します。

name: Deploy to Google Cloud Run Functions

on:
  push:
    branches:
      - main

# GitHub Actionsを動かすのに、権限が必要
permissions:
  id-token: write
  contents: read

jobs:
  #Ubuntuで各コマンドを実行する
  deploy:
    runs-on: ubuntu-latest

    # 環境変数を定義
    # REGION_NAMEはGCSバケットと同じリージョンにする
    env:
      YOUR_FUNCTIONS_NAME: {Cloud Run Functions の名前}
      BUCKET_NAME: {GCSバケット名}
      REGION_NAME: {リージョン名(GSCバケットに合わせる)}
      RUNTIME_NAME: java21
      ENTRY_POINT: gcfv2storage.StorageFunction

    steps:
      #自分のリポジトリのコードを取得する
      - name: 'Checkout code'
        uses: 'actions/checkout@v4'

        # Javaのセットアップ
      - name: 'Set up Java'
        uses: 'actions/setup-java@v4'
        with:
          java-version: '21'
          distribution: 'temurin'

        # Mavenでビルドする
      - name: 'Build with Maven'
        run: 'mvn clean package'

        # Google Cloudに認証する
      - id: 'auth'
        name: 'Authenticate to Google Cloud'
        uses: 'google-github-actions/auth@v1'
        with:
          workload_identity_provider: 'projects/{プロジェクト番号}/locations/global/workloadIdentityPools/github-actions-pool/providers/github'
          service_account: 'deploy-test@{プロジェクトID}.iam.gserviceaccount.com'
            # サービスアカウントは例示なので、自分の作成したものに置き換えてください

        # Google Cloud SDKをセットアップする
      - name: 'Set up Cloud SDK'
        uses: 'google-github-actions/setup-gcloud@v1'
        with:
          version: '>= 379.0.0'

        # Cloud Run Functionsをデプロイする
      - name: 'Deploy to Google Cloud Functions'
        run: |
          gcloud functions deploy $YOUR_FUNCTIONS_NAME \
            --trigger-resource $BUCKET_NAME \
            --trigger-event google.storage.object.finalize \
            --region $REGION_NAME \
            --runtime $RUNTIME_NAME \
            --entry-point $ENTRY_POINT

プロジェクト番号は、プロジェクトのトップページに記載されています。

サービスアカウントは、Cloud Run Functions を動かすためのものを指定してください。

デプロイの確認

GitHub リポジトリにプッシュすると、GitHub Actions がトリガーされ、Cloud Run Functions にデプロイされます。
このままリポジトリにプッシュしてみましょう。
※画像は記事執筆にあたって、色々デバッグしたので、失敗の跡が写っています。
上手く設定が書けていれば、成功するはずです。

GitHub Actions が成功したら、Cloud Run Functions の詳細ページに移動し、関数がデプロイされていることを確認します。

正常に関数が作成されていれば完成になります。

まとめ

今回は、GitHub Actions と Workload Identity を使って Cloud Run Functions をデプロイする方法を紹介しました。
Workload Identity は少々使いにくいですが、セキュリティ面でのリスクが低減されます。
これで、いちいち Google Cloud のコンソールに行かなくても、GitHub にプッシュすれば、自動でデプロイができるようになりました。
読んでいただき、ありがとうございました。

Discussion