All about Cloud, mostly about Amazon Web Services (AWS)

Building CloudFormation Custom Resources in Go

 2018-01-20 /  1343 words /  7 minutes

Recently Amazon announced support for the Go programming language (also known as GoLang) in AWS Lambda. Go is unusual amongst modern languages (such as Java, C#, or JavaScript) because it isn’t interpreted or compiled into bytecode that runs within a container (like the .Net Runtime or Java Virtual Machine). Instead, Go compiles into executable machine code. This avoids the startup cost of Java and runs much faster than Python or Node.js. So, how do we go about Building AWS CloudFormation Custom Resources in Go? This post explains it.

Building CloudFormation Custom Resources in Go

There are basically two phases:

First we’ll install the Go based Lambda function, and then, We’ll upload and execute the CloudFormation template. Since Custom Resources in CloudFormation is quite an advanced topic, some aspects of this tutorial will be very high level. In particular, creating Lambda functions in general, creating IAM roles, and working with CloudFormation.

Installing a Go based Lambda Function

Installing Go is quite simple. Navigate to https://golang.org/dl/ and follow the instructions there. By default, Go likes a directory called “go” in the home directory, (we’re all about taking the defaults), so create a directory in your home directory called “go”. Next, we need to install some additional packages:

$ go get github.com/aws/aws-lambda-go/lambda
$ go get github.com/google/uuid
$ go get github.com/aws/aws-lambda-go/lambda
$ go get github.com/google/uuid

Then, copy the Go program listed below into that directory:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"

    "net/http"

    "github.com/aws/aws-lambda-go/lambda"
    "github.com/google/uuid"
)

type Request struct {
    RequestType string `json:"RequestType"`
    /*
      The request type is set by the AWS CloudFormation stack operation (create-stack, update-stack, or delete-stack) that was initiated by the template developer for the stack that contains the custom resource.
      Must be one of: Create, Update, or Delete. For more information, see Custom Resource Request Types.
      Required: Yes
      Type: String
    */

    ResponseURL string `json:"ResponseURL"`
    /*
      The response URL identifies a presigned S3 bucket that receives responses from the custom resource provider to AWS CloudFormation.
      Required: Yes
      Type: String
    */

    StackId string `json:"StackId"`
    /*
      The Amazon Resource Name (ARN) that identifies the stack that contains the custom resource.
      Combining the StackId with the RequestId forms a value that you can use to uniquely identify a request on a particular custom resource.
      Required: Yes
      Type: String
    */

    RequestId string `json:"RequestId"`
    /*
      A unique ID for the request.
      Combining the StackId with the RequestId forms a value that you can use to uniquely identify a request on a particular custom resource.
      Required: Yes
      Type: String
    */

    ResourceType string `json:"ResourceType"`
    /*
      The template developer-chosen resource type of the custom resource in the AWS CloudFormation template. Custom resource type names can be up to 60 characters long and can include alphanumeric and the following characters: _@-.
      Required: Yes
      Type: String
    */

    LogicalResourceId string `json:"LogicalResourceId"`
    /*
      The template developer-chosen name (logical ID) of the custom resource in the AWS CloudFormation template. This is provided to facilitate communication between the custom resource provider and the template developer.
      Required: Yes
      Type: String
    */

    PhysicalResourceId string `json:"PhysicalResourceId"`
    /*
      A required custom resource provider-defined physical ID that is unique for that provider.
      Required: Always sent with Update and Delete requests; never sent with Create.
      Type: String
    */

    /*
    ResourceProperties string `json:"ResourceProperties"`
      This field contains the contents of the Properties object sent by the template developer. Its contents are defined by the custom resource provider.
      Required: No
      Type: JSON object
    */

    /*
    OldResourceProperties object `json:"OldResourceProperties"`
      Used only for Update requests. Contains the resource properties that were declared previous to the update request.
      Required: Yes
      Type: JSON object
    */
}

type Response struct {
    StackId   string `json:"StackId"`
    RequestId string `json:"RequestId"`

    LogicalResourceId  string `json:"LogicalResourceId"`
    PhysicalResourceId string `json:"PhysicalResourceId"`
    Status             string `json:"Status"`
    Reason             string `json:"Reason"`
    Data               struct {
        Value string `json:"Value"`
    }
}

func Handler(request Request) {
    log.Println("Go Lambda: ", request)
    var response Response
    response = buildResponse(request, "SUCCESS", "", "Hello, from Go!")
    log.Println("    created response: ", response)
    doPut(request.ResponseURL, response)
    log.Println("    sent message to " + request.ResponseURL)
}

