GCP GAEにGitHub ActionsでLaravelをデプロイする

Google Cloud Platform(GCP)のGoogle App Engine(GAE)にGitHub Actionsを使ってLaravelをデプロイします。

## 前提

- [Cloud SDK](https://cloud.google.com/sdk/docs?hl=ja)等諸々インストール済み
- プロジェクトなど元々ある前提
- 手順のみで詳しい解説は省きます(参考リンク参照ください)

## 【重要】サービスアカウントキーを使った認証は非推奨

サービスアカウントキーを使った認証は非推奨であり、今後のリリースで削除となります。
ワークロードID連携(Workload Identity Federation)が推奨されます。

### 2021/10/27 GitHub Actions OpenID Connect (OIDC) サポート

GitHub Actions の OpenID Connect サポートについて
https://zenn.dev/miyajan/articles/github-actions-support-openid-connect

- GitHubがGitHub Actions ワークフローにOIDCトークンを導入

### 2021/12 GitHub Actions からのキーなしの認証の有効化

- Workload Identity 連携を使用して GitHub Actions から Google Cloud に認証可能
- サービスアカウントキーのエクスポートが不要

![2_GitHub_Actions.max-1100x1100.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/52879/82d1ac47-17e2-685f-8ed7-107f474d12f9.jpeg)

### 新しい GitHub Action – auth

認証周りは `google-github-actions/auth` ライブラリに移行されました。

- https://github.com/google-github-actions/setup-gcloud
- https://github.com/google-github-actions/auth

## ワークロードID連携とは

https://cloud.google.com/blog/ja/products/identity-security/enabling-keyless-authentication-from-github-actions
https://cloud.google.com/iam/docs/workload-identity-federation?hl=ja

<iframe width="560" height="315" src="https://www.youtube.com/embed/4vajaXzHN08" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

### 従来のサービスアカウントキー

- 従来、Google Cloud の外部で実行されているアプリケーションは、サービスアカウントキーを使用して Google Cloud リソースにアクセス
- サービスアカウントキーは非常に強力な認証情報
- 正しく管理しなければセキュリティ上のリスク

### ワークロードID連携のメリット

- ワークロードID連携は、Identity and Access Management（IAM）を使用
- 外部IDに対して、サービスアカウントになりすます機能を含む IAM ロールを付与
- 有効期間の短いアクセストークンを使用してリソースに直接アクセス

### ワークロードプール

Workload Identity プールは、外部IDを編成および管理できます。

### ワークロードプールのプロバイダ

Workload Identity プールのプロバイダは、Google Cloud と外部 ID プロバイダとの関係を表すエンティティです。

## 概要

- Google Cloudサービスアカウントを作成
- サービスアカウントにIAMポリシーを付与
- IAM Service Account Credentials APIを有効化
- Workload Identityプールを作成
- Workload Identityプロバイダを作成
- IAMポリシーバインディングを作成
- GitHub Actionsのワークフローを作成
  - リポジトリのチェックアウト
  - Google Cloud認証(ワークロードIDフェデレーション)
  - Google Cloud SDKインストール
  - PHPインストール
  - Composerインストール
  - Laravelの設定
  - Google App Engineへデプロイ
  - Cloud SQL Proxyインストール
  - マイグレーションの実行

## Cloud SDKのアップデート

```
$ gcloud components update

$ gcloud version
Google Cloud SDK 380.0.0
beta 2022.04.01
bq 2.0.74
core 2022.04.01
gsutil 5.8
```

今回はこのバージョンを使用します。

## プロジェクトの一覧

```
$ gcloud projects list
```

## 環境変数を設定

誤入力防止のために環境変数を設定しておきます。

```
$ export PROJECT_ID="your-project-id"
$ export SERVICE_ACCOUNT_NAME="github-actions" # 任意のサービスアカウント名を付ける
$ export SERVICE_ACCOUNT_EMAIL="${SERVICE_ACCOUNT_NAME}"@"${PROJECT_ID}".iam.gserviceaccount.com
$ export WORKLOAD_IDENTITY_POOL_NAME="github-actions-pool" # 任意のWorkload Identityプール名を付ける
$ export WORKLOAD_IDENTITY_POOL_PROVIDER="github-actions-provider" # 任意のWorkload Identityプロバイダ名を付ける
$ export GITHUB_REPO="your-name/your-repository-name" # GitHubのユーザー名/リポジトリ名を設定する
```

※端末を閉じると環境変数が消えてしまうので、同じ端末で作業します。

## プロジェクト切り替え

一応切り替えておきます。

```
$ gcloud config set project "${PROJECT_ID}"
```

## サービスアカウントを作成

GitHub Actions上で利用するサービスアカウントを作成します。

```
$ gcloud iam service-accounts create "${SERVICE_ACCOUNT_NAME}" \
  --project "${PROJECT_ID}" \
  --display-name "${SERVICE_ACCOUNT_NAME}"
```

作成されたサービスアカウントを確認します。

```
$ gcloud iam service-accounts list --project "${PROJECT_ID}"
$ gcloud iam service-accounts describe "${SERVICE_ACCOUNT_EMAIL}" --project "${PROJECT_ID}"
```

## サービスアカウントにIAMポリシーを付与

作成したサービスアカウントにIAMポリシーを付与します。
どんなIAMポリシーがあるかは公式ドキュメントを参照してください。

https://cloud.google.com/iam/docs/understanding-roles?hl=ja

必要な権限はプロジェクトや構成によりますが、Laravelプロジェクトだと以下の権限が最低限必要でした。

- roles/iam.serviceAccountUser
- roles/appengine.deployer
- roles/appengine.serviceAdmin
- roles/storage.objectCreator
- roles/storage.admin
- roles/storage.objectAdmin
- roles/cloudbuild.builds.editor
- roles/cloudsql.client
- roles/iam.securityReviewer

必要な権限を洗い出したので割り当てていきます。
残念なことにIAMポリシーをまとめて割り当てるオプションはなかったので、一つずつコマンドを実行して権限を付与します。

```
$ gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --role="roles/iam.serviceAccountUser" \
  --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}"

$ gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --role="roles/appengine.deployer" \
  --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}"

$ gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --role="roles/appengine.serviceAdmin" \
  --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}"

$ gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --role="roles/storage.objectCreator" \
  --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}"

$ gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --role="roles/storage.admin" \
  --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}"

$ gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --role="roles/storage.objectAdmin" \
  --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}"

$ gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --role="roles/cloudbuild.builds.editor" \
  --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}"

$ gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --role="roles/cloudsql.client" \
  --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}"

$ gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --role="roles/iam.securityReviewer" \
  --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}"
```

IAMポリシーを付与できたか確認します。

```
$ gcloud projects get-iam-policy ${PROJECT_ID} \
  --flatten="bindings[].members" \
  --filter="bindings.members:${SERVICE_ACCOUNT_NAME}"
```

## IAM Service Account Credentials APIを有効化

GitHub Actions上で一時的なサービスアカウントキーを生成するAPIを有効化します。

```
$ gcloud services enable iamcredentials.googleapis.com --project "${PROJECT_ID}"
```

```
$ gcloud services list --project "${PROJECT_ID}"

NAME                                  TITLE
...
iam.googleapis.com                    Identity and Access Management (IAM) API
```

`iam.googleapis.com` のサービスが表示されていればOK

## Workload Identityプールを作成

```
$ gcloud iam workload-identity-pools create "${WORKLOAD_IDENTITY_POOL_NAME}" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --display-name="${WORKLOAD_IDENTITY_POOL_NAME}"
```

### Workload IdentityプールのIDを取得

```
$ gcloud iam workload-identity-pools describe "${WORKLOAD_IDENTITY_POOL_NAME}" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --format="value(name)"
```

取得したプールIDを環境変数にセットします。

```
# 例
$ export WORKLOAD_IDENTITY_POOL_ID="projects/123456789/locations/global/workloadIdentityPools/github-actions-pool"
```

## Workload Identityプロバイダを作成

Workload Identityプールの中にWorkload Identityプロバイダを作成します。

```
$ gcloud iam workload-identity-pools providers create-oidc "${WORKLOAD_IDENTITY_POOL_PROVIDER}" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="${WORKLOAD_IDENTITY_POOL_NAME}" \
  --display-name="${WORKLOAD_IDENTITY_POOL_PROVIDER}" \
  --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \
  --issuer-uri="https://token.actions.githubusercontent.com"
```

```
$ gcloud iam workload-identity-pools providers describe "${WORKLOAD_IDENTITY_POOL_PROVIDER}" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="${WORKLOAD_IDENTITY_POOL_NAME}"

attributeMapping:
  attribute.actor: assertion.actor
  attribute.repository: assertion.repository
  google.subject: assertion.sub
displayName: github-actions-provider
name: projects/1234567890/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider
oidc:
  issuerUri: https://token.actions.githubusercontent.com
state: ACTIVE
```

## IAMポリシーバインディングを作成

IAMポリシーバインディングを作成します。
作成したサービスアカウントを使用できるGitHubリポジトリを紐づけます。

```
$ gcloud iam service-accounts add-iam-policy-binding "${SERVICE_ACCOUNT_EMAIL}" \
  --project="${PROJECT_ID}" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${GITHUB_REPO}"
```

## .gitignore

プロジェクトルートの `.gitignore` ファイルに下記の行を追加します。
GitHub Actions上で生成されるサービスアカウントキーです。

```
gha-creds-*.json
```

## GitHub Actionsのワークフローを作成

ワークフローのサンプルコードです。

```
name: Deploy

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    permissions:
      contents: read
      id-token: write

    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
          ref: main

      - id: auth
        uses: google-github-actions/auth@v0
        with:
          workload_identity_provider: YOUR_WORKLOAD_IDENTITY_POOL_ID
          service_account: YOUR_SERVICE_ACCOUT_EMAIL

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

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: 7.4

      - name: Cache Vendor
        id: cache
        uses: actions/cache@v2
        with:
          path: ./src/vendor
          key: ${{ runner.os }}-composer-${{ hashFiles('./src/composer.lock') }}
          restore-keys: |
            ${{ runner.os }}-composer-

      - name: Install Dependencies
        working-directory: src
        if: steps.cache.outputs.cache-hit != 'true'
        run: composer install

      - name: Laravel Settings
        working-directory: src
        run: |
          cp .env.example .env

      - name: PHP, Composer, Laravel Versions
        working-directory: src
        run: |
          php --version
          composer --version
          php artisan --version

      - name: Deploy to Google App Engine
        env:
          CI: true
          PROJECT_ID: your-project-id
          DB_PASSWORD: ${{ secrets.GCLOUD_DB_PASSWORD }}
        working-directory: ./src
        run: |
          cp .env.example .env
          cp secret.yaml.example secret.yaml
          sed -i -e s/__DB_PASSWORD__/$DB_PASSWORD/ secret.yaml
          gcloud app deploy app.yaml --quiet --no-cache --project ${PROJECT_ID}

      - name: Install Cloud SQL Proxy
        working-directory: src
        run: |
          wget https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64 -O cloud_sql_proxy
          chmod +x cloud_sql_proxy

      - name: Run Migration
        env:
          DB_CONNECTION: mysql
          DB_HOST: 127.0.0.1
          DB_PORT: 3306
          DB_DATABASE: YOUR_DATABASE_NAME
          DB_USERNAME: YOUR_DATABASE_USER
          DB_PASSWORD: ${{ secrets.GCLOUD_DB_PASSWORD }}
          APP_ENV: staging
        working-directory: src
        run: |
          ./cloud_sql_proxy -instances=your-project-id:asia-northeast1:your-project-name=tcp:3306 &
          php artisan config:clear
          sleep 5 # cloud_sql_proxy waiting...
          php artisan migrate --force
          php artisan db:seed --force --class 'Database\Seeders\ProductionDatabaseSeeder'
```

やってることは大体下記の通りです。

- リポジトリのチェックアウト
- Google Cloud認証(ワークロードIDフェデレーション)
- Google Cloud SDKインストール
- PHPインストール
- Composerインストール
- Laravelの設定
- Google App Engineへデプロイ
- Cloud SQL Proxyインストール
- マイグレーションの実行

大事なところはGoogle Cloud認証のところです。

```
- id: auth
  uses: google-github-actions/auth@v0
  with:
    workload_identity_provider: YOUR_WORKLOAD_IDENTITY_POOL_PROVIDER_ID
    service_account: YOUR_SERVICE_ACCOUT_EMAIL
```

`YOUR_WORKLOAD_IDENTITY_POOL_PROVIDER_ID`には`projects/123456789/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider`といったプールプロバイダーIDを入れます。

### 補足

```yaml:app.yaml
runtime: php74

instance_class: B1
basic_scaling:
  max_instances: 2
  idle_timeout: 3m

includes:
  - secret.yaml

env_variables:
  APP_ENV: production
  APP_URL: https://example.com
  APP_DEBUG: false
  APP_KEY: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  APP_STORAGE: /tmp
  VIEW_COMPILED_PATH: /tmp
  LOG_CHANNEL: stdout
  DB_CONNECTION: mysql
  DB_HOST: 127.0.0.1
  DB_PORT: 3306
  DB_DATABASE: YOUR_DATABASE_NAME
  DB_USERNAME: root
  DB_SOCKET: YOUR_DATABASE_SOCKET
```

```yaml:secret.yaml.example
env_variables:
  DB_PASSWORD: __DB_PASSWORD__
```

## 参考

https://dev.classmethod.jp/articles/google-cloud-auth-with-workload-identity

Workload Identity連携について詳しく書かれています。

## よくあるエラー集

### Invalid value for "audience". This value should be the full resource name of the Identity Provider

```
Error: google-github-actions/setup-gcloud failed with: failed to execute command `gcloud --quiet auth login --cred-file /home/runner/work/your-repository-name/your-repository-name/gha-creds-9884ae770017c361.json`: ERROR: gcloud crashed (OAuthError): ('Error code invalid_request: Invalid value for "audience". This value should be the full resource name of the Identity Provider. See https://cloud.google.com/iam/docs/reference/sts/rest/v1/TopLevel/token for the list of possible formats.', '{"error":"invalid_request","error_description":"Invalid value for \\"audience\\". This value should be the full resource name of the Identity Provider. See https://cloud.google.com/iam/docs/reference/sts/rest/v1/TopLevel/token for the list of possible formats."}')
```

似た感じのIssue
https://github.com/google-github-actions/deploy-appengine/issues/247

```
      - id: auth
        uses: google-github-actions/auth@v0
        with:
          # workload_identity_provider: github-actions-provider # NG: プロバイダー名じゃなくプロバイダーIDを指定する
          workload_identity_provider: projects/123456789/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider
```

`workload_identity_provider` の指定ミスってると出る

```
ERROR: (gcloud.app.deploy) Permissions error fetching application [apps/project-id]. Please make sure that you have permission to view applications on the project and that github-actions@project-id.iam.gserviceaccount.com has the App Engine Deployer (roles/appengine.deployer) role.
```

### Please make sure that you have permission to view applications on the project

```
Please make sure that you have permission to view applications on the project and that your-service-provider@your-project.iam.gserviceaccount.com has the App Engine Deployer (roles/appengine.deployer) role.
```

サービスアカウントにロールが付与されていないので付与する。
使用しているサービスによるのでエラーが出たら設定してあげる。

```
$ gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --role="roles/iam.serviceAccountUser" \
  --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}"
```

### The client is not authorized to make this request., notAuthorized

```
2022/04/14 08:36:48 current FDs rlimit set to 65536, wanted limit is 8500. Nothing to do here.
2022/04/14 08:36:48 using credential file for authentication; path="/home/runner/work/your-repository/your-repository/gha-creds-[26](https://github.com/your-name/your-repository/runs/6020943803?check_suite_focus=true#step:12:26)14f78da682c488.json"
Configuration cache cleared!
2022/04/14 08:36:48 errors parsing config:
	googleapi: Error 403: The client is not authorized to make this request., notAuthorized
[2022-04-14 17:36:53] local.CRITICAL: SQLSTATE[HY000] [2002] Connection refused (SQL: select * from information_schema.tables where table_schema = plus_insurance_staging and table_name = migrations and table_type = 'BASE TABLE') {"exception":"[object] (Illuminate\\Database\\QueryException(code: 2002): SQLSTATE[HY000] [2002] Connection refused (SQL: select * from information_schema.tables where table_schema = your-database and table_name = migrations and table_type = 'BASE TABLE') at /home/runner/work/your-repository/your-repository/src/vendor/laravel/framework/src/Illuminate/Database/Connection.php:671)
[stacktrace]
#0 /home/runner/work/your-repository/your-repository/src/vendor/laravel/framework/src/Illuminate/Database/Connection.php(745): Illuminate\\Database\\Connection->runQueryCallback()
```

Cloud SQL Client (roles/cloudsql.client) ロールがないのが原因でした。

https://cloud.google.com/sql/docs/mysql/sql-proxy?hl=en#permissions
https://cloud.google.com/iam/docs/understanding-roles?hl=ja#cloud-sql-roles

### The principal (user or service account) lacks IAM permission "cloudscheduler.locations.list" for the resource

```
ERROR: (gcloud.app.deploy) User [github-actions@your-project.iam.gserviceaccount.com] does not have permission to access projects instance [project-id] (or it may not exist): The principal (user or service account) lacks IAM permission "cloudscheduler.locations.list" for the resource "projects/project-id" (or the resource may not exist).
```

理由がわからないが、Google App Engineデプロイ時に `cloudscheduler.locations.list` 権限がないエラーが発生する。
セキュリティ審査担当者(roles/iam.securityReviewer)のロールを割り当てて解決。

```
$ gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
  --role="roles/iam.securityReviewer" \
  --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}"
```