Skip to content

Commit

Permalink
Merge branch 'main' into 26109-add-graphql-route53-target
Browse files Browse the repository at this point in the history
  • Loading branch information
mergify[bot] authored Nov 29, 2024
2 parents 947e3c1 + 8ccdff4 commit 17eb322
Show file tree
Hide file tree
Showing 10 changed files with 1,363 additions and 23 deletions.
142 changes: 142 additions & 0 deletions packages/aws-cdk-lib/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1629,6 +1629,148 @@ 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());
```

### Aspect Stabilization

The modern behavior is that Aspects automatically run on newly added nodes to the construct tree. This is controlled by the
flag `@aws-cdk/core:aspectStabilization`, which is default for new projects (since version 2.172.0).

The old behavior of Aspects (without stabilization) was that Aspect invocation runs once on the entire construct
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.

To enable the stabilization behavior for older versions, use this feature by putting the following into your `cdk.context.json`:

```json
{
"@aws-cdk/core:aspectStabilization": true
}
```

### Aspect Priorities

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.

By default, newly created nodes always inherit aspects. Priorities are mainly for ordering between mutating aspects on the construct tree.

CDK provides standard priority values for mutating and readonly aspects to help ensure consistency across different construct libraries:

```ts
/**
* 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 = 500;
}
```

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
any readonly aspects.

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
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)
```

### Inspecting applied aspects and changing priorities

We also give customers the ability to view all of their applied aspects and override the priority on these aspects.
The `AspectApplication` class represents an Aspect that is applied to a node of the construct tree with a priority.

Users can access AspectApplications on a node by calling `applied` 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).applied;
for (const aspectApplication of aspectApplications) {
// The aspect we are applying
console.log(aspectApplication.aspect);
// The construct we are applying the aspect to
console.log(aspectApplication.construct);
// The priority it was applied with
console.log(aspectApplication.priority);
// Change the priority
aspectApplication.priority = 700;
}
```

### Acknowledging Warnings

If you would like to run with `--strict` mode enabled (warnings will throw
Expand Down
108 changes: 100 additions & 8 deletions packages/aws-cdk-lib/core/lib/aspect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = 500;
}

/**
* 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.
Expand All @@ -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,
Expand All @@ -35,24 +68,83 @@ export class Aspects {
return aspects;
}

private readonly _aspects: IAspect[];
private readonly _scope: IConstruct;
private readonly _appliedAspects: AspectApplication[];

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 applied(): 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;
}
}
Loading

0 comments on commit 17eb322

Please sign in to comment.