[AWS]GithubActionsで terraform のCI/CDやってみた

# はじめに
記事を書こう思いつつも、なかなかかけていなかったので、初のアドベントカレンダー参加＆久々に投稿したいと思いいます

ということで、
前回かその前ぐらいに、 terraform を使った AWS 環境の構築をしました。

https://qiita.com/Ixy_194/items/ac9623434537e81b8618

今回は、 GithubActions を使って  terraform の CI/CD をやってみようと思います。
# 今回のゴール
こんな感じの CI/CD フローを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974822/87b73b2a-f1b9-cf36-0d7c-9ab862ced75f.png)


流れとしてはこんな感じ。
1. main ブランチから feature ブランチを作成
1. ローカルに clone
1. コードの修正
1. feature ブランチへ push
    1. CI Workflow 実行
1. PR 作成
    1. CI Workflow 実行
1. main へマージ
    1. CD Workflow 実行
        1. AWS 環境へ terraform apply


# やること
## 前提
AWS 環境を構築する terraform のコードは以下を使用します。

https://github.com/Ixy-194/go-api-sample-todo-terraform

今回の作業は、上記のリポジトリをコピーして別のリポジトリを作成し行います。

https://github.com/Ixy-194/go-api-sample-todo-terraform-cicd

## 事前準備
### OIDC と IAM Role の作成と設定
Github Actions から terraform で AWS のリソース操作をするための認証設定と IAM Role を作成します。

### 作業の流れ
1. AWS での OIDC の設定
1. AWS での IAM ロールの作成
1. Github リポジトリへの設定

#### 1. AWS での OIDC の設定
AWS のコンソール画面から OIDC プロバイダの作成をします。

IAM コンソールから `ID プロバイダ` -> `プロバイダを作成`を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974822/6a136727-e085-14e0-4360-85604e84e423.png)

以下内容を入力で`プロバイダを追加`します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974822/8c0d0bd5-fb06-a7e5-1a82-7b8b6ad11fad.png)
|                     |                                             | 
| ------------------- | ------------------------------------------- | 
| プロバイダのタイプ	 | OpenID Connect<br>                          | 
| プロバイダの URL	   | https://token.actions.githubusercontent.com | 
| 対象者	             | sts.amazonaws.com<br>                       | 

こんな感じに作成されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974822/bac8d2f1-143b-ca11-1ee3-f4e482e0a47e.png)

#### 2. IAM ロールの作成
続いて Github Actions に渡す IAM ロールを作成します。

IAM コンソールから `ロール` -> `ロールの作成`を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974822/adbe107e-fb62-0f03-e649-7cd5b64bb89c.png)

以下の画面が表示されるので、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974822/bfe725b9-a174-4346-4431-f81593c2a085.png)

ここでは、`信頼されたエンティティタイプ`では`カスタム信頼ポリシー`を選択し、
`カスタム信頼ポリシー`には以下設定を行います。
※あくまで一例なので、自身の環境にあわせ適切な権限を設定してください
 参考:[Amazon Web Services での OpenID Connect の設定](https://docs.github.com/en/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services)
```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::<AWSアカウントID>:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                }
                "StringLike": {
                 "token.actions.githubusercontent.com:sub": "repo:Ixy-194/go-api-sample-todo-terraform-cicd:*"
                }

            }
        }
    ]
}
```
:::note alert
Condition は必ず設定しましょう
`Condition`の `StringLike`で リポジトリの制限をかけています。
`"token.actions.githubusercontent.com:sub": "repo:Ixy-194/go-api-sample-todo-terraform-cicd:*"`
これをしないと、 別のリポジトリからも IAM ロールが使えるようになり、悪用されるリスクになります。必ず設定しましょう。
:::
ここでは、リポジトリの制限しかかけていないですが、他にも`特定のブランチ`、`特定の操作のみ(push, pr)`といった制限をかけることもできます。[参考](https://zenn.dev/miyajan/articles/github-actions-support-openid-connect#github-actions-%E3%81%AE-oidc-%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3%E3%81%AE-sub-%E3%81%AB%E3%81%AF%E3%81%AA%E3%81%AB%E3%81%8C%E5%85%A5%E3%82%8B%E3%81%AE%E3%81%8B%EF%BC%9F)

入力したら`次へ`を選択します。
続いて、 IAM ロールにポリシーをアタッチします。
ここでは、`AdministratorAccess`を設定しますが、適宜環境に合わせ必要な権限のみ設定するようにしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974822/45dc622c-4b7c-906e-1fa1-ac2599158764.png)

最後にロール名を設定します。
ここでは`githubactions-oidc`としました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974822/dc24936b-c683-b247-7e4c-19d66d9d9d95.png)

