adrianhesketh.com

AWS IoT with Go

I’ve been working on a project where I wanted to use AWS IoT to receive notifications. The documentation for AWS IoT lists SDKs for JavaScript, Python and Java(!?), but nothing for Go. However, it’s fairly straightforward to use Go with AWS IoT using a 3rd party library.

I wanted something server-side to initiate the push to the IoT device, but I started on the IoT side first. The Python SDK has an example at https://github.com/aws/aws-iot-device-sdk-python/blob/master/samples/basicPubSub/basicPubSub.py, which demonstrates pushing an event, so I set out to reproduce that with Go.

I checked out MQTT libraries in Go, and the most popular and well-maintained one appeared to be github.com/eclipse/paho.mqtt.golang so I used that. I read through the example, worked out the right format for the connection string and removed the bit where it allows invalid TLS certificates. To connect to AWS IoT over MQTT requires the client to trust the AWS Root CA certificate, and to have a certificate and private key to make the connection. These are generated when you create a device in the registry (https://docs.aws.amazon.com/iot/latest/developerguide/register-device.html). For this example, I just put the files next to the main.go file on disk.

package main

import (
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"log"
	"time"

	mqtt "github.com/eclipse/paho.mqtt.golang"
)

// Adapted from https://github.com/eclipse/paho.mqtt.golang/blob/master/cmd/ssl/main.go
// Also see https://www.eclipse.org/paho/clients/golang/
func NewTLSConfig() (config *tls.Config, err error) {
	// Import trusted certificates from CAfile.pem.
	certpool := x509.NewCertPool()
	pemCerts, err := ioutil.ReadFile("AmazonRootCA1.pem")
	if err != nil {
		return
	}
	certpool.AppendCertsFromPEM(pemCerts)

	// Import client certificate/key pair.
	cert, err := tls.LoadX509KeyPair("4950ab3d29-certificate.pem.crt", "4950ab3d29-private.pem.key")
	if err != nil {
		return
	}

	// Create tls.Config with desired tls properties
	config = &tls.Config{
		// RootCAs = certs used to verify server cert.
		RootCAs: certpool,
		// ClientAuth = whether to request cert from server.
		// Since the server is set up for SSL, this happens
		// anyways.
		ClientAuth: tls.NoClientCert,
		// ClientCAs = certs used to validate client cert.
		ClientCAs: nil,
		// Certificates = list of certs client sends to server.
		Certificates: []tls.Certificate{cert},
	}
	return
}

var f mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) {
	fmt.Printf("TOPIC: %s\n", msg.Topic())
	fmt.Printf("MSG: %s\n", msg.Payload())
}

func main() {
	tlsconfig, err := NewTLSConfig()
	if err != nil {
		log.Fatalf("failed to create TLS configuration: %v", err)
	}
	opts := mqtt.NewClientOptions()
	opts.AddBroker("tls://a3rmn7yfsg6nhl-ats.iot.eu-west-2.amazonaws.com:8883")
	opts.SetClientID("clientID").SetTLSConfig(tlsconfig)
	opts.SetDefaultPublishHandler(f)

	// Start the connection.
	c := mqtt.NewClient(opts)
	if token := c.Connect(); token.Wait() && token.Error() != nil {
		log.Fatalf("failed to create connection: %v", token.Error())
	}

	// Send shadow update.
	update := `{
    "state": {
        "desired" : {
            "color" : { "r" : 10 },
            "engine" : "ON"
        }
    }
}`
	fmt.Println("Sending update.")
	if token := c.Publish("$aws/things/iotexample/shadow/update", 0, false, update); token.Wait() && token.Error() != nil {
		log.Fatalf("failed to send update: %v", token.Error())
	}

	fmt.Println("Listening for new events.")
	if token := c.Subscribe("$aws/things/iotexample/shadow/update/accepted", 0, nil); token.Wait() && token.Error() != nil {
		log.Fatalf("failed to create subscription: %v", token.Error())
	}

	fmt.Println("Sleeping.")
	time.Sleep(time.Second * 60)
	fmt.Println("Disconnecting.")
	c.Disconnect(250)
}

Once I had this up-and-running, I was able to push messages via the AWS console and see them appear on my terminal. Next, I wanted to send an update to the device from within AWS Lambda. I took a look through the Go SDK, and although I could see 250 methods and functions in the “iot” package, I couldn’t see one which allowed me to push a change to the device state, however, the documentation mentioned a REST API I could use instead.

The documentation for the REST API at https://docs.aws.amazon.com/iot/latest/developerguide/API_UpdateThingShadow.html states that I’d need to use a certificate (just like with MQTT) or I’d have to use a “Signature Version 4 with IAM credentials” authentication. I wanted to push the changes via Lambda, which runs with an IAM role, so I thought that would be best and set out working out to send a signed HTTP request.

That turned out to be fairly straightforward, but complete unnecessary - there’s a separate package called github.com/aws/aws-sdk-go/service/iotdataplane that has the required function on it, which is a lot simpler, it has the https://docs.aws.amazon.com/sdk-for-go/api/service/iotdataplane/#IoTDataPlane.UpdateThingShadow method I need without needing the REST endpoint.