I'm attempting to write some tests for a CDK Construct that validates security group rules defined as part of the construct.
The Construct looks something like the following.
export interface SampleConstructProps extends StackProps {
srcSecurityGroupId: string;
}
export class SampleConstruct extends Construct {
securityGroup: SecurityGroup;
constructor(scope: Construct, id: string, props: SampleConstructProps) {
super(scope, id, props);
// const vpc = Vpc.fromLookup(...);
this.securityGroup = new SecurityGroup(this, "SecurityGroup", {
vpc: vpc,
allowAllOutbound: true,
});
const srcSecurityGroupId = SecurityGroup.fromSecurityGroupId(stack, "SrcSecurityGroup", props.srcSecurityGroupId);
this.securityGroup.addIngressRule(srcSecurityGroup, Port.tcp(22));
}
}
And I want to write a test that looks something like the following.
test("Security group config is correct", () => {
const stack = new Stack();
const srcSecurityGroupId = "id-123";
const testConstruct = new SampleConstruct(stack, "TestConstruct", {
srcSecurityGroupId: srcSecurityGroupId
});
expect(stack).to(
haveResource(
"AWS::EC2::SecurityGroupIngress",
{
IpProtocol: "tcp",
FromPort: 22,
ToPort: 22,
SourceSecurityGroupId: srcSecurityGroupId,
GroupId: {
"Fn::GetAtt": [testConstruct.securityGroup.logicalId, "GroupId"], // Can't do this
},
},
undefined,
true
)
);
});
The issue here is that the test is validated against the synthesized CloudFormation template, so if you want to verify that the security group created by this construct has a rule allowing access from srcSecurityGroup
, you need the Logical ID of the security group that was created as part of the Construct.
You can see this in the generated CloudFormation template here.
{
"Type": "AWS::EC2::SecurityGroupIngress",
"Properties": {
"IpProtocol": "tcp",
"FromPort": 22,
"GroupId": {
"Fn::GetAtt": [
"TestConstructSecurityGroup95EF3F0F", <-- This
"GroupId"
]
},
"SourceSecurityGroupId": "id-123",
"ToPort": 22
}
}
That Fn::GetAtt
is the crux of this issue. Since these tests really just do an object comparison, you need to be able to replicate the Fn::Get
invocation, which requires the CloudFormation Logical ID.
Note that the CDK does provide a handful of identifiers for you.
securityGroup.uniqueId
returns TestStackTestConstructSecurityGroup10D493A7
whereas the CloudFormation template displays TestConstructSecurityGroup95EF3F0F
. You can note the differences are the uniqueId
prepends the Construct ID to the logical identifier and the appended hash is different in each.SecurityGroup
as the construct ID and TestConstructSecurityGroup95EF3F0F
as the logical ID in the synthesized template.Is there a straightforward way getting the logical ID of CDK resources?
Logical IDs Unique IDs serve as the logical identifiers, which are sometimes called logical names, of resources in the generated AWS CloudFormation templates for those constructs that represent AWS resources.
Logical ID Use the logical name to reference the resource in other parts of the template. For example, if you want to map an Amazon Elastic Block Store volume to an Amazon EC2 instance, you reference the logical IDs to associate the block stores with the instance.
What is Metadata # Metadata is a CloudFormation feature that allows us to specify additional details about our template using json or yaml. Metadata is used by the CDK team in order to collect analytics in regards to how developers use the CDK service.
After writing up this whole post and digging through the CDK code, I stumbled on the answer I was looking for. If anybody has a better approach for getting the logical ID from a higher level CDK construct, the contribution would be much appreciated.
If you need to get the logical ID of a CDK resource you can do the following:
const stack = new Stack();
const construct = new SampleConstruct(stack, "SampleConstruct");
const logicalId = stack.getLogicalId(construct.securityGroup.node.defaultChild as CfnSecurityGroup);
Note that you you already have a CloudFormation resource (eg something that begins with with Cfn
) then it's a little easier.
// Pretend construct.securityGroup is of type CfnSecurityGroup
const logicalId = stack.getLogicalId(construct.securityGroup);
From my testing, it seems that stack.getLogicalId
will always return the original, CDK allocated logicalId, it won't change if you call overrideLogicalId
, so it won't always match the synthed output.
This worked for me, even with a logicalId override set:
stack.resolve((construct.node.defaultChild as cdk.CfnElement).logicalId)
stack.resolve
is necessary because .logicalId
is a token.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With