Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ npx nx cdk examples -- destroy --all #

To skip IAM approval prompts (e.g. in CI): add `--require-approval never` to deploy commands.

## Feature flags

CDK feature flags are declared, not inherited. [`cdk.json`](cdk.json) carries the context the CLI synthesises with, and [`src/app-context.ts`](src/app-context.ts) carries the same map for the tests — CDK applies CLI context _after_ an `App`'s `context` prop, so a divergence would leave the tests asserting a template CI never deploys. `test/app-context.test.ts` asserts the two stay in sync.

Examples are the pattern people copy, so a default left implicit here becomes a default inherited downstream — and CDK's unconfigured behaviour shifts between releases.

| Flag | Value | Why |
| ------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `@aws-cdk/core:defaultCrossStackReferences` | `"weak"` | Cross-stack consumers read the producer's output with `Fn::GetStackOutput` rather than importing a CloudFormation export. Strong references stop a producer from deleting an export a consumer still reads — worth having for a long-lived system, and the wrong trade for stacks CI deploys and destroys on every run. This is CDK's recommended value; unconfigured, CDK behaves as `"strong"` and warns that the choice was never made. |

Despite what the flag's own description says, it is not limited to cross-region references: a same-account, same-region consumer resolves to `Fn::GetStackOutput` under `"weak"` too, which is what [`ComposureCDK-MultiStackApiStack`](src/multi-stack-app.ts)'s snapshot shows.

**Migrating an existing system from strong to weak takes two deploys**, not one: set the flag to `"both"`, deploy everywhere, then set it to `"weak"`. A producer cannot drop an export while a consumer still imports it, so `"both"` keeps the export alive while consumers move across. Setting `"weak"` directly is only safe from a clean slate — which is what these examples are.

## Costs