ロールが作成されました。
このあとに、 ARN の値を Github リポジトリに登録するので値をメモっておきましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974822/2c965fca-8872-d636-dfe7-c22efe47605f.png)

#### 3.Github リポジトリへの設定
作成した IAM ロールの ARN を　GithubActions のワークフローから参照できるようにリポジトリに環境変数として登録をします。
リポジトリ画面を開いて、`Settings` タブから `Secres and variables` -> `Actions` を選択し、`New repository secret` から登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974822/ef0203d5-589a-5ba2-8f54-9a92d4384ae0.png)

これで、事前準備は終了です。
これもコード化したいなー(´・ω・)

# CI/CD ワークフローの作成
では、ここから実際に GithubActions での CI/CD ワークフローを作成します。
作成するワークフローファイル（yaml）は`.github/workflows`配下に作成します。
CI/CD の作成にあたり、以下のファイルを追加しています
```shell
├── .github
│   └── workflows
│       ├── dev-apply.yml  # CD Workflow
│       └── validation.yml # CI Workflow
├── .tflint.hcl            # CI tflint setting file
```

## CI ワークフローの作成
まずは CI の実装です。
CI のワークフローでは、リポジトリへのソースコードの PUSH、 PR をトリガーに以下を行います。
- terraform コマンドによる静的チェック
    - terraform fmt
    - terraform validate
- review dog を使った tflint による静的チェック
- terraform plan による実行計画の書き出し（PRにコメントとして記載）

