-
Notifications
You must be signed in to change notification settings - Fork 4.3k
feat(aspects): priority-ordered aspect invocation #32097
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 64 commits
9b9ce2f
6e6cb16
76c923b
ec1d7b0
c88f355
af5522b
e8388eb
3a7d05a
70a437d
12d54f9
6123866
5e016f6
549fbbd
7e91acb
8f208b1
40f2a82
0786786
477deaa
9706de9
2341245
99910e9
09f8643
f4816be
8141a0e
a1c9642
f905bb8
5f7fd33
dad0462
477d003
3ad2afe
2fee57c
13f0327
6b39626
27d14d5
2ab5c8c
eaa4675
83bcd50
7d8f829
9a6a8c1
3c08fa1
4254865
063acd2
c650bd0
ee9a93c
dafc3aa
e2cb8d2
018cc41
320d664
1df18e3
aaef83f
184bd70
67398c3
6d0dd20
81d458e
ff1df34
d3409da
e4ef96c
169a768
1e325dd
8161bfc
88dd0a9
b4f80bb
8694931
fd47e1d
654c429
cee54b8
42f6de2
ff7263b
76c8bc4
0f0b59c
0dda439
f8e7d95
23c0f49
3e62ef7
c156482
ed93f22
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1629,6 +1629,175 @@ scenarios are (non-exhaustive list): | |
| valid) | ||
| - Warn if the user is using a deprecated API | ||
|
|
||
| ## Aspects | ||
|
|
||
| [Aspects](https://docs.aws.amazon.com/cdk/v2/guide/aspects.html) is a feature in CDK that allows you to apply operations or transformations across all | ||
| constructs in a construct tree. Common use cases include tagging resources, enforcing encryption on S3 Buckets, or applying specific security or | ||
| compliance rules to all resources in a stack. | ||
|
|
||
| Conceptually, there are two types of Aspects: | ||
|
|
||
| * **Read-only aspects** scan the construct tree but do not make changes to the tree. Common use cases of read-only aspects include performing validations | ||
| (for example, enforcing that all S3 Buckets have versioning enabled) and logging (for example, collecting information about all deployed resources for | ||
| audits or compliance). | ||
| * **Mutating aspects** either (1.) add new nodes or (2.) mutate existing nodes of the tree in-place. One commonly used mutating Aspect is adding Tags to | ||
| resources. An example of an Aspect that adds a node is one that automatically adds a security group to every EC2 instance in the construct tree if | ||
| no default is specified. | ||
|
|
||
| Here is a simple example of creating and applying an Aspect on a Stack to enable versioning on all S3 Buckets: | ||
|
|
||
| ```ts | ||
| import { IAspect, IConstruct, Tags, Stack } from 'aws-cdk-lib'; | ||
|
|
||
| class EnableBucketVersioning implements IAspect { | ||
| visit(node: IConstruct) { | ||
| if (node instanceof CfnBucket) { | ||
| node.versioningConfiguration = { | ||
| status: 'Enabled' | ||
| }; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const app = new App(); | ||
| const stack = new MyStack(app, 'MyStack'); | ||
|
|
||
| // Apply the aspect to enable versioning on all S3 Buckets | ||
| Aspects.of(stack).add(new EnableBucketVersioning()); | ||
| ``` | ||
|
|
||
| Users can specify the order in which Aspects are applied on a construct by using the optional priority parameter when applying an Aspect. Priority | ||
| values must be non-negative integers, where a higher number means the Aspect will be applied later, and a lower number means it will be applied sooner. | ||
|
|
||
| CDK provides standard priority values for mutating and readonly aspects to help ensure consistency across different construct libraries: | ||
|
|
||
| ```ts | ||
| const MUTATING_PRIORITY = 200; | ||
| const READONLY_PRIORITY = 1000; | ||
| const DEFAULT_PRIORITY = 600; | ||
|
||
| ``` | ||
|
|
||
| If no priority is provided, the default value will be 600. This ensures that aspects without a specified priority run after mutating aspects but before | ||
| any readonly aspects. | ||
|
|
||
| Correctly applying Aspects with priority values ensures that mutating aspects (such as adding tags or resources) run before validation aspects, and new | ||
| nodes created by mutating aspects inherit aspects from their parent constructs. This allows users to avoid misconfigurations and ensure that the final | ||
sumupitchayan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| construct tree is fully validated before being synthesized. | ||
|
|
||
| ### Applying Aspects with Priority | ||
|
|
||
| ```ts | ||
| import { Aspects, Stack, IAspect, Tags } from 'aws-cdk-lib'; | ||
| import { Bucket } from 'aws-cdk-lib/aws-s3'; | ||
|
|
||
| class MyAspect implements IAspect { | ||
| visit(node: IConstruct) { | ||
| // Modifies a resource in some way | ||
| } | ||
| } | ||
|
|
||
| class ValidationAspect implements IAspect { | ||
| visit(node: IConstruct) { | ||
| // Perform some readonly validation on the cosntruct tree | ||
| } | ||
| } | ||
|
|
||
| const stack = new Stack(); | ||
|
|
||
| Aspects.of(stack).add(new MyAspect(), { priority: AspectPriority.MUTATING } ); // Run first (mutating aspects) | ||
| Aspects.of(stack).add(new ValidationAspect(), { priority: AspectPriority.READONLY } ); // Run later (readonly aspects) | ||
| ``` | ||
|
|
||
| We also give customers the ability to view all of their applied aspects and override the priority on these aspects. | ||
sumupitchayan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| The `AspectApplication` class to represents an Aspect that is applied to a node of the construct tree with a priority: | ||
|
|
||
| ```ts | ||
| /** | ||
| * Object respresenting an Aspect application. Stores the Aspect, the pointer to the construct it was applied | ||
| * to, and the priority value of that Aspect. | ||
| */ | ||
| export class AspectApplication { | ||
| /** | ||
| * The construct that the Aspect was applied to. | ||
| */ | ||
| public readonly construct: IConstruct; | ||
|
|
||
| /** | ||
| * The Aspect that was applied. | ||
| */ | ||
| public readonly aspect: IAspect; | ||
|
|
||
| /** | ||
| * The priority value of this Aspect. Must be non-negative integer. | ||
| */ | ||
| private priority: number; | ||
sumupitchayan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| ``` | ||
sumupitchayan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| Users can access AspectApplications on a node by calling `list` from the Aspects class as follows: | ||
|
|
||
| ```ts | ||
| const app = new App(); | ||
| const stack = new MyStack(app, 'MyStack'); | ||
|
|
||
| Aspects.of(stack).add(new MyAspect()); | ||
|
|
||
| let aspectApplications: AspectApplication[] = Aspects.of(root).list; | ||
| ``` | ||
|
|
||
| #### Aspects with Third-Party Constructs | ||
sumupitchayan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| When a third-party construct adds and applies its own aspect, we can override that Aspect priority like so: | ||
|
|
||
| ```ts | ||
| // Import third-party aspect | ||
| import { ThirdPartyConstruct } from 'some-library'; | ||
|
|
||
| const stack: Stack; | ||
| const construct = new ThirdPartyConstruct(stack, 'third-party-construct'); | ||
|
|
||
| // Author's aspect - adding to the stack | ||
| const validationAspect = new ValidationAspect(); | ||
| Aspects.of(stack).add(validationAspect, { priority: AspectPriority.READONLY } ); // Run later (validation) | ||
|
|
||
| // Getting the Aspect from the ThirdPartyConstruct | ||
| const thirdPartyAspectApplication: AspectApplication = Aspects.of(construct).list[0]; | ||
| // Overriding the Aspect Priority from the ThirdPartyConstruct to run first | ||
| thirdPartyAspectApplication.priority = 0; | ||
| ``` | ||
|
|
||
| An important thing to note about the `list` function is that it will not return Aspects that are applied to a node by another | ||
| Aspect - these Aspects are only added to the construct tree when `invokeAspects` is called during synthesis. | ||
|
|
||
| When using aspects from a library but controlling their application: | ||
|
|
||
| ```ts | ||
| // Import third-party aspect | ||
| import { SecurityAspect } from 'some-library'; | ||
|
|
||
| const stack: Stack; | ||
|
|
||
| // Application author has full control of ordering | ||
| const securityAspect = new SecurityAspect(); | ||
| Aspects.of(stack).add(securityAspect, { priority: 50 } ); | ||
|
|
||
| // Add own aspects in relation to third-party one | ||
| Aspects.of(stack).add(new MyOtherAspect(), { priority: 75 } ); | ||
| ``` | ||
|
|
||
| In all scenarios, application authors can use priority values to ensure their aspects run in the desired order relative to other aspects, whether | ||
| those are their own or from third-party libraries. The standard priority ranges (200 for mutating, 600 default, 1000 for readonly) provide | ||
| guidance while still allowing full flexibility through custom priority values. | ||
|
|
||
| ### Aspect Stabilization | ||
sumupitchayan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| By default, Aspect invocation runs once on the construct tree. This means that nested Aspects (Aspects that create | ||
| new Aspects) are not invoked and nodes created by Aspects at a higher level of the construct tree will not be visited. | ||
|
|
||
| Using the `@aws-cdk/core:aspectStabilization` feature flag (or passing in `{aspectStabilization: true}` when calling | ||
| `synth()`) will run a stabilization loop when invoking aspects to allow Aspects created by other Aspects to be invoked | ||
| and to ensure that all new nodes created on the construct tree are visited and invoke their inherited Aspects. | ||
|
|
||
| ### Acknowledging Warnings | ||
|
|
||
| If you would like to run with `--strict` mode enabled (warnings will throw | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,39 @@ export interface IAspect { | |
| visit(node: IConstruct): void; | ||
| } | ||
|
|
||
| /** | ||
| * Default Priority values for Aspects. | ||
| */ | ||
| export class AspectPriority { | ||
| /** | ||
| * Suggested priority for Aspects that mutate the construct tree. | ||
| */ | ||
| static readonly MUTATING: number = 200; | ||
|
|
||
| /** | ||
| * Suggested priority for Aspects that only read the construct tree. | ||
| */ | ||
| static readonly READONLY: number = 1000; | ||
|
|
||
| /** | ||
| * Default priority for Aspects that are applied without a priority. | ||
| */ | ||
| static readonly DEFAULT: number = 600; | ||
| } | ||
|
|
||
| /** | ||
| * Options when Applying an Aspect. | ||
| */ | ||
| export interface AspectOptions { | ||
| /** | ||
| * The priority value to apply on an Aspect. | ||
| * Priority must be a non-negative integer. | ||
| * | ||
| * @default - AspectPriority.DEFAULT | ||
| */ | ||
| readonly priority?: number; | ||
| } | ||
|
|
||
| /** | ||
| * Aspects can be applied to CDK tree scopes and can operate on the tree before | ||
| * synthesis. | ||
|
|
@@ -24,7 +57,7 @@ export class Aspects { | |
| public static of(scope: IConstruct): Aspects { | ||
| let aspects = (scope as any)[ASPECTS_SYMBOL]; | ||
| if (!aspects) { | ||
| aspects = new Aspects(); | ||
| aspects = new Aspects(scope); | ||
|
|
||
| Object.defineProperty(scope, ASPECTS_SYMBOL, { | ||
| value: aspects, | ||
|
|
@@ -35,24 +68,83 @@ export class Aspects { | |
| return aspects; | ||
| } | ||
|
|
||
| private readonly _aspects: IAspect[]; | ||
| private readonly _scope: IConstruct; | ||
| private readonly _appliedAspects: AspectApplication[]; | ||
sumupitchayan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private constructor() { | ||
| this._aspects = []; | ||
| private constructor(scope: IConstruct) { | ||
| this._appliedAspects = []; | ||
| this._scope = scope; | ||
| } | ||
|
|
||
| /** | ||
| * Adds an aspect to apply this scope before synthesis. | ||
| * @param aspect The aspect to add. | ||
| * @param options Options to apply on this aspect. | ||
| */ | ||
| public add(aspect: IAspect) { | ||
| this._aspects.push(aspect); | ||
| public add(aspect: IAspect, options?: AspectOptions) { | ||
| this._appliedAspects.push(new AspectApplication(this._scope, aspect, options?.priority ?? AspectPriority.DEFAULT)); | ||
| } | ||
|
|
||
| /** | ||
| * The list of aspects which were directly applied on this scope. | ||
| */ | ||
| public get all(): IAspect[] { | ||
| return [...this._aspects]; | ||
| return this._appliedAspects.map(application => application.aspect); | ||
| } | ||
|
|
||
| /** | ||
| * The list of aspects with priority which were directly applied on this scope. | ||
| * | ||
| * Also returns inherited Aspects of this node. | ||
| */ | ||
| public get list(): AspectApplication[] { | ||
|
||
| return [...this._appliedAspects]; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Object respresenting an Aspect application. Stores the Aspect, the pointer to the construct it was applied | ||
| * to, and the priority value of that Aspect. | ||
| */ | ||
| export class AspectApplication { | ||
| /** | ||
| * The construct that the Aspect was applied to. | ||
| */ | ||
| public readonly construct: IConstruct; | ||
|
|
||
| /** | ||
| * The Aspect that was applied. | ||
| */ | ||
| public readonly aspect: IAspect; | ||
|
|
||
| /** | ||
| * The priority value of this Aspect. Must be non-negative integer. | ||
| */ | ||
| private _priority: number; | ||
|
|
||
| /** | ||
| * Initializes AspectApplication object | ||
| */ | ||
| public constructor(construct: IConstruct, aspect: IAspect, priority: number) { | ||
| this.construct = construct; | ||
| this.aspect = aspect; | ||
| this._priority = priority; | ||
| } | ||
|
|
||
| /** | ||
| * Gets the priority value. | ||
| */ | ||
| public get priority(): number { | ||
| return this._priority; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Sets the priority value. | ||
| */ | ||
| public set priority(priority: number) { | ||
| if (priority < 0) { | ||
| throw new Error('Priority must be a non-negative number'); | ||
| } | ||
| this._priority = priority; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.