GitHub ActionsとTerraformでGCP管理を始める

# やること

GitHub Actions + Terraform + GCP (OIDC)

# ステップ

## 1. (GCP) Service Account 作成

GitHub Actions用のService Accountを作成 + Editor Roleをつける。(今回Console上で)

変数をセット

```bash
export PROJECT_ID=<your project id>
export POOL_NAME=<pool name> # github-actions 
export PROVIDER_NAME=<provider name> # gha-provider
export SA_EMAIL=<service_account name>@<project_name>.iam.gserviceaccount.com
export GITHUB_REPO=<org>/<repo_name>
```

iamcredentialsのAPIを有効化

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

workload-identity-poolsの作成とIDを変数`WORKLOAD_IDENTITY_POOL_ID`へ格納
```bash
gcloud iam workload-identity-pools create "${POOL_NAME}" \
    --project="${PROJECT_ID}" --location="global" \
    --display-name="use from GitHub Actions"
export WORKLOAD_IDENTITY_POOL_ID=$( \
    gcloud iam workload-identity-pools describe "${POOL_NAME}" \
      --project="${PROJECT_ID}" --location="global" \
      --format="value(name)" \
  )
```
`WORKLOAD_IDENTITY_POOL_ID`の中身確認
```
echo $WORKLOAD_IDENTITY_POOL_ID
```

workload-identity-poolsにGitHub用の OIDC providers作成

```bash
gcloud iam workload-identity-pools providers create-oidc "${PROVIDER_NAME}" \
    --project="${PROJECT_ID}" --location="global" \
    --workload-identity-pool="${POOL_NAME}" \
    --display-name="use from GitHub Actions provider" \
    --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.actor=assertion.actor,attribute.aud=assertion.aud" \
    --issuer-uri="https://token.actions.githubusercontent.com"
```

Service Account にPolicyをバインドする

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

ProviderをDescribeして`workload_identity_provider`を確認

```bash
gcloud iam workload-identity-pools providers describe "${PROVIDER_NAME}" \
    --project="${PROJECT_ID}" --location="global" \
    --workload-identity-pool="${POOL_NAME}" \
    --format="value(name)"
```

結果: `workload_identity_provider` (GitHub Actionsの設定で必要になるのでメモしておく)
```
projects/<id>/locations/global/workloadIdentityPools/<pool_name>/providers/<provider_name>
```

## 2. (GCP) Cloud Storage Bucket作成

`Region`に`asia-northeast1 (Tokyo)`を選び、残りはすべてDefaultでBucketを作成する

## 3. terraformのコード準備

今回は、terraformのコードを`gcp`というDirの下に置いておく (GitHub Actionsで`WORKING_DIR: gcp`としたり、`gcp`以下のコード変更でGitHub Actionsが動くようにしておく)

```hcl
terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 4.0" # OIDC https://github.com/hashicorp/terraform-provider-google/releases/tag/v3.61.0 or later
    }
  }

  backend "gcs" {
    bucket = "<bucket_name>"
    prefix = "state" # any prefix is ok.
  }
}
```

```
terraform init
```

`.terraform.lock.hcl`も入れておく

> Terraform automatically creates or updates the dependency lock file each time you run the terraform init command. You should include this file in your version control repository 

https://www.terraform.io/language/files/dependency-lock#lock-file-location

## 4. GitHub Actions作成

上で設定したService Accountから必要な情報
- `workload_identity_provider`: `projects/<id>/locations/global/workloadIdentityPools/<pool_name>/providers/<provider_name>`
- `service_account`: `<service_account_name>@<project_name>.iam.gserviceaccount.com`

今回作るGitHub Actions:
1. PRでPlanしてPRにコメントで結果を書く
1. `main`ブランチにマージされたら,Apply



