Skip to content

Commit c1d209d

Browse files
author
Elad Ben-Israel
authored
feat(custom-resources): make physical resource id optional (provider framework) (#4946)
In order to make it easier to get started and implement custom resources that do not require changes to physical resource IDs, the provider framework now allows `onEvent` to omit the `PhysicalResourceId` return value. For `CREATE` operations, it will default to the `RequestId`. For `UPDATE` and `DELETE` it will return the current `PhysicalResourceId`. Misc: in aws-custom-resource, use `fs.readFileSync(__dirname)` instead of `require` to load `sdk-api-metadata.json`, so that the typescript compiler won't yell that this file is not defined in tsconfig.json.
1 parent 3adff8e commit c1d209d

10 files changed

Lines changed: 172 additions & 123 deletions

File tree

packages/@aws-cdk/aws-cloudformation/lib/custom-resource.ts

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -78,25 +78,32 @@ export class CustomResourceProvider implements ICustomResourceProvider {
7878
*/
7979
export interface CustomResourceProps {
8080
/**
81-
* The provider which implements the custom resource
82-
*
83-
* @example invoke an AWS Lambda function when a lifecycle event occurs
84-
*
85-
* CustomResourceProvider.fromLambda(myFunction)
86-
*
87-
* @example publish lifecycle events to an SNS topic
88-
*
89-
* CustomResourceProvider.fromTopic(myTopic)
90-
*
91-
* @example use the custom resource provider framework
92-
*
93-
* import cr = require('@aws-cdk/custom-resources');
94-
* const myProvider = new cr.Provider(...)
95-
*
96-
* new cfn.CustomResource(this, 'myResource', {
97-
* provider: myProvider
98-
* });
99-
*
81+
* The provider which implements the custom resource.
82+
*
83+
* You can implement a provider by listening to raw AWS CloudFormation events
84+
* through an SNS topic or an AWS Lambda function or use the CDK's custom
85+
* [resource provider framework] which makes it easier to implement robust
86+
* providers.
87+
*
88+
* [resource provider framework]: https://docs.aws.amazon.com/cdk/api/latest/docs/custom-resources-readme.html
89+
*
90+
* ```ts
91+
* // use the provider framework from aws-cdk/custom-resources:
92+
* provider: new custom_resources.Provider({
93+
* onEventHandler: myOnEventLambda,
94+
* isCompleteHandler: myIsCompleteLambda, // optional
95+
* });
96+
* ```
97+
*
98+
* ```ts
99+
* // invoke an AWS Lambda function when a lifecycle event occurs:
100+
* provider: CustomResourceProvider.fromLambda(myFunction)
101+
* ```
102+
*
103+
* ```ts
104+
* // publish lifecycle events to an SNS topic:
105+
* provider: CustomResourceProvider.fromTopic(myTopic)
106+
* ```
100107
*/
101108
readonly provider: ICustomResourceProvider;
102109

packages/@aws-cdk/custom-resources/README.md

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ and powerful custom resources and includes the following capabilities:
4343
deployments
4444
* Validates handler return values to help with correct handler implementation
4545
* Supports asynchronous handlers to enable long operations which can exceed the AWS Lambda timeout
46+
* Implements default behavior for physical resource IDs.
4647

4748
The following code shows how the `Provider` construct is used in conjunction
4849
with `cfn.CustomResource` and a user-provided AWS Lambda function which
@@ -109,7 +110,7 @@ The return value from `onEvent` must be a JSON object with the following fields:
109110

110111
|Field|Type|Required|Description
111112
|-----|----|--------|-----------
112-
|`PhysicalResourceId`|String|Yes|The allocated/assigned physical ID of the resource. Must be returned for all request types, including `Update` and `Delete`, even if the physical ID hasn't changed.
113+
|`PhysicalResourceId`|String|No|The allocated/assigned physical ID of the resource. If omitted for `Create` events, the event's `RequestId` will be used. For `Update`, the current physical ID will be used. If a different value is returned, CloudFormation will follow with a subsequent `Delete` for the previous ID (resource replacement). For `Delete`, it will always return the current physical resource ID, and if the user returns a different one, an error will occur.
113114
|`Data`|JSON|No|Resource attributes, which can later be retrieved through `Fn::GetAtt` on the custom resource object.
114115

115116
[Custom Resource Provider Request]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requests.html#crpg-ref-request-fields
@@ -132,9 +133,10 @@ with the message "Operation timed out".
132133
If an error is thrown, the framework will submit a "FAILED" response to AWS
133134
CloudFormation.
134135

135-
The input event to `isComplete` is similar to [`onEvent`](#handling-lifecycle-events-onevent),
136-
with an additional guarantee that `PhysicalResourceId` is defines and contains the value returned
137-
from `onEvent`.
136+
The input event to `isComplete` is similar to
137+
[`onEvent`](#handling-lifecycle-events-onevent), with an additional guarantee
138+
that `PhysicalResourceId` is defines and contains the value returned from
139+
`onEvent` or the described default. At any case, it is guaranteed to exist.
138140

139141
The return value must be a JSON object with the following fields:
140142

@@ -148,28 +150,25 @@ The return value must be a JSON object with the following fields:
148150
Every resource in CloudFormation has a physical resource ID. When a resource is
149151
created, the `PhysicalResourceId` returned from the `Create` operation is stored
150152
by AWS CloudFormation and assigned to the logical ID defined for this resource
151-
in the template.
153+
in the template. If a `Create` operation returns without a `PhysicalResourceId`,
154+
the framework will use `RequestId` as the default. This is sufficient for
155+
various cases such as "pseudo-resources" which only query data.
152156

153-
When an `Update` operation occurs, if the returned `PhysicalResourceId` is
154-
different from the one currently stored (and passed in the event through the
155-
`PhysicalResourceId` field), AWS CloudFormation will treat this as a **resource
156-
replacement**, and it will issue a subsequent `Delete` operation for the old
157-
resource.
157+
For `Update` and `Delete` operations, the resource event will always include the
158+
current `PhysicalResourceId` of the resource.
159+
160+
When an `Update` operation occurs, the default behavior is to return the current
161+
physical resource ID. if the `onEvent` returns a `PhysicalResourceId` which is
162+
different from the current one, AWS CloudFormation will treat this as a
163+
**resource replacement**, and it will issue a subsequent `Delete` operation for
164+
the old resource.
158165

159166
As a rule of thumb, if your custom resource supports configuring a physical name
160167
(e.g. you can specify a `BucketName` when you define an `AWS::S3::Bucket`), you
161168
must return this name in `PhysicalResourceId` and make sure to handle
162169
replacement properly. The `S3File` example demonstrates this
163170
through the `objectKey` property.
164171

165-
If your custom resource doesn't support configuring a physical name for the
166-
resource, it is safe to use `RequestId` as `PhysicalResourceId` in the `Create`
167-
operation, but you must return the same value in subsequent `Update` operations,
168-
or otherwise CloudFormation will think your resource is being replaced and will
169-
issue a `Delete` event. The `S3Assert` example demonstrates this by returning
170-
`event.PhysicalResourceId` if defined (in `Update` and `Delete`) and otherwise
171-
`event.RequestId` (for `Create`).
172-
173172
### Error Handling
174173

175174
As mentioned above, if any of the user handlers fail (i.e. throws an exception)

packages/@aws-cdk/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ import { CustomResource, CustomResourceProvider } from '@aws-cdk/aws-cloudformat
22
import iam = require('@aws-cdk/aws-iam');
33
import lambda = require('@aws-cdk/aws-lambda');
44
import cdk = require('@aws-cdk/core');
5+
import fs = require('fs');
56
import path = require('path');
6-
import metadata = require('./sdk-api-metadata.json');
7+
8+
// don't use "require" since the typescript compiler emits errors since this
9+
// file is not listed in tsconfig.json.
10+
const metadata = JSON.parse(fs.readFileSync(path.join(__dirname, 'sdk-api-metadata.json'), 'utf-8'));
711

812
/**
913
* AWS SDK service metadata.

packages/@aws-cdk/custom-resources/lib/provider-framework/runtime/framework.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,23 +134,47 @@ function createResponseEvent(cfnRequest: AWSLambda.CloudFormationCustomResourceE
134134
//
135135
// validate that onEventResult always includes a PhysicalResourceId
136136

137-
if (!onEventResult || !onEventResult.PhysicalResourceId) {
138-
throw new Error(`onEvent response must include a PhysicalResourceId for all request types`);
139-
}
137+
onEventResult = onEventResult || { };
138+
139+
// if physical ID is not returned, we have some defaults for you based
140+
// on the request type.
141+
const physicalResourceId = onEventResult.PhysicalResourceId || defaultPhysicalResourceId(cfnRequest);
140142

141143
// if we are in DELETE and physical ID was changed, it's an error.
142-
if (cfnRequest.RequestType === 'Delete' && onEventResult.PhysicalResourceId !== cfnRequest.PhysicalResourceId) {
144+
if (cfnRequest.RequestType === 'Delete' && physicalResourceId !== cfnRequest.PhysicalResourceId) {
143145
throw new Error(`DELETE: cannot change the physical resource ID from "${cfnRequest.PhysicalResourceId}" to "${onEventResult.PhysicalResourceId}" during deletion`);
144146
}
145147

146148
// if we are in UPDATE and physical ID was changed, it's a replacement (just log)
147-
if (cfnRequest.RequestType === 'Update' && onEventResult.PhysicalResourceId !== cfnRequest.PhysicalResourceId) {
149+
if (cfnRequest.RequestType === 'Update' && physicalResourceId !== cfnRequest.PhysicalResourceId) {
148150
log(`UPDATE: changing physical resource ID from "${cfnRequest.PhysicalResourceId}" to "${onEventResult.PhysicalResourceId}"`);
149151
}
150152

151153
// merge request event and result event (result prevails).
152154
return {
153155
...cfnRequest,
154-
...onEventResult
156+
...onEventResult,
157+
PhysicalResourceId: physicalResourceId,
155158
};
159+
}
160+
161+
/**
162+
* Calculates the default physical resource ID based in case user handler did
163+
* not return a PhysicalResourceId.
164+
*
165+
* For "CREATE", it uses the RequestId.
166+
* For "UPDATE" and "DELETE" and returns the current PhysicalResourceId (the one provided in `event`).
167+
*/
168+
function defaultPhysicalResourceId(req: AWSLambda.CloudFormationCustomResourceEvent): string {
169+
switch (req.RequestType) {
170+
case 'Create':
171+
return req.RequestId;
172+
173+
case 'Update':
174+
case 'Delete':
175+
return req.PhysicalResourceId;
176+
177+
default:
178+
throw new Error(`Invalid "RequestType" in request "${JSON.stringify(req)}"`);
179+
}
156180
}

packages/@aws-cdk/custom-resources/lib/provider-framework/types.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,11 @@ interface OnEventResponse {
6565
* deleted, and CloudFormation will immediately send a `Delete` event with
6666
* the old physical ID.
6767
* - For `Delete`, this must be the same value received in the event.
68+
*
69+
* @default - for "Create" requests, defaults to the event's RequestId, for
70+
* "Update" and "Delete", defaults to the current `PhysicalResourceId`.
6871
*/
69-
readonly PhysicalResourceId: string;
72+
readonly PhysicalResourceId?: string;
7073

7174
/**
7275
* Resource attributes to return.

0 commit comments

Comments
 (0)