Description
Describe the bug
Using stream.grantWrite(role)
to grant permissions does not seem to establish a CloudFormation dependency for the generated IAM Policy, causing resources like CfnDestination
that use the role.roleArn
to fail deployment due to missing permissions during creation/validation.
Regression Issue
- Select this option if this issue appears to be a regression.
Last Known Working CDK Version
No response
Reproduction Steps
Consider the following CDK TypeScript code which sets up a CloudWatch Logs Destination (CfnDestination
) pointing to a Kinesis Stream (Stream
). A role (Role
) is created for CloudWatch Logs to assume, and permissions to write to the Kinesis stream are granted using stream.grantWrite(role)
:
import { RemovalPolicy, Stack, StackProps } from "aws-cdk-lib";
import { Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
import { Stream } from "aws-cdk-lib/aws-kinesis";
import { CfnDestination } from "aws-cdk-lib/aws-logs";
import { Construct } from "constructs";
export class MonitoringStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps = {}) {
super(scope, id, props);
const DESTINATION_NAME = "test-destination";
// Create a Kinesis stream
const testStream = new Stream(this, "TestStream", {
shardCount: 1,
removalPolicy: RemovalPolicy.DESTROY,
});
// Role for CloudWatch Logs to assume
const cwlToKinesisRole = new Role(this, "CwlToKinesisRole", {
assumedBy: new ServicePrincipal("logs.amazonaws.com"),
});
// Grant write permissions to the role
testStream.grantWrite(cwlToKinesisRole);
// Create the CloudWatch Logs Destination
new CfnDestination(this, "TestDestination", {
destinationName: DESTINATION_NAME,
targetArn: testStream.streamArn,
roleArn: cwlToKinesisRole.roleArn,
destinationPolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Sid: "AllowSubscriptionFilter",
Effect: "Allow",
Principal: "*",
Action: ["logs:PutSubscriptionFilter", "logs:PutAccountPolicy"],
Resource: `arn:aws:logs:${this.region}:${this.account}:destination:${DESTINATION_NAME}`,
Condition: {
StringEquals: {
"aws:PrincipalOrgID": ["o-xxxxxxxxxx"],
},
},
},
],
}),
});
}
}
Observed Behavior
When deploying this stack, the creation of the AWS::Logs::Destination
resource (TestDestination
) fails with the following CloudFormation error:
CREATE_FAILED | AWS::Logs::Destination | TestDestination
Resource handler returned message: "Invalid request provided: AWS::Logs::Destination. Could not deliver test message to specified destination. Check if the destination is valid. (Service: CloudWatchLogs, Status Code: 400, ...)"
It appears there's a race condition. The CfnDestination
resource attempts creation before the IAM Policy granting kinesis:PutRecord
permissions (generated by testStream.grantWrite()
) is fully attached to the cwlToKinesisRole
. Since the role lacks the necessary permissions at the moment CloudWatch Logs tries to validate the destination by sending a test message, the creation fails.
Expected Behavior
Calling testStream.grantWrite(role)
should ideally create an implicit dependency. The CfnDestination
resource, which uses role.roleArn
, should depend not only on the AWS::IAM::Role
resource itself but also on the AWS::IAM::Policy
resource generated by the grantWrite()
call that attaches permissions to that role. This would ensure the permissions are in place before the destination is created and validated.
Possible Solution
Defining the policy inline within the Role
construct resolves the issue, because it forces CloudFormation to create the role and its inline policy together, before the CfnDestination
(which depends from the roleArn
) is created:
import { Effect, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
import { Stream } from "aws-cdk-lib/aws-kinesis";
import { CfnDestination } from "aws-cdk-lib/aws-logs";
import { Construct } from "constructs";
import { RemovalPolicy, Stack, StackProps } from "aws-cdk-lib";
export class MonitoringStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps = {}) {
super(scope, id, props);
const DESTINATION_NAME = "test-destination";
// Create a Kinesis stream
const testStream = new Stream(this, "TestStream", {
shardCount: 1,
removalPolicy: RemovalPolicy.DESTROY,
});
// Role for CloudWatch Logs to assume
const cwlToKinesisRole = new Role(this, "CwlToKinesisRole", {
assumedBy: new ServicePrincipal("logs.amazonaws.com"),
inlinePolicies: {
KinesisWritePolicy: new PolicyDocument({
statements: [
new PolicyStatement({
actions: ["kinesis:PutRecord", "kinesis:PutRecords"],
resources: [testStream.streamArn],
effect: Effect.ALLOW,
}),
],
}),
},
});
// Create the CloudWatch Logs Destination
new CfnDestination(this, "TestDestination", {
destinationName: DESTINATION_NAME,
targetArn: testStream.streamArn,
roleArn: cwlToKinesisRole.roleArn,
destinationPolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Sid: "AllowSubscriptionFilter",
Effect: "Allow",
Principal: "*",
Action: ["logs:PutSubscriptionFilter", "logs:PutAccountPolicy"],
Resource: `arn:aws:logs:${this.region}:${this.account}:destination:${DESTINATION_NAME}`,
Condition: {
StringEquals: {
"aws:PrincipalOrgID": ["o-xxxxxxxxxx"],
},
},
},
],
}),
});
}
}
Additional Information/Context
No response
CDK CLI Version
2.192.0
Framework Version
No response
Node.js Version
22.15.0
OS
Ubuntu 24.04
Language
TypeScript
Language Version
No response
Other information
No response