Aspects
Aspects allow you to apply an operation to all constructs within a given scope. You may want to use them in your CDK for Terraform (CDKTF) application to mutate elements (e.g., add tags to cloud resources) or for validation (e.g., ensure all S3 Buckets are encrypted).
Define Aspects
To create an aspect, you must import the Aspects
class and the IAspect
interface and implement one or more methods for IAspect
. Then, you can call the aspect one or more times on any construct within your application.
Everything within a CDKTF application descends from the Construct
class, so you could call the construct on any instantiated element. This includes the entire application, a particular stack, or all of the resources for a specific provider. When you call the aspect, CDKTF applies its methods to all of the the constructs that fall within the specified scope.
The following example defines an aspect to add tags to resources.
import { Construct, IConstruct } from "constructs";
import { Aspects, IAspect, TerraformStack, App } from "cdktf";
import { AwsProvider } from "./.gen/providers/aws/provider";
import { S3Bucket } from "./.gen/providers/aws/s3-bucket";
// Not all constructs are taggable, so we need to filter it
type TaggableConstruct = IConstruct & {
tags?: { [key: string]: string };
tagsInput?: { [key: string]: string };
};
function isTaggableConstruct(x: IConstruct): x is TaggableConstruct {
return "tags" in x && "tagsInput" in x;
}
export class TagsAddingAspect implements IAspect {
constructor(private tagsToAdd: Record<string, string>) {}
// This method is called on every Construct within the specified scope (resources, data sources, etc.).
visit(node: IConstruct) {
if (isTaggableConstruct(node)) {
// We need to take the input value to not create a circular reference
const currentTags = node.tagsInput || {};
node.tags = { ...this.tagsToAdd, ...currentTags };
}
}
}
export class AspectTaggingStack extends TerraformStack {
constructor(scope: Construct, name: string) {
super(scope, name);
new AwsProvider(this, "aws", {
region: "us-west-2",
});
new S3Bucket(this, "bucket", {
bucket: "demo",
tags: {
owner: "cdktf",
},
});
// Add tags to every resource defined within this stack.
Aspects.of(this).add(new TagsAddingAspect({ createdBy: "cdktf" }));
}
}
public class TagsAddingAspect implements IAspect {
private final Map<String, String> tags;
public TagsAddingAspect(Map<String, String> tags) {
this.tags = tags;
}
public Method getInputTags(IConstruct node) {
Method getTagsInput;
try {
getTagsInput = node.getClass().getDeclaredMethod("getTagsInput");
} catch (NoSuchMethodException e) {
// Not all constructs are taggable, so we need to filter it
throw new RuntimeException("Resource " + node.getClass() + " is un-taggable");
}
getTagsInput.setAccessible(true);
return getTagsInput;
}
public Method getTagsSet(IConstruct node) {
Method setTags;
try {
setTags = node.getClass().getDeclaredMethod("setTags", Map.class);
} catch (NoSuchMethodException e) {
// Not all constructs are taggable, so we need to filter it
throw new RuntimeException("Resource " + node.getClass() + " is un-taggable");
}
setTags.setAccessible(true);
return setTags;
}
// This method is called on every Construct within the defined scope (resource,
// data sources, etc.).
public void visit(IConstruct node) {
Method getTagsInput = getInputTags(node);
Method setTags = getTagsSet(node);
Map<String, String> currentTags;
try {
// We need to take the input value to not create a circular reference
currentTags = (Map<String, String>) getTagsInput.invoke(node);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
try {
HashMap<String, String> newTags = new HashMap<>();
newTags.putAll(this.tags);
if (currentTags != null) {
newTags.putAll(currentTags);
}
setTags.invoke(node, newTags);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
// Add tags to every resource defined within `myStack`.
Aspects.of(bucket).add(new TagsAddingAspect(new HashMap<String, String>() {
{
put("createdBy", "cdktf");
}
}));
from constructs import Construct, IConstruct
from cdktf import TerraformStack, Aspects, IAspect
from imports.aws.instance import Instance
from imports.aws.provider import AwsProvider
@jsii.implements(IAspect)
class TagsAddingAspect:
def __init__(self, tagsToAdd):
self.tagsToAdd = tagsToAdd
# This method is called on every Construct within the specified scope (resources, data sources, etc.).
def visit(self, node: IConstruct):
# Not all constructs are taggable, so we need to check
if(hasattr(node, "tags") and hasattr(node, "tags_input")):
# We need to take the input value to not create a circular reference
currentTags = node.tags_input if node.tags_input is not None else {}
node.tags = {**self.tagsToAdd, **currentTags}
class MySingleStack(TerraformStack):
def __init__(self, scope: Construct, id: str):
super().__init__(scope, id)
AwsProvider(self, "aws",
region = "us-east-1"
)
Instance(self, "Hello",
ami = "ami-2757f631",
instance_type = "t2.micro"
)
# Add tags to every resource defined within `MySingleStack`.
Aspects.of(self).add(TagsAddingAspect({ "createdBy": "cdktf" }))
# Add tags to every resource defined within `myStack`.
Aspects.of(myStack).add(TagsAddingAspect({ "createdBy": "cdktf" }))
using System.Collections.Generic;
using Constructs;
using HashiCorp.Cdktf;
using aws.S3Bucket;
using aws.Provider;
using Amazon.JSII.Runtime;
namespace Examples
{
public class TagsAddingAspect : Amazon.JSII.Runtime.Deputy.DeputyBase, IAspect
{
private readonly IDictionary<string, string> tagsToAdd;
public TagsAddingAspect(IDictionary<string, string> tagsToAdd)
{
this.tagsToAdd = tagsToAdd;
}
public void Visit(IConstruct node)
{
var nodeType = node.GetType();
var tagsProperty = nodeType.GetProperty("Tags");
var tagsInputProperty = nodeType.GetProperty("TagsInput");
if (tagsProperty != null && tagsInputProperty != null)
{
var inputTags = (IDictionary<string, string>)tagsInputProperty.GetValue(node);
IDictionary<string, string> newTags;
if (inputTags == null)
{
newTags = tagsToAdd;
}
else
{
newTags = new Dictionary<string, string>(tagsToAdd);
foreach ((string key, string value) in inputTags)
{
newTags[key] = value;
}
}
tagsProperty.SetValue(node, newTags);
}
}
}
class MyAspectsStack : TerraformStack
{
public MyAspectsStack(Construct scope, string name) : base(scope, name)
{
new AwsProvider(this, "Aws", new AwsProviderConfig
{
Region = "us-east-1"
});
S3Bucket bucket = new S3Bucket(this, "bucket", new S3BucketConfig
{
Bucket = "demo"
});
Aspects.Of(this).Add(new TagsAddingAspect(new Dictionary<string, string> {
{ "createdBy", "cdktf" }
}));
}
}
}
package main
import (
"github.com/aws/constructs-go/constructs/v10"
"github.com/aws/jsii-runtime-go"
"github.com/hashicorp/terraform-cdk-go/cdktf"
aws "github.com/hashicorp/terraform-cdk/examples/go/documentation/generated/hashicorp/aws/provider"
"github.com/hashicorp/terraform-cdk/examples/go/documentation/generated/hashicorp/aws/s3bucket"
"golang.org/x/exp/maps"
)
type Taggable interface {
TagsInput() *map[string]*string
SetTags(val *map[string]*string)
}
type TagsAddingAspect struct {
Tags *map[string]*string
}
func (taa TagsAddingAspect) Visit(node constructs.IConstruct) {
if taggable, ok := node.(Taggable); ok {
existing := *taggable.TagsInput()
tags := *taa.Tags
maps.Copy(existing, tags) // requires Go 1.18
taggable.SetTags(&existing)
}
}
func NewTagsAddingAspect(tags *map[string]*string) *TagsAddingAspect {
return &TagsAddingAspect{Tags: tags}
}
func NewAspectsStack(scope constructs.Construct, name string) cdktf.TerraformStack {
stack := cdktf.NewTerraformStack(scope, &name)
aws.NewAwsProvider(stack, jsii.String("aws"), &aws.AwsProviderConfig{
Region: jsii.String("us-east-1"),
})
s3bucket.NewS3Bucket(stack, jsii.String("bucket"), &s3bucket.S3BucketConfig{
Bucket: jsii.String("demo"),
Tags: &map[string]*string{
"owner": jsii.String("cdktf"),
},
})
cdktf.Aspects_Of(stack).Add(
NewTagsAddingAspect(&map[string]*string{"createdBy": jsii.String("cdktf")}),
)
return stack
}
You can also use aspects for validation. The following example defines an aspect that checks whether all S3 Buckets start with the correct prefix.
import { Annotations, Aspects, IAspect, TerraformStack } from "cdktf";
import { Construct, IConstruct } from "constructs";
import { AwsProvider } from "./.gen/providers/aws/provider";
import { S3Bucket } from "./.gen/providers/aws/s3-bucket";
export class ValidateS3IsPrefixed implements IAspect {
constructor(private prefix: string) {}
// This method is called on every Construct within the defined scope (resource, data sources, etc.).
visit(node: IConstruct) {
if (node instanceof S3Bucket) {
if (node.bucketInput && !node.bucketInput.startsWith(this.prefix)) {
// You can include `addInfo`, `addWarning`, and `addError`.
// CDKTF prints these messages when the user runs `synth`, `plan`, or `deploy`.
Annotations.of(node).addError(
`Each S3 Bucket name needs to start with ${this.prefix}`
);
}
}
}
}
export class AspectValidationStack extends TerraformStack {
constructor(scope: Construct, name: string) {
super(scope, name);
new AwsProvider(this, "aws", {
region: "us-west-2",
});
new S3Bucket(this, "bucket", {
bucket: "myPrefixDemo",
});
Aspects.of(this).add(new ValidateS3IsPrefixed("myPrefix"));
}
}
public class ValidateS3IsPrefixed implements IAspect {
private final String prefix;
public ValidateS3IsPrefixed(String prefix) {
this.prefix = prefix;
}
// This method is called on every Construct within the defined scope (resource,
// data sources, etc.).
public void visit(IConstruct node) {
if (node instanceof S3Bucket) {
if (((S3Bucket) node).getBucketInput() != null && !((S3Bucket) node).getBucketInput().startsWith(this.prefix)) {
// You can include `addInfo`, `addWarning`, and `addError`.
// CDKTF prints these messages when the user runs `synth`, `plan`, or `deploy`.
Annotations.of(node).addError(
"Each S3 Bucket name needs to start with " + this.prefix);
}
}
}
}
// Check the prefix for every resource within `myStack`.
Aspects.of(bucket).add(new ValidateS3IsPrefixed("myPrefix"));
from constructs import IConstruct
from cdktf import Aspects, IAspect, Annotations
from imports.aws.s3_bucket import S3Bucket
@jsii.implements(IAspect)
class ValidateS3IsPrefixed:
def __init__(self, prefix: str):
self.prefix = prefix
# This method is called on every Construct within the defined scope (resource, data sources, etc.).
def visit(self, node: IConstruct):
if isinstance(node, S3Bucket):
if(node.bucket_input and not node.bucket_input.startswith(self.prefix)):
# You can include `addInfo`, `addWarning`, and `addError`.
# CDKTF prints these messages when the user runs `synth`, `plan`, or `deploy`.
Annotations.of(node).add_error(
"Each S3 Bucket name needs to start with {}".format(self.prefix)
)
# Check the prefix for every resource within `myStack`.
Aspects.of(myStack).add(ValidateS3IsPrefixed("myPrefix"))
using System.Collections.Generic;
using Constructs;
using HashiCorp.Cdktf;
using aws.S3Bucket;
using aws.Provider;
using Amazon.JSII.Runtime;
namespace Examples
{
public class ValidateS3IsPrefixed : Amazon.JSII.Runtime.Deputy.DeputyBase, IAspect
{
private readonly string prefix;
public ValidateS3IsPrefixed(string prefix)
{
this.prefix = prefix;
}
public void Visit(IConstruct node)
{
if (node is S3Bucket bucket)
{
if (bucket.BucketInput != null && !bucket.BucketInput.StartsWith(this.prefix))
{
Annotations.Of(node).AddError($"Each S3 Bucket name needs to start with {this.prefix}");
}
}
}
}
class MyAspectsValidationStack : TerraformStack
{
public MyAspectsValidationStack(Construct scope, string name) : base(scope, name)
{
new AwsProvider(this, "Aws", new AwsProviderConfig
{
Region = "us-east-1"
});
S3Bucket bucket = new S3Bucket(this, "bucket", new S3BucketConfig
{
Bucket = "myPrefixDemo"
});
Aspects.Of(this).Add(new ValidateS3IsPrefixed("myPrefix"));
}
}
}
package main
import (
"strings"
"github.com/aws/constructs-go/constructs/v10"
"github.com/aws/jsii-runtime-go"
"github.com/hashicorp/terraform-cdk-go/cdktf"
aws "github.com/hashicorp/terraform-cdk/examples/go/documentation/generated/hashicorp/aws/provider"
"github.com/hashicorp/terraform-cdk/examples/go/documentation/generated/hashicorp/aws/s3bucket"
)
type S3Bucket interface {
GetBucketInput() *string
}
type ValidateS3IsPrefixedAspect struct {
Prefix string
}
func (vpa ValidateS3IsPrefixedAspect) Visit(node constructs.IConstruct) {
if bucket, ok := node.(s3bucket.S3Bucket); ok && strings.HasPrefix(*bucket.BucketInput(), vpa.Prefix) {
cdktf.Annotations_Of(node).AddError(jsii.String("Each S3 Bucket name needs to start with " + vpa.Prefix))
}
}
func NewValidateS3IsPrefixedAspect(prefix string) *ValidateS3IsPrefixedAspect {
return &ValidateS3IsPrefixedAspect{Prefix: prefix}
}
func NewPrefixAspectsStack(scope constructs.Construct, name string) cdktf.TerraformStack {
stack := cdktf.NewTerraformStack(scope, &name)
aws.NewAwsProvider(stack, jsii.String("aws"), &aws.AwsProviderConfig{
Region: jsii.String("us-east-1"),
})
s3bucket.NewS3Bucket(stack, jsii.String("bucket"), &s3bucket.S3BucketConfig{
Bucket: jsii.String("myPrefixDemo"),
})
cdktf.Aspects_Of(stack).Add(NewValidateS3IsPrefixedAspect("myPrefix"))
return stack
}