実際に作成したワークフローはこんな感じ。
```yaml:validation.yml
name: Terraform Validation
permissions:
  id-token: write
  contents: read
  pull-requests: write

on:
  pull_request:
  push:

jobs:
  terraform:
    name: Terraform Validation
    runs-on: ubuntu-latest

    defaults:
      run:
        shell: bash

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: ap-northeast-1
          role-to-assume: ${{ secrets.GITHUBACTIONS_OIDC_ARN }}

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.9.8

      - name: Check Terraform Format
        run: terraform fmt -recursive -check

      - name: Initialize Terraform (Dev)
        working-directory: ./env/dev
        run: terraform init

      - name: Validate Terraform (Dev)
        working-directory: ./env/dev
        run: terraform validate

      - name: Run TFLint
        uses: reviewdog/action-tflint@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          reporter: github-pr-review
          tflint_init: "true"
          tflint_config: "${{ github.workspace }}/.tflint.hcl"
          level: "error"
          fail_on_error: "true"
          filter_mode: "added"
          flags: "--recursive"
        
      - name: Create Terraform Plan (Dev)
        working-directory: ./env/dev
        id: dev_plan
        run: terraform plan -no-color

      - name: Update Pull Request with Terraform Plan
        uses: actions/github-script@v7
        if: github.event_name == 'pull_request'
        env:
          Dev_PLAN: "terraform\n${{ steps.dev_plan.outputs.stdout }}"
        with:
          result-encoding: string
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const output = `#### Check! Terraform Show Plans 🖌\`
            <details><summary>Dev Show Plan</summary>

            \`\`\`\n
            ${process.env.Dev_PLAN}
            \`\`\`

            </details>

            *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;

            github.rest.issues.createComment({
                issue_number: context.issue.number,
                owner: context.repo.owner,
                repo: context.repo.repo,
                body: output
            });
```
### 主な設定ポイント
#### ワークフロー内での権限設定
OIDC、Workflow、reviewdog に対して必要な権限の付与をしています。
```yaml
permissions:
  id-token: write       # OIDC を利用するのに必要な権限
  contents: read        # actions/checkout@v4 を利用するのに必要な権限
  pull-requests: write  # reciewdog によって PR にコメントするのに必要な権限
```
詳細は[公式ドキュメント](https://docs.github.com/ja/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services)を確認してください。

#### ワークフローの実行トリガー
以下の記述により、PR 作成時、リポジトリへの PUSH 時にワークフローが起動するようにしています。
```yaml
on:
  pull_request:
  push:
```

#### OIDC による認証
ここで、事前準備で作成した IAM Role を使って認証を行います。
```yaml
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: ap-northeast-1
          role-to-assume: ${{ secrets.GITHUBACTIONS_OIDC_ARN }}
```

#### reviewdog（tflint）による静的チェック
reviewdog による静的チェックの定義です。
```yaml
      - name: Run TFLint
        uses: reviewdog/action-tflint@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          reporter: github-pr-review
          tflint_init: "true"
          tflint_config: "${{ github.workspace }}/.tflint.hcl"
          level: "error"
          fail_on_error: "true"
          filter_mode: "added"
          flags: "--recursive"
```
`with`句内で、 reviewdog の動作設定や設定ファイルの指定を行なっています。
設定ファイルの内容はこんな感じです。
```hcl:.tflint.hcl
plugin "aws" {
    enabled = true
    deep_check = true
    version = "0.34.0"
    source  = "github.com/terraform-linters/tflint-ruleset-aws"
}
```
ここでは、AWS のプラグインを使用して`AWS に特化した`内容でチェックをするようにしています。
また、`deep_check = true`することにより、単なる静的チェックだけではなく、
実際のリソース確認、例えば 指定した EC2 インスタンスの存在チェックまで行ってくれるようになります。
※こちらは後述します

reviewdog の詳細は[公式ドキュメント](https://github.com/reviewdog/action-tflint)を確認してください。

#### PR への terraform plan 結果の書き込み
`reate Terraform Plan (Dev)`の実行結果（標準出力）を`actions/github-script@v7`を使って PR に書き出すようにしています。
terraform の レビューをする時、実行計画も見ないとなんともいえないですからね。
```yaml
      - name: Create Terraform Plan (Dev)
        working-directory: ./env/dev
        id: dev_plan
        run: terraform plan -no-color

      - name: Update Pull Request with Terraform Plan
        uses: actions/github-script@v7
        if: github.event_name == 'pull_request'
        env:
          Dev_PLAN: "terraform\n${{ steps.dev_plan.outputs.stdout }}"
        with:
          result-encoding: string
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const output = `#### Check! Terraform Show Plans 🖌\`
            <details><summary>Dev Show Plan</summary>

            \`\`\`\n
            ${process.env.Dev_PLAN}
            \`\`\`

            </details>

            *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;

            github.rest.issues.createComment({
                issue_number: context.issue.number,
                owner: context.repo.owner,
                repo: context.repo.repo,
                body: output
            });
```

### 実際に動かしてみる
#### 正常系
これが実際の PR です。
https://github.com/Ixy-194/go-api-sample-todo-terraform-cicd/pull/7

PR を作成するとこんな感じに CI の結果が表示されるのと、
terraform plan の結果がコメントされます。
https://github.com/Ixy-194/go-api-sample-todo-terraform-cicd/pull/7#issuecomment-2424148355

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974822/30d8973a-48b6-d2c3-99ff-8d79bec3940a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974822/d08a0fc4-361d-fbaf-efc6-091222ded96e.png)

レビューをする際には、実際に変更されたファイルに加え、実行プランの確認ができていいですね。


#### 異常系
続いて、異常系のパターンです。
わざとエラーを出すために、 tf ファイルの一部を書き換えました。
EC2 インスタンス作成時の `instance_type` を実際には存在しないインスタンスタイプを指定するようにしました。
さて、どうなるでしょうか？
```yaml
resource "aws_instance" "this" {
  ami                  = data.aws_ssm_parameter.this.value
  instance_type        = "t2.nanoex" # 存在しない インスタンスタイプ
  subnet_id            = var.subnet_id
  iam_instance_profile = aws_iam_instance_profile.this.name

  vpc_security_group_ids = [aws_security_group.this.id]

  tags = {
    Name        = "${var.env}-bastion"
    Terraform   = "true"
    Environment = var.env
  }
}


```
結果、このようにエラーとしてしっかり検知してくれています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974822/0f8e5360-fe69-433a-6457-55496f1792d5.png)

