Google Apps ScriptからGoogle Cloud Functionsに処理を移譲するためのあれこれ


# はじめに

Google Apps Script(GAS)で処理を色々書いていると色々と不自由を感じることもあったので、選択肢の1つとしてCloud Functionsと連携して処理を任せるハウツーを書き残します。

# Google Cloud Functionsってなに

Google Cloud Platform(GCP)で利用可能な、イベントやhttp呼び出しに対応してあらかじめ記述したコードをサーバレスで実行できるサービスです。
類似したサービスにAWS Lambdaがあり、そちらのGCP版と言って伝わるならそれが一番早いと思います。

「とりあえずどんなものか使ってみる」という場合、少し古いものの以下の記事が大変参考になります。
[これから始める Cloud Functions 入門](https://medium.com/google-cloud-jp/cloud-functions-basic-cd18686f0198)

## GASと比較した場合のCloud Functionsのメリット

- 処理系が基本的にGASより強い
  - node.js、python、goなどランタイムを選択可
  - node.jsなので、GASとは違ってnpmモジュールも自由に使える
- Cloud StrageやCloud SQLなどGCPの各種サービスと連携できる
    - Cloud Function本来の機能としてGCP側のイベントから発火も可

GASで何かしらの処理を行わせることを検討する場合、Cloud Functionsを呼び出して処理させることでより強力な環境を使うことができます。
というわけで、以下連携を試すにあたって色々引っかかった点などのメモです。

# GAS -> Cloud Functionの認証つき呼び出し

GASからのCloud Functionのhttp呼び出しは`UrlFetchApp`でできるのですが、Cloud FunctionsにはGCPのIAMを利用した認証機構があります。
これを利用することでセキュアな連携を実現できるので、必ず設定しておくのがいいと思います。

認証を通す記述方法は[公式ドキュメント](https://cloud.google.com/functions/docs/securing/authenticating#authenticating_developer_testing)に記載があり、適切なIAMロールが割り当てられたユーザのIDトークンを`Authorization`ヘッダにて指定することで呼び出すことができます。

GASのスクリプトエディタから設定して認証を通す方法を実践している人が既にいるので、基本的にこちらに従えば問題ないと思います。
[GASから認証付きのCloud Functionsを実行する。](https://qiita.com/kyhei_0727/items/d6b1cb33ce90b99e8129)



# Cloud Function -> Google Appsの認証

GASの大きな利点として、各種Google Appsとの連携がポチポチ認証するだけで楽に行えるという点があります。
Cloud Functionでも各種Google APIを呼び出すことで同様の機能が扱えますが、あらかじめ参照したいドキュメント等に共有設定を行っておく必要があります。

- Cloud Function実行元のサービスアカウント(App Engine default service account)を控えておく。
![serviceaccount.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528082/2606225c-a3db-3b64-1e92-2dbfd86bd2ae.jpeg)

- 参照先のドキュメント等の共有設定に該当のサービスアカウントのメールアドレスを追加する。

実行するCloud Functionsコードの例(スプレッドシートのIDを`req.body.message`で渡し、空白のシートを追加してみるだけ):

```javascript
const {google} = require("googleapis");

function addEmptySheet(sheetsAPI, sheetId) {
  return new Promise((resolve, reject) => {
    const emptySheetParams = {
      spreadsheetId: sheetId,
      resource: {
        requests: [
          {
            addSheet: {
              properties: {
                title: 'test'
              }
            }
          }
        ]
      }
    };
    sheetsAPI.spreadsheets.batchUpdate( emptySheetParams, function(err, response) {
        if (err) {
          reject("The Sheets API returned an error: " + err);
        } else {
          const sheetId = response.data.replies[0].addSheet.properties.sheetId;
          console.log("Created empty sheet: " + sheetId);
          resolve(sheetId);
        }
      }
    );
  });
}

exports.exec = async (req, res) => {
  const auth = await google.auth.getClient({
    scopes: [
      "https://www.googleapis.com/auth/spreadsheets",
      "https://www.googleapis.com/auth/devstorage.read_only"
    ]
  });

  const sheetsAPI = google.sheets({version: 'v4', auth});

  const sheetId = await addEmptySheet(sheetsAPI, req.body.message)
  res.status(200).send(String(sheetId));
};

```

参考: [Cloud Function to Automate CSV data import into Google Sheets](https://codelabs.developers.google.com/codelabs/cloud-function2sheet#0)

# Cloud Functionsのローカル開発

決して使いやすくはないGASのスクリプトエディタですが、Cloud Functionsのオンラインエディタの使い勝手は正直それに輪をかけて劣悪な面があります。
幸いにしてローカルで開発可能な仕組みがあるので、ぜひ活用していきましょう。

## ローカル環境での動作テスト

Cloud Functionsのデプロイには結構な時間がかかるので、ローカルでの動作確認用にFunctions Frameworkなるものが用意されています。
[公式ドキュメント](https://cloud.google.com/functions/docs/functions-framework?hl=ja)
言語別に用意されているので、自分が使いたい環境のものを導入しましょう。

基本は各言語ごとのクイックスタートに従って行えばいいのですが、個人的にはまってしまった点としてグローバルにインストールするのはやめた方がいいです。

```console:bad
npm install @google-cloud/functions-framework -g
```

何が良くないかというと、Functions Frameworkの起動コマンドは言語によらず`functions-framework`で同一なので後から他の言語も試そうとか思った際に衝突します。
（`functions-framework-xxx`な感じで分けて呼び出せるみたいなことも書いてありますが、少なくとも僕の場合npm->pipの順でインストールしようとして後者が失敗しました）
横着せずに`npm init`なり`venv`なりコンテナなりで環境を分けるのが良いと思います。

## Github Actionsでのデプロイ

勝手にデプロイしてくれる仕組みはぜひとも作っておきたいところです。
しかしGithubから認証を通すために事前にIAMやらWorkload Identityの設定が必要です。サービスアカウントキーで認証する方法でもいいのですが、鍵管理が不要なこっちの方がナウいらしいです。
基本的な流れは[`gcloud`CLIツール](https://cloud.google.com/sdk)を導入した上で、[google-github-actions/authにおけるSetting up Workload Identity Federationの手順](https://github.com/google-github-actions/auth#setting-up-workload-identity-federation)を追いかけていけばOKですが、いくつか注意点を補足しておきます。

- authアクションに指定するサービスアカウントが`my-service-account@my-project.iam.gserviceaccount.com`のフォーマットに従っている必要がある
 - 例えばFunctions実行元のサービスアカウントを流用してしまおう、とか横着なことを考えているとここで弾かれます。上記リンクの2.(Optional)の項に従って`gcloud iam service-accounts create`すれば適切なサービスアカウントが新規に作成できるので、そうするのがよいと思います。
 
- 作成したサービスアカウントに下記コマンドにて「Cloud Functions開発者」のIAMロール、及びappspotアカウントの借用権限を付与する
 - リンクは認証までの手順なので触れていませんが、Cloud Functionsのデプロイには上記権限が必要です。
 
```console
gcloud projects add-iam-policy-binding my-project \
--member="serviceAccount:my-service-account@my-project.iam.gserviceaccount.com" \
--role="roles/cloudfunctions.developer"

gcloud iam service-accounts add-iam-policy-binding \
my-project@appspot.gserviceaccount.com \
--member='serviceAccount:my-service-account@my-project.iam.gserviceaccount.com' \
--role=roles/iam.serviceAccountUser --project my-project

```

workflowはとりあえずこんな感じになりました。

```yaml:workflows/deploy.yml
name: 'Deploy'

on:
  push:
      branches: [ master ]
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    permissions:
      contents: 'read'
      id-token: 'write'

    steps:
    - uses: 'actions/checkout@v2'
    - id: 'auth'
      uses: 'google-github-actions/auth@v0.4.1'
      with:
        token_format: 'access_token'
        workload_identity_provider: '${{secrets.GCP_WIPRVIDER}}'
        service_account: 'actions-account@${{secrets.GCP_PROJECT_ID}}.iam.gserviceaccount.com'

    - id: 'gcloud'
      name: 'run gcloud'
      run: |-
        gcloud auth login --brief --cred-file='${{ steps.auth.outputs.credentials_file_path }}'
#       gcloud services list  認証が通っているかテストしたい場合はこちらで
        gcloud functions deploy nodejs_deploy_test --project '${{secrets.GCP_PROJECT_ID}}' --runtime nodejs14 --trigger-http --entry-point helloWorld
```

ちなみに[google-github-actions/deploy-cloud-functions](https://github.com/google-github-actions/deploy-cloud-functions)というactionも公開されているのですが、Workload Identity認証と上手く併用できなかったのでgcloud叩いてdeployしています。

参考:
[GitHub Actions + google-github-actions/auth で GCP keyless CI/CD](https://zenn.dev/vvakame/articles/gha-and-gcp-workload-identity)
[Github公式ドキュメント](https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-google-cloud-platform)
[GCP公式ドキュメント(サービスアカウントの権限借用について)](https://cloud.google.com/iam/docs/impersonating-service-accounts?hl=ja)

# おわりに
気づいたらほとんど権限周りのポイントでした。
最初に面倒な設定は多いものの、IAMの仕組みを利用することでGCP上である程度セキュアに完結させられる、というのはGASの代替もしくは併用先として強力なCloud Functionsに目を向けるメリットの1つになるのではないかと思います。鍵管理等が不要になっているのもいいですね。
皆さんもGASのことが嫌いになりかけた時はCloud Functionsのことを思い出してみてはいかがでしょうか。