func doPut(url string, response Response) {
    client := &http.Client{}
    byteData, _ := json.MarshalIndent(response, "", "    ")
    jsonData := bytes.NewReader(byteData)
    fmt.Println("    Writing JSON: " + string(byteData))
    request, _ := http.NewRequest("PUT", url, jsonData)
    request.ContentLength = int64(len(string(byteData)))
    responseData, err := client.Do(request)
    if err != nil {
        log.Fatal("Failed to PUT: ", err)
    } else {
        defer responseData.Body.Close()
        contents, err := ioutil.ReadAll(responseData.Body)
        if err != nil {
            log.Fatal("Response Error: ", err)
        }
        fmt.Println("   Status: ", responseData.StatusCode)
        hdr := responseData.Header
        for key, value := range hdr {
            fmt.Println("    Header:   ", key, ":", value)
        }
        fmt.Println("    Response: ", contents)
    }
}

func buildResponse(request Request, status string, reason string, value string) Response {
    var response Response
    response.StackId = request.StackId
    response.RequestId = request.RequestId
    response.LogicalResourceId = request.LogicalResourceId
    response.PhysicalResourceId = uuid.New().String()
    response.Status = status
    response.Reason = reason
    response.Data.Value = value

    return response
}

func main() {
    lambda.Start(Handler)
}

The code consists of six main parts:

Request struct. The Request struct represents the incoming request. Details of the format of this data can be found here. There’s a few parts missing from the Go structure because no parameters are passed in this simple example. Response struct. The Response struct represents the response from the Lambda function. Note that this response isn’t returned to the caller, but instead gets uploaded to Amazon Simple Storage Service (S3) at a location specified in the Request struct. Handler. The Handler function performs the work. It receives a Request struct as a parameter. doPut. The doPut function writes the Response struct to the S3 bucket. buildResponse. The buildResponse func creates a Response struct. main. The main func registers the Handler function for use with CloudFormation. We need to compile the source code into an executable. If you’re running this on Linux, it’s easy to build the executable using go build -o main. On my Mac, I need to build a Linux executable. Go makes this really easy:

GOOS=linux go build -o main

Next, we need to create a ZIP file. The command on the Mac is:

zip -v deployment.zip main

Create a new role which should include the AWSLambdaBasicExecutionRole policy.

Finally, we need to upload the code into AWS Lambda. Use the following command and replace with the ARN identifier for the role just created, labelled “Role ARN”.

aws –region us-east-1 lambda create-function –function-name GoCloudFormationCustomResource –zip-file fileb://./deployment.zip –runtime go1.x –role –handler main

Upload and Execute the CloudFormation Template The following YAML CloudFormation template is as simple as it gets:


AWSTemplateFormatVersion: “2010-09-09”

Description: CloudFormation Custom Resource Test

Resources: GoCFCR1: Type: “Custom::CustomResource” Properties: ServiceToken: “arn:aws:lambda:us-east-1::function:goCloudFormationCustomResource”

Outputs: outval1: Description: Information about the value Value: Fn::GetAtt: - GoCFCR1 - Value 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

AWSTemplateFormatVersion: “2010-09-09”

Description: CloudFormation Custom Resource Test

Resources: GoCFCR1: Type: “Custom::CustomResource” Properties: ServiceToken: “arn:aws:lambda:us-east-1::function:goCloudFormationCustomResource”

Outputs: outval1: Description: Information about the value Value: Fn::GetAtt: - GoCFCR1 - Value The must be replaced with your account number.

Create a new (or reuse an existing) Amazon Simple Storage Service (S3) bucket and upload the CloudFormation template to it. Then, create the stack.

If everything worked as expected, the stack should complete successfully, and there should be an output called “Value” set to “Hello, from Go!”. Now, you know how to go about building CloudFormation Custom Resources in Go.


Tags:  AWS  Amazon CloudFormation  Go
Categories:  AWS  Amazon CloudFormation

See Also

 Top Ten Tags

AWS (43)   Kinesis (9)   Streams (8)   AWS Console (5)   Go (5)   Analytics (4)   Data (4)   database (4)   Amazon DynamoDB (3)   Amazon Elastic Compute Cloud (EC2) (3)  


All Tags (173)

Disclaimer

All data and information provided on this site is for informational purposes only. cloudninja.cloud makes no representations as to accuracy, completeness, currentness, suitability, or validity of any information on this site and will not be liable for any errors, omissions, or delays in this information or any losses, injuries, or damages arising from its display or use. All information is provided on an as-is basis.

This is a personal weblog. The opinions expressed here represent my own and not those of my employer. My opinions may change over time.