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:

  1. TaggingAspect implements cdk.IAspect: This declares our class as a CDK Aspect.
  2. constructor(tagName: string, tagValue: string): We accept the tag key and value as constructor arguments, making the Aspect reusable for different tags.
  3. visit(node: IConstruct): void: This is the core method. It’s called for every IConstruct in the tree.
  4. 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., Stack itself, or logical constructs like Vpc that aggregate multiple resources).
  5. cdk.Tag.add(node, this.tagName, this.tagValue): If the node is taggable, we use cdk.Tag.add to 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 visit method 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 check node.node.constructor.name or use node 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.TagManager class. 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.

Want structured learning?

Take the full Cdk course →