I Automated my AWS Lambda Artifacts with Go

I Automated my AWS Lambda Artifacts with Go

I'm having some fun with AWS Lambda lately. It is cheap, easy to integrate with other services or APIs, and almost effortless to get up and running. Almost. As I previously noted in another post, there is no web editor option for Golang functions in the AWS console. Instead, it is necessary to build a binary, create a zip archive and upload it to the WebUI (it should be possible also via CLI, I guess).

Ok, no problem: at first, I just entered the individual go build and zip commands in the terminal like any decent fellow. Unfortunately, as I started to take an increasing interest in my tiny functions, I began to package many different programs all the time. Soon, I wished to have something more organized and I created this script:

#!/bin/bash
filename=lambda-function-$(date +%Y_%m_%d).zip
GOOS=linux go build main.go
zip $filename main

I was satisfied, for a while.

Designing the Gozip CLI

Of course, the solution explained above is a pretty decent one for sporadic prototyping. However, to remain practical, this snippet has to live in the same directory as the app entry point, while I had to do a lot of copy/pasting and move back and forth through the filesystem. I grew weary of this modus operandi: I wanted malleable clay in my hands, but I only had a stiff snippet of code.

At this point, I realized that I needed a convenient new CLI utility under my belt, although I was aware that I shouldn't go too far and spend an entire weekend overengineering something too fancy. For starters, I quickly gathered some requirements for the CLI:

  • obviously, it should build a Go program and then package it as a zip archive
  • it should be written in Go (yes, Go all the way down!)
  • it should allow selecting a target directory anywhere in the filesystem
  • it should allow customizing some kind of revision tag, to differentiate different executable versions
  • it should feature sensible defaults so that it can be executed without specifying any arguments

According to the points listed above, I needed two main capabilities: access to command-line arguments and interaction with the shell. Luckily, Go's standard library had this covered already.

On the workbench

In general, the task of processing command-line arguments can be easily done in Go by invoking os.Args. In this way, it is possible to return a list of string tokens representing the command-line arguments (including the name of the program being executed) and use them. However, every decent CLI offers the ability to tinker with its options and change the default behavior. For instance, I wanted to run my utility from any directory with a command like this:

$ gozip --target path/to/main.go --revision v1_3_7

Striking once by entering just the gozip command should be possible as well, so no option should be really mandatory. Of course, managing options and defaults from scratch it's definitely possible, but the Go standard library offers something more practical: the flag package. As the name suggests, flag allows you to easily configure command-line flags:

Pretty simple, isn't it? Another cool feature of flag is that it autogenerates useful documentation for the CLI:

$ gozip -h
Usage of gozip:
  -name string
        name of the executable aws-lambda handler (default "main")
  -output string
        The desired path to store the zip archive. If not explicitly set, gozip tries to create a sub-directory at the target location
  -revision string
        a version number for the executable. If not explicitly set, gozip uses the current datetime
  -target string
        path to the Go program (default ".")

Now that I had a way to capture my own input and process it, packaging a Go program become a matter of correctly checking if the option arguments were valid file paths (a job for os.Stat()) and interacting with the command line to build & zip the program. As mentioned above, I didn't want to be dragged into the rabbit hole of recreating my own zip utility - although it may be an interesting exercise on its own -  and I wanted to continue leveraging the native capabilities of Go. Our friend os/exec swiftly comes to the rescue here:

exec.Command("go", "build", "main.go")

In this way, gozip acts as a terminal wrapper, so I can directly submit commands to the shell and it's not necessary to rebuild anything from scratch.

Connecting the dots

All the different pieces were ready, so putting them together was straightforward at this point:

There are potentially many useful extensions that could be made to improve gozip, like connecting with Docker registries or S3 buckets, adding support for additional languages, and even developing an end-to-end integration with AWS Lambda. For the time being, I am satisfied with what I have, considering that it took a negligible amount of time to develop and will surely prevent a few headaches in the future.

Conclusion

In this article, I talked about my issues with AWS Lambda deployments and how I remediated them with a basic CLI that leverages the flag and os/exec packages in Golang's standard library. While additional improvement and extensions would make my utility way more valuable, I feel like I reached a good compromise between increased efficiency and development effort.