GitHub Actions でTerraform planを実行する

## やりたいこと
githubactionsを使用して、特定のブランチにPRが作成された場合にterraform planを実行するワークフローを構築していきます！
`terraform plan` 実行時にはAWSに対しての認証が必要になりますが、今回はAWSのOpenID Connectを使用していきます。
これにより以下のようなメリットがあります。
- Actionsで使用するキー情報をSecretで管理せずに済む
- Actionsで使用するIAM Userを作成する必要がない

特にIAM Userの認証情報を使用してActionsを実施する従来のパターンで度々起こっていた、IAM Userのキー情報が流出する等のインシデントを防げる点が大きいかと思います。
また今回は構築時にハマったポイントや、改善点として実施してみて良かったことも記載していきますので参考になりますと幸いです！

## 全体の構成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492874/c65c6691-54d3-f70b-a5ad-69eb249ad5a4.png)


今回の構成ではAWSでGithubActions用のIDプロバイダーを構築していきます。
これによりterrraform plan,applyといったAWS側への認証、認可が必要なGithubActioinsを実行する事ができます。
具体的には全体構成図の`3,4`の部分で、Github側のIDプロバイダーとAWS側に作成したIDプロバイダーで認証を行い、
AWS側のIDプロバイダーに割り当てたIAM Roleの許可ポリシーの範囲でActionsを実行できます。

## GitHub Actions用のOIDCを構築する
AWS側で構築していきます。
githubactions用のIDプロバイダの設定値として以下は固定値です。
[公式の解説](https://docs.github.com/ja/actions/security-for-github-actions/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services)がわかりやすかったので参考にしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492874/9fcdf5d0-95fc-319b-f115-1c3e65c76151.png)

作成したIDプロバイダに割り当てるIAM　Roleの信頼ポリシーは環境によって変動します。
以下ではワイルドカードを使用していますが、ブランチ単位でも設定できます。

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::${IDプロバイダのARN}: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:＄｛githubの組織名｝/&{リポジトリ名}:*"
                }
            }
        }
    ]
}
```

上記の信頼ポリシーで`aud`,`sub`の部分について少し解説します。
- aud(Audience)
    - トークンがどのサービスを対象に発行されているかチェックする。全体構成でいうと`2`の部分でAWSのIDプロバイダーに対してのトークンが発行されるのでここは基本固定値になります。
- sub(Subject)
    - トークンがどのリソースに関連付けられているかを表します。今回はGithubの組織名、リポジトリ、ブランチを指定しています。

AWSのIAM Role側から見ると、githubのIDプロバイダーで発行されたトークンがどのリソース(sub)と関連付けられているか、何を対象（aud）にしているのかを確認しています。

## Workflows
以下の内容で実施しました。
Actionsの概要については[前回の記事](https://qiita.com/hoyat4967/items/e573e00a2b1c17058375)を参考にしていただけますと幸いです。

```yml
name: 'Terraform test'
on:
  pull_request:
    branches:
      - main
jobs:
  Plan:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
      pull-requests: write

    steps:
    - name: Checkout repository
      uses: actions/checkout@v3

    - name: Setup AWS
      uses: aws-actions/configure-aws-credentials@v1
      with:
        role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
        aws-region: ${{ secrets.AWS_REGION }}

    - name: Get Terraform version
      id: terraform-version
      run: |
          echo "value=$(cat .terraform-version)" >> $GITHUB_OUTPUT

    - name: setup terraform
      uses: hashicorp/setup-terraform@v3
      with:
          terraform_version: ${{ steps.terraform-version.outputs.value }}

    - name: Terraform Format
      id: fmt
      run: terraform fmt -check

    - name: Terraform Init
      run: terraform init

    - name: Terraform Plan
      working-directory: .
      run: terraform plan
      continue-on-error: true
```
上記の` - name: Setup AWS`で指定しているsecretの概要は以下です。
-  ${{ secrets.AWS_ROLE_ARN }}
    -  AWSで作成したIDプロバイダーに割り当てたRoleのARNをSecretに保存して指定
-  ${{ secrets.AWS_REGION }}
    -  Actionsを実行するregionをSecretに保存して指定


## 実施
ここまで準備したらあとはmainブランチに対してPRを作成すればactionnsが実行されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492874/2790d73d-92e4-5189-8e82-88e4e8f618fb.png)


## ハマったこと
実際に上記planを実行できるようになるまでにハマったポイントがあるのでご紹介します。

まず、筆者の環境ではIdentity Centerを使用したSSOログインを使用しており、`.aws/config`でprofileを指定しています。
そしてワークフローが実行される前に、terraformの挙動をローカルで確認したかったので、profileを以下のように指定してplan内容に問題がないかローカルで確認していました。

```provider.tf
provider "aws" {
  region  = var.region
  profile = var.profile
}
```

```variables.tf
variable "region" {
  default = "ap-northeast-1"
}
variable "profile" {}

```

```terraform.tfvars
profile = "your-profile"
```
tfvarsに記載したクレデンシャル情報はgithub上に載せたくなかったので.`gitignore`で弾いています。
そしてplan内容に問題がないことを確認しからpush⇢PR作成⇢Actions実行までいったのですが、以下のように`terraform plan`実行されず先に進みませんでした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3492874/3223e4b7-4d75-14f0-2f01-4a181746719b.png)

当初の予想では、Actions実行の際に使用する認証情報には、`Secret`に保存したIAM Roleが使用されるので、provider.tfでprofileを記載したままでも問題ないかと思っていたのですが、providerで指定したprofileを使用しようとする挙動があり、うまくいきませんでした。
```provider.tf
provider "aws" {
  region  = var.region
  profile = var.profile
}
```

## 解決策
そこで一旦`provider.tf`からprofile,regionの情報を以下のように削除したところ正常にActionsが実行されました。
```provider.tf
provider "aws" {
}
```

成功したActionsと、先に進まなかったActionsともにIDプロバイダー間の認証は成功していたので、おそらく`profile`を明示的に指定した事が原因と思われます。

## direnvを使ってみる
よし、これでOK！と思ったのですが、こうするとローカルでplanを実行したい時にまたprofile情報を書いて、pushするときは消して、、のようにかなり面倒だと感じました。
そこでなんとか`profile`情報をコード上に記載しなくてもローカルで読み取れないか考えました。
`profile`を環境変数で設定すれば解決できそうですが、そうすると他ディレクトリの`profile`でも同じ値が読み込まれるので問題が起こりそうです。

そこで今回は`direnv`を使用しました。
以下の記事がわかりやすかったので参考にしてください。
https://qiita.com/sasshi_i/items/609044aa106cdcb43a89

簡単に解説すると、terraformコマンドを実行するディレクトリの配下に`.envrc`ファイルを用意してそこに`profile`情報を記載して読み込ませるようにします。
```.envrc
export AWS_PROFILE="your-profile"
export AWS_DEFAULT_REGION="ap-northeast-1"
export AWS_SDK_LOAD_CONFIG=1
```
そうすることで`terrafoprm plan`を実行する際に、ローカルでは`.envrc`ファイルが読み込まれ、ActionsではSecretに保存したIAM Roleを読み込むようにできました。

## 終わりに
多分筆者がつまずいた部分は、多くの方は意識しなくてもなんとなくそうなんだろうなで回避できるんだろうなあと思いつつ、そこからどこに原因があるのか検証したり、改善点を見つけることの大切さを知りました。。
この記事が誰かの参考になれば幸いです！
また記事内で誤っている部分があればコメントにて指摘していただけますと幸いです。