AWS CDK で非 Stage から Stage への移行で置換が発生するリソースがいくつかあるので、その例や原因をご紹介します。
目次
Stage とは
AWS CDK には Stage という概念があります。これはスタックを抽象化したようなものであり、Stage を使用することで複数のスタックをまとめて管理しやすくなります。
具体的な例に関しては、以下の記事で挙げています。
Stage におけるリソースの置換
CDK では、リソースの論理 ID が変更されるとそのリソースは置換されます。
また、非 Stage のリソースを Stage に移行することで、Construct のパスが変更されます。
しかし、論理 ID はスタックから下のコンストラクトパスをもとに生成されるため、Stage に移行しても基本的には論理 ID は変わりません。つまり、ほとんどのケースでリソースの置換は発生しません。
Stage への移行で置換が発生するリソースとその原因
ただし一部のリソースでは、非 Stage から Stage への移行で置換が発生します。
これらのリソースの置換が発生する原因には、以下の 3 つがあります。
- 論理 ID の変更
- 物理名の変更
- Replacement プロパティの変更
また基本的には、非 Stage のリソースを Stage に移行することで変わる情報というのは、Construct のパスに基づく情報です。
例えば、this.node.path
や this.node.addr
です。他にも、その情報を使用してユニークな ID を生成する Names.nodeUniqueId
や Names.uniqueId
なども同様です。
では、具体的なリソースの例を見ていきましょう。
※注意: 以下で紹介するリソースの例は、置換が発生するリソースの一部です。すべてを網羅しているわけではありません。他にも置換が発生するリソースはあるので注意してください。
SecurityGroup
まずSecurityGroup
ですが、CloudFormation での GroupDescription
という、Replacement となるプロパティの変更が原因で置換が発生します。
SecurityGroupProps
の description
に明示的に値を指定している場合は置換が発生しないのですが、未指定の場合は Construct のパスである this.node.path
が使用されます。Stage に移行するとパスが変わるため、結果として GroupDescription
の値が変わってしまいます。
原因部分の CDK 内部のコードは以下のようになっています。
packages/aws-cdk-lib/aws-ec2/lib/security-group.ts
const groupDescription = props.description || this.node.path;
SecurityGroup の Ingress/Egress ルール
SecurityGroup
の Ingress/Egress ルールでも置換が発生します。
具体的には、addIngressRule
(SecurityGroupIngress
) や、allowAllOutbound
が false
のときの addEgressRule
(SecurityGroupEgress
) で置換が発生します。
またこれは、論理 ID の変更が原因です。
const sg1 = new SecurityGroup(this, 'SecurityGroup1', { vpc: vpc, allowAllOutbound: true, }); const sg2 = new SecurityGroup(this, 'SecurityGroup2', { vpc: vpc, allowAllOutbound: false, // true だと addEgressRule をしてもルールが追加されない }); sg2.addIngressRule(sg1, Port.tcp(80), 'Allow traffic from sg1 to sg2 on port 80'); sg2.addEgressRule(sg1, Port.tcp(80), 'Allow traffic from sg2 to sg1 on port 80');
原因となる CDK 内部のコードは以下です。
this.uniqueId
や peer.uniqueId
では内部的に Names.nodeUniqueId(this.node)
という、コンストラクトパスをもとにユニークな ID を出力するメソッドが使用されています。
この値が最終的にリソースの Construct ID に渡されて論理 ID として使用されるため、Stage に移行するとこの結果が変わり、論理 ID が変更されることで置換が発生します。
packages/aws-cdk-lib/aws-ec2/lib/security-group.ts
protected determineRuleScope( peer: IPeer, connection: Port, fromTo: 'from' | 'to', remoteRule?: boolean): RuleScope { if (remoteRule && SecurityGroupBase.isSecurityGroup(peer) && differentStacks(this, peer)) { // Reversed const reversedFromTo = fromTo === 'from' ? 'to' : 'from'; return { scope: peer, id: `${this.uniqueId}:${connection} ${reversedFromTo}` }; } else { // Regular (do old ID escaping to in order to not disturb existing deployments) return { scope: this, id: `${fromTo} ${this.renderPeer(peer)}:${connection}`.replace('/', '_') }; } } private renderPeer(peer: IPeer) { if (Token.isUnresolved(peer.uniqueId)) { // Need to return a unique value each time a peer // is an unresolved token, else the duplicate skipper // in `sg.addXxxRule` can detect unique rules as duplicates return this.peerAsTokenCount++ ? `'{IndirectPeer${this.peerAsTokenCount}}'` : '{IndirectPeer}'; } else { return peer.uniqueId; } }
CustomResource の Provider 内の StepFunctions の LogGroup
CustomResource の Provider という Construct では、isCompleteHandler
が指定されている場合、内部で StepFunctions の StateMachine が作成されます。
const isCompleteHandler = new Function(this, 'MyFunction', { runtime: Runtime.NODEJS_22_X, handler: 'index.handler', code: Code.fromAsset(path.join(__dirname, 'lambda')), }); new Provider(this, 'Provider', { onEventHandler, isCompleteHandler, });
この StateMachine の LogGroup では、ロググループ名(logGroupName
)、つまり物理名に this.node.addr
が使用されています。
また、this.node.addr
はコンストラクトパスをもとに生成されるハッシュ値です。
つまり Stage に移行するとこの値が変わるため、物理名が変わり、置換が発生します。
原因となる CDK 内部のコードは以下です。
packages/aws-cdk-lib/custom-resources/lib/provider-framework/waiter-state-machine.ts
const logGroup = logOptions?.destination ?? new LogGroup(this, 'LogGroup', { // Log group name should start with `/aws/vendedlogs/` to not exceed Cloudwatch Logs Resource Policy // size limit. // https://docs.aws.amazon.com/step-functions/latest/dg/bp-cwl.html // // By using the auto-generated name of the Lambda created in the `Provider` that calls this // `WaiterStateMachine` construct, even if the `Provider` (or its parent) is deleted and then // created again, the log group name will not duplicate previously created one with removal // policy `RETAIN`. This is because that the Lambda will be re-created again with auto-generated name. // The `node.addr` is also used to prevent duplicate names no matter how many times this construct // is created in the stack. It will not duplicate if called on other stacks. logGroupName: `/aws/vendedlogs/states/waiter-state-machine-${this.isCompleteHandler.functionName}-${this.node.addr}`, });
Lambda の Event Source
Lambda Function の addEventSource
メソッドで SqsEventSource
や SnsEventSource
などを使用している場合も、Stage 移行によって論理 ID が変わるため、置換が発生します。他にも、S3EventSource
、DynamoEventSource
、KinesisEventSource
なども同様です。
const func = new Function(this, 'MyFunction', { runtime: Runtime.NODEJS_22_X, handler: 'index.handler', code: Code.fromAsset(path.join(__dirname, 'lambda')), }); func.addEventSource(new SqsEventSource(queue)); func.addEventSource(new SnsEventSource(topic));
しかし、addEventSourceMapping
メソッドや EventSourceMapping
Construct を使用している場合は置換は発生しません。
func.addEventSourceMapping('MyEventSourceMapping', { eventSourceArn: 'arn:aws:sqs:us-east-1:123456789012:MyQueue', }); new EventSourceMapping(this, id, { target: func, eventSourceArn: 'arn:aws:sqs:us-east-1:123456789012:MyQueue', });
該当する CDK 内部のコードは以下のようになっています。
SqsEventSource
では Names.nodeUniqueId
が使用されており、これが EventSourceMapping の Construct ID に渡され、論理 ID の生成に使用されます。つまり、Stage に移行するとこの値が変わり、論理 ID が変更されるため置換が発生します。
packages/aws-cdk-lib/aws-lambda-event-sources/lib/sqs.ts
public bind(target: lambda.IFunction) { const eventSourceMapping = target.addEventSourceMapping(`SqsEventSource:${Names.nodeUniqueId(this.queue.node)}`, { batchSize: this.props.batchSize, maxBatchingWindow: this.props.maxBatchingWindow, maxConcurrency: this.props.maxConcurrency, reportBatchItemFailures: this.props.reportBatchItemFailures, enabled: this.props.enabled, eventSourceArn: this.queue.queueArn, filters: this.props.filters, filterEncryption: this.props.filterEncryption, metricsConfig: this.props.metricsConfig, });
また、上記の SqsEventSource
では AWS::Lambda::EventSourceMapping
の論理 ID が変更されるのですが、SnsEventSource
では AWS::Lambda::Permission
の論理 ID が変更されます。
これは EventSource というより、内部で呼び出されている LambdaSubscription
クラスの実装に起因します。ここでも、Names.nodeUniqueId
が使用されています。
packages/aws-cdk-lib/aws-sns-subscriptions/lib/lambda.ts
public bind(topic: sns.ITopic): sns.TopicSubscriptionConfig { // Create subscription under *consuming* construct to make sure it ends up // in the correct stack in cases of cross-stack subscriptions. if (!Construct.isConstruct(this.fn)) { throw new ValidationError('The supplied lambda Function object must be an instance of Construct', topic); } this.fn.addPermission(`AllowInvoke:${Names.nodeUniqueId(topic.node)}`, { sourceArn: topic.topicArn, principal: new iam.ServicePrincipal('sns.amazonaws.com'), });
SNS Topic の Lambda Subscription
上述の通り、SNS Topic の addSubscription
メソッドで LambdaSubscription
(aws-cdk-lib/aws-sns-subscriptions
) を使用している場合でも、Stage 移行により置換が発生します。
const topic = new Topic(this, 'MyTopic'); const func = new Function(this, 'MyFunction', { runtime: Runtime.NODEJS_22_X, handler: 'index.handler', code: Code.fromAsset(path.join(__dirname, 'lambda')), }); topic.addSubscription(new LambdaSubscription(func));
S3 Bucket の Lambda Notification (Destination)
S3 Bucket の addEventNotification
メソッドで LambdaDestination
(aws-cdk-lib/aws-s3-notifications
) を使用している場合でも、Stage 移行により上記と同じような AWS::Lambda::Permission
の置換が発生します。
const bucket = new Bucket(this, 'MyBucket'); const func = new Function(this, 'MyFunction', { runtime: Runtime.NODEJS_22_X, handler: 'index.handler', code: Code.fromAsset(path.join(__dirname, 'lambda')), }); bucket.addEventNotification(EventType.OBJECT_CREATED, new LambdaDestination(func));
RDS の Credentials における fromGeneratedSecret
RDS / Aurora の DatabaseCluster
などで、 credentials
に fromGeneratedSecret
を使用している場合も、Stage 移行によって論理 ID が変わるため置換が発生します。ここでは、SecretsManager の Secret
の論理 ID が変更されます。
また、fromPassword
や fromUsername
、 fromSecret
などのメソッドを使用している場合は置換は発生しません。
new DatabaseCluster(this, 'Cluster', { engine, vpc, writer, credentials: Credentials.fromGeneratedSecret('clusteradmin'), });
該当する CDK 内部のコードは以下です。
具体的には、内部で呼ばれる DatabaseSecret
の中で、Names.uniqueId
を使用した値で論理 ID を上書き(overrideLogicalId
)しています。
packages/aws-cdk-lib/aws-rds/lib/database-secret.ts
if (props.replaceOnPasswordCriteriaChanges) { const hash = md5hash( JSON.stringify({ // Use here the options that influence the password generation. // If at some point we add other password customization options // they should be added here below (e.g. `passwordLength`). excludeCharacters, }), ); const logicalId = `${Names.uniqueId(this)}${hash}`; const secret = this.node.defaultChild as secretsmanager.CfnSecret; secret.overrideLogicalId(logicalId.slice(-255)); // Take last 255 chars }
まとめ
基本的には Stage に移行しても論理 ID は変わらないため置換は発生しませんが、一部のリソースでは発生する可能性があるので、スナップショットテストなどで確認しておくと良いでしょう。