Slim app for getting SSM Parameters in Go
11 September 2023
We often use Alpine Linux for our containers. It's small, it has small footprint on memory so it is perfect for containers. However, if we want to interface with AWS services, we either have to: use SDK, use HTTP API or use AWS CLI. AWS CLI weighs a bit (200MB), HTTP API is complex because you have to authenticate before doing anything and SDK doesn't always fit our project - for example we need only to pull SSM configuration for Nginx. So, today I will guide you through creating a small Go application that will read SSM Parameters using detected credentials (IAM EC2 role/profile) and it will be small (<20MB) - as the AWS SDK for Go is modular.
See the full code here
Getting required modules
Let's start a new folder, Git repository and initialize the Go module.
$ mkdir ssm-get && cd ssm-get
$ git init
$ go mod init github.com/ppabis/ssm-get
Next we need to install necessary Go libraries. We will use aws-sdk-go-v2
with
the base modules, ssm
and config
.`
$ go get github.com/aws/aws-sdk-go-v2
$ go get github.com/aws/aws-sdk-go-v2/config
$ go get github.com/aws/aws-sdk-go-v2/service/ssm
config
is important in that matter that it enables for automatic discovery of
credentials if we are running on EC2/ECS and IAM role is available. Start a new
file main.go
and create a boilerplate code that will require two arguments to
the application: the parameter name and file where to store the value.
package main
import (
"fmt"
"os"
)
func main() {
if len(os.Args) < 3 {
fmt.Printf("Usage: %s <ParameterPath> <OutputFile>\n", os.Args[0])
os.Exit(0)
}
// ... Will continue here
}
Implementing the functionality
First we need to define the region for which the config should be created. For
me, the default would be eu-central-1
but you can change it to whatever you
want. I also leave an option to pass AWS_REGION
via environment variables.
// Imports
"context"
"github.com/aws/aws-sdk-go-v2/config"
// Inside main() after args check
region := "eu-central-1"
if os.Getenv("AWS_REGION") != "" {
region = os.Getenv("AWS_REGION")
}
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion(region))
if err != nil {
fmt.Printf("[ERROR] unable to load SDK config, %v\n", err)
}
Next we will create a client and getting the parameter value using collected app
arguments. It's also useful to set flag WithDecryption
to true because it will
either way be ignored if we read normal String
or StringList
. For
SecureString
it will automatically decrypt if KMS permissions allow for that.
AWS managed default KMS key for SSM (alias/aws/ssm
) should be accessible to
anyone.
// Imports
"github.com/aws/aws-sdk-go-v2/service/ssm"
// Inside main() after config.LoadDefaultConfig()
client := ssm.NewFromConfig(cfg)
mTrue := true
output, err := client.GetParameter(context.TODO(), &ssm.GetParameterInput{
Name: ¶meterName,
WithDecryption: &mTrue,
})
if err != nil {
fmt.Printf("[ERROR] getting parameter %s: %s\n", parameterName, err)
os.Exit(0)
}
Next we will write the value to a file for persistent use. Simply write output value to a file.
file, err := os.Create(fileName)
if err != nil {
fmt.Printf("[ERROR] creating file %s: %s\n", fileName, err)
os.Exit(0)
}
defer file.Close()
_, err = file.WriteString(*output.Parameter.Value)
if err != nil {
fmt.Printf("[ERROR] writing file %s: %s\n", fileName, err)
os.Exit(0)
}
In practice the best place to put such sensitive files is /run
as it is
mounted as tmpfs
and is not persisted on the disk. However, these files will
be then accessible by any process.
Dockerfile
Let's build our app with Docker and create a sample container with Nginx and our
new application. The ssm-get
will be run on entrypoint and download Nginx
configuration.
FROM golang:1.20-alpine AS builder
WORKDIR /app
COPY go.mod .
COPY go.sum .
COPY main.go .
RUN go build -o ssm-get
FROM nginx:alpine
COPY --from=builder /app/ssm-get /usr/local/bin/ssm-get
COPY 40-nginx-ssm.sh /docker-entrypoint.d/
RUN chmod +x /docker-entrypoint.d/40-nginx-ssm.sh\
&& chmod +x /usr/local/bin/ssm-get
Entrypoint - pull configuration
Let's write a script 40-nginx-ssm.sh
that will pull the configuration from SSM
Parameter Store and add it to Nginx configuration.
#!/bin/sh
set -e
ssm-get /nginx/config /etc/nginx/conf.d/default.conf
# For testing purposes, print the file to logs
cat /etc/nginx/conf.d/default.conf
Testing on AWS
Let's spin up a test instance on EC2. We will install Docker on it and build our container. Copy the files with SCP (or paste if you prefer) to the new instance. I will use Amazon Linux 2023 and install Docker from default repositories. This container should work also on ECS.
Building of the following Go project might need around 1GB of RAM, so if you are running on a smaller instance, you might want to create a swap file.
$ ssh ec2-user@<instance-ip>
$ sudo yum install docker -y
$ sudo systemctl enable docker
$ sudo systemctl start docker
$ cd ~/ssm-get
$ sudo docker build -m 1G -t test-nginx-ssm:latest .
We can look how large is the output image and ssm-get
itself.
$ sudo docker run --rm -it test-nginx-ssm:latest ls -sh /usr/local/bin/ssm-get
9.0M /usr/local/bin/ssm-get
$ sudo docker image ls test-nginx-ssm
REPOSITORY TAG IMAGE ID CREATED SIZE
test-nginx-ssm latest 713ec6b49ee5 41 seconds ago 62.2MB
As you see the new application takes only 9MB and the complete image is 62.2MB. Compared it to pure AWS CLI 400MB.
$ sudo docker image ls amazon/aws-cli:latest
REPOSITORY TAG IMAGE ID CREATED SIZE
amazon/aws-cli latest fb1dd853adc1 4 days ago 406MB
So, now we will put some configuration to SSM Parameter Store and run our container to see if it works. Also open port 80 on the instance to test if the configuration for Nginx is parsed by the server.
server {
listen 80;
error_log /dev/stderr;
access_log /dev/stdout;
server_name _;
default_type text/html;
location / {
return 200 "OK: Nginx $nginx_version,\n host $hostname,\n remote ip $remote_addr\n";
}
}
The instance should have role with permissions to read SSM Parameters. It is
enough to just apply AmazonSSMManagedInstanceCore
policy.
Let's start the container and in our browser go to the instance IP.
$ sudo docker run --rm -it -p 80:80 test-nginx-ssm:latest
Our configuration was loaded! Now every start of the container, the configuration will be downloaded from SSM Parameter Store.