【AWS CDK】エスケープハッチ IAM Policyの上書き

## はじめに
AWS CDKを利用する場合、基本的にL2 or L3 Constructsを利用すると思います。
これらのConstructsは非常に便利でCDKを利用する大きな理由になると思います。
しかし、、たまに実装したい設定とConstructsで実装される環境が微妙に違うし、調整できるようなMethodも用意されていないというケースがあると思います。
その際は、一度部分的にL1に落とし込んで、プロパティを上書する手法がありそれをエスケープハッチと呼びます。

ただこのエスケープハッチの対応するのはちょっとめんどくさかったりします。
なので、備忘録&個人的なチートシートの意味を込めての作ってみた系のエスケープハッチを作っていく経緯を追いながらご紹介していければと思います。

※ 本ブログに記載した内容は個人の見解であり、所属する会社、組織とは全く関係ありません。

## 今回のケース
IAM Grant Methodを使うとシナリオに沿ったPolicyを自動生成し、対象のRoleにアタッチしてくれます。
この自動生成されたPolicyを上書したいというケースになります。
普通であれば、特別なシナリオが定義されていない「Grant Method」を使うケースの方が多いと思います。
なので、エスケープハッチは遊びです。

今回は、エスケープハッチと素直に「Grant Method」を使うケース両方をご紹介させて頂きます。

## ユースケース
### ユースケースイメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635217/67303223-da90-833c-d221-33eb8f77e98f.png)



上記イメージのようにBatchFargateはSQSからReceive(Consumer)としたい場合の構成を例にとります。
そのうえで、BatchFargateに許可したいMainSQSへのアクションは以下とします。
* sqs:DeleteMessage
* sqs:GetQueueAttributes
* sqs:GetQueueUrl
* sqs:ReceiveMessage

「grantConsumeMessages Method」を利用すると許可されるActionは以下になります。
* sqs:ChangeMessageVisibility
* sqs:DeleteMessage
* sqs:GetQueueAttributes
* sqs:GetQueueUrl
* sqs:ReceiveMessage

実装したい構成との差分としては、「sqs:ChangeMessageVisibility」のAction許可がいらないということになります。
なので、このActionを「エスケープハッチを使う方法」と「Grant Methodを使う方法」それぞれで取り除きます。

## エスケープハッチを使う方法
### エスケープハッチ実装イメージ
エスケープハッチを利用するためには、Constructs treeの構造を把握する必要があります。
そのため、純粋に「grantConsumeMessages Method」を利用し、仮組します。

### 仮組
~~~typescript:usecase-role.ts
    const mainQueue = new sqs.Queue(this, 'SQS', { //SQS
      queueName: "MainSQS",
    });
    
    const batchFargateRole = new iam.Role(this, `batchFargateRole`, { //batchFargate Role
      assumedBy: new iam.ServicePrincipal(`ecs-tasks.amazonaws.com`),
      roleName: "BatchFargateRole"
    })
    
    mainQueue.grantConsumeMessages(batchFargateRole); // IAM Grant Method
~~~

この仮組状態で一度「cdk synthesize」を実行し、「tree.json」を生成します。
この「tree.json」がエスケープハッチを組む時の重要な情報源となります。

