CDK Aspects are a powerful mechanism to inject common logic across your entire CDK stack, and a common use case is automatically tagging resources for cost allocation, security, or operational tracking.
Imagine you have a CDK application with dozens of stacks, and each stack defines numerous resources like S3 buckets, EC2 instances, and Lambda functions. You need to ensure every single one of these resources has a specific tag, say Environment: Production, for billing purposes. Manually adding this tag to every resource definition would be tedious, error-prone, and difficult to maintain. This is where Aspects shine.
Here’s a simple CDK application structure:
.
├── bin
│ └── my-cdk-app.ts
├── lib
│ ├── my-cdk-app-stack.ts
│ └── another-stack.ts
└── package.json
And the stacks might look something like this:
lib/my-cdk-app-stack.ts:
import * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';
export class MyCdkAppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new s3.Bucket(this, 'MySampleBucket');
}
}
lib/another-stack.ts:
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export class AnotherStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
new lambda.Function(this, 'MySampleLambda', {
runtime: lambda.Runtime.NODEJS_18_X,
handler: 'index.handler',
code: lambda.Code.fromInline('exports.handler = async (event) => { console.log("Hello from Lambda!"); };'),
});
}
}
And in bin/my-cdk-app.ts:
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { MyCdkAppStack } from '../lib/my-cdk-app-stack';
import { AnotherStack } from '../lib/another-stack';
const app = new cdk.App();
new MyCdkAppStack(app, 'MyCdkAppStack');
new AnotherStack(app, 'AnotherStack');
Without an Aspect, to add a tag to MySampleBucket and MySampleLambda, you’d modify their respective resource definitions:
// In MyCdkAppStack
new s3.Bucket(this, 'MySampleBucket', {
tags: [{ key: 'Environment', value: 'Production' }]
});
// In AnotherStack
new lambda.Function(this, 'MySampleLambda', {
// ... other properties
tags: { Environment: 'Production' } // Note: Lambda tags are an object, not an array of objects
});
This quickly becomes unmanageable.
How Aspects Work
An Aspect in CDK is an interface that you can implement to visit every construct in your application’s construct tree. When you apply an Aspect to an App or a Stack, the CDK traverses the tree starting from that point and calls the visit method for every construct it encounters.
Inside the visit method, you can inspect the construct and, if it’s a type you want to modify (like an S3 Bucket or Lambda Function), you can add or modify its properties. The key is that you can do this declaratively without altering the original construct’s definition.
Implementing the Tagging Aspect
First, create a new file for your Aspect, e.g., lib/tagging-aspect.ts:
import * as cdk from 'aws-cdk-lib';
import { IConstruct } from 'constructs';
export class TaggingAspect implements cdk.IAspect {
private readonly tagName: string;
private readonly tagValue: string;
constructor(tagName: string, tagValue: string) {
this.tagName = tagName;
this.tagValue = tagValue;
}
public visit(node: IConstruct): void {
// Check if the construct has tags and is a resource we want to tag
// We are looking for constructs that implement CfnResource, which is the base
// for most AWS resources in CDK.
if (cdk.Tag.isTaggable(node)) {
cdk.Tag.add(node, this.tagName, this.tagValue);
}
}
}
Explanation:
TaggingAspect implements cdk.IAspect: This declares our class as a CDK Aspect.constructor(tagName: string, tagValue: string): We accept the tag key and value as constructor arguments, making the Aspect reusable for different tags.visit(node: IConstruct): void: This is the core method. It’s called for everyIConstructin the tree.cdk.Tag.isTaggable(node): This is a utility function that checks if a given construct is capable of having tags applied to it. This is crucial because not all constructs are directly taggable (e.g.,Stackitself, or logical constructs likeVpcthat aggregate multiple resources).cdk.Tag.add(node, this.tagName, this.tagValue): If the node is taggable, we usecdk.Tag.addto apply our specified tag. This function intelligently handles different resource types and their specific tag formats.
Applying the Aspect
Now, modify your bin/my-cdk-app.ts file to apply the Aspect:
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { MyCdkAppStack } from '../lib/my-cdk-app-stack';
import { AnotherStack } from '../lib/another-stack';
import { TaggingAspect } from '../lib/tagging-aspect'; // Import the Aspect
const app = new cdk.App();
const stack1 = new MyCdkAppStack(app, 'MyCdkAppStack');
const stack2 = new AnotherStack(app, 'AnotherStack');
// Apply the Aspect to the entire app
cdk.Aspects.of(app).add(new TaggingAspect('Environment', 'Production'));
// Alternatively, you could apply it to specific stacks:
// cdk.Aspects.of(stack1).add(new TaggingAspect('Environment', 'Production'));
// cdk.Aspects.of(stack2).add(new TaggingAspect('Environment', 'Production'));
When you run cdk synth, the generated CloudFormation template will now include the Environment: Production tag on all resources that are taggable within MyCdkAppStack and AnotherStack.
Example CloudFormation Snippet (for MySampleBucket):
Resources:
MySampleBucketE7C0F825:
Type: AWS::S3::Bucket
Properties:
Tags:
- Key: Environment
Value: Production
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
# ... other resources
Example CloudFormation Snippet (for MySampleLambda):
Resources:
MySampleLambda3C28A717:
Type: AWS::Lambda::Function
Properties:
FunctionName: AnotherStackMySampleLambdaXXXXXX # Example generated name
Runtime: nodejs18.x
Handler: index.handler
Code:
ZipFile: exports.handler = async (event) => { console.log("Hello from Lambda!"); };
Tags:
- Key: Environment
Value: Production
# ... other resources
Advanced Considerations
-
Filtering Constructs: The
visitmethod can be more sophisticated. You might only want to tag specific resource types, or resources that don’t already have a particular tag. You can checknode.node.constructor.nameor usenode instanceof SomeResourceClass. For example, to only tag S3 Buckets:import * as s3 from 'aws-cdk-lib/aws-s3'; // ... inside visit method if (node instanceof s3.Bucket) { cdk.Tag.add(node, this.tagName, this.tagValue); } -
Conditional Tagging: You can add logic to skip tagging based on existing tags or other conditions.
// ... inside visit method if (cdk.Tag.isTaggable(node)) { const existingTags = cdk.TagManager.getTaggable(node).tags; if (!existingTags.hasTag('SkipTagging')) { // Assuming a 'SkipTagging' tag exists cdk.Tag.add(node, this.tagName, this.tagValue); } } -
Tag Management: For complex tagging strategies, consider using the
cdk.TagManagerclass. It provides methods to merge, remove, and inspect tags, which can be useful when dealing with tags that might be defined elsewhere or need to be overridden. -
Aspect Order: If you have multiple Aspects, their order of execution matters if they modify the same properties. The CDK applies Aspects in the order they are added.
-
Construct Metadata: Aspects can also be used to add metadata to constructs, which can be consumed by other parts of your CDK application or by custom tooling.
By using Aspects, you centralize your tagging strategy, ensuring consistency and reducing boilerplate code across your entire CDK application. This makes your infrastructure more manageable and easier to audit.
The next common pattern after automated tagging is implementing automated security checks or compliance policies using Aspects.