These examples create minimal resources (Lambda functions, API Gateway endpoints, S3 buckets, CloudFront distributions, t3.micro EC2 instances) and should fall within the [AWS Free Tier](https://aws.amazon.com/free/) for the first 12 months. EC2 instances and VPC flow logs (CloudWatch Logs ingestion + storage) accrue charges once the free-tier window closes, so destroy stacks when done to avoid unexpected charges.
Expand Down
5 changes: 4 additions & 1 deletion packages/examples/cdk.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
{
"app": "node dist/bin/app.js"
"app": "node dist/bin/app.js",
"context": {
"@aws-cdk/core:defaultCrossStackReferences": "weak"
}
}
5 changes: 3 additions & 2 deletions packages/examples/src/agent-volume-app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { App, Size, Stack } from "aws-cdk-lib";
import { Size, Stack } from "aws-cdk-lib";
import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
import {
InstanceClass,
Expand All @@ -17,6 +17,7 @@ import {
type VpcBuilderResult,
} from "@composurecdk/ec2";
import { createTopicBuilder } from "@composurecdk/sns";
import { exampleApp } from "./app-context.js";

/**
* A VPC + EC2 instance with a persistent EBS data volume attached at
Expand All @@ -37,7 +38,7 @@ import { createTopicBuilder } from "@composurecdk/sns";
*
* NAT gateways are disabled to keep deploy/destroy fast and cheap.
*/
export function createAgentVolumeApp(app = new App()) {
export function createAgentVolumeApp(app = exampleApp()) {
const stack = new Stack(app, "ComposureCDK-AgentVolumeStack");

const { alerts } = compose(
Expand Down
55 changes: 55 additions & 0 deletions packages/examples/src/app-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { App, type AppProps } from "aws-cdk-lib";

/**
* The CDK context every example app synthesises with.
*
* `cdk.json` carries the same map so the CLI (`cdk synth`, `cdk deploy`) and
* the tests agree — the CLI passes its context through `CDK_CONTEXT_JSON`,
* which `App` applies *after* `props.context`, so an accidental divergence
* would silently give the tests one template and CI's deploy another. The
* `app-context` test asserts the two stay in sync.
*
* Feature flags are deliberately declared rather than inherited. The examples
* are the pattern users copy, so whatever they leave implicit becomes the
* pattern people inherit — and CDK's unconfigured defaults shift between
* releases (see issue #341).
*/
export const EXAMPLE_CONTEXT: Record<string, unknown> = {
/**
* Cross-stack references are weak: the consumer reads the producer's output
* with `Fn::GetStackOutput` instead of importing a CloudFormation export.
*
* `"weak"` is CDK's recommended value; unconfigured, CDK behaves as
* `"strong"` and warns that the choice was never made. Strong references
* block the producer from being deleted or from removing an export while a
* consumer still reads it — real protection for a long-lived system, and
* exactly the wrong trade for stacks CI deploys and tears down on every run.
*
* The flag is read from the **consuming** stack's context and, despite what
* its own description says, is not limited to cross-region references:
* same-account same-region consumers resolve to `Fn::GetStackOutput` under
* `"weak"` too, which is what the multi-stack example's snapshot shows.
*
* An already-deployed system migrating from strong must stage it —
* `"both"`, deploy everywhere, then `"weak"` — because the producer's
* exports cannot disappear while consumers still import them. Setting
* `"weak"` directly is only safe from a clean slate, which is what the
* examples are.
*/
"@aws-cdk/core:defaultCrossStackReferences": "weak",
};

/**
* Creates an `App` carrying {@link EXAMPLE_CONTEXT}.
*
* Every example defaults its `app` parameter to this rather than to a bare
* `new App()`, so a stack synthesised by its own test sees the same context
* the CLI supplies when CI deploys it.
*
* @param props - `App` props to merge; a `context` entry here wins over
* {@link EXAMPLE_CONTEXT}.
* @returns A new `App` configured with the examples' context.
*/
export function exampleApp(props: AppProps = {}): App {
return new App({ ...props, context: { ...EXAMPLE_CONTEXT, ...props.context } });
}
3 changes: 2 additions & 1 deletion packages/examples/src/apps.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { App } from "aws-cdk-lib";
import { exampleApp } from "./app-context.js";
import { cleanDeskPolicy } from "./clean-desk-policy.js";
import { createAgentVolumeApp } from "./agent-volume-app.js";
import { createCrudApiApp } from "./crud-api-app.js";
Expand All @@ -20,7 +21,7 @@ import { createTaggedSystemApp } from "./tagged-system-app.js";
* tests that assert across all stacks at once. Register a new example here
* and it is covered by both.
*/
export function buildExampleApp(app = new App()): App {
export function buildExampleApp(app = exampleApp()): App {
cleanDeskPolicy(app);

createAgentVolumeApp(app);
Expand Down
5 changes: 3 additions & 2 deletions packages/examples/src/crud-api-app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { App, Stack } from "aws-cdk-lib";
import { Stack } from "aws-cdk-lib";
import {
AwsIntegration,
PassthroughBehavior,
Expand All @@ -10,6 +10,7 @@ import { createRestApiBuilder } from "@composurecdk/apigateway";
import { createTableBuilder, tableGrants, type TableBuilderResult } from "@composurecdk/dynamodb";
import { createServiceRoleBuilder, type RoleBuilderResult } from "@composurecdk/iam";
import { createKeyBuilder, type KeyBuilderResult } from "@composurecdk/kms";
import { exampleApp } from "./app-context.js";

/** Every method here returns a bare `200` — no error mapping. A production
* API would add `selectionPattern` integration responses (e.g. matching
Expand Down Expand Up @@ -157,7 +158,7 @@ const DELETE_OPERATION = gadgetIntegration(
* cannot infer — a principal decrypting ciphertext it fetched elsewhere, say —
* and adding one here would be redundant permission, not extra safety.
*/
export function createCrudApiApp(app = new App()) {
export function createCrudApiApp(app = exampleApp()) {
const stack = new Stack(app, "ComposureCDK-CrudApiStack");

compose(
Expand Down
5 changes: 3 additions & 2 deletions packages/examples/src/dns-zone-app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { App, Duration, Fn } from "aws-cdk-lib";
import { Duration, Fn } from "aws-cdk-lib";
import { HttpOrigin } from "aws-cdk-lib/aws-cloudfront-origins";
import { Alpn, HttpsRecordValue } from "aws-cdk-lib/aws-route53";
import { compose, ref } from "@composurecdk/core";
Expand Down Expand Up @@ -28,6 +28,7 @@ import {
TXT,
zoneRecords,
} from "@composurecdk/route53/zone";
import { exampleApp } from "./app-context.js";

/**
* A production-like public DNS zone, expressed in the BIND-style zone DSL.
Expand All @@ -49,7 +50,7 @@ import {
* - Composing the record set with a hosted zone and surfacing the delegation
* name servers as a CloudFormation output
*/
export function createDnsZoneApp(app = new App()): void {
export function createDnsZoneApp(app = exampleApp()): void {
const { stack } = createStackBuilder()
.description("Public DNS zone (DSL example)")
// Route 53 query logging requires us-east-1 — see packages/route53/README.md.
Expand Down
5 changes: 3 additions & 2 deletions packages/examples/src/dual-function-app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { App, Duration, Stack } from "aws-cdk-lib";
import { Duration, Stack } from "aws-cdk-lib";
import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
import { Schedule } from "aws-cdk-lib/aws-events";
import { Code, Runtime, Tracing } from "aws-cdk-lib/aws-lambda";
Expand All @@ -7,6 +7,7 @@ import { alarmActionsPolicy } from "@composurecdk/cloudwatch";
import { createRuleBuilder, lambdaTarget } from "@composurecdk/events";
import { createFunctionBuilder, type FunctionBuilderResult } from "@composurecdk/lambda";
import { createTopicBuilder } from "@composurecdk/sns";
import { exampleApp } from "./app-context.js";

/**
* Two Lambda functions — an API handler and an async worker — composed
Expand All @@ -24,7 +25,7 @@ import { createTopicBuilder } from "@composurecdk/sns";
* - Routing every alarm (function + rule) to the alert topic via
* `alarmActionsPolicy`
*/
export function createDualFunctionApp(app = new App()) {
export function createDualFunctionApp(app = exampleApp()) {
const stack = new Stack(app, "ComposureCDK-DualFunctionStack");

const { alerts } = compose(
Expand Down
5 changes: 3 additions & 2 deletions packages/examples/src/dynamo-stream-processor-app.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { App, Duration, Stack } from "aws-cdk-lib";
import { Duration, Stack } from "aws-cdk-lib";
import { AttributeType, StreamViewType } from "aws-cdk-lib/aws-dynamodb";
import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
import { compose, ref } from "@composurecdk/core";
import { createTableV2Builder, type TableV2BuilderResult } from "@composurecdk/dynamodb";
import { createFunctionBuilder, dynamoEventSource } from "@composurecdk/lambda";
import { createQueueBuilder, type QueueBuilderResult } from "@composurecdk/sqs";
import { exampleApp } from "./app-context.js";

/**
* A DynamoDB table streaming change events to a Lambda processor, with a
Expand All @@ -25,7 +26,7 @@ import { createQueueBuilder, type QueueBuilderResult } from "@composurecdk/sqs";
* alarms (`IteratorAge`, failed-invocation, dropped-event) once the source is
* attached, and least-privilege `grantStreamRead` on the table's stream.
*/
export function createDynamoStreamProcessorApp(app = new App()) {
export function createDynamoStreamProcessorApp(app = exampleApp()) {
const stack = new Stack(app, "ComposureCDK-DynamoStreamProcessorStack");

compose(
Expand Down
5 changes: 3 additions & 2 deletions packages/examples/src/ec2-app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { App, Stack } from "aws-cdk-lib";
import { Stack } from "aws-cdk-lib";
import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
import {
type ISecurityGroup,
Expand All @@ -20,6 +20,7 @@ import {
type VpcBuilderResult,
} from "@composurecdk/ec2";
import { createTopicBuilder } from "@composurecdk/sns";
import { exampleApp } from "./app-context.js";

/**
* A VPC + two explicit security groups + an EC2 bastion host + an SNS
Expand Down Expand Up @@ -57,7 +58,7 @@ import { createTopicBuilder } from "@composurecdk/sns";
* service endpoints, or `Peer.anyIpv4()` if you only need internet
* egress and accept the wider blast radius).
*/
export function createEc2App(app = new App()) {
export function createEc2App(app = exampleApp()) {
const stack = new Stack(app, "ComposureCDK-Ec2Stack");

const { alerts } = compose(
Expand Down
4 changes: 2 additions & 2 deletions packages/examples/src/mock-api-app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { App } from "aws-cdk-lib";
import {
type Integration,
type MethodOptions,
Expand All @@ -8,6 +7,7 @@ import {
import { compose } from "@composurecdk/core";
import { createRestApiBuilder } from "@composurecdk/apigateway";
import { createStackBuilder } from "@composurecdk/cloudformation";
import { exampleApp } from "./app-context.js";

function jsonMock(statusCode: string, body: Record<string, unknown>): [Integration, MethodOptions] {
return [
Expand Down Expand Up @@ -49,7 +49,7 @@ function jsonMock(statusCode: string, body: Record<string, unknown>): [Integrati
* └── DELETE → { "id": "123", "deleted": true }
* ```
*/
export function createMockApiApp(app = new App()) {
export function createMockApiApp(app = exampleApp()) {
const { stack } = createStackBuilder()
.description("A mock CRUD API for demonstration")
.build(app, "ComposureCDK-MockApiStack");
Expand Down
9 changes: 6 additions & 3 deletions packages/examples/src/multi-stack-app.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { App, Duration } from "aws-cdk-lib";
import { Duration } from "aws-cdk-lib";
import { LambdaIntegration } from "aws-cdk-lib/aws-apigateway";
import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
import { compose, ref } from "@composurecdk/core";
import { createRestApiBuilder } from "@composurecdk/apigateway";
import { createStackBuilder } from "@composurecdk/cloudformation";
import { createFunctionBuilder, type FunctionBuilderResult } from "@composurecdk/lambda";
import { exampleApp } from "./app-context.js";

/**
* A REST API and its backing Lambda, split across two stacks using the
* compose-level stack map.
*
* Demonstrates:
* - Routing components to different stacks via {@link ComposedSystem.withStacks}
* - Cross-stack references resolved automatically by CDK
* - Cross-stack references resolved automatically by CDK — weakly, via
* `Fn::GetStackOutput`, because the app's context sets
* `@aws-cdk/core:defaultCrossStackReferences` (see `app-context.ts`)
* - Components without a stack mapping fall back to the default scope
* - `.copy()` for deriving stack variants from a shared base configuration
*/
export function createMultiStackApp(app = new App()) {
export function createMultiStackApp(app = exampleApp()) {
const baseStack = createStackBuilder().tag("project", "multi-stack-example");

const { stack: serviceStack } = baseStack
Expand Down
5 changes: 3 additions & 2 deletions packages/examples/src/neptune-graph-app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { App, Stack } from "aws-cdk-lib";
import { Stack } from "aws-cdk-lib";
import {
InstanceClass,
InstanceSize,
Expand All @@ -22,6 +22,7 @@ import {
} from "@composurecdk/ec2";
import { createClusterBuilder } from "@composurecdk/neptune";
import { InstanceType as NeptuneInstanceType } from "@aws-cdk/aws-neptune-alpha";
import { exampleApp } from "./app-context.js";

/**
* A VPC + a serverless Amazon Neptune cluster + an SSM-managed bastion that
Expand Down Expand Up @@ -56,7 +57,7 @@ import { InstanceType as NeptuneInstanceType } from "@aws-cdk/aws-neptune-alpha"
* this is a real-system exemplar. The CI deploy/destroy cycle flips those to
* allow teardown via `cleanDeskPolicy`, applied at the app level.
*/
export function createNeptuneGraphApp(app = new App()) {
export function createNeptuneGraphApp(app = exampleApp()) {
const stack = new Stack(app, "ComposureCDK-NeptuneGraphStack");

const ssmEndpointBase = createInterfaceEndpointBuilder()
Expand Down
5 changes: 3 additions & 2 deletions packages/examples/src/openapi-petstore-app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { App, Aws } from "aws-cdk-lib";
import { Aws } from "aws-cdk-lib";
import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
import { compose, ref } from "@composurecdk/core";
import { createSpecRestApiBuilder, inlineSpecDefinition } from "@composurecdk/apigateway";
Expand All @@ -9,6 +9,7 @@ import {
functionGrants,
type FunctionBuilderResult,
} from "@composurecdk/lambda";
import { exampleApp } from "./app-context.js";

/** The names the specification's integration refers its backend by — the shape
* a model-first export takes, written before the infrastructure exists. */
Expand Down Expand Up @@ -177,7 +178,7 @@ const petstoreSpec = {
* └── GET → { id, name: "Fido", … } (Lambda, via aws_proxy)
* ```
*/
export function createOpenApiPetstoreApp(app = new App()) {
export function createOpenApiPetstoreApp(app = exampleApp()) {
const { stack } = createStackBuilder()
.description("A PetStore API defined by an OpenAPI specification")
.build(app, "ComposureCDK-OpenApiPetstoreStack");
Expand Down
5 changes: 3 additions & 2 deletions packages/examples/src/order-processor-app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { App, Duration, Stack } from "aws-cdk-lib";
import { Duration, Stack } from "aws-cdk-lib";
import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
import { SqsSubscription } from "aws-cdk-lib/aws-sns-subscriptions";
Expand All @@ -7,6 +7,7 @@ import { alarmActionsPolicy } from "@composurecdk/cloudwatch";
import { createFunctionBuilder, sqsEventSource } from "@composurecdk/lambda";
import { createTopicBuilder } from "@composurecdk/sns";
import { createQueueBuilder, type QueueBuilderResult } from "@composurecdk/sqs";
import { exampleApp } from "./app-context.js";

/**
* Order intake fanned out through SNS to an SQS work queue, which feeds a
Expand Down Expand Up @@ -41,7 +42,7 @@ import { createQueueBuilder, type QueueBuilderResult } from "@composurecdk/sqs";
* - Composing the queues alongside `createTopicBuilder` and routing all
* alarm actions through `alarmActionsPolicy`
*/
export function createOrderProcessorApp(app = new App()) {
export function createOrderProcessorApp(app = exampleApp()) {
const stack = new Stack(app, "ComposureCDK-OrderProcessorStack");

const { alerts } = compose(
Expand Down
5 changes: 3 additions & 2 deletions packages/examples/src/static-website/app.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { App, Duration } from "aws-cdk-lib";
import { Duration } from "aws-cdk-lib";
import { SnsAction } from "aws-cdk-lib/aws-cloudwatch-actions";
import { Source } from "aws-cdk-lib/aws-s3-deployment";
import { HttpOrigin, S3BucketOrigin } from "aws-cdk-lib/aws-cloudfront-origins";
Expand All @@ -16,6 +16,7 @@ import {
createDistributionBuilder,
type DistributionBuilderResult,
} from "@composurecdk/cloudfront";
import { exampleApp } from "../app-context.js";

/**
* A static website hosted on S3 with CloudFront CDN, composed into a single stack.
Expand Down Expand Up @@ -45,7 +46,7 @@ import {
* Control
* ```
*/
export function createStaticWebsiteApp(app = new App()) {
export function createStaticWebsiteApp(app = exampleApp()) {
const { stack } = createStackBuilder()
.description("Static website hosted on S3 with CloudFront CDN")
.build(app, "ComposureCDK-StaticWebsiteStack");
Expand Down
Loading