ちゃんと PR にコメントも書き込まれていますね、いいですね。

ただ、これ、 terraform の plan では検知してくれないんです。
reviedog で検知した内容になっています。
 Githubactions のログから terraform plan の結果を確認するとこんな感じになっていて、エラーとして検知されず通っているんですよね。
```
  + resource "aws_iam_role" "this" {
~~~~~~ 一部省略
      + instance_type                        = "t2.nanoex"
  # module.bastion.aws_security_group.this will be created
~~~~~~ 一部省略
  
Plan: 5 to add, 0 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.
 ```
なので、 terraform のチェックだけでは心許ないなということで、今回 reviewdog（tflint）を入れてみました。
reviewdog（tflint）の設定ファイル「.tflint.hcl」で、`deep_check = true`にすることによって、実際に API call して ami やインスタンスタイプの有効性をチェックしてくれんです。素晴らしいですね。

これで、CI は OK だったのに、実際に apply したら爆死する、という不幸を減らせそうです。


 ## CD ワークフローの作成
 続いて、CD のワークフローです。
 CD のワークフローでは、main ブランチへのマージをトリガーに CI が実行
 実行された CI が成功した時のみ、 CD が走り、`terraform apply`が行われるようにしています。
 実際のコードはこんな感じ。
 ```yaml:dev-apply.yml
 name: Terraform Apply

permissions:
  id-token: write
  contents: read

on:
  workflow_run:
    workflows:
      - Terraform Validation
    branches:
      - main
    types:
      - completed

jobs:
  terraform:
    name: Terraform Apply
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'success' }}

    defaults:
      run:
        shell: bash

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: ap-northeast-1
          role-to-assume: ${{ secrets.GITHUBACTIONS_OIDC_ARN }}

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.9.8

      - name: Initialize Terraform
        working-directory: ./env/dev
        run: terraform init

      - name: Apply Terraform
        working-directory: ./env/dev
        run: terraform apply -auto-approve
 ```
### 主な設定ポイント
#### ワークフローの実行トリガー
以下の記述により、main ブランチで CI が実行され、それが完了・正常終了した時のみ　CD が実行されるようになっています。
```yaml
on:
  workflow_run:
    workflows:
      - Terraform Validation
    branches:
      - main
    types:
      - completed
jobs:
  terraform:
    name: Terraform Apply
    runs-on: ubuntu-latest
    # 前のワークフローが成功した時のみ実行
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
```

#### terraform apply -auto-approve
オプションによって、作成予定のリソース表示や実行確認 yes をスキップするように、Githubactions のワークフローで apply が自動で行えるようにしています。

```yamlaml
      - name: Apply Terraform
        working-directory: ./env/dev
        run: terraform apply -auto-approve
```
### 実際に動かしてみる
PR を作成してマージをするとこんな感じにジョブが実行されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974822/f8337f6a-292f-d241-dbce-3125500360f0.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974822/a2f9bf33-ec6a-333e-2f2c-803e04db1183.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1974822/f6ad40ac-3986-35e8-4afd-dc92ceec7031.png)

ちゃんと `terraform apply`されて、リソースも作られています。良かった。

# 最後に
こんな感じで、 terraform での AWS インフラの CI/CD を実装してみましたがいかがでしたでしょうか。
IaC でインフラもコード化することによって、再現性、再利用性の高いものが作れるようになり、
設定内容や利用しているリソースが簡単に可視化できるようになりました。
また、CI　によって事前検証もできるので、品質向上・事故防止にも役立つのではないでしょうか。

reviewdog（tflint）、初めて使ったのですが思っていたよりすごいですね。
もっと深掘りできれば良かったのですが、今回は CI/CD がメインだったのでまたいずれ。

今回のソースコードはこちらに公開しています。
ぜひご参考までに。

https://github.com/Ixy-194/go-api-sample-todo-terraform-cicd

PR 正常系

https://github.com/Ixy-194/go-api-sample-todo-terraform-cicd/pull/7

PR 異常系

https://github.com/Ixy-194/go-api-sample-todo-terraform-cicd/pull/8


最後までありがとうございました。