Writing an AWS lambda function with Golang

About 10 days ago there was a discussion on HN about Golang.

I made a comment about how I love Go and what I do with it and people followed up with some questions.

Once of the things that sparked the most curiosity was that I said I write AWS Lambda functions with Golang with a thin wrapper on Node.js around them.

I quickly open sourced SNS Lambda notifier Golang just as a response to the comment but I wanted to follow up on some of the workflows and how it’s actually done.

What I absolutely love about AWS Lambda is that it can be triggered by other AWS resources like SNS notifications, S3 files and more. If you think of the workflows you can achieve with this you can easily see some powerful applications can be built on top of it.

Lets move on though and focus on the code since that’s what we’re here for :)

For our example, lets create a Lambda function that will be a subscriber to SNS notification.

To be exact, lets say we want to get a Slack notification every time a deployment fails on ElasticBeanstalk so that the engineers can check it out.

SNS Message

This is what an SMS message looks like

{
    "Records": [
        {
            "EventSource": "aws:sns",
            "EventVersion": "1.0",
            "EventSubscriptionArn": "",
            "Sns": {
                "Type": "Notification",
                "MessageId": "",
                "TopicArn": "",
                "Subject": "AWS Elastic Beanstalk Notification - New application version was deployed to running EC2 instances",
                "Message": "Timestamp: Wed Mar 30 18:28:06 UTC 2016\nMessage: Failed to deploy application..\n\nEnvironment: staging-api\nApplication: app-api\n\nEnvironment URL:",
                "Timestamp": "2016-03-30T18:28:54.268Z",
                "SignatureVersion": "1",
                "Signature": "",
                "SigningCertUrl": "",
                "UnsubscribeUrl": "",
                "MessageAttributes": {}
            }
        }
    ]
};

Parsing the message

I wrote a quick SNS message parser here: https://github.com/KensoDev/sns-parser

This parser implements a method called IncludesMessage to check whether the SNS payload includes some string in the message. Pretty simple and straightforward.

Now, we can focus on our function code

NodeJS Wrapper

main.js

var child_process = require('child_process');

exports.handler = function(event, context) {
  var proc = child_process.spawn('./notifier', [ JSON.stringify(event) ], { stdio: 'inherit' });

  proc.on('close', function(code) {
    if(code !== 0) {
      return context.done(new Error("Process exited with non-zero status code"));
    }

    context.done(null);
  });
}

You can see here that the Node code only spawns a child process called notifier and sends the JSON event via os args so the process can continue and then exits.

This is all the Node code you need, from here on everything will be done via Golang.

Go code

notifier.go

package main

import (
	"bytes"
	"fmt"
	"github.com/kensodev/sns-parser"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"strconv"
)

func main() {
	m := os.Args[1]
	parser := snsparser.NewSNSParser([]byte(m))
	failed, message := parser.IncludesMessage("Failed to deploy application")

	if failed {
		sendMessage(message)
	} else {
		fmt.Printf("Everything is OK, nothing to report in this message")
	}
}

func sendMessage(message snsparser.SNS) {
	data := getData(message)
	req, _ := http.NewRequest("POST", "SLACK_HOOK", bytes.NewBufferString(data.Encode()))
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))

	client := &http.Client{}
	resp, _ := client.Do(req)

	body, _ := ioutil.ReadAll(resp.Body)
	fmt.Println("Message is 'Failed to deploy application', send to slack: ", string(body))
}

func getData(message snsparser.SNS) url.Values {
	data := url.Values{}
	jsonPayload := `
			{
				"channel": "#devs",
				"username": "webhookbot",
				"text": "ALERT: <!here> ElasticBeanstalk failed to deploy application %v",
				"icon_emoji": ":red_circle:"
			}
		`

	jsonMessage := fmt.Sprintf(jsonPayload, message.TopicArn)
	data.Set("payload", jsonMessage)
	return data
}

So simple it hurts :)

Receiving the message via os args m := os.Args[1], calling the parser parser := snsparser.NewSNSParser([]byte(m) and checking if the message includes what we want failed, message := parser.IncludesMessage("Failed to deploy application"). From there we continue with what we want to do.

Extend for your use case

I chose the simplest use case possible of receiving a JSON object and processing it. Even though this workflow is pretty simple I think you can extend it to pretty much anything you want.

One of the things I am going to migrate away from the application and into these function is callbacks from third party services. All callbacks to Gogobot will go through Lambda functions eventually. Queue processing is another perfect example on how you can use these.

Hope you enjoyed this post, let me know if you have any questions.