Skip to content

Commit 35ad5f6

Browse files
bdkosherJoe Wolf
and
Joe Wolf
authored
feat: create Java cfn-response equivalent (#558) (#560)
* Create Java cfn-response equivalent (#558) Introduces Java implementation of cfn-response module based on NodeJS design. Put the sole class in a separate powertools-cloudformation Maven module. 100% code coverage Addresses #558 * Add base RequestHandler class for custom resources Provides abstract methods for generating the Response to be sent, represented as its own type. Use Objects::nonNull instead of custom method. Addresses #558 * Put Response body attributes (status, noEcho, etc) in Response builder itself Instead of method args to various CloudFormationResponse::send methods, reducing the number of polymorphic send methods to two: one with a Response arg and one without. Rename ResponseException to CustomResourceResponseException. AbstractCustomResourceHandlerTest simplifications. * Lock down CloudFormationResponse; default SdkHttpClient - Include powertools-cloudformation in .github config - Address mutatable ObjectMapper spotbugs finding - JavaDoc cleanup * Cloudformation module documentation * CloudFormation documentation tweaks * Include custom resources doc in menu Co-authored-by: Joe Wolf <[email protected]>
1 parent 927aca4 commit 35ad5f6

File tree

13 files changed

+1695
-0
lines changed

13 files changed

+1695
-0
lines changed

.github/workflows/build.yml

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches:
66
- master
77
paths:
8+
- 'powertools-cloudformation/**'
89
- 'powertools-core/**'
910
- 'powertools-logging/**'
1011
- 'powertools-sqs/**'
@@ -19,6 +20,7 @@ on:
1920
branches:
2021
- master
2122
paths:
23+
- 'powertools-cloudformation/**'
2224
- 'powertools-core/**'
2325
- 'powertools-logging/**'
2426
- 'powertools-sqs/**'

.github/workflows/spotbugs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches:
66
- master
77
paths:
8+
- 'powertools-cloudformation/**'
89
- 'powertools-core/**'
910
- 'powertools-logging/**'
1011
- 'powertools-sqs/**'

docs/utilities/custom_resources.md

+178
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
---
2+
title: Custom Resources description: Utility
3+
---
4+
5+
[Custom resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html)
6+
provide a way for [AWS Lambda functions](
7+
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html) to execute
8+
provisioning logic whenever CloudFormation stacks are created, updated, or deleted. The CloudFormation utility enables
9+
developers to write these Lambda functions in Java.
10+
11+
The utility provides a base `AbstractCustomResourceHandler` class which handles [custom resource request events](
12+
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requests.html), constructs
13+
[custom resource responses](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html), and
14+
sends them to the custom resources. Subclasses implement the provisioning logic and configure certain properties of
15+
these response objects.
16+
17+
## Install
18+
19+
To install this utility, add the following dependency to your project.
20+
21+
=== "Maven"
22+
23+
```xml
24+
<dependency>
25+
<groupId>software.amazon.lambda</groupId>
26+
<artifactId>powertools-cloudformation</artifactId>
27+
<version>1.7.3</version>
28+
</dependency>
29+
```
30+
31+
=== "Gradle"
32+
33+
```groovy
34+
dependencies {
35+
...
36+
implementation 'software.amazon.lambda:powertools-cloudformation:1.7.3'
37+
aspectpath 'software.amazon.lambda:powertools-cloudformation:1.7.3'
38+
}
39+
```
40+
41+
## Usage
42+
43+
Create a new `AbstractCustomResourceHandler` subclass and implement the `create`, `update`, and `delete` methods with
44+
provisioning logic in the appropriate methods(s).
45+
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`.
48+
49+
```java hl_lines="8 9 10 11"
50+
import com.amazonaws.services.lambda.runtime.Context;
51+
import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent;
52+
import software.amazon.lambda.powertools.cloudformation.AbstractCustomResourceHandler;
53+
import software.amazon.lambda.powertools.cloudformation.Response;
54+
55+
public class ProvisionOnCreateHandler extends AbstractCustomResourceHandler {
56+
57+
@Override
58+
protected Response create(CloudFormationCustomResourceEvent createEvent, Context context) {
59+
doProvisioning();
60+
return Response.success();
61+
}
62+
63+
@Override
64+
protected Response update(CloudFormationCustomResourceEvent updateEvent, Context context) {
65+
return null;
66+
}
67+
68+
@Override
69+
protected Response delete(CloudFormationCustomResourceEvent deleteEvent, Context context) {
70+
return null;
71+
}
72+
}
73+
```
74+
75+
### Signaling Provisioning Failures
76+
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()`.
79+
80+
### Configuring Response Objects
81+
82+
When provisioning results in data to be shared with other parts of the stack, include this data within the returned
83+
`Response` instance.
84+
85+
This Lambda function creates a [Chime AppInstance](https://docs.aws.amazon.com/chime/latest/dg/create-app-instance.html)
86+
and maps the returned ARN to a "ChimeAppInstanceArn" attribute.
87+
88+
```java hl_lines="11 12 13 14"
89+
public class ChimeAppInstanceHandler extends AbstractCustomResourceHandler {
90+
@Override
91+
protected Response create(CloudFormationCustomResourceEvent createEvent, Context context) {
92+
CreateAppInstanceRequest chimeRequest = CreateAppInstanceRequest.builder()
93+
.name("my-app-name")
94+
.build();
95+
CreateAppInstanceResponse chimeResponse = ChimeClient.builder()
96+
.region("us-east-1")
97+
.createAppInstance(chimeRequest);
98+
99+
Map<String, String> chimeAtts = Map.of("ChimeAppInstanceArn", chimeResponse.appInstanceArn());
100+
return Response.builder()
101+
.value(chimeAtts)
102+
.build();
103+
}
104+
}
105+
```
106+
107+
For the example above the following response payload will be sent.
108+
109+
```json
110+
{
111+
"Status": "SUCCESS",
112+
"PhysicalResourceId": "2021/10/01/e3a37e552eff4718a5675c1e31f0649e",
113+
"StackId": "arn:aws:cloudformation:us-east-1:123456789000:stack/Custom-stack/59e4d2d0-2fe2-10ec-b00e-124d7c1c5f15",
114+
"RequestId": "7cae0346-0359-4dff-b80a-a82f247467b6",
115+
"LogicalResourceId:": "ChimeTriggerResource",
116+
"NoEcho": false,
117+
"Data": {
118+
"ChimeAppInstanceArn": "arn:aws:chime:us-east-1:123456789000:app-instance/150972c2-5490-49a9-8ba7-e7da4257c16a"
119+
}
120+
}
121+
```
122+
123+
Once the custom resource receives this response, it's "ChimeAppInstanceArn" attribute is set and the
124+
[Fn::GetAtt function](
125+
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html) may be used to
126+
retrieve the attribute value and make it available to other resources in the stack.
127+
128+
#### Sensitive Response Data
129+
130+
If any attributes are sensitive, enable the "noEcho" flag to mask the output of the custom resource when it's retrieved
131+
with the Fn::GetAtt function.
132+
133+
```java hl_lines="6"
134+
public class SensitiveDataHandler extends AbstractResourceHandler {
135+
@Override
136+
protected Response create(CloudFormationCustomResourceEvent createEvent, Context context) {
137+
return Response.builder()
138+
.value(Map.of("SomeSecret", sensitiveValue))
139+
.noEcho(true)
140+
.build();
141+
}
142+
}
143+
```
144+
145+
#### Customizing Serialization
146+
147+
Although using a `Map` as the Response's value is the most straightforward way to provide attribute name/value pairs,
148+
any arbitrary `java.lang.Object` may be used. By default, these objects are serialized with an internal Jackson
149+
`ObjectMapper`. If the object requires special serialization logic, a custom `ObjectMapper` can be specified.
150+
151+
```java hl_lines="21 22 23 24"
152+
public class CustomSerializationHandler extends AbstractResourceHandler {
153+
/**
154+
* Type representing the custom response Data.
155+
*/
156+
static class Policy {
157+
public ZonedDateTime getExpires() {
158+
return ZonedDateTime.now().plusDays(10);
159+
}
160+
}
161+
162+
/**
163+
* Mapper for serializing Policy instances.
164+
*/
165+
private final ObjectMapper policyMapper = new ObjectMapper()
166+
.registerModule(new JavaTimeModule())
167+
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
168+
169+
@Override
170+
protected Response create(CloudFormationCustomResourceEvent createEvent, Context context) {
171+
Policy policy = new Policy();
172+
return Response.builder()
173+
.value(policy)
174+
.objectMapper(policyMapper) // customize serialization
175+
.build();
176+
}
177+
}
178+
```

mkdocs.yml

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ nav:
1313
- utilities/sqs_large_message_handling.md
1414
- utilities/batch.md
1515
- utilities/validation.md
16+
- utilities/custom_resources.md
1617

1718
theme:
1819
name: material

pom.xml

+11
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<module>powertools-parameters</module>
3636
<module>powertools-validation</module>
3737
<module>powertools-test-suite</module>
38+
<module>powertools-cloudformation</module>
3839
</modules>
3940

4041
<scm>
@@ -121,6 +122,16 @@
121122
<type>pom</type>
122123
<scope>import</scope>
123124
</dependency>
125+
<dependency>
126+
<groupId>software.amazon.awssdk</groupId>
127+
<artifactId>http-client-spi</artifactId>
128+
<version>${aws.sdk.version}</version>
129+
</dependency>
130+
<dependency>
131+
<groupId>software.amazon.awssdk</groupId>
132+
<artifactId>url-connection-client</artifactId>
133+
<version>${aws.sdk.version}</version>
134+
</dependency>
124135
<dependency>
125136
<groupId>io.burt</groupId>
126137
<artifactId>jmespath-jackson</artifactId>

powertools-cloudformation/pom.xml

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<artifactId>powertools-cloudformation</artifactId>
8+
<packaging>jar</packaging>
9+
10+
<parent>
11+
<artifactId>powertools-parent</artifactId>
12+
<groupId>software.amazon.lambda</groupId>
13+
<version>1.7.3</version>
14+
</parent>
15+
16+
<name>AWS Lambda Powertools Java library Cloudformation</name>
17+
<description>
18+
A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating
19+
custom metrics asynchronously easier.
20+
</description>
21+
<url>https://aws.amazon.com/lambda/</url>
22+
<issueManagement>
23+
<system>GitHub Issues</system>
24+
<url>https://github.com/awslabs/aws-lambda-powertools-java/issues</url>
25+
</issueManagement>
26+
<scm>
27+
<url>https://github.com/awslabs/aws-lambda-powertools-java.git</url>
28+
</scm>
29+
<developers>
30+
<developer>
31+
<name>AWS Lambda Powertools team</name>
32+
<organization>Amazon Web Services</organization>
33+
<organizationUrl>https://aws.amazon.com/</organizationUrl>
34+
</developer>
35+
</developers>
36+
37+
<distributionManagement>
38+
<snapshotRepository>
39+
<id>ossrh</id>
40+
<url>https://aws.oss.sonatype.org/content/repositories/snapshots</url>
41+
</snapshotRepository>
42+
</distributionManagement>
43+
44+
<dependencies>
45+
<dependency>
46+
<groupId>software.amazon.awssdk</groupId>
47+
<artifactId>http-client-spi</artifactId>
48+
</dependency>
49+
<dependency>
50+
<groupId>software.amazon.awssdk</groupId>
51+
<artifactId>url-connection-client</artifactId>
52+
</dependency>
53+
<dependency>
54+
<groupId>com.amazonaws</groupId>
55+
<artifactId>aws-lambda-java-core</artifactId>
56+
</dependency>
57+
<dependency>
58+
<groupId>com.amazonaws</groupId>
59+
<artifactId>aws-lambda-java-events</artifactId>
60+
</dependency>
61+
<dependency>
62+
<groupId>com.fasterxml.jackson.core</groupId>
63+
<artifactId>jackson-databind</artifactId>
64+
</dependency>
65+
<dependency>
66+
<groupId>org.aspectj</groupId>
67+
<artifactId>aspectjrt</artifactId>
68+
</dependency>
69+
70+
<!-- Test dependencies -->
71+
<dependency>
72+
<groupId>org.junit.jupiter</groupId>
73+
<artifactId>junit-jupiter-api</artifactId>
74+
<scope>test</scope>
75+
</dependency>
76+
<dependency>
77+
<groupId>org.junit.jupiter</groupId>
78+
<artifactId>junit-jupiter-engine</artifactId>
79+
<scope>test</scope>
80+
</dependency>
81+
<dependency>
82+
<groupId>org.junit.jupiter</groupId>
83+
<artifactId>junit-jupiter-params</artifactId>
84+
<scope>test</scope>
85+
</dependency>
86+
<dependency>
87+
<groupId>org.mockito</groupId>
88+
<artifactId>mockito-core</artifactId>
89+
<scope>test</scope>
90+
</dependency>
91+
<dependency>
92+
<groupId>org.assertj</groupId>
93+
<artifactId>assertj-core</artifactId>
94+
<scope>test</scope>
95+
</dependency>
96+
</dependencies>
97+
98+
</project>

0 commit comments

Comments
 (0)