~~~json:tree.json
              "batchFargateRole": {
                "id": "batchFargateRole",
                "path": "RoleStack/test/batchFargateRole",
                "children": {
                  "ImportbatchFargateRole": {
                    "id": "ImportbatchFargateRole",
                    "path": "RoleStack/test/batchFargateRole/ImportbatchFargateRole",
                    "constructInfo": {
                      "fqn": "aws-cdk-lib.Resource",
                      "version": "2.99.0"
                    }
                  },
                  "Resource": {
                    "id": "Resource",
                    "path": "RoleStack/test/batchFargateRole/Resource",  //　項番1
                    "attributes": {
                      "aws:cdk:cloudformation:type": "AWS::IAM::Role",
                      "aws:cdk:cloudformation:props": {
                        "assumeRolePolicyDocument": {
                          "Statement": [
                            {
                              "Action": "sts:AssumeRole",
                              "Effect": "Allow",
                              "Principal": {
                                "Service": "ecs-tasks.amazonaws.com"
                              }
                            }
                          ],
                          "Version": "2012-10-17"
                        },
                        "roleName": "BatchFargateRole"
                      }
                    },
                    "constructInfo": {
                      "fqn": "aws-cdk-lib.aws_iam.CfnRole",　//　項番1
                      "version": "2.99.0"
                    }
                  },
                  "DefaultPolicy": {
                    "id": "DefaultPolicy",
                    "path": "RoleStack/test/batchFargateRole/DefaultPolicy",　//　項番2
                    "children": {
                      "Resource": {
                        "id": "Resource",
                        "path": "RoleStack/test/batchFargateRole/DefaultPolicy/Resource",　//　項番3
                        "attributes": {
                          "aws:cdk:cloudformation:type": "AWS::IAM::Policy",
                          "aws:cdk:cloudformation:props": {
                            "policyDocument": {
                              "Statement": [
                                {
                                  "Action": [
                                    "sqs:ChangeMessageVisibility",
                                    "sqs:DeleteMessage",
                                    "sqs:GetQueueAttributes",
                                    "sqs:GetQueueUrl",
                                    "sqs:ReceiveMessage"
                                  ],
                                  "Effect": "Allow",
                                  "Resource": {
                                    "Fn::GetAtt": [
                                      "mainSQS22A964E2",
                                      "Arn"
                                    ]
                                  }
                                }
                              ],
                              "Version": "2012-10-17"
                            },
                            "policyName": "testbatchFargateRoleDefaultPolicy6240E59E",
                            "roles": [
                              {
                                "Ref": "testbatchFargateRole836934D6"
                              }
                            ]
                          }
                        },
                        "constructInfo": {
                          "fqn": "aws-cdk-lib.aws_iam.CfnPolicy",　//　項番3
                          "version": "2.99.0"
                        }
                      }
                    },
                    "constructInfo": {
                      "fqn": "aws-cdk-lib.aws_iam.Policy", //　項番2
                      "version": "2.99.0"
                    }
                  }
                },
                "constructInfo": {
                  "fqn": "aws-cdk-lib.aws_iam.Role",
                  "version": "2.99.0"
                }
              }
  ~~~

この「tree.json」の構造から以下のことが分かります。
|項番|Path  |　ID| 紐つくConstructs  |備考  |
|---|---|---|---|---|
|1|RoleStack/test/batchFargateRole/Resource|Resource|aws-cdk-lib.aws_iam.CfnRole  |IDがResourceなので、batchFargateRole nodeのdefaultchildであることが分かります。また、キャストする際は、L1「aws-cdk-lib.aws_iam.CfnRole」を利用する必要があることが分かります。|
|2|RoleStack/test/batchFargateRole/DefaultPolicy|DefaultPolicy|aws-cdk-lib.aws_iam.Policy  |IDがDefaultPolicyなので、batchFargateRoleにてnode IDをDefaultPolicyと指定すれば取得できることが分かります。また、キャストする際は、L2「aws-cdk-lib.aws_iam.Policy」を利用する必要があることが分かります。|
|3|RoleStack/test/batchFargateRole/DefaultPolicy/Resource|Resource|aws-cdk-lib.aws_iam.Policy  |IDがResourceなので、項番2のdefaultchildであることが分かります。また、キャストする際は、L1「aws-cdk-lib.aws_iam.Policy」を利用する必要があることが分かります。|

以上より、以下のようにキャスト出来ることがわかります。
|項番|入る型  |　キャスト方法|
|---|---|---|
|1|aws-cdk-lib.aws_iam.CfnRole| batchFargateRole.node.defaultchild as iam.CfnRole|
|2|aws-cdk-lib.aws_iam.Policy| batchFargateRole.node.tryFindChild("DefaultPolicy") as iam.Policy|
|3|aws-cdk-lib.aws_iam.CfnPolicy|項番2.node.defaultChild as iam.CfnPolicy|

今回は、DefaultPolicyの上書なので、「iam.CfnPolicy」が欲しいです。なので、項番2、項番3を使えばエスケープハッチが組めることがわかります。
  

### 仮組エスケープハッチの取り込み
~~~typescript:usecase-role.ts
    const batchFargateRole = new iam.Role(this, `batchFargateRole`, {
      assumedBy: new iam.ServicePrincipal(`ecs-tasks.amazonaws.com`),
      roleName: "BatchFargateRole"
    })
    
    mainQueue.grantConsumeMessages(batchFargateRole);
    
    const batchFargateRoleL2DefautPolicy = batchFargateRole.node.tryFindChild("DefaultPolicy") as iam.Policy  // 一旦L2 「aws-cdk-lib.aws_iam.Policy」型にキャスト
    const batchFargateRoleL1DefautPolicy = batchFargateRoleL2DefautPolicy.node.defaultChild as iam.CfnPolicy　//L1 「aws-cdk-lib.aws_iam.CfnPolicy」型にキャスト
~~~

これでDefaultPolicyをL1 「aws-cdk-lib.aws_iam.CfnPolicy」型にキャスト出来たので、L1 Constructsで用意されているMethodを利用することが出来ます。
今回は、DefaultPolicyの上書きなので、「addOverride Method」を利用し上書きます。

