Skip to content

Commit 8ccdff4

Browse files
feat(aspects): priority-ordered aspect invocation (#32097)
Closes #21341 This feature was designed in [RFC648](aws/aws-cdk-rfcs#651) ### Reason for this change The current algorithm for invoking aspects (see invokeAspects in [synthesis.ts](https://github.com/aws/aws-cdk/blob/8b495f9ec157c0b00674715f62b1bbcabf2096ac/packages/aws-cdk-lib/core/lib/private/synthesis.ts#L217)) does not handle all use cases — specifically, when an Aspect adds a new node to the Construct tree and when Aspects are applied out of order. ### Description of changes This PR introduces a priority-based ordering system for aspects in the CDK to allow users to control the order in which aspects are applied on the construct tree. This PR also adds a stabilization loop for invoking aspects that can be enabled via the feature flag `@aws-cdk/core:aspectStabilization` - the stabilization loop ensures that newly added Aspects to the construct tree are visited and nested Aspects are invoked. ### Description of how you validated changes Plenty of unit tests - see `aspects.test.ts`. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 0e79874 commit 8ccdff4

File tree

8 files changed

+1359
-22
lines changed

8 files changed

+1359
-22
lines changed

packages/aws-cdk-lib/core/README.md

+142
Original file line numberDiff line numberDiff line change
@@ -1629,6 +1629,148 @@ scenarios are (non-exhaustive list):
16291629
valid)
16301630
- Warn if the user is using a deprecated API
16311631

