Avatar (Fabio Alessandro Locati|Fale)'s blog

GoLang vanity urls on AWS Lambda

March 21, 2019

After the article on the reasons to use vanity URLs in Go and the one about how to implement a lightweight vanity URLs provider, I’d like to share with you how you can leverage AWS Lambda to implement a vanity URLs provider.

The first thing we will need is to import the github.com/aws/aws-lambda-go package. This package will provide us with the needed functions to easily integrate our Go code with AWS Lambda. In our main we will just need to start the Lambda with a handler like this:

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

We then need to define the handler itself. Due to how the Start function works, we are fairly free to decide how many parameters to accept and return.

func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	for _, c := range conversions {
		if request.Path[1:] == c.Vanity || strings.HasPrefix(request.Path[1:], c.Vanity+"/") {
			var tplOutput bytes.Buffer
			if err := tpl.Execute(&tplOutput, c); err != nil {
				return events.APIGatewayProxyResponse{Body: err.Error(), StatusCode: 500}, nil
			}
			return events.APIGatewayProxyResponse{Body: tplOutput.String(), StatusCode: 200}, nil
		}
	}
	return events.APIGatewayProxyResponse{Body: "Not found", StatusCode: 404}, nil
}

We now need an HTML template called tpl to add all the needed HTML tags that the Go dependency downloader needs:

var tpl = template.Must(template.New("main").Parse(`<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <meta name="go-import" content="{{ .Domain }}/{{ .Vanity }} git {{ .Real }}">
    </head>
</html>
`))

We also need to define the slice of objects conversions and its base type:

type conversion struct {
	Vanity string
	Real   string
	Domain string
}

var conversions = []conversion{
	conversion{Domain: "go.kelfa.io", Vanity: "aws-cloudfront-logCompactor", Real: "https://github.com/kelfa/aws-cloudfront-logCompactor"},
	conversion{Domain: "go.kelfa.io", Vanity: "elf", Real: "https://github.com/kelfa/elf"},
	conversion{Domain: "go.kelfa.io", Vanity: "go.kelfa.io", Real: "https://github.com/kelfa/go.kelfa.io"},
}

The conversions slice values are to be tweaked based on the packages the AWS Lambda function will need to handle.

The whole code together would be:

package main

import (
	"bytes"
	"html/template"
	"strings"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

type conversion struct {
	Vanity string
	Real   string
	Domain string
}

var conversions = []conversion{
	conversion{Domain: "go.kelfa.io", Vanity: "aws-cloudfront-logCompactor", Real: "https://github.com/kelfa/aws-cloudfront-logCompactor"},
	conversion{Domain: "go.kelfa.io", Vanity: "elf", Real: "https://github.com/kelfa/elf"},
	conversion{Domain: "go.kelfa.io", Vanity: "go.kelfa.io", Real: "https://github.com/kelfa/go.kelfa.io"},
}

var tpl = template.Must(template.New("main").Parse(`<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <meta name="go-import" content="{{ .Domain }}/{{ .Vanity }} git {{ .Real }}">
    </head>
</html>
`))

func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	for _, c := range conversions {
		if request.Path[1:] == c.Vanity || strings.HasPrefix(request.Path[1:], c.Vanity+"/") {
			var tplOutput bytes.Buffer
			if err := tpl.Execute(&tplOutput, c); err != nil {
				return events.APIGatewayProxyResponse{Body: err.Error(), StatusCode: 500}, nil
			}
			return events.APIGatewayProxyResponse{Body: tplOutput.String(), StatusCode: 200}, nil
		}
	}
	return events.APIGatewayProxyResponse{Body: "Not found", StatusCode: 404}, nil
}

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

We still have to overcome a small AWS Lambda-specific hurdle: the creation of the function itself. To do so, we can run in a terminal:

GOOS=linux go build main.go
zip function.zip main
aws lambda update-function-code --function-name go-kelfa-io --zip-file fileb://function.zip
rm main
rm function.zip

Those commands will allow us to compile the function in the proper form and upload the code to the function called go-kelfa-io. The name will need to be changed to your specific usage. Also, remember that the update-function-code command expects the AWS Lambda function to be already present in your account and the aws CLI tool to be configured appropriately.

I hope this helps other people looking for an easy and very cheap way of hosting their own Go packages with vanity URLs.