iam_policy_attachment を移行するときの落とし穴

以前こういう記事を書いたんですが、
[terraform の aws_iam_policy_attachment は使わないほうが無難](https://qiita.com/billthelizard/items/8b54c40351e2ff39afa0)

`iam_policy_attachment` で定義してあるリソースを `iam_*_policy_attachment` に移行しようとするときに、場合によっては **かなり致命的な落とし穴** があったので共有します。

なお、そもそもなぜ移行しないといけないか？ については上記の記事を読んでください。

## TL;DR

* `iam_policy_attachment` で定義してあるリソースをソースコードから消して `iam_*_policy_attachment` を作り、 `terraform apply` する方法だと `apply` 後に Policy が剥がれた状態になることがある
    * terraform がリソースを delete するのと create する順番は不定
    * `iam_poilicy_attachment` の destroy が `iam_*_policy_attachment` の create よりも後に来る場合、 **最終結果で policy が剥がれた状態になる**
* 代わりに、以下のような手順を踏む必要がある
    1. 既存で定義してある `iam_policy_attachment` を `*.tfstate` の管理から外す(`terraform state rm`)
    2. `iam_*_policy_attachment` リソースを定義し、 `terraform apply` する

## 事故例

実際に私の所属しているチームで起こった事故を紹介します。

### 前提

ManagedPolicy である `AmazonS3ReadOnlyAccess` が各 IAM User にアタッチされているとします:

```:iam_policy.tf
data "aws_iam_policy" "s3_readonly_access" {
  arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}

variable users {
  default = [
    "Taro",
    "Jiro",
    "Saburo",
  ]
}

resource "aws_iam_policy_attachment" "s3_readonly_access" {
  name       = "s3_readonly_access"
  users      = ["${var.users}"]
  policy_arn = "${data.aws_iam_policy.s3_readonly_access.arn}"
}
```

User に attach された Policy の状態を確認します:

```console
<~>-1-> aws iam list-attached-user-policies --user-name Taro --profile peakystack
{
    "AttachedPolicies": [
  1 data "aws_iam_policy" "s3_readonly_access" {
        {
            "PolicyName": "AmazonS3ReadOnlyAccess",
            "PolicyArn": "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
        }
    ]
}
<~>-> aws iam list-attached-user-policies --user-name Jiro --profile peakystack
{
    "AttachedPolicies": [
        {
            "PolicyName": "AmazonS3ReadOnlyAccess",
            "PolicyArn": "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
data "aws_iam_policy" "s3_readonly_access" {
        }
    ]
}
<~>-> aws iam list-attached-user-policies --user-name Saburo --profile peakystack
{
    "AttachedPolicies": [
        {
            "PolicyName": "AmazonS3ReadOnlyAccess",
            "PolicyArn": "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
        }
    ]
}
```

### `aws_iam_policy_attachment` を `aws_iam_user_policy_attachment` に移行する

まず、上記のコードを以下のようなコードに書き換えます。

```:iam_policy.tf
data "aws_iam_policy" "s3_readonly_access" {
  arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}

variable users {
  default = [
    "Taro",
    "Jiro",
    "Saburo",
  ]
}

resource "aws_iam_user_policy_attachment" "s3_readonly_access" {
  count      = "${length(var.users)}"
  user       = "${element(var.users, count.index)}"
  policy_arn = "${data.aws_iam_policy.s3_readonly_access.arn}"
}
```

### terraform plan

上記のコードで

`terraform plan` すると、以下のような差分が表示されます:

```console
Terraform will perform the following actions:

  - aws_iam_policy_attachment.s3_readonly_access

  + aws_iam_user_policy_attachment.s3_readonly_access[0]
      id:         <computed>
      policy_arn: "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
      user:       "Taro"

  + aws_iam_user_policy_attachment.s3_readonly_access[1]
      id:         <computed>
      policy_arn: "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
      user:       "Jiro"

  + aws_iam_user_policy_attachment.s3_readonly_access[2]
      id:         <computed>
      policy_arn: "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
      user:       "Saburo"


Plan: 3 to add, 0 to change, 1 to destroy.
```

delte と create のパターンですね。
plan の段階では、**最終結果はそれぞれのユーザに `AmazonS3ReadOnlyAccess` ポリシーが付与されるように見えます**。

### terraform apply

`terraform apply` を実施します

```
aws_iam_policy_attachment.s3_readonly_access: Destroying... (ID: s3_readonly_access)
aws_iam_user_policy_attachment.s3_readonly_access[1]: Creating...
  policy_arn: "" => "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
  user:       "" => "Jiro"
aws_iam_user_policy_attachment.s3_readonly_access[0]: Creating...
  policy_arn: "" => "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
  user:       "" => "Taro"
aws_iam_user_policy_attachment.s3_readonly_access[2]: Creating...
  policy_arn: "" => "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
  user:       "" => "Saburo"
aws_iam_user_policy_attachment.s3_readonly_access[1]: Creation complete after 3s
aws_iam_user_policy_attachment.s3_readonly_access[2]: Creation complete after 3s
aws_iam_user_policy_attachment.s3_readonly_access[0]: Creation complete after 3s (ID: Taro-20190404153229914700000002)
aws_iam_policy_attachment.s3_readonly_access: Destruction complete after 3s

Apply complete! Resources: 3 added, 0 changed, 1 destroyed.
```

**destroy のほうが create よりも後に実行されている!!**

IAM User にアタッチされたポリシーを見ると、もともとあった `AWSS3ReadonlyPolicy` が剥がれています。

```console
<~/tmp>-> aws iam list-attached-user-policies --user-name Taro --profile peakystack
{
    "AttachedPolicies": []
}
<~/tmp>-> aws iam list-attached-user-policies --user-name Jiro --profile peakystack
{
    "AttachedPolicies": []
}
<~/tmp>-> aws iam list-attached-user-policies --user-name Saburo --profile peakystack
{
    "AttachedPolicies": []
}
```

このように、 `aws_iam_policy_attachment` の悪しき振る舞いによって、 **applyしたときの最終状態がplanしたときの結果と同じにならないんですよね...！！** （これは仕様じゃなくてバグなのかも）

## 正しい移行方法

上記のような問題を回避するために、以下のような手順で移行します。

1. `aws_iam_policy_attachment` を teraform 管理下から外す

悪しき `aws_iam_policy_attachment` には退場してもらいます。

```console
<~/tmp>-> terraform state rm aws_iam_policy_attachment.s3_readonly_access
1 items removed.
Item removal successful.
```

2. ソースコードを修正する

```
```:iam_policy.tf
data "aws_iam_policy" "s3_readonly_access" {
  arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}

variable users {
  default = [
    "Taro",
    "Jiro",
    "Saburo",
  ]
}

resource "aws_iam_user_policy_attachment" "s3_readonly_access" {
  count      = "${length(var.users)}"
  user       = "${element(var.users, count.index)}"
  policy_arn = "${data.aws_iam_policy.s3_readonly_access.arn}"
}
```

3. terraform apply する

この場合は、 delete & create じゃなくて純粋に create だけになります。

```console
<~/tmp>-> terraform apply
data.aws_iam_policy.s3_readonly_access: Refreshing state...

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + aws_iam_user_policy_attachment.s3_readonly_access[0]
      id:         <computed>
      policy_arn: "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
      user:       "Taro"

  + aws_iam_user_policy_attachment.s3_readonly_access[1]
      id:         <computed>
      policy_arn: "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
      user:       "Jiro"

  + aws_iam_user_policy_attachment.s3_readonly_access[2]
      id:         <computed>
      policy_arn: "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
      user:       "Saburo"


Plan: 3 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_iam_user_policy_attachment.s3_readonly_access[1]: Creating...
  policy_arn: "" => "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
  user:       "" => "Jiro"
aws_iam_user_policy_attachment.s3_readonly_access[2]: Creating...
  policy_arn: "" => "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
  user:       "" => "Saburo"
aws_iam_user_policy_attachment.s3_readonly_access[0]: Creating...
  policy_arn: "" => "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
  user:       "" => "Taro"
aws_iam_user_policy_attachment.s3_readonly_access[0]: Creation complete after 3s (ID: Taro-20190404154236127100000003)
aws_iam_user_policy_attachment.s3_readonly_access[2]: Creation complete after 3s (ID: Saburo-20190404154236122800000001)
aws_iam_user_policy_attachment.s3_readonly_access[1]: Creation complete after 3s (ID: Jiro-20190404154236127100000002)

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
```

ポリシーの紐づけが剥がれていないことがわかります。

```
<~/tmp>-> aws iam list-attached-user-policies --user-name Taro --profile peakystack
{
    "AttachedPolicies": [
        {
            "PolicyName": "AmazonS3ReadOnlyAccess",
            "PolicyArn": "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
        }
    ]
}
<~/tmp>-> aws iam list-attached-user-policies --user-name Jiro --profile peakystack
{
    "AttachedPolicies": [
        {
            "PolicyName": "AmazonS3ReadOnlyAccess",
            "PolicyArn": "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
        }
    ]
}
<~/tmp>-> aws iam list-attached-user-policies --user-name Saburo --profile peakystack
{
    "AttachedPolicies": [
        {
            "PolicyName": "AmazonS3ReadOnlyAccess",
            "PolicyArn": "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
        }
    ]
}
```

## まとめ

私の所属するチームでは、 **`AdministratorAccess` がすべての管理者ユーザから剥がされる** という大惨事になりました。 root アカウントを使って権限を復活するという対応をするハメに...

terraform でリソースを delete & create する場合は delete と create の順番に注意しましょう。