```yaml
name: gcp
on:
  pull_request:
    paths:
      - 'gcp/**'
      - '!gcp/**md'
      - '.github/workflows/gcp.yml'
    branches:
      - main
  push:
    paths:
      - 'gcp/**'
      - '!gcp/**md'
      - '.github/workflows/gcp.yml'
    branches:
      - main

env:
  TERRAFORM_VERSION: 1.1.8
  WORKING_DIR: gcp # Use `gcp` dir in this example.

jobs:
  terraform:
    # Add "id-token" with the intended permissions.
    permissions:
      contents: read
      id-token: write
      pull-requests: write
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ${{ env.WORKING_DIR }}
    steps:
      - uses: actions/checkout@v3

      - id: 'auth'
        name: 'Authenticate to Google Cloud'
        uses: google-github-actions/auth@v0.7.0
        with:
          create_credentials_file: 'true'
          workload_identity_provider: projects/<project_id>/locations/global/workloadIdentityPools/github-actions/providers/gha-provider
          service_account: <service_account_name>@<project_name>.iam.gserviceaccount.com

      - uses: hashicorp/setup-terraform@v1

      - name: Terraform fmt
        id: fmt
        run: terraform fmt -check
        continue-on-error: true

      - name: Terraform Init
        id: init
        run: terraform init

      - name: Terraform Validate
        id: validate
        run: terraform validate -no-color

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color -lock=false # TODO
        continue-on-error: true

      - uses: actions/github-script@v6
        if: github.event_name == 'pull_request'
        env:
          PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
            #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
            #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`
            <details><summary>Validation Output</summary>
            \`\`\`\n
            ${{ steps.validate.outputs.stdout }}
            \`\`\`
            </details>
            #### Terraform Plan 📖\`${{ steps.plan.outcome }}\`
            <details><summary>Show Plan</summary>
            \`\`\`\n
            ${process.env.PLAN}
            \`\`\`
            </details>
            *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`, Working Directory: \`${{ env.tf_actions_working_dir }}\`, Workflow: \`${{ github.workflow }}\`*`;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })
      - if: steps.plan.outcome == 'failure' || steps.fmt.outcome == 'failure'
        name: make it fail when plan failed
        run: exit 1

      - name: Terraform Apply
        id: apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve -input=false -lock=false # TODO
```

## 5. PR作成＆Merge

動作確認:
- PRでコメントにPlan結果が書かれる
- MergeでApplyが走る

# 注意点&課題

## 1. Terraform Google Provider Version

[v3.61.0](https://github.com/hashicorp/terraform-provider-google/releases/tag/v3.61.0) or later (The provider now supports Workload Identity Federation.)
OIDCを使うには、Provider のVersionをv3.61.0以降にする必要がある
## 2. lock問題

lockファイルができてしまうと、Plan/Applyできなくなる (暫定対応は `-lock=false`だが奨励されていない。最悪の場合gsutilでlockを消す)
```
Terraform acquires a state lock to protect the state from being written by multiple users at the same time. Please resolve the issue above and try again. For most commands, you can disable locking with the "-lock=false" flag, but this is not recommended.
```
## 3. [setup-terraform#usage](https://github.com/hashicorp/setup-terraform#usage)のexampleだと`plan`で失敗してもGitHub Actionsの結果はSuccess

以下のステップを追加
```yaml
  - if: steps.plan.outcome == 'failure'
    name: make it fail when plan failed
    run: exit 1
```
## 4. Enable API
APIをEnableしていないと以下のようなErrorが出る

```
Error: Error creating Address: googleapi: Error 403: Compute Engine API has not been used in project xxxxxx before or it is disabled. Enable it by visiting
```

# 参考

- https://github.com/hashicorp/setup-terraform
- https://engineer.retty.me/entry/2021/09/22/123000
- https://scrapbox.io/pokutuna/GCP_API_%E3%82%92_Terraform_%E3%81%8B%E3%82%89%E6%9C%89%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B
- https://qiita.com/mintak21/items/5232a089f3a39a71b155 <- あとから見つけたけど、Service Accountの作成も含めてTerraformで出来てるのでこちらのほうがいい!