How do I use AWS SAM to build a Lambda-backed custom resource in Java for CloudFormation?

6 minute read
1

I want to use Java to build an AWS Lambda-backed custom resource to implement in AWS CloudFormation.

Short description

Building a Java-based custom resource in Lambda for CloudFormation is a complex process, especially if you do this process manually. To set up the environment manually, you need to set up the Java development kit and runtime before you build the resource. Then, you need to package and upload the code to Amazon Simple Storage Service (Amazon S3) before you finally create the Lambda function.

To simplify this process, you can use the AWS Serverless Application Model (AWS SAM). AWS SAM is an open-source framework that you can use to build serverless applications on AWS. Use this service to help build the Java custom resource, upload your codes, and deploy the custom resource and Lambda function.

Resolution

Create an instance for development

1.    Create an Amazon Elastic Compute Cloud (Amazon EC2) instance to develop your resource. You can use any environment for your instance, but it's a best practice to create your instance in Amazon Linux 2 for this use case.

2.    Create and assign an Amazon EC2 SSH key pair to your EC2 instance.

3.    Set up an instance profile with permissions to deploy a stack and resource. Specifically, grant permissions to perform the following actions:

  • Create a Lambda function
  • Update function code
  • Invoke a function
  • Create a log group that stores the Lambda's log

4.    After you launch your instance, log in with SSH. Use this instance as your development environment in the following section.

Set up your development environment

Important: Before you begin, install and configure the AWS Command Line Interface (AWS CLI). If you receive errors when running AWS CLI commands, make sure that you’re using the most recent version of the AWS CLI.

Install java-corretto11

To install corretto11, run the following command:

sudo rpm --import https://yum.corretto.aws/corretto.key   
sudo curl -L -o /etc/yum.repos.d/corretto.repo https://yum.corretto.aws/corretto.repo  
sudo yum install -y java-11-amazon-corretto-devel

Verify Installation with the following command:

java -version

For more information, see Install Amazon Corretto 11 on RPM-Based Linux.

Install AWS SAM

To install AWS SAM, run the following command:

wget https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-linux-x86_64.zip
unzip aws-sam-cli-linux-x86_64.zip -d sam-installation
sudo ./sam-installation/install
sam --version

For more information, see Installing the AWS SAM CLI.

Install Gradle

To install Gradle, run the following command:

wget https://services.gradle.org/distributions/gradle-7.6-bin.zip
sudo mkdir /opt/gradle
sudo unzip -d /opt/gradle gradle-7.6-bin.zip
export PATH=$PATH:/opt/gradle/gradle-7.6/bin
gradle -v

For more information, see Installing with a package manager on the Gradle website.

Note: You might need to export the PATH again after you reboot. To avoid this step, append the following line to the ~/.bashrc file:

export PATH=$PATH:/opt/gradle/gradle-7.6/bin

Create the project and files

To create the root folder for the SAM project, run the following command:

mkdir javaBasedCustomResource
cd javaBasedCustomResource/

To create the necessary folders for your project, run the following command:

mkdir -p src/Function/src/main/java/

Create the project files through the Vim text editor. For more information, see the Vim website. Copy the following content to run in Vim:

vim template.yaml
vim ./src/Function/build.gradle
vim ./src/Function/src/main/java/Handler.java

Your project's structure resembles the following structural tree:

.
└── javaBasedCustomResource
    ├── src
    │   └── Function
    │       ├── build.gradle
    │       └── src
    │           └── main
    │               └── java
    │                   └── Handler.java
    └── template.yaml

See the following sections for the file templates to build your project.

template.yaml

Use the following template for the stack. This defines the Lambda function, log group, and CustomResource:

