AWS CDKでIAMロール/ポリシーを宣言するスタックとワークロード+CloudWatchロググループを宣言するスタックを分けて後者から前者を参照した時、意図せず循環参照が発生してハマってしまったので解決のメモ書きです。
試した環境
- バージョン ... Node.js 14.15.4, npm 7.5.2, aws-cdk 1.88.0
- 言語 ... TypeScript
問題のスタック構成
以下のようなスタックを作成してリソースの一括作成を試みます。
- AppStack ... ECSのTaskDefinition、CloudWatch LogsのLogGroupを宣言する。
- SecurityStack ... AppStackのTaskDefinitionで渡すIAM Role (Task Role/Task Execution Role)とポリシーを宣言する。
スタックは以下のような記述をしていました。
一見問題なさそうですが cdk synth を実行すると、なんと循環参照でエラーになってしまいます。
エラーメッセージ
Error: 'SecurityStack' depends on 'AppStack' (SecurityStack -> AppStack/AppContainerLogGroup/Resource.Arn). Adding this dependency (AppStack -> SecurityStack/MyEcsDefaultTaskExecutionRole/Resource.Arn) would create a cyclic reference.
エラーになるスタック
// security-stack.ts /** * IAM Roleを宣言するスタック */ export class SecurityStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // Task Role const ECS_PRINCIPAL = 'ecs-tasks.amazonaws.com'; this.defaultTaskRole = new Role(this, 'MyEcsDefaultTaskRole', { assumedBy: new ServicePrincipal(ECS_PRINCIPAL), description: 'Default IAM role for ECS Task.' }); // Task Execution Role this.defaultTaskExecutionRole = new Role(this, 'MyEcsDefaultTaskExecutionRole', { assumedBy: new ServicePrincipal(ECS_PRINCIPAL), description: 'Default IAM role for ECS Task execution.', managedPolicies: [ ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'), ], }); } }
// app-stack.ts interface ServiceAppRoles { defaultTaskRole: IRole; defaultTaskExecutionRole: IRole; } interface AppStackProps extends cdk.StackProps { appRoles: ServiceAppRoles; } /** * ECSのタスクとロググループを宣言するスタック */ export class AppStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props: AppStackProps) { super(scope, id, props); // Backend Task const appContainerLogGroup = new LogGroup(this, 'AppContainerLogGroup', { retention: RetentionDays.ONE_DAY, removalPolicy: cdk.RemovalPolicy.DESTROY, }); const appTask = new TaskDefinition(this, 'MyEcsFargateTaskDef', { compatibility: Compatibility.FARGATE, cpu: '256', memoryMiB: '1024', networkMode: NetworkMode.AWS_VPC, taskRole: props.appRoles.defaultTaskRole, executionRole: props.appRoles.defaultTaskExecutionRole, }); const appContainer = appTask.addContainer('myapp', { image: ContainerImage.fromEcrRepository( Repository.fromRepositoryName(this, 'AppRepo', 'my-app'), 'v1' ), essential: true, logging: LogDriver.awsLogs({ logGroup: appContainerLogGroup, streamPrefix: 'AppContainer', }), }); appContainer.addPortMappings( { containerPort: 80, protocol: Protocol.TCP, } ); } }
原因
上記の例では、logging設定を追加したTaskDefinitionに、SecurityStackで作成したexecutionRoleを渡していました。
この状態で cdk synth を実行すると、 既存のIAMポリシーにロググループへの書き込み権限が追記・上書きされます。
具体的には、IAMポリシーの適用対象リソースにロググループのARNを含めようとします。
本来はAppStack --> SecurityStackの依存関係しかない想定でしたが、 上記の理由によってSecurityStack --> AppStackという参照が発生し、結果的に循環参照が発生してしまったようです。
対策
SecurityStackで Role.withoutPolicyUpdates() を呼びだしてIAMポリシーの上書きを防いで解決しました。
// immutable-role-security-stack.ts /** * IAM Roleを宣言するスタック */ export class SecurityStack extends cdk.Stack { constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // Task Role は省略 // Task Execution Role this.defaultTaskExecutionRole = new Role(this, 'MyEcsDefaultTaskExecutionRole', { assumedBy: new ServicePrincipal(ECS_PRINCIPAL), description: 'Default IAM role for ECS Task execution.', managedPolicies: [ ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'), ], }).withoutPolicyUpdates(); // このメソッドを追加する! } }
ドキュメントからはIAMポリシーの上書きの挙動が読み取れなかったのですが、GitHubで類似のIssueを見つけました。 どうもECRのコンテナイメージを指定した場合も同様の挙動があるらしいです。
CDKを使うことで記述量はCloudFormationのテンプレートを書くよりもずっと少なくなりましたけど、 中の挙動が隠されているとこういったハマりポイントの発見が難しくなりますね。
CDKはお手本の生成スクリプトの位置づけにして、素直にCloudFormationのテンプレートを触った方が良い気もしてきました。
以上。