Google Cloud FunctionsをGithub Actionsでデプロイする

先日の記事 「Cloud SchedulerでGoのCloud Functionを定期的に実行する」で書いたCloud Functionsは、これまでgcloudコマンドを人間系で実行してデプロイしていたのですが、流石に面倒くさくなりました... ので、Github Actionsを使って、自動でデプロイしようと試みたところ、なんとかうまくいったので纏めておきます。

やったこと

今回試したのは、「masterブランチにコミットがプッシュされたら、Github Actionsを使って、Cloud FunctionsをGCPにデプロイする」です。

リポジトリ構成

Github Actionsを使ってデプロイしようとしたのは、Go言語で書かれたGCF(Google Cloud Functions)です。このコードは、Github上でPrivateなリポジトリで管理しています。 Github Actions導入前のリポジトリの中身は、↓のような感じでした。

some-functions
├── .git
├── .go-version
├── README.md
└── function-a
    ├── function-a.go
    ├── go.mod
    └── go.sum

デプロイしたいのは、上記でいうところのfunction-a.goになります。

Github Actionsの導入

Github Actionsを使うのは今回が初めてでした。 公式ドキュメントを読みながら、導入していきました。

最初にやったのは、ワークフローを定義するファイルをリポジトリに配置することでした。 リポジトリ直下に、.github/workflows というディレクトリを作成し、その中に.ymlファイルを作ります。今回は、上記のfunction-aのデプロイを定義するということで、deploy-function-a.ymlという名前にしました。リポジトリ内のディレクトリ構成は、↓のようになりました。

some-functions
├── .git
├── .github
│   └── workflows
│       └── deploy-function-a.yml
├── .go-version
├── README.md
└── function-a
    ├── function-a.go
    ├── go.mod
    └── go.sum

workflowを定義する

次にワークフローの中身を.ymlに書いていきました。 結論を書くと↓のような内容になりました。

name: Deploy function-a
on:
  push:
    branches:
      - master
    paths:
      - 'function-a/**'

jobs:
  deploy:
    name: Deploy Functions
    runs-on: ubuntu-latest
    env:
      REGION: デプロイするGCPのリージョン
      ENTRY_POINT: エントリーポイントとなる関数名
      TOPIC: トリガーとなるPubSubのトピックID
    steps:
      - uses: actions/checkout@v2
      # 2021-04-22
      # 元々、GoogleCloudPlatform/github-actions/setup-gcloud@master
      # を使う記述をしていましたが、実際に実行すると、
      # google-github-actions/setup-gcloud を使ってくれ、と警告されるようになりました。
      # - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
      - uses: google-github-actions/setup-gcloud@master
        with:
          project_id: GCPのプロジェクトID
          service_account_email: GCPのサービスアカウントのメールアドレス
          service_account_key: ${{ secrets.Secretの名前 }}
      - name: Deploy Functions
        run: |
          gcloud functions deploy Function名 --source ./function-a --region=${REGION} --entry-point ${ENTRY_POINT} --runtime go113 --trigger-topic=${TOPIC}

今回は、masterブランチにfunction-aディレクトリ配下のファイルのコミットがプッシュされたらこのワークフローを起動させたいので、

on:
  push:
    branches:
      - master
    paths:
      - 'function-a/**'

としました。

次に、実際の関数のデプロイですが、簡単に言うと

  • ubuntuのコンテナを起動して、その上で以降の作業をする
  • リポジトリをチェックアウトする
  • 環境変数を準備する
  • gcloudコマンドを使えるようにする
  • gcloudコマンドで関数をデプロイする

という流れになるようです。 個人的にポイントとなる点と思ったのは、

  • GCP上でGCFのデプロイができるサービスアカウントを作っておく
  • サービスアカウントのアクセスキーをGithubにシークレットとして登録しておく

でしょうか...
それぞれの詳細を書いておきます。

GCPでサービスアカウントを作成する

Github Actions上でgcloudコマンドを実行する際に、GCPのサービスアカウントが必要になります。これは、GCPの管理画面の「IAMと管理」メニューの「サービスアカウント」で作成できます。

作成したサービスアカウントを使ってGCFのデプロイを実行したいので、「Cloud Functions開発者」のロールを割り当てました。

