Skip to content

Commit

Permalink
feat: create Java cfn-response equivalent (#558) (#560)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
bdkosher and Joe Wolf authored Oct 26, 2021
1 parent 927aca4 commit 35ad5f6
Show file tree
Hide file tree
Showing 13 changed files with 1,695 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches:
- master
paths:
- 'powertools-cloudformation/**'
- 'powertools-core/**'
- 'powertools-logging/**'
- 'powertools-sqs/**'
Expand All @@ -19,6 +20,7 @@ on:
branches:
- master
paths:
- 'powertools-cloudformation/**'
- 'powertools-core/**'
- 'powertools-logging/**'
- 'powertools-sqs/**'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/spotbugs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches:
- master
paths:
- 'powertools-cloudformation/**'
- 'powertools-core/**'
- 'powertools-logging/**'
- 'powertools-sqs/**'
Expand Down
178 changes: 178 additions & 0 deletions docs/utilities/custom_resources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
---
title: Custom Resources description: Utility
---

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

The utility provides a base `AbstractCustomResourceHandler` class which handles [custom resource request events](
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-requests.html), constructs
[custom resource responses](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html), and
sends them to the custom resources. Subclasses implement the provisioning logic and configure certain properties of
these response objects.

## Install

To install this utility, add the following dependency to your project.

=== "Maven"

```xml
<dependency>
<groupId>software.amazon.lambda</groupId>
<artifactId>powertools-cloudformation</artifactId>
<version>1.7.3</version>
</dependency>
```

=== "Gradle"

```groovy
dependencies {
...
implementation 'software.amazon.lambda:powertools-cloudformation:1.7.3'
aspectpath 'software.amazon.lambda:powertools-cloudformation:1.7.3'
}
```

## Usage

Create a new `AbstractCustomResourceHandler` subclass and implement the `create`, `update`, and `delete` methods with
provisioning logic in the appropriate methods(s).

As an example, if a Lambda function only needs to provision something when a stack is created, put the provisioning
logic exclusively within the `create` method; the other methods can just return `null`.

```java hl_lines="8 9 10 11"
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent;
import software.amazon.lambda.powertools.cloudformation.AbstractCustomResourceHandler;
import software.amazon.lambda.powertools.cloudformation.Response;

public class ProvisionOnCreateHandler extends AbstractCustomResourceHandler {

@Override
protected Response create(CloudFormationCustomResourceEvent createEvent, Context context) {
doProvisioning();
return Response.success();
}

@Override
protected Response update(CloudFormationCustomResourceEvent updateEvent, Context context) {
return null;
}

@Override
protected Response delete(CloudFormationCustomResourceEvent deleteEvent, Context context) {
return null;
}
}
```

### Signaling Provisioning Failures

If provisioning fails, the stack creation/modification/deletion as a whole can be failed by either throwing a
`RuntimeException` or by explicitly returning a `Response` with a failed status, e.g. `Response.failure()`.

### Configuring Response Objects

When provisioning results in data to be shared with other parts of the stack, include this data within the returned
`Response` instance.

This Lambda function creates a [Chime AppInstance](https://docs.aws.amazon.com/chime/latest/dg/create-app-instance.html)
and maps the returned ARN to a "ChimeAppInstanceArn" attribute.

```java hl_lines="11 12 13 14"
public class ChimeAppInstanceHandler extends AbstractCustomResourceHandler {
@Override
protected Response create(CloudFormationCustomResourceEvent createEvent, Context context) {
CreateAppInstanceRequest chimeRequest = CreateAppInstanceRequest.builder()
.name("my-app-name")
.build();
CreateAppInstanceResponse chimeResponse = ChimeClient.builder()
.region("us-east-1")
.createAppInstance(chimeRequest);

Map<String, String> chimeAtts = Map.of("ChimeAppInstanceArn", chimeResponse.appInstanceArn());
return Response.builder()
.value(chimeAtts)
.build();
}
}
```

For the example above the following response payload will be sent.

```json
{
"Status": "SUCCESS",
"PhysicalResourceId": "2021/10/01/e3a37e552eff4718a5675c1e31f0649e",
"StackId": "arn:aws:cloudformation:us-east-1:123456789000:stack/Custom-stack/59e4d2d0-2fe2-10ec-b00e-124d7c1c5f15",
"RequestId": "7cae0346-0359-4dff-b80a-a82f247467b6",
"LogicalResourceId:": "ChimeTriggerResource",
"NoEcho": false,
"Data": {
"ChimeAppInstanceArn": "arn:aws:chime:us-east-1:123456789000:app-instance/150972c2-5490-49a9-8ba7-e7da4257c16a"
}
}
```

Once the custom resource receives this response, it's "ChimeAppInstanceArn" attribute is set and the
[Fn::GetAtt function](
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html) may be used to
retrieve the attribute value and make it available to other resources in the stack.

#### Sensitive Response Data

If any attributes are sensitive, enable the "noEcho" flag to mask the output of the custom resource when it's retrieved
with the Fn::GetAtt function.

```java hl_lines="6"
public class SensitiveDataHandler extends AbstractResourceHandler {
@Override
protected Response create(CloudFormationCustomResourceEvent createEvent, Context context) {
return Response.builder()
.value(Map.of("SomeSecret", sensitiveValue))
.noEcho(true)
.build();
}
}
```

#### Customizing Serialization

Although using a `Map` as the Response's value is the most straightforward way to provide attribute name/value pairs,
any arbitrary `java.lang.Object` may be used. By default, these objects are serialized with an internal Jackson
`ObjectMapper`. If the object requires special serialization logic, a custom `ObjectMapper` can be specified.

```java hl_lines="21 22 23 24"
public class CustomSerializationHandler extends AbstractResourceHandler {
/**
* Type representing the custom response Data.
*/
static class Policy {
public ZonedDateTime getExpires() {
return ZonedDateTime.now().plusDays(10);
}
}

/**
* Mapper for serializing Policy instances.
*/
private final ObjectMapper policyMapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

@Override
protected Response create(CloudFormationCustomResourceEvent createEvent, Context context) {
Policy policy = new Policy();
return Response.builder()
.value(policy)
.objectMapper(policyMapper) // customize serialization
.build();
}
}
```
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ nav:
- utilities/sqs_large_message_handling.md
- utilities/batch.md
- utilities/validation.md
- utilities/custom_resources.md

theme:
name: material
Expand Down
11 changes: 11 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<module>powertools-parameters</module>
<module>powertools-validation</module>
<module>powertools-test-suite</module>
<module>powertools-cloudformation</module>
</modules>

<scm>
Expand Down Expand Up @@ -121,6 +122,16 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>http-client-spi</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>url-connection-client</artifactId>
<version>${aws.sdk.version}</version>
</dependency>
<dependency>
<groupId>io.burt</groupId>
<artifactId>jmespath-jackson</artifactId>
Expand Down
98 changes: 98 additions & 0 deletions powertools-cloudformation/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<artifactId>powertools-cloudformation</artifactId>
<packaging>jar</packaging>

<parent>
<artifactId>powertools-parent</artifactId>
<groupId>software.amazon.lambda</groupId>
<version>1.7.3</version>
</parent>

<name>AWS Lambda Powertools Java library Cloudformation</name>
<description>
A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating
custom metrics asynchronously easier.
</description>
<url>https://aws.amazon.com/lambda/</url>
<issueManagement>
<system>GitHub Issues</system>
<url>https://github.com/awslabs/aws-lambda-powertools-java/issues</url>
</issueManagement>
<scm>
<url>https://github.com/awslabs/aws-lambda-powertools-java.git</url>
</scm>
<developers>
<developer>
<name>AWS Lambda Powertools team</name>
<organization>Amazon Web Services</organization>
<organizationUrl>https://aws.amazon.com/</organizationUrl>
</developer>
</developers>

<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://aws.oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
</distributionManagement>

<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>http-client-spi</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>url-connection-client</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-events</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Loading

0 comments on commit 35ad5f6

Please sign in to comment.