AWSでterraformするときにinstance-profileからassume-roleする

AWSで `terraform apply` するにはAWSの操作権限の全てを与えておくのが現実的である。さもないと、未利用だったサービス・新しいサービスを使おうとする度にpolicy修正が必要になってしまう。

terraform 作業専用の instanceを用意する場合でも、強すぎる権限を常時全開にするわけにはいかないだろう。instance-profileとしては ReadOnlyAccess などもっと軽い権限だけにして、 `terraform` コマンドのときだけ全権を与えたい。そうおもってやってみたらはまりポイントがあったのでここに記録する。

※terraformオペレータのIAMユーザをMFA必須にして、そのユーザからの assume-roleをするのが、たぶんベストバランスだろう。この記事はその一歩手前のところである。assume-roleするための方法としては共通のはず。

## role/ReadOnly-Role

- Usecase: EC2
    - instance-profile として使える role
    - これを terraform作業用instance のIAM roleとして割り当てる
- ReadOnlyAccess AWS 管理ポリシー
- ProfileOnlyPolicy
    - instance-profile:ReadOnly-Role の instanceでの利用でなければ何もさせない

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Resource": "*",
            "Action": "*",
            "Effect": "Deny",
            "Condition": {
                "ArnEquals": {
                    "ec2:InstanceProfile": "arn:aws:iam::<account-id>:role/ReadOnly-Role"
                }
            }
        }
    ]
}
```

## role/Terraform-Role

- AdministratorAccess AWS 管理ポリシー
    - Terraform用に原則全部の操作を可能とする
- ProfileOnlyPolicy
    - instance-profile:ReadOnly-Role の instanceでの利用でなければ何もさせない
- PolicyDocument
    - role/ReadOnly-Role からの switch だけを許す

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<account-id>:role/ReadOnly-Role"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

## provider.tf

```
provider "aws" {
  region  = "ap-northeast-1"
  version = ">1.56.0"
  profile = "terraform-role"
}

terraform {
  backend "s3" {
    bucket = "foo-terraform"
    key    = "bar/terraform.tfstate"
    region = "ap-northeast-1"
    skip_metadata_api_check = true
    profile = "terraform-role"
  }
}
```

## ~/.aws/credentials

```
[terraform-role]
role_arn = arn:aws:iam::<account-id>:role/Terraform-Role
credential_source = Ec2InstanceMetadata
```

## policy/role/credentials の動作検証

terraform抜きで確認できる範囲をまずは検証する。

```sh
$ aws sts get-caller-identity
{
    "Account": <account-id>,
    "UserId": "xxxx:<instance-id>",
    "Arn": "arn:aws:sts::<account-id>:assumed-role/ReadOnly-Role/<instance-id>"
}
$ aws sts get-caller-identity --profile terraform-role
{
    "Account": <account-id>,
    "UserId": "xxxx:botocore-session-1598506181",
    "Arn": "arn:aws:sts::<account-id>:assumed-role/Terraform-Role/botocore-session-1598506181"
}
```

## terraform apply

既存のリソースに新規Tagを追加するとか、注記用のTagの文字列を少し変えるとかで、`terraform apply`

## はまりポイント

terraform state を S3保存するための `backend "s3"` ブロックが難物だった。

terraformオペレータが複数になることや、terraform作業instanceが障害や誤操作で失われることを想定すると、S3保存は外せない。

しかし `provider` ブロックで `profile` 指定しても `export AWS_PROFILE=` の環境変数指定しても、エラーになる。

```
Failed to save state: failed to upload state: AccessDenied: Access Denied
```

検証用に追加したTagやTag文字列はリソースに反映されているので、assume-roleはできているはずなのに。

コードブロックにあるように、`backend` ブロックにも `profile` 指定が要るのだった。 `backend` 保存処理は `provider` ブロックとは独立の credential設定になっているようだ。おまけにこちらには、`skip_metadata_api_check = true` オプションが必要だった。

参照した issue
[terraform-providers/terraform-provider-aws#5018](https://github.com/terraform-providers/terraform-provider-aws/issues/5018#issuecomment-465332353)

> The s3 backend does not use the provider block.

次にはまったときはこれも試そう
> I run terraform init with TF_LOG=DEBUG