aws-cdk-lambdaless-custom-resource
v0.0.19
Published
AWS CDK construct library for creating CloudFormation custom resources without Lambda functions, using Step Functions instead
Maintainers
Readme
AWS CDK Lambdaless Custom Resource
AWS CDK construct library for creating CloudFormation custom resources without Lambda functions, using Step Functions instead.
Why?
When building custom resources with AWS CDK, Lambda functions are the standard approach. But Lambda runtimes reach end-of-life, and you'll start receiving EOL notifications for stacks you deployed long ago and forgot about.
If it's your own custom resource, you can update it. But what about custom resources embedded in construct libraries someone else wrote? When the EOL notice arrives, updating them can be a hassle.
This library uses AWS Step Functions instead of Lambda, eliminating runtime maintenance entirely. Step Functions has no runtime EOL — once deployed, it just works.
When to use?
- Stacks you want to deploy and forget
- Sample code or workshops you publish and don't actively maintain
- Construct libraries you distribute to others
In these cases, using this library means neither you nor your users will ever receive Lambda runtime EOL notifications.
How expressive is it?
Step Functions is less flexible than Lambda, but with AWS SDK integrations, HTTP API calls, and JSONata expressions, you can handle most use cases. See the examples for real-world patterns.
Installation
npm install aws-cdk-lambdaless-custom-resourceUsage
Basic Example
import { CustomResourceFlow, LambdalessCustomResource } from 'aws-cdk-lambdaless-custom-resource';
import { Pass, StateMachine, DefinitionBody } from 'aws-cdk-lib/aws-stepfunctions';
import * as cdk from 'aws-cdk-lib';
const flow = new CustomResourceFlow(this, 'ExampleFlow', {
onCreate: Pass.jsonata(this, 'Create', {
outputs: {
PhysicalResourceId: 'example-id',
Data: {
message: '{% $states.input.ResourceProperties.myProperty %}',
},
},
}),
onUpdate: Pass.jsonata(this, 'Update', {
outputs: {
Data: {
message: '{% $states.input.ResourceProperties.myProperty %}',
},
},
}),
onDelete: Pass.jsonata(this, 'Delete'),
});
const customResource = new LambdalessCustomResource(this, 'MyCustomResource', {
stateMachine: new StateMachine(this, 'StateMachine', {
definitionBody: DefinitionBody.fromChainable(flow),
}),
properties: {
myProperty: 'Hello, World!',
},
});
// Access custom resource attributes
new cdk.CfnOutput(this, 'Message', {
value: customResource.getAttString('message'),
});Examples
See the examples directory for real-world patterns.
ASL Templates
The templates directory contains the ASL (Amazon States Language) definition used by the internal orchestrator. This can be a useful reference when building your own state machines.
State Machine Requirements
The state machine you pass to LambdalessCustomResource is invoked by the internal orchestrator whenever CloudFormation sends a Create, Update, or Delete event. You can use CustomResourceFlow to route these events, or build your own state machine from scratch.
Input
Your state machine receives the following input. When using CustomResourceFlow, these are also available as variables (e.g. $RequestType).
| Variable | Description |
|---|---|
| $RequestType | "Create", "Update", or "Delete" |
| $ResourceProperties | Properties passed to the custom resource |
| $OldResourceProperties | Previous properties (Update only, otherwise null) |
| $PhysicalResourceId | Physical resource ID (Create: null) |
| $LogicalResourceId | Logical resource ID |
| $StackId | CloudFormation stack ID |
| $RequestId | Unique request ID |
Output
Your state machine must return a JSON object. The orchestrator uses this to respond to CloudFormation on your behalf.
{
"PhysicalResourceId": "unique-id",
"Data": {
"key1": "value1",
"key2": "value2"
},
"NoEcho": false
}| Field | Required | Description |
|---|---|---|
| PhysicalResourceId | Yes (on Create) | Unique identifier for the resource. If omitted, RequestId is used. |
| Data | No | Key-value pairs accessible via getAtt() / getAttString(). Nested objects are flattened with dot notation. |
| NoEcho | No | If true, masks the output in CloudFormation events. Defaults to false. |
Access outputs in your CDK code:
customResource.getAttString('key1') // Returns 'value1'Architecture
graph LR
CFn[CloudFormation] -->|serviceToken| SNS[SNS Topic]
SNS --> SQS[SQS Queue]
SQS --> Pipes[EventBridge Pipes]
Pipes -->|invoke| Express[Express State Machine]
Express -->|startExecution| Standard[Your State Machine]
Express -->|describeExecution| Standard
Express -->|HttpInvoke| CFn
Express -.->|timeout| PipesLambdalessWaitCondition
For long-running async operations that exceed the Custom Resource's 1-hour timeout, use LambdalessWaitCondition. It integrates with CloudFormation WaitCondition to support operations up to 12 hours.
Your state machine uses the same input/output format as LambdalessCustomResource. The output Data should typically be an explicit object with one key-value pair. If you need to return multiple values, see Advanced: Multiple key-value pairs.
const flow = new CustomResourceFlow(this, 'Flow', {
onCreate: // ... your long-running workflow ...
Pass.jsonata(this, 'Done', {
outputs: {
PhysicalResourceId: '{% $jobId %}',
Data: {
s3Prefix: '{% $artifactS3Prefix %}',
},
},
}),
onDelete: Pass.jsonata(this, 'Delete'),
});
const waitCondition = new LambdalessWaitCondition(this, 'CompileJob', {
stateMachine: new StateMachine(this, 'StateMachine', {
definitionBody: DefinitionBody.fromChainable(flow),
}),
timeout: Duration.hours(12),
properties: {
jobDefinitionArn: jobDefinition.jobDefinitionArn,
jobQueueArn: jobQueue.jobQueueArn,
},
});
const s3Prefix = waitCondition.getAttString('s3Prefix');Advanced: Multiple key-value pairs
LambdalessWaitCondition uses CloudFormation WaitCondition internally. In a standard WaitCondition, count specifies the number of signals to wait for (e.g., from multiple EC2 instances), each identified by a UniqueId.
This construct repurposes that mechanism: each key in your Data object is treated as one signal, with the key name as UniqueId and the value as the signal's Data. This means:
countmust exactly match the number of keys inData. Ifcountis too high, the stack waits forever. If too low, some values cannot be referenced.- Always return an explicit
Dataobject with known keys. Never return an API response object directly — unknown or extra keys will causecountto mismatch.
// ✅ Good: explicit key-value pairs, count matches
Pass.jsonata(this, 'Done', {
outputs: {
PhysicalResourceId: '{% $jobId %}',
Data: {
s3Prefix: '{% $artifactS3Prefix %}',
version: '{% $modelVersion %}',
},
},
});
const waitCondition = new LambdalessWaitCondition(this, 'WC', {
stateMachine,
count: 2, // Matches the 2 keys in Data
});
const s3Prefix = waitCondition.getAttString('s3Prefix');
const version = waitCondition.getAttString('version');// ❌ Bad: passing API response directly — keys are unknown and count will mismatch
Pass.jsonata(this, 'Done', {
outputs: {
PhysicalResourceId: '{% $jobId %}',
Data: '{% $states.result %}', // Don't do this!
},
});License
Apache-2.0
