Props drilling, massive functions/classes in one file, and deployment deadlock are just some of the common issues you can find in AWS CDK code written by yourself or others (me included). Amazon Web Services Cloud Development Kit is the framework I work with most. AWS CDK is very powerful. Built around CloudFormation it makes architecting, building, and deploying cloud resources very easy...until it’s not. This blog post explores using a central register to share resources anywhere in the CDK code base, including between stacks and pitfalls to avoid when doing so.
A year ago I ago I wrote an article "A Novel Pattern for Documenting DynamoDB Access Patterns" (update coming soon) and …
Props drilling, massive functions/classes in one file, and deployment deadlock are just some of the common issues you can find in AWS CDK code written by yourself or others (me included). Amazon Web Services Cloud Development Kit is the framework I work with most. AWS CDK is very powerful. Built around CloudFormation it makes architecting, building, and deploying cloud resources very easy...until it’s not. This blog post explores using a central register to share resources anywhere in the CDK code base, including between stacks and pitfalls to avoid when doing so.
A year ago I ago I wrote an article "A Novel Pattern for Documenting DynamoDB Access Patterns" (update coming soon) and I mentioned in there the pattern for using a central register for my resources. So here I am over a year later finally writing about it.
The Central Register pattern (check out this page about it on geeksforgeeks, I like how NehalNavlani explains it) has with it one part that is you have a central place where you register resources, and you can lookup those resources from anywhere in your code base using an ID. See below for an example of what I wrote for lambda’s central register in CDK so I can register resources and have some error checking I’m not using the same names. I also have error checking to tell the code that a resources doesn’t exist from a provided ID, which can help prevent bad references.
Here’s the code that includes 3 main functions: addLambda, getLambda, and validateAndReturnLambdaArn:
Here we create a Lamba, and add it to the Lambda Central Register:
To use a Lambda in say a step function within the same stack, we can simply do getLambda with the same ID (This example of a different lambda from the above). The key is that we created the lambda somewhere else, but can easily reference it by just importing the getLambda function, and using the correct key:
This works great, reduces the need to do props drilling or adding a lot of properties to look up and keep track of in the stack class or environments that get passed in from one stack to another, but it can come with issues when deploying resources in multiple stacks: Deployment Deadlock. This issue I first saw explained by Lee Gilmore in one of his articles.
One of the key gotchas to watch out for when using the cross-stack reference approach is breaking your app’s deployment with deployment deadlock, which is usually shown with the following error “Export cannot be deleted as it is in use by another Stack”.
I run into this when using getLambda in an API Gateway stack that depends on a lambda in another stack. Then, either the lambda is renamed, deleted, or some other things can happen that cause a loop where the API Gateway stack cannot deploy before the Lambda stack does, but the Lambda stack won’t deploy because the API Gateway stack depends on it. A great solution to this I have found is to use instantiated references using the "by arn" method. Lots of CDK resources you can create have similar methods, so it doesn’t actually create a hard dependency between stacks. The resource does need to exist the first time you deploy, but after that you can delete or update either stack’s resources and the other won’t care about it. For Lambda, I created the validateAndReturnLambdaArn which acts just like getLambda, with error checking included, but returns the expected arn the lambda will have (not the one generated by CDK, which I believe would link them as dependencies...) so that the "Function.fromFunctionArn" can use it and be passed to API Gateway method creation functions:
Using this has been successful on my projects, and makes it very quick to create and use resources in the CDK, while also reducing headaches when deploying updates in pipelines. In the past with Deployment Deadlock, it was almost always a painful manual process to unlink the stacks, redeploy each, then link back together. Something I didn’t want to do in the CICD pipelines. Now the pipelines easily keep things up to date as they are updated in our code!
Please feel free to connect with me on LinkedIn, or to join the Believe in Serverless discord where there are over 1000 serverless enthusiasts ranging from just a few days in to people who literally wrote the book on the subject.
Images of code above are nice, but if you want to copy/paste here is the code in text:
import { Function } from "aws-cdk-lib/aws-lambda";
import { getPartitionIdentifier } from "../utilities";
const { region = "", account = "" } = process.env;
export const lambdaLookup: Map<string, Function> = new Map();
export const addLambda = (lambdaName: string, lambda: Function) => {
if (lambdaLookup.has(lambdaName)) {
throw Error(`Lambda ID has already been used for this deployment: ${lambdaName}`);
}
lambdaLookup.set(lambdaName, lambda);
};
export const getLambda = (lambdaName: string) => {
const lambda = lambdaLookup.get(lambdaName);
if (!lambda) {
throw Error(`Could not find lambda in lookup from ID: ${lambdaName}`);
}
return lambda;
};
export const validateAndReturnLambdaArn = (lambdaName: string) => {
const awsPartition = getPartitionIdentifier(region);
const lambdaExists = lambdaLookup.has(lambdaName);
if (!lambdaExists) {
throw Error(`Could not find lambda in lookup from ID: ${lambdaName}`);
}
return `arn:${awsPartition}:lambda:${region}:${account}:function:${lambdaName}`;
};
const functionName = `${deploymentType}-getQueryData`;
const lambdaInstance = new DockerImageFunction(construct, functionName, {
code: DockerImageCode.fromImageAsset(dockerfile),
functionName,
timeout: cdk.Duration.seconds(30),
memorySize: 256,
role,
loggingFormat:LoggingFormat.JSON,
});
addLambda(functionName, lambdaInstance);
return lambdaInstance;
export const createLambdaGetIntegration = ({
construct,
lambdaName,
methodName,
parentResource,
}: CreateLambdaPostIntegrationProps) => {
const resource = parentResource.addResource(methodName);
const lambdaIntegrateArn = validateAndReturnLambdaArn(lambdaName);
const lambdaToIntegrate = Function.fromFunctionArn(
construct,
`${parentResource.path}-GET-${lambdaName}`,
lambdaIntegrateArn
);
resource.addMethod("GET",new LambdaIntegration(lambdaToIntegrate));
};
new LambdaInvoke(construct, `${uniquePrefix}-update-processing-status`, {
stateName: `${uniquePrefix} - Update Processing Status`,
lambdaFunction: getLambda(`${deploymentType}-updateProcessingStatus`),
retryOnServiceExceptions: false,
resultPath: "$.updateProcessingStatus",
resultSelector: {
"payload.$": "$.Payload",
},
});