1632+
## Aspects
1633+
1634+
[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
1635+
constructs in a construct tree. Common use cases include tagging resources, enforcing encryption on S3 Buckets, or applying specific security or
1636+
compliance rules to all resources in a stack.
1637+
1638+
Conceptually, there are two types of Aspects:
1639+
1640+
- **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
1641+
(for example, enforcing that all S3 Buckets have versioning enabled) and logging (for example, collecting information about all deployed resources for
1642+
audits or compliance).
1643+
- **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
1644+
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
1645+
no default is specified.
1646+
1647+
Here is a simple example of creating and applying an Aspect on a Stack to enable versioning on all S3 Buckets:
1648+
1649+
```ts
1650+
import { IAspect, IConstruct, Tags, Stack } from 'aws-cdk-lib';
1651+
1652+
class EnableBucketVersioning implements IAspect {
1653+
visit(node: IConstruct) {
1654+
if (node instanceof CfnBucket) {
1655+
node.versioningConfiguration = {
1656+
status: 'Enabled'
1657+
};
1658+
}
1659+
}
1660+
}
1661+
1662+
const app = new App();
1663+
const stack = new MyStack(app, 'MyStack');
1664+
1665+
// Apply the aspect to enable versioning on all S3 Buckets
1666+
Aspects.of(stack).add(new EnableBucketVersioning());
1667+
```
1668+
1669+
### Aspect Stabilization
1670+
1671+
The modern behavior is that Aspects automatically run on newly added nodes to the construct tree. This is controlled by the
1672+
flag `@aws-cdk/core:aspectStabilization`, which is default for new projects (since version 2.172.0).
1673+
1674+
The old behavior of Aspects (without stabilization) was that Aspect invocation runs once on the entire construct
1675+
tree. This meant that nested Aspects (Aspects that create new Aspects) are not invoked and nodes created by Aspects at a higher level of the construct tree are not visited.
1676+
1677+
To enable the stabilization behavior for older versions, use this feature by putting the following into your `cdk.context.json`:
1678+
1679+
```json
1680+
{
1681+
"@aws-cdk/core:aspectStabilization": true
1682+
}
1683+
```
1684+
1685+
### Aspect Priorities
1686+
1687+
Users can specify the order in which Aspects are applied on a construct by using the optional priority parameter when applying an Aspect. Priority
1688+
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.
1689+
1690+
By default, newly created nodes always inherit aspects. Priorities are mainly for ordering between mutating aspects on the construct tree.
1691+
1692+
CDK provides standard priority values for mutating and readonly aspects to help ensure consistency across different construct libraries:
1693+
1694+
```ts
1695+
/**
1696+
* Default Priority values for Aspects.
1697+
*/
1698+
export class AspectPriority {
1699+
/**
1700+
* Suggested priority for Aspects that mutate the construct tree.
1701+
*/
1702+
static readonly MUTATING: number = 200;
1703+
1704+
/**
1705+
* Suggested priority for Aspects that only read the construct tree.
1706+
*/
1707+
static readonly READONLY: number = 1000;
1708+
1709+
/**
1710+
* Default priority for Aspects that are applied without a priority.
1711+
*/
1712+
static readonly DEFAULT: number = 500;
1713+
}
1714+
```
1715+
1716+
If no priority is provided, the default value will be 500. This ensures that aspects without a specified priority run after mutating aspects but before
1717+
any readonly aspects.
1718+
1719+
Correctly applying Aspects with priority values ensures that mutating aspects (such as adding tags or resources) run before validation aspects. This allows users to avoid misconfigurations and ensure that the final
1720+
construct tree is fully validated before being synthesized.
1721+
1722+
### Applying Aspects with Priority
1723+
1724+
```ts
1725+
import { Aspects, Stack, IAspect, Tags } from 'aws-cdk-lib';
1726+
import { Bucket } from 'aws-cdk-lib/aws-s3';
1727+
1728+
class MyAspect implements IAspect {
1729+
visit(node: IConstruct) {
1730+
// Modifies a resource in some way
1731+
}
1732+
}
1733+
1734+
class ValidationAspect implements IAspect {
1735+
visit(node: IConstruct) {
1736+
// Perform some readonly validation on the cosntruct tree
1737+
}
1738+
}
1739+
1740+
const stack = new Stack();
1741+
1742+
Aspects.of(stack).add(new MyAspect(), { priority: AspectPriority.MUTATING } ); // Run first (mutating aspects)
1743+
Aspects.of(stack).add(new ValidationAspect(), { priority: AspectPriority.READONLY } ); // Run later (readonly aspects)
1744+
```
1745+
1746+
### Inspecting applied aspects and changing priorities
1747+
1748+
We also give customers the ability to view all of their applied aspects and override the priority on these aspects.
1749+
The `AspectApplication` class represents an Aspect that is applied to a node of the construct tree with a priority.
1750+
1751+
Users can access AspectApplications on a node by calling `applied` from the Aspects class as follows:
1752+
1753+
```ts
1754+
const app = new App();
1755+
const stack = new MyStack(app, 'MyStack');
1756+
1757+
Aspects.of(stack).add(new MyAspect());
1758+
1759+
let aspectApplications: AspectApplication[] = Aspects.of(root).applied;
1760+
1761+
for (const aspectApplication of aspectApplications) {
1762+
// The aspect we are applying
1763+
console.log(aspectApplication.aspect);
1764+
// The construct we are applying the aspect to
1765+
console.log(aspectApplication.construct);
1766+
// The priority it was applied with
1767+
console.log(aspectApplication.priority);
1768+
1769+
// Change the priority
1770+
aspectApplication.priority = 700;
1771+
}
1772+
```
1773+
16321774
### Acknowledging Warnings
16331775

16341776
If you would like to run with `--strict` mode enabled (warnings will throw

packages/aws-cdk-lib/core/lib/aspect.ts

+100-8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,39 @@ export interface IAspect {
1212
visit(node: IConstruct): void;
1313
}
1414

15+
/**
16+
* Default Priority values for Aspects.
17+
*/
18+
export class AspectPriority {
19+
/**
20+
* Suggested priority for Aspects that mutate the construct tree.
21+
*/
22+
static readonly MUTATING: number = 200;
23+
24+
/**
25+
* Suggested priority for Aspects that only read the construct tree.
26+
*/
27+
static readonly READONLY: number = 1000;
28+
29+
/**
30+
* Default priority for Aspects that are applied without a priority.
31+
*/
32+
static readonly DEFAULT: number = 500;
33+
}
34+
35+
/**
36+
* Options when Applying an Aspect.
37+
*/
38+
export interface AspectOptions {
39+
/**
40+
* The priority value to apply on an Aspect.
41+
* Priority must be a non-negative integer.
42+
*
43+
* @default - AspectPriority.DEFAULT
44+
*/
45+
readonly priority?: number;
46+
}
47+
1548
/**
1649
* Aspects can be applied to CDK tree scopes and can operate on the tree before
1750
* synthesis.
@@ -24,7 +57,7 @@ export class Aspects {
2457
public static of(scope: IConstruct): Aspects {
2558
let aspects = (scope as any)[ASPECTS_SYMBOL];
2659
if (!aspects) {
27-
aspects = new Aspects();
60+
aspects = new Aspects(scope);
2861

2962
Object.defineProperty(scope, ASPECTS_SYMBOL, {
3063
value: aspects,
@@ -35,24 +68,83 @@ export class Aspects {
3568
return aspects;
3669
}
3770

38-
private readonly _aspects: IAspect[];
71+
private readonly _scope: IConstruct;
72+
private readonly _appliedAspects: AspectApplication[];
3973

40-
private constructor() {
41-
this._aspects = [];
74+
private constructor(scope: IConstruct) {
75+
this._appliedAspects = [];
76+
this._scope = scope;
4277
}
4378

4479
/**
4580
* Adds an aspect to apply this scope before synthesis.
4681
* @param aspect The aspect to add.
82+
* @param options Options to apply on this aspect.
4783
*/
48-
public add(aspect: IAspect) {
49-
this._aspects.push(aspect);
84+
public add(aspect: IAspect, options?: AspectOptions) {
85+
this._appliedAspects.push(new AspectApplication(this._scope, aspect, options?.priority ?? AspectPriority.DEFAULT));
5086
}
5187

5288
/**
5389
* The list of aspects which were directly applied on this scope.
5490
*/
5591
public get all(): IAspect[] {
56-
return [...this._aspects];
92+
return this._appliedAspects.map(application => application.aspect);
93+
}
94+
95+
/**
96+
* The list of aspects with priority which were directly applied on this scope.
97+
*
98+
* Also returns inherited Aspects of this node.
99+
*/
100+
public get applied(): AspectApplication[] {
101+
return [...this._appliedAspects];
102+
}
103+
}
104+
105+
/**
106+
* Object respresenting an Aspect application. Stores the Aspect, the pointer to the construct it was applied
107+
* to, and the priority value of that Aspect.
108+
*/
109+
export class AspectApplication {
110+
/**
111+
* The construct that the Aspect was applied to.
112+
*/
113+
public readonly construct: IConstruct;
114+
115+
/**
116+
* The Aspect that was applied.
117+
*/
118+
public readonly aspect: IAspect;
119+
120+
/**
121+
* The priority value of this Aspect. Must be non-negative integer.
122+
*/
123+
private _priority: number;
124+
125+
/**
126+
* Initializes AspectApplication object
127+
*/
128+
public constructor(construct: IConstruct, aspect: IAspect, priority: number) {
129+
this.construct = construct;
130+
this.aspect = aspect;
131+
this._priority = priority;
132+
}
133+
134+
/**
135+
* Gets the priority value.
136+
*/
137+
public get priority(): number {
138+
return this._priority;
57139
}
58-
}
140+
141+
/**
142+
* Sets the priority value.
143+
*/
144+
public set priority(priority: number) {
145+
if (priority < 0) {
146+
throw new Error('Priority must be a non-negative number');
147+
}
148+
this._priority = priority;
149+
}
150+
}

0 commit comments

Comments
 (0)