Cross-Stack References in CDK
AWS Cloud Development Kit (CDK) is a powerful Infrastructure as Code (IaC) tool, it’s used by AWS internally and many people are favoring CDK’s simple and powerful capabilities. CDK has many options, as a someone who worked many years in TypeScript/JavaScript world, it feels natural to me.
In this tutorial I will show you a very simple solution for managing cross-stack references. If you are familiar with CloudFormation, here is how you would be solving that problem
Use the Export output field to flag the value of a resource output for export. Then, use the Fn::ImportValue intrinsic function to import the value.
Define the references as Outputs.
…..
"Outputs" : {
"VPCId" : {
"Description" : "VPC ID",
"Value" : { "Ref" : "VPC" },
"Export" : { "Name" : {"Fn::Sub": "${AWS::StackName}-VPCID" }}
},
"PublicSubnet" : {
"Description" : "The subnet ID to use for public web servers",
"Value" : { "Ref" : "PublicSubnet" },
"Export" : { "Name" : {"Fn::Sub": "${AWS::StackName}-SubnetID" }}
},
"WebServerSecurityGroup" : {
"Description" : "The security group ID to use for public web servers",
"Value" : { "Fn::GetAtt" : ["WebServerSecurityGroup", "GroupId"] },
"Export" : { "Name" : {"Fn::Sub": "${AWS::StackName}-SecurityGroupID" }}
}
}
Then later referencing from another stack.
….
"Properties": {
"InstanceType" : "t2.micro",
"ImageId": { "Fn::FindInMap": [ "AWSRegionArch2AMI", { "Ref": "AWS::Region" } , "HVM64" ] },
"NetworkInterfaces" : [{
"GroupSet" : [{ "Fn::ImportValue" : {"Fn::Sub": "${NetworkStackName}-SecurityGroupID" } }],
"AssociatePublicIpAddress" : "true",
"DeviceIndex" : "0",
"DeleteOnTermination" : "true",
"SubnetId" : { "Fn::ImportValue" : {"Fn::Sub": "${NetworkStackName}-SubnetID" } }
}],
Let’s try to replicate the same thing with two simple CDK stacks. In the first stack, we will be creating a DynamoDB table. In the second Stack, we will create a Lambda function and pass a reference to the DynamoDB table referenced in the first Stack.
Here is the DynamoDB Stack.
class DynamoStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const dynamoTable = new dynamodb.Table(this, "dynamo-table", {
partitionKey: {
name: "id",
type: dynamodb.AttributeType.STRING
},
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST
});
}
}
Just one DynamoDB table, Pay per Request billing option. Let’s define the Lambda Stack.
interface LambdaStackProps extends cdk.StackProps {
DYNAMO_TABLE_NAME: string;
}
class LambdaStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: LambdaStackProps) {
super(scope, id, props);
const LambdaFunction = new lambda.Function(this, "lambda-function", {
code: lambda.InlineCode.fromInline(`exports.handler = function(event, ctx, cb) {
console.log(process.env.DYNAMO_TABLE_NAME);
return cb(null, 'hello world');
}`),
handler: "index.handler",
runtime: lambda.Runtime.NODEJS_18_X,
environment: {
DYNAMO_TABLE_NAME: props.DYNAMO_TABLE_NAME
}
});
}
}
Notice that we are using props.DYNAMO_TABLE_NAME variable. We need a way to pass this information.
const app = new cdk.App();
const dynamoStack = new DynamoStack(app, "DynamoStack", {
env: {
region: process.env.AWS_REGION,
account: process.env.AWS_ACCOUNT_ID
}
});
const lambdaStack = new LambdaStack(app, "LambdaStack", {
env: {
region: process.env.AWS_REGION,
account: process.env.AWS_ACCOUNT_ID
},
DYNAMO_TABLE_NAME: dynamoStack.stackName
})
Very simple, yet effective. Now you might ask why we can’t have both resources in the same Stack. The answer is yes, you can. But when your Stacks get bigger, it’s a good idea to separate Stacks into Logical Units. For example, your database resources can be under Data Stack, your Lambda functions might be in Lambda Stack, whereas your Kubernetes cluster might be in EKS Stack. I will write another blog post on managing and designing Multi-Stack CDK projects, so stay tuned.
AWS SSM Parameter Store can also be used to manage Cross-Stack references. https://docs.aws.amazon.com/cdk/v2/guide/get_ssm_value.html