また、作成したサービスアカウントを認証するためのキーを作成し、JSONファイルとしてダウンロードしておきます。(このファイルが漏洩するとまずいので、厳重に管理する必要があります。)

Githubでシークレットを作成する

次に、サービスアカウントの認証用のキーを、Githubにシークレットとして登録します。これは、Githubのリポジトリの「Settings」タブで「Secrets」メニューを選択して進ます。

Github Secrets Menu

「New Secret」ボタンをクリックすると、新しいSecretを登録できるので、適切な名前をつけて、サービスアカウントの認証用のキーを値として登録します。これがワークフロー定義の中で${{ secrets.Secretの名前 }}とすることで使用できます。

ワークフローの中身については、おおよそこんな感じです。

実際に期待通りに動作させるまでに、いくつかハマったので、その点を纏めておきます。

ハマったこと

何故か.github/workflow配下のpushに失敗

.github/workflowディレクトリとワークフロー定義ファイルを作ってコミットし、いざプッシュしようとしたら、次のエラーが発生し、プッシュできませんでした。(ちなみに、これはターミナルでgitコマンドを実行した際に発生しました。)

! [remote rejected] master -> master (refusing to allow an OAuth App to create or update workflow `.github/workflows/deploy-function-a.yml` without `workflow` scope)
error: failed to push some refs to 'https://github.com/shimar/some-functions'

調べてみると、VSCodeやGithub Desktopでの話題がヒットしたものの、ターミナルで僕と同じ現象になった例は見つけられませんでした。仕方ないので、Macの「キーチェーンアクセス」アプリで、Githubへの認証情報を削除し、ターミナルでgitコマンドを実行し、Githubの認証を再実行したところ、無事にプッシュすることができました。

サービスアカウントのキーはbase64化しないと...

GCPのサービスアカウントのキーは、JSONで発行しました。これをGithubのSecretとして登録する場合、base64化する必要がありました。最初、JSONファイルの中身をそのまま登録していたのですが、ワークフロー実行時にエラーになりました。

JSONファイルの中身をbase64化するには、Macの場合はbase64コマンドでできます。

cat key-of-service-account.json | base64

--sourceオプションはディレクトリパスを指定する

gcloudコマンドでGCFのでプロイする際のオプションに--sourceがあります。最初、これをデプロイしたいファイルのパス(つまり./function-a/function-a.go)としていたのですが、ここに指定するのはディレクトリパスが正しいようです。つまり、./function-aを指定します。

サービスアカウントのロール

今回「Cloud Functions開発者」のロールを割り当てたGCPのサービスアカウントを使ってワークフローの中でGCFのデプロイを実施しようとしましたが、次のようなエラーが発生しました。

ERROR: (gcloud.functions.deploy) ResponseError: status=[403], code=[Forbidden], message=[Missing necessary permission iam.serviceAccounts.actAs for $MEMBER on the service account プロジェジェクトID@appspot.gserviceaccount.com.
Ensure that service account プロジェクトID@appspot.gserviceaccount.com is a member of the project プロジェクトID, and then grant $MEMBER the role 'roles/iam.serviceAccountUser'. 
You can do that by running 'gcloud iam service-accounts add-iam-policy-binding プロジェクトID@appspot.gserviceaccount.com --member=$MEMBER --role=roles/iam.serviceAccountUser' 
In case the member is a service account please use the prefix 'serviceAccount:' instead of 'user:'. Please visit https://cloud.google.com/functions/docs/troubleshooting for in-depth troubleshooting documentation.]

どうやら、今回作成したサービスアカウトにiam.serviceAccounts.actAsなる権限が足りていない、ということのようです。(正直、どういうことなのかわかっていません...) ただ、エラーメッセージに対応方法が書かれていたので、素直に実行してみたところ、無事にデプロイができるようになりました。

まとめ

Github Actionsを使って、GCFをデプロイする方法と、ハマったポイントについて纏めてみました。Github Actionsそのものよりも、GCPのサービスアカウント周りの方が難しいです... 今回、初めてGithub Actionsを使ってみましたが、Github上のリポジトリとは相性が良さそうなので、今後積極的に使ってみようと思います。

参考サイト