特定の起動テンプレートのみ起動停止できるIAMポリシーの設定方法

特定の起動テンプレートのみ起動停止できるIAMポリシーの設定をする必要があったので、備忘録としてまとめています。

## 詰まりどころ

起動テンプレートを起動する権限というのは無く、`ec2:RunInstances` アクションのリソース対象が 起動テンプレートという構成になる。
しかし下記のように設定すると権限不足で実行が失敗してしまう。

```json
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "ec2:RunInstances"
            ],
            "Resource": "arn:aws:ec2:*:{account_id}:launch-template/*"
        },
```

これの理由としては、`ec2:RunInstances`は内部にて複数のAPIが実行されているようで、リソース先には `launch-template`や `instance`の複数が指定されているため、
上記のポリシーでは`launch-template`の指定まではできるが、`instance`の立ち上げの際にリソースとして指定がされていないので、権限不足で失敗してしまう。

## 解決案

`ec2:RunInstances`のリソースは`*`で制限をかけずに、`Deny`のポリシーで特定の起動テンプレートの起動以外は拒否するようにする。
また起動テンプレート以外で単独のインスタンスを起動をしようとした`ec2:RunInstances`についても拒否するようにする。
これらを組み合わせれば、`ec2:RunInstances`で`*`を設定したとしても、特定の起動テンプレートを指定した場合だけ起動が可能になります。

すべてのインスタンス起動を許可する
```json
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances",
                "ec2:CreateTags",
                "ec2:RunInstances"
            ],
            "Resource": "*"
        },
```

`lt-xxxxxx`以外の起動テンプレートを指定した`ec2:RunInstances`を拒否する
```json
        {
            "Sid": "VisualEditor5",
            "Effect": "Deny",
            "Action": "ec2:RunInstances",
            "Resource": "arn:aws:ec2:*:{account_id}:launch-template/*",
            "Condition": {
                "ForAnyValue:ArnNotEquals": {
                    "ec2:LaunchTemplate": "arn:aws:ec2:ap-northeast-1:{account_id}:launch-template/lt-xxxxxx"
                }
            }
        }
```

単独でインスタンスを起動をしようとした時に拒否されるようにする

```json
        {
            "Sid": "VisualEditor4",
            "Effect": "Deny",
            "Action": "ec2:RunInstances",
            "Resource": "arn:aws:ec2:*:*:instance/*",
            "Condition": {
                "ForAnyValue:Bool": {
                    "ec2:IsLaunchTemplateResource": "false"
                }
            }
        },
```

また停止時は起動テンプレートを介して停止することはできないため、タグベースで制御を行います。

```json
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "ec2:TerminateInstances",
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "aws:ResourceTag/Name": "{instance_name_tag}"
                }
            }
        },
```

## 検証

- 期待している起動テンプレートで正常に起動できる
- 期待していない起動テンプレートで起動できない
- 起動テンプレートを指定しないと起動できない

の3パターンを検証して問題なさそうでした。

```ruby
# 期待している起動テンプレートで正常
client.run_instances(
  launch_template: { launch_template_id: 'lt-xxxxxx' },
  min_count: 1,
  max_count: 1
)

# 期待していない起動テンプレートで起動できない
client.run_instances(
  launch_template: { launch_template_id: 'lt-yyyyyy' },
  min_count: 1,
  max_count: 1
)

# 起動テンプレートを指定しないと起動できない
client.run_instances(
   image_id: "ami-xxxxxx", 
  instance_type: "t2.micro",
  min_count: 1,
  max_count: 1
)

```

## 結論

出来上がったポリシーを組み合わせると下記のようなjsonになります。

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "VisualEditor0",
      "Effect": "Allow",
      "Action": "ec2:TerminateInstances",
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "aws:ResourceTag/Name": "{instance_name_tag}"
        }
      }
    },
    {
      "Sid": "VisualEditor1",
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeInstances",
        "ec2:CreateTags",
        "ec2:RunInstances"
      ],
      "Resource": "*"
    },
    {
      "Sid": "VisualEditor5",
      "Effect": "Deny",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:*:{account_id}:launch-template/*",
      "Condition": {
        "ForAnyValue:ArnNotEquals": {
          "ec2:LaunchTemplate": "arn:aws:ec2:ap-northeast-1:{account_id}:launch-template/lt-xxxxxx"
        }
      }
    },
    {
      "Sid": "VisualEditor4",
      "Effect": "Deny",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "ForAnyValue:Bool": {
          "ec2:IsLaunchTemplateResource": "false"
        }
      }
    }
  ]
}
```