Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do we access and respond to CloudFormation custom resources using an AWS Lambda function written in Java?

I have am AWS Lambda function written in Java that I would like to use as part of a response to an AWS CloudFormation function. Amazon provides two detailed examples on how to create a CloudFormation custom resource that returns its value based on an AWS Lambda function written in Node.js, however I have been having difficulty translating the Lambda examples into Java. How can we setup our AWS Java function so that it reads the value of the pre-signed S3 URL passed in as a parameter to the Lambda function from CloudFormation and send back our desired response to the waiting CloudFormation template?

like image 669
Neil Avatar asked Sep 27 '15 19:09

Neil


1 Answers

After back and forth conversation with AWS, here are some code samples I've created that accomplish this.

First of all, assuming you want to leverage the predefined interfaces for creating Handlers, you can implement RequestsHandler and define the HandleRequest methods like so:

public class MyCloudFormationResponder implements RequestHandler<Map<String, Object>, Object>{
    public Object handleRequest(Map<String,Object> input, Context context) {
        ...
    }
}

The Map<String, Object>is a Map of the values sent from your CloudFormation resource to the Lambda function. An example CF resource:

"MyCustomResource": {
  "Type" : "Custom::String",
  "Version" : "1.0",
  "Properties": {
    "ServiceToken": "arn:aws:lambda:us-east-1:xxxxxxx:function:MyCloudFormationResponderLambdaFunction",
    "param1": "my value1",
    "param2": ["t1.micro", "m1.small", "m1.large"]
  }
}

can be analyzed with the following code

    String responseURL = (String)input.get("ResponseURL");
    context.getLogger().log("ResponseURLInput: " + responseURL);
    context.getLogger().log("StackId Input: " + input.get("StackId"));
    context.getLogger().log("RequestId Input: " + input.get("RequestId"));
    context.getLogger().log("LogicalResourceId Context: " + input.get("LogicalResourceId"));
    context.getLogger().log("Physical Context: " + context.getLogStreamName());
    @SuppressWarnings("unchecked")
    Map<String,Object> resourceProps = (Map<String,Object>)input.get("ResourceProperties");
    context.getLogger().log("param 1: " + resourceProps.get("param1"));
    @SuppressWarnings("unchecked")
    List<String> myList = (ArrayList<String>)resourceProps.get("param2");
    for(String s : myList){
        context.getLogger().log(s);
    }

The key things to point out here, beyond what is explained in the NodeJS examples in the AWS documentation are

  • (String)input.get("ResponseURL") is the pre-signed S3 URL that you need to respond back to (more on this later)
  • (Map<String,Object>)input.get("ResourceProperties") returns the map of your CloudFormation custom resource "Properties" passed into the Lambda function from your CF template. I provided a String and ArrayList as two examples of object types that can be returned, though several others are possible

In order to respond back to the CloudFormation template custom resource instantiation, you need to execute an HTTP PUT call back to the ResponseURL previously mentioned and include most of the following fields in the variable cloudFormationJsonResponse. Below is how I've done this

    try {
        URL url = new URL(responseURL);
        HttpURLConnection connection=(HttpURLConnection)url.openConnection();
        connection.setDoOutput(true);
        connection.setRequestMethod("PUT");
        OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream());
        JSONObject cloudFormationJsonResponse = new JSONObject();
        try {
            cloudFormationJsonResponse.put("Status", "SUCCESS");
            cloudFormationJsonResponse.put("PhysicalResourceId", context.getLogStreamName());
            cloudFormationJsonResponse.put("StackId", input.get("StackId"));
            cloudFormationJsonResponse.put("RequestId", input.get("RequestId"));
            cloudFormationJsonResponse.put("LogicalResourceId", input.get("LogicalResourceId"));
            cloudFormationJsonResponse.put("Data", new JSONObject().put("CFAttributeRefName", "some String value useful in your CloudFormation template"));
        } catch (JSONException e) {
            e.printStackTrace();
        }
        out.write(cloudFormationJsonResponse.toString());
        out.close();
        int responseCode = connection.getResponseCode();
        context.getLogger().log("Response Code: " + responseCode);
    } catch (IOException e) {
        e.printStackTrace();
    }

Of particular note is the node "Data" above which references an additional com.amazonaws.util.json.JSONObject in which I include any attributes that are required in my CloudFormation template. In this case, it would be retrieved in CF template with something like { "Fn::GetAtt": [ "MyCustomResource", "CFAttributeRefName" ] }

Finally, you can simply return null since nothing would be returned from this function as it's the HTTPUrlConnection that actually responds to the CF call.

like image 167
Neil Avatar answered Oct 23 '22 08:10

Neil