Skip to content

Commit f8731bf

Browse files
authored
docs(cloudformation-module): Improve Docs (#1090)
1 parent 5577f3a commit f8731bf

File tree

1 file changed

+144
-30
lines changed

1 file changed

+144
-30
lines changed

docs/utilities/custom_resources.md

Lines changed: 144 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,14 @@ title: Custom Resources
33
description: Utility
44
---
55

6-
[Custom resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html)
6+
[CloudFormation Custom resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html)
77
provide a way for [AWS Lambda functions](
88
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html) to execute
9-
provisioning logic whenever CloudFormation stacks are created, updated, or deleted. The CloudFormation utility enables
10-
developers to write these Lambda functions in Java.
9+
provisioning logic whenever CloudFormation stacks are created, updated, or deleted.
1110

12-
The utility provides a base `AbstractCustomResourceHandler` class which handles [custom resource request events](
13-
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requests.html), constructs
14-
[custom resource responses](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html), and
15-
sends them to the custom resources. Subclasses implement the provisioning logic and configure certain properties of
16-
these response objects.
11+
Powertools-cloudformation makes it easy to write Lambda functions in Java that are used as CloudFormation custom resources.
12+
The utility reads incoming CloudFormation events, calls your custom code depending on the operation (CREATE, UPDATE or DELETE) and sends responses back to CloudFormation.
13+
By using this library you do not need to write code to integrate with CloudFormation, and you only focus on writing the custom provisioning logic inside the Lambda function.
1714

1815
## Install
1916

@@ -40,57 +37,90 @@ To install this utility, add the following dependency to your project.
4037

4138
## Usage
4239

43-
Create a new `AbstractCustomResourceHandler` subclass and implement the `create`, `update`, and `delete` methods with
44-
provisioning logic in the appropriate methods(s).
40+
To utilise the feature, extend the `AbstractCustomResourceHandler` class in your Lambda handler class.
41+
Next, implement and override the following 3 methods: `create`, `update` and `delete`. The `AbstractCustomResourceHandler` invokes the right method according to the CloudFormation [custom resource request event](
42+
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requests.html) it receives.
43+
Inside the methods, implement your custom provisioning logic, and return a `Response`. The `AbstractCustomResourceHandler` takes your `Response`, builds a
44+
[custom resource responses](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html) and sends it to CloudFormation automatically.
4545

46-
As an example, if a Lambda function only needs to provision something when a stack is created, put the provisioning
47-
logic exclusively within the `create` method; the other methods can just return `null`.
46+
Custom resources notify cloudformation either of `SUCCESS` or `FAILED` status. You have 2 utility methods to represent these responses: `Response.success(physicalResourceId)` and `Response.failed(physicalResourceId)`.
47+
The `physicalResourceId` is an identifier that is used during the lifecycle operations of the Custom Resource.
48+
You should generate a `physicalResourceId` during the `CREATE` operation, CloudFormation stores the `physicalResourceId` and includes it in `UPDATE` and `DELETE` events.
4849

49-
```java hl_lines="8 9 10 11"
50+
Here an example of how to implement a Custom Resource using the powertools-cloudformation library:
51+
52+
```java hl_lines="10-16 21-27 32-38"
5053
import com.amazonaws.services.lambda.runtime.Context;
5154
import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent;
5255
import software.amazon.lambda.powertools.cloudformation.AbstractCustomResourceHandler;
5356
import software.amazon.lambda.powertools.cloudformation.Response;
5457

55-
public class ProvisionOnCreateHandler extends AbstractCustomResourceHandler {
58+
public class MyCustomResourceHandler extends AbstractCustomResourceHandler {
5659

5760
@Override
5861
protected Response create(CloudFormationCustomResourceEvent createEvent, Context context) {
59-
doProvisioning();
60-
return Response.success();
62+
String physicalResourceId = "sample-resource-id-" + UUID.randomUUID(); //Create a unique ID for your resource
63+
ProvisioningResult provisioningResult = doProvisioning(physicalResourceId);
64+
if(provisioningResult.isSuccessful()){ //check if the provisioning was successful
65+
return Response.success(physicalResourceId);
66+
}else{
67+
return Response.failed(physicalResourceId);
68+
}
6169
}
6270

6371
@Override
6472
protected Response update(CloudFormationCustomResourceEvent updateEvent, Context context) {
65-
return null;
73+
String physicalResourceId = updateEvent.getPhysicalResourceId(); //Get the PhysicalResourceId from CloudFormation
74+
UpdateResult updateResult = doUpdates(physicalResourceId);
75+
if(updateResult.isSuccessful()){ //check if the update operations were successful
76+
return Response.success(physicalResourceId);
77+
}else{
78+
return Response.failed(physicalResourceId);
79+
}
6680
}
6781

6882
@Override
6983
protected Response delete(CloudFormationCustomResourceEvent deleteEvent, Context context) {
70-
return null;
84+
String physicalResourceId = deleteEvent.getPhysicalResourceId(); //Get the PhysicalResourceId from CloudFormation
85+
DeleteResult deleteResult = doDeletes(physicalResourceId);
86+
if(deleteResult.isSuccessful()){ //check if the delete operations were successful
87+
return Response.success(physicalResourceId);
88+
}else{
89+
return Response.failed(physicalResourceId);
90+
}
7191
}
7292
}
7393
```
7494

75-
### Signaling Provisioning Failures
95+
### Missing `Response` and exception handling
7696

77-
If provisioning fails, the stack creation/modification/deletion as a whole can be failed by either throwing a
78-
`RuntimeException` or by explicitly returning a `Response` with a failed status, e.g. `Response.failure()`.
97+
If a `Response` is not returned by your code, `AbstractCustomResourceHandler` defaults the response to `SUCCESS`.
98+
If your code raises an exception (which is not handled), the `AbstractCustomResourceHandler` defaults the response to `FAILED`.
7999

80-
### Configuring Response Objects
100+
In both of the scenarios, powertools-java will return the `physicalResourceId` to CloudFormation based on the following logic:
101+
- For CREATE operations, the `LogStreamName` from the Lambda context is used.
102+
- For UPDATE and DELETE operations, the `physicalResourceId` provided in the `CloudFormationCustomResourceEvent` is used.
81103

82-
When provisioning results in data to be shared with other parts of the stack, include this data within the returned
83-
`Response` instance.
104+
#### Why do you need a physicalResourceId?
84105

85-
This Lambda function creates a [Chime AppInstance](https://docs.aws.amazon.com/chime/latest/dg/create-app-instance.html)
106+
It is recommended that you always explicitly provide a `physicalResourceId` in your response rather than letting powertools generate if for you because `physicalResourceId` has a crucial role in the lifecycle of a CloudFormation custom resource.
107+
If the `physicalResourceId` changes between calls from Cloudformation, for instance in response to an `Update` event, Cloudformation [treats the resource update as a replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cfn-customresource.html).
108+
109+
### Customising a response
110+
111+
As well as the `Response.success(physicalResourceId)` and `Response.failed(physicalResourceId)`, you can customise the `Response` by using the `Response.builder()`.
112+
You customise the responses when you need additional attributes to be shared with other parts of the CloudFormation stack.
113+
114+
In the example below, the Lambda function creates a [Chime AppInstance](https://docs.aws.amazon.com/chime/latest/dg/create-app-instance.html)
86115
and maps the returned ARN to a "ChimeAppInstanceArn" attribute.
87116

88-
```java hl_lines="11 12 13 14"
117+
```java hl_lines="12-17"
89118
public class ChimeAppInstanceHandler extends AbstractCustomResourceHandler {
90119
@Override
91120
protected Response create(CloudFormationCustomResourceEvent createEvent, Context context) {
121+
String physicalResourceId = "my-app-name-" + UUID.randomUUID(); //Create a unique ID
92122
CreateAppInstanceRequest chimeRequest = CreateAppInstanceRequest.builder()
93-
.name("my-app-name")
123+
.name(physicalResourceId)
94124
.build();
95125
CreateAppInstanceResponse chimeResponse = ChimeClient.builder()
96126
.region("us-east-1")
@@ -99,6 +129,8 @@ public class ChimeAppInstanceHandler extends AbstractCustomResourceHandler {
99129
Map<String, String> chimeAtts = Map.of("ChimeAppInstanceArn", chimeResponse.appInstanceArn());
100130
return Response.builder()
101131
.value(chimeAtts)
132+
.status(Response.Status.SUCCESS)
133+
.physicalResourceId(physicalResourceId)
102134
.build();
103135
}
104136
}
@@ -113,14 +145,15 @@ For the example above the following response payload will be sent.
113145
"StackId": "arn:aws:cloudformation:us-east-1:123456789000:stack/Custom-stack/59e4d2d0-2fe2-10ec-b00e-124d7c1c5f15",
114146
"RequestId": "7cae0346-0359-4dff-b80a-a82f247467b6",
115147
"LogicalResourceId:": "ChimeTriggerResource",
148+
"PhysicalResourceId:": "my-app-name-db4a47b9-0cac-45ba-8cc4-a480490c5779",
116149
"NoEcho": false,
117150
"Data": {
118151
"ChimeAppInstanceArn": "arn:aws:chime:us-east-1:123456789000:app-instance/150972c2-5490-49a9-8ba7-e7da4257c16a"
119152
}
120153
}
121154
```
122155

123-
Once the custom resource receives this response, it's "ChimeAppInstanceArn" attribute is set and the
156+
Once the custom resource receives this response, its "ChimeAppInstanceArn" attribute is set and the
124157
[Fn::GetAtt function](
125158
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html) may be used to
126159
retrieve the attribute value and make it available to other resources in the stack.
@@ -130,11 +163,14 @@ retrieve the attribute value and make it available to other resources in the sta
130163
If any attributes are sensitive, enable the "noEcho" flag to mask the output of the custom resource when it's retrieved
131164
with the Fn::GetAtt function.
132165

133-
```java hl_lines="6"
166+
```java hl_lines="9"
134167
public class SensitiveDataHandler extends AbstractResourceHandler {
135168
@Override
136169
protected Response create(CloudFormationCustomResourceEvent createEvent, Context context) {
170+
String physicalResourceId = "my-sensitive-resource-" + UUID.randomUUID(); //Create a unique ID
137171
return Response.builder()
172+
.status(Response.Status.SUCCESS)
173+
.physicalResourceId(physicalResourceId)
138174
.value(Map.of("SomeSecret", sensitiveValue))
139175
.noEcho(true)
140176
.build();
@@ -148,7 +184,7 @@ Although using a `Map` as the Response's value is the most straightforward way t
148184
any arbitrary `java.lang.Object` may be used. By default, these objects are serialized with an internal Jackson
149185
`ObjectMapper`. If the object requires special serialization logic, a custom `ObjectMapper` can be specified.
150186

151-
```java hl_lines="21 22 23 24"
187+
```java hl_lines="14-16 26"
152188
public class CustomSerializationHandler extends AbstractResourceHandler {
153189
/**
154190
* Type representing the custom response Data.
@@ -168,11 +204,89 @@ public class CustomSerializationHandler extends AbstractResourceHandler {
168204

169205
@Override
170206
protected Response create(CloudFormationCustomResourceEvent createEvent, Context context) {
207+
String physicalResourceId = "my-policy-name-" + UUID.randomUUID(); //Create a unique ID
171208
Policy policy = new Policy();
172209
return Response.builder()
210+
.status(Response.Status.SUCCESS)
211+
.physicalResourceId(physicalResourceId)
173212
.value(policy)
174213
.objectMapper(policyMapper) // customize serialization
175214
.build();
176215
}
177216
}
178217
```
218+
219+
## Advanced
220+
221+
### Understanding the CloudFormation custom resource lifecycle
222+
223+
While the library provides an easy-to-use interface, we recommend that you understand the lifecycle of CloudFormation custom resources before using them in production.
224+
225+
#### Creating a custom resource
226+
When CloudFormation issues a CREATE on a custom resource, there are 2 possible states: `CREATE_COMPLETE` and `CREATE_FAILED`
227+
```mermaid
228+
stateDiagram
229+
direction LR
230+
createState: Create custom resource
231+
[*] --> createState
232+
createState --> CREATE_COMPLETE
233+
createState --> CREATE_FAILED
234+
```
235+
236+
If the resource is created successfully, the `physicalResourceId` is stored by CloudFormation for future operations.
237+
If the resource failed to create, CloudFormation triggers a rollback operation by default (rollback can be disabled, see [stack failure options](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stack-failure-options.html))
238+
239+
#### Updating a custom resource
240+
CloudFormation issues an UPDATE operation on a custom resource only when one or more custom resource properties change.
241+
During the update, the custom resource may update successfully, or may fail the update.
242+
```mermaid
243+
stateDiagram
244+
direction LR
245+
updateState: Update custom resource
246+
[*] --> updateState
247+
updateState --> UPDATE_COMPLETE
248+
updateState --> UPDATE_FAILED
249+
```
250+
251+
In both of these scenarios, the custom resource can return the same `physicalResourceId` it received in the CloudFormation event, or a different `physicalResourceId`.
252+
Semantically an `UPDATE_COMPLETE` that returns the same `physicalResourceId` it received indicates that the existing resource was updated successfully.
253+
Instead, an `UPDATE_COMPLETE` with a different `physicalResourceId` means that a new physical resource was created successfully.
254+
```mermaid
255+
flowchart BT
256+
id1(Logical resource)
257+
id2(Previous physical Resource)
258+
id3(New physical Resource)
259+
id2 --> id1
260+
id3 --> id1
261+
```
262+
Therefore, after the custom resource update completed or failed, there may be other cleanup operations by Cloudformation during the rollback, as described in the diagram below:
263+
```mermaid
264+
stateDiagram
265+
state if_state <<choice>>
266+
updateState: Update custom resource
267+
deletePrev: DELETE resource with previous physicalResourceId
268+
updatePrev: Rollback - UPDATE resource with previous properties
269+
noOp: No further operations
270+
[*] --> updateState
271+
updateState --> UPDATE_COMPLETE
272+
UPDATE_COMPLETE --> if_state
273+
if_state --> noOp : Same physicalResourceId
274+
if_state --> deletePrev : Different physicalResourceId
275+
updateState --> UPDATE_FAILED
276+
UPDATE_FAILED --> updatePrev
277+
```
278+
279+
#### Deleting a custom resource
280+
281+
CloudFormation issues a DELETE on a custom resource when:
282+
- the CloudFormation stack is being deleted
283+
- a new `physicalResourceId` was received during an update, and CloudFormation proceeds to rollback(DELETE) the custom resource with the previous `physicalResourceId`.
284+
285+
```mermaid
286+
stateDiagram
287+
direction LR
288+
deleteState: Delete custom resource
289+
[*] --> deleteState
290+
deleteState --> DELETE_COMPLETE
291+
deleteState --> DELETE_FAILED
292+
```

0 commit comments

Comments
 (0)