To Build or to Reuse? A CDK Question
Learn how to make your architecture more flexible, by optionally reusing existing resources (Code included 😉).
Unless you are starting on a fresh application, the project you are working on will likely have resources already in use. One of the benefits of using CDK over other Infrastructure-as-Code tools is conditionals! Along with the ability to import resources via their ARN, we can create solutions that can use existing resources, or create new ones. This means we can deploy into both fresh and existing environments from the same codebase!
Getting Started
Let’s assume you have a CDK application made and have bootstrapped the target Region & Account. Starting with a new stack, we can begin to scaffold what we’ll need.
import { Stack , StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export interface NewStackProps extends StackProps {};
export default class NewStack extends Stack {
constructor(scope: Construct, id: string, props: NewStackProps) {
super(scope, id, props);
}
}
Working backward, we’ll say our example use case is to add a new lambda that processes data once it’s dropped in a bucket. For deploying into production, the bucket is already in use and managed by another project. But for the testing environment, there is no bucket. So we will need to make one.
To begin let’s add the lambda, as it’s a constant regardless of where it’s deployed. So the implementation is simple.
import { Stack , StackProps } from 'aws-cdk-lib';
import * as node from 'aws-cdk-lib/aws-lambda-nodejs';
import { Construct } from 'constructs';
export interface NewStackProps extends StackProps {};
export default class NewStack extends Stack {
public readonly bucketProcessor: node.NodeJsFunction;
constructor(scope: Construct, id: string, props: NewStackProps) {
super(scope, id, props);
this.bucketProcessor = new node.NodeJsFunction();
}
}
Easy enough!
Now onto the juicy stuff. We’ll add a property to the NewStackProps
interface to have the bucket ARN supplied externally.
import { Stack , StackProps } from 'aws-cdk-lib';
import * as node from 'aws-cdk-lib/aws-lambda-nodejs';
import { Construct } from 'constructs';
export interface NewStackProps extends StackProps {
// Mark the property as optional, this way we can _optionall_ build the bucket if it's not supplied
bucketArn?: string;
};
export default class NewStack extends Stack {
public readonly bucketProcessor: node.NodeJsFunction;
constructor(scope: Construct, id: string, props: NewStackProps) {
super(scope, id, props);
// pull the arn out into a construct-scoped variable for later use.
const { bucketArn } = props;
// Tip: For ultra clean code, you would want to add validation against this `bucketArn` string to confirm it is in fact an ARN.
this.bucketProcessor = new node.NodeJsFunction();
}
}
Now armed with the knowledge of “Do we have an existing bucket”, we can either make a new one or reuse the old one
import { Stack , StackProps } from 'aws-cdk-lib';
import * as node from 'aws-cdk-lib/aws-lambda-nodejs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';
export interface NewStackProps extends StackProps {
// Mark the property as optional, this way we can _optionall_ build the bucket if it's not supplied
bucketArn?: string;
};
export default class NewStack extends Stack {
public readonly bucketProcessor: node.NodeJsFunction;
constructor(scope: Construct, id: string, props: NewStackProps) {
super(scope, id, props);
const { bucketArn } = props;
this.bucketProcessor = new node.NodeJsFunction();
let bucket;
if (bucketArn) {
bucket = s3.Bucket.fromBucketArn(this, 'importedBucket', bucketArn);
} else {
bucket = s3.Bucket(this, 'createdBucket')
}
}
}
To wrap up, we’ll add a property to the stack so that the bucket can be referenced elsewhere in the CDK application and hook the lambda up to the bucket so it can process events.
import { Stack , StackProps } from 'aws-cdk-lib';
import * as node from 'aws-cdk-lib/aws-lambda-nodejs';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as s3n from 'aws-cdk-lib/aws-s3-notifications';
import { Construct } from 'constructs';
export interface NewStackProps extends StackProps {
// Mark the property as optional, this way we can _optionall_ build the bucket if it's not supplied
bucketArn?: string;
};
export default class NewStack extends Stack {
public readonly bucketProcessor: node.NodeJsFunction;
public readonly bucket: s3.IBucket;
/**
Use of the interface here is important. The `fromBucketArn` method returns an instance of the interface. But the new Bucket returns `s3.Bucket` which _implements_ the `IBucket` interface. So specificying the property as `IBucket` ensures that we can house _either_ the imported or created bucket. The tradeoff is that we lose access to some of the created buckets "fancier" features.
**/
constructor(scope: Construct, id: string, props: NewStackProps) {
super(scope, id, props);
const { bucketArn } = props;
this.bucketProcessor = new node.NodeJsFunction();
if (bucketArn) {
this.bucket = s3.Bucket.fromBucketArn(this, 'importedBucket', bucketArn);
} else {
this.bucket = s3.Bucket(this, 'createdBucket')
}
this.bucket.addEventNotification(s3.EventType.OBJECT_CREATED, new s3n.LambdaDestination(this.bucketProcessor), {prefix: 'some/object/prefix/*'});
}
}
Conclusion
With CDK, it’s possible now more than ever to reuse infrastructure patterns across environments. No matter the state the target environment is in. I would even consider this the tip of the iceberg! To go even further, you could make a custom L3 construct containing the reuse-or-build logic to simplify the process further! The possibilities are endless!