Transform: AWS::Serverless-2016-10-31
Resources:
  CustomResourceJavaBased:
    Type: AWS::CloudFormation::CustomResource
    Properties:
      ServiceToken: !Join ["", [ !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:", !Ref CustomResourceLambdaInJava]]
      DummyKey: DummyValue

  CustomResourceLambdaInJava:
    Type: AWS::Serverless::Function
    Properties:
      Description: !Sub
        - Stack ${AWS::StackName} Function ${ResourceName}
        - ResourceName: CustomResourceLambdaInJava
      CodeUri: src/Function
      Handler: Handler::handleRequest
      Runtime: java11
      MemorySize: 3008
      Timeout: 30
      Tracing: Active
  CustomResourceLambdaInJavaLogGroup:
    Type: AWS::Logs::LogGroup
    DeletionPolicy: Retain
    Properties:
      LogGroupName: !Sub /aws/lambda/${CustomResourceLambdaInJava}

build.gradle

Use the following file template to facilitate your java build:

apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.amazonaws:aws-lambda-java-core:1.2.2'
    implementation 'com.amazonaws:aws-lambda-java-events:3.11.0'
    implementation 'com.google.code.gson:gson:2.10'
    
}

task buildZip(type: Zip) {
    from compileJava
    from processResources
    into('lib') {
        from configurations.compileClasspath
    }
}

build.dependsOn buildZip

Handler.java

Lambda runs the following code. This sets the input event to a Map from which you can get the necessary endpoint URL to send back the response. Put all the needed parameters in a JSON string, and send an HTTP request to the endpoint:

import com.google.gson.Gson;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

import java.util.*;
import java.io.*;

public class Handler {
    public String handleRequest(Object event) {
    /* Customer workload */
    System.out.println("Dummy CustomResource Job");

    /* sending signal back to CFN stack */
    try {
        sendResponse(event);
    }
    catch (Exception e){
        System.out.println("Got Exception!!");
        e.printStackTrace(System.out);
    }

        return "JobFinished";
    }

    
    public void sendResponse(Object event) throws IOException, InterruptedException {
    System.out.println("start sending signal");
    
    Gson gson = new Gson();
    String eventJson = gson.toJson(event);
    Map map = gson.fromJson(eventJson, Map.class);
    
    System.out.println("Request event: " + eventJson);

    /* Generate response  parameters */
    String putEndpoint = (String)map.get("ResponseURL");

    String Status = "SUCCESS"; // "SUCCESS" or "FAILED"
    
    String Reason = "Dummy reason for Java based Custom Resource";

    String PhysicalResourceId = "CustomResourcePhysicalID";

    String StackId = (String)map.get("StackId");

    String RequestId = (String)map.get("RequestId");

    String LogicalResourceId = (String)map.get("LogicalResourceId");

    /* Building response */
        String responseJson = "{\"Status\":\"" + Status + "\",\"Reason\":\"" + Reason + "\",\"PhysicalResourceId\":\"" + PhysicalResourceId + "\",\"StackId\":\"" + StackId + "\",\"RequestId\":\"" + RequestId + "\",\"LogicalResourceId\":\"" + LogicalResourceId + "\",\"NoEcho\":false,\"Data\":{\"Key\":\"Value\"}}";

    System.out.println("Response event: " + responseJson);

    var request = HttpRequest.newBuilder()
            .uri(URI.create(putEndpoint))
            .header("Content-Type", "application/json")
            .PUT(HttpRequest.BodyPublishers.ofString(responseJson))
            .build();

        var client = HttpClient.newHttpClient();
    
    /* Sending Response */
    System.out.println("Sending Response to stack, response code: ");

        var response = client.send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(response.statusCode());
        System.out.println(response.body());

    System.out.println("Finish sending signal");
    }
}

Deploy the project

1.    After you create your AWS SAM project, run following command under the root project folder javaBasedCustomResource:

sam build

2.    To deploy the project, run the following command. This initiates a guide that prompts you to specify the the stack name and AWS Region where you want the resources to be created:

sam deploy --guided

This deploys a CloudFormation stack in your account with the name that you specified. The stack contains a Lambda function that runs the code from the previous steps. CloudFormation also creates the custom resource type AWS::CloudFormation::CustomResource in the same stack.

When CloudFormation creates AWS::CloudFormation::CustomResource, CloudFormation also invokes the above Lambda function. In response, the Lambda function sends a SUCCESS signal back to CloudFormation. This SUCCESS signal sends the resource to CreateComplete.

When you see the custom resource in the stack create successfully, this means that Lambda is also running successfully. Lambda returns the signal to the stack, and the java based custom resource initiates successfully.

You can check the Lambda logs for more details on metrics such as the request event and response event. You can modify the function code to specify your own tasks or create your own resources.

AWS OFFICIAL
AWS OFFICIALUpdated a year ago