### 「addOverride Method」での上書き

~~~typescript:usecase-role.ts
    batchFargateRoleL1DefautPolicy.addOverride(
      `Properties.PolicyDocument.Statement.0.Action` //　①上書きたいPropertiyのPathを指定
    ,["sqs:DeleteMessage", "sqs:GetQueueAttributes", "sqs:GetQueueUrl", "sqs:ReceiveMessage"] //②上書く内容を記載
    )
~~~
#### ①上書きたいPropertiyのPathを指定
今回はPolicyのActionの箇所を指定します。
ネストされたフィールドは「.」で連結します。
フィールドを確認するためには、「cdk synthesize」で生成されたCloudFromation templateを見ると読みやすいです。
 ~~~json:RoleStack.template.json
"testbatchFargateRoleDefaultPolicy6240E59E": {
   "Type": "AWS::IAM::Policy",
   "Properties": {
    "PolicyDocument": {
     "Statement": [
      {
       "Action": [
 ~~~
キャストしたaws_iam.CfnPolicyでは、CloudFormationの仕様に沿って上記のようなPropertyがネストされたフィールドで組まれています。
今回はActionをいじりたいので、Propertiesから順に「.」で連結していきます。
Statementは何個が生成されるケースが多いと思います。なので、何番目のStatementかを指定します。
※今回は一番上なので、0番目

#### ②上書く内容を記載
今回はPolicyのActionを上書きたいので、許可したいActionを全て書きます。

以上で、エスケープハッチを利用したPolicyの上書は完了です。


## Grant Methodを使う方法
「IAM Grant Method」には特別なシナリオは定義されておらず、許可したいActionを書く形になります。
そのため、今回のケースでは以下のようになります。

### CDK
~~~typescript:usecase-role.ts
    const mainQueue = new sqs.Queue(this, 'SQS', { //MainSQS生成
      queueName: "MainSQS",
    });
   
    const batchFargateRole = new iam.Role(this, `batchFargateRole`, { // batchFargateRole生成
      assumedBy: new iam.ServicePrincipal(`ecs-tasks.amazonaws.com`),
      roleName: "BatchFargateRole"
    })
    
    mainQueue.grant( // MainSQSに対するAction許可をbatchFargateRoleに与える
      batchFargateRole,
      "sqs:DeleteMessage", "sqs:GetQueueAttributes", "sqs:GetQueueUrl", "sqs:ReceiveMessage"
    )
~~~
上段でMainSQS、Batchargate用のRoleを生成しています。
そのうえで、BatchFargateはMainSQSに対して「"sqs:DeleteMessage", "sqs:GetQueueAttributes", "sqs:GetQueueUrl", "sqs:ReceiveMessage"」Actionがしたいので、SQSが持つ「grant Method」を利用し許可しています。
これにより、以下のようなCloudFromation templateが生成されます。
この場合、上書くというより、grantを使って許可したいActionを指定するイメージです。

### CDKから生成されるCloudFormation
~~~json:RoleStack.template.json
  "testbatchFargateRoleDefaultPolicy6240E59E": {
   "Type": "AWS::IAM::Policy",
   "Properties": {
    "PolicyDocument": {
     "Statement": [
      {
       "Action": [
        "sqs:DeleteMessage",
        "sqs:GetQueueAttributes",
        "sqs:GetQueueUrl",
        "sqs:ReceiveMessage"
       ],
       "Effect": "Allow",
       "Resource": {
        "Fn::GetAtt": [
         "mainSQS22A964E2",
         "Arn"
        ]
       }
      }
     ],
     "Version": "2012-10-17"
    },
    "PolicyName": "testbatchFargateRoleDefaultPolicy6240E59E",
    "Roles": [
     {
      "Ref": "testbatchFargateRole836934D6"
     }
    ]
   },
   "Metadata": {
    "aws:cdk:path": "RoleStack/test/batchFargateRole/DefaultPolicy/Resource"
   }
  },
~~~

## まとめ
今回のユースケースでは、圧倒的に「Grant Methodを使う方法」の方が楽ですね。
「エスケープハッチを使う方法」は完全にお遊びです。せっかく遊んだので、ご紹介させて頂きました。
ただ、エスケープハッチ自体は非常に便利で、L2,L3の仕様で縛られている部分をカスタマイズすることが出来ます。
L1にキャストしていじるので、基本的にはなんでも対応可能です。
作る過程にちょっと手間が多いですが。。。
なので、今後も遊んだエスケープハッチに関しては、、備忘録&個人的なチートシートな感じでご紹介させて頂ければと思います。

※ 本ブログに記載した内容は個人の見解であり、所属する会社、組織とは全く関係ありません。