AWS CDKでImmutableRoleを利用してL2コンストラクトによるロールへのインラインポリシーの付与を防ぐ
初めに
今回すでにAWS CDKで構築されている環境において生成されているロールのインラインポリシーを廃止し全てカスタマー管理ポリシーに変更する必要がありましたが、すでに構築されている環境は良くも悪くもL2コンストラクトによりインラインポリシー強制的に付与されてしまっている状態となっておりこれを除去する必要がありました。
こう言ったケースは一定存在するのでは?と思っていたのですが今回の結論から逆引きしても意外と日本語の情報自体がないので備忘録として残しておきます。
(普段CDKを触らないのでもしかするとそもそもこう言った書き方をせず根本的に別の対応をするのかもしれませんが...)
L2コンストラクトの便利さと制約
AWS CDKでは抽象度の高いL2/L3コンストラクトを利用することでコードをシンプルにできますが、その分ユーザ側でコントロールできる部分も少なくなります。管理は少ないものの制約事項のあるマネージドサービスと、管理は多くなる分自由度の高いセルフマネージドのサービスの関係にある種考えとして似ている部分はあるかもしれません。
例えばFargateServiceを利用してECS Serviceを作成する場合、enableExecuteCommand: true
を渡すとECS Execの機能有効化と合わせ、サービスに渡したタスク定義に含まれるタスクロールに対してECS Execに必要なポリシーを付与する作りになっています。
そのため各々の環境にマッチすればコードの記述量を減らしシンプルなコードにまとめられる一方、今回のように要件と合わないような部分が出てしまうと対応が苦しくなってしまう場面もあります。
そういった場合はCloudFormationにより近く自由度の高いL1コンストラクトにより対応できますが、今回はすでにL2コンストラクトでECS Serviceが構築されておりここから組み直すにはリソースの再生成等による影響が大きく、かといって上記のようにL2コンストラクト側にその付与を無効化するような条件分岐はないため対応が難しいのではないかと考えていました。
withoutPolicyUpdates()
CDKの仕組み上そのコードにより直接リソースを変換するのではなくあくまでCloudFormationテンプレートへ変換を行いそれを元にリソースを生成するため、逆転の発想として付与された後のPolicy自体を除去できないか?という視点で探していたところ、削除自体には辿り着かなかったものの派生的にaws_iam.Roleに含まれるwithoutPolicyUpdates()
を利用することで操作される側でポリシーの変更処理を無効化できるということに辿り着きました。
ざっと見とはなりますがwithoutPolicyUpdates()
は元のRole
オブジェクトをポリシーが変更不能なImmutableRole
に変換・複製し返却するメソッドとなるようです。
ImmutableRole
具体的にはRoleクラス同様にIRole
をインターフェースとして持ちつつも、attachInlinePolicy()
やaddManagedPolicy()
といったポリシー変更を伴うようなメソッドの処理を空として無効化したクラスとなります。
これによりRole
オブジェクトと一定の互換性は保たれるため各種処理でほぼRoleオブジェクトと同等に扱いつつも、仮にポリシーの変更処理が呼び出されたとしてもその処理を無効化し現状のポリシーを維持できます。
既に付与済みのポリシーは維持される
あくまで付与処理は無効化されるものの生成されるImmutableRole
のベースはwithoutPolicyUpdates()
が実行された時点でのRoleオブジェクトとなるため、それまでに付与されたポリシーは除去されないという特徴があります。
先に書いた通りImmutableRole
クラスではaddManagedPolicy()
の処理が無効化されているため、例えば以下の処理を実行した場合、taskexec-role
にはservice-role/AmazonECSTaskExecutionRolePolicy
は付与されない形となります。
//Roleの生成とともにImmutableRoleに変換して取り扱う
const executionRole = new aws_iam.Role(scope, 'ExecutionRole', {
roleName: "taskexec-role",
assumedBy: new aws_iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
}).withoutPolicyUpdates() as aws_iam.Role;
//既に`addManagedPolicy()`の処理が無になっているため何も処理されない
executionRole.addManagedPolicy(aws_iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'));
//...
const taskDefinition = new aws_ecs.FargateTaskDefinition(scope, 'TaskDefinition', {
//...
//これ以降taskDefinition.executionRoleのポリシーの変更は無効化される
executionRole: executionRole.withoutPolicyUpdates()
//...
}
//...
new aws_ecs.FargateService(...);
一方で以下のようにポリシーを付与した後のタイミングでImmutableRole
に変換するとaddManagedPolicy()
が呼び出された時点ではまだRole
オブジェクトのためポリシーは付与されます。
その後FargateTaskDefinition
のコンストラクタ呼び出しの前にImmutableRole
に変換されるためその変換処理の中、およびtaskDefinition
の内部に含まれるexecutionRole
にポリシーを付与するような操作を行なってもそのポリシーは付与されない形となります。
const executionRole = new aws_iam.Role(scope, 'ExecutionRole', {
roleName: "taskexec-role",
assumedBy: new aws_iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
});
//この時点ではまだImmutableRoleではないのでこのポリシーは付与される。
executionRole.addManagedPolicy(aws_iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'));
const taskDefinition = new aws_ecs.FargateTaskDefinition(scope, 'TaskDefinition', {
//...
executionRole: executionRole.withoutPolicyUpdates()
}
//...
new aws_ecs.FargateService(...);
そのためRoleオブジェクトの間に必要なポリシーを付与する処理を先に行っておき、L2コンストラクトに引き渡すタイミングでImmutableRole
に変換することで、ユーザ側で直接指定を行っていないポリシー付与処理を無効化しつつもユーザ側で指定したポリシーを与えることが可能となります。
終わりに
今回ImmutableRole
を利用してL2コンストラクトの内部処理によるインラインポリシーの付与の無効化を行ってみました。
後から必要に迫られた場合や、L2コンストラクトによる抽象度の高さを受けつつも細かなポリシーの制御は自前で行いたいような場合等々それなりに役に立つケースはありそうです。
一方でL2コンストラクトの抽象度の高さを潰し自前で書き込む(低レイヤーを触る)という選択肢を取る形となります。
制約事項が多く多用する環境であればL1コンストラクトで書き込んだ方が良い可能性もありますのでそちらも検討してみましょう。
とはいえそれが後から生まれる等様々なケースがあるかと思いますので良し悪しを把握しその上で活用していきましょう。