We run containerized services in Amazon ECS (EC2 Container Service) and wanted
a way to set environment variables containing sensitive configuration in
running containers without defining those environment variables in ECS task
definitions. We do this by storing encrypted (at rest)
key/value JSON objects in S3 and reading them into the environment in either a
docker entrypoint script or in the application’s bootstrap process. We wrote
Snagsby
(https://github.com/roverdotcom/snagsby)
to make this process easier. We’ve found it a convenient way to read
configuration into lambda functions as well.
JSON Objects in S3 for Snagsby
Snagsby makes it easy to read a key/value JSON object from S3, output in a
format that can be evaluated by a shell for the purpose of setting environment
variables. This idea is covered in this AWS blog post:
The referenced AWS blog post describes ways to restrict an S3 bucket to only
allow kms encrypted objects, as well as requiring secure transport and even
restricting access using VPC S3 endpoints. We typically encrypt our JSON
objects using unique KMS keys that we then give the appropriate application the
ability to decrypt when using snagsby to read configuration. Individual KMS
keys allow for nice auditing.
An example python boto3 script to upload KMS encrypted snagsby compatible JSON
to S3:
import json
import boto3
s3 = boto3.resource('s3')
s3.Bucket('my-config-bucket').put_object(
Body=json.dumps({
'API_KEY': '12345',
'CREDENTIAL': 'ABC',
}),
Key='myapp/production/config.json',
ServerSideEncryption='aws:kms',
SSEKMSKeyId='7777abcd',
)
Snagsby only requires s3:GetObject
to read a key, so the following IAM policy
would be all an app needed to read the above configuration using snagsby.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::myapp/production/config.json"
]
},
{
"Effect": "Allow",
"Action": [
"kms:Decrypt"
],
"Resource": [
"arn:aws:kms:us-west-2:777777777777:key/7777abcd"
]
}
]
}
Using Snagsby
Snagsby is written in golang and binaries are available on github. After uploading a JSON object to S3 you would evaluate the output of running the snagsby command in a shell. Snagsby reads a list of sources from either the SNAGSBY_SOURCE
environment variable, or from command line arguments. It will output export
statements which can be evaluated by a shell.
Say you upload this JSON to s3://my-config/myapp/production/config.json
:
{
"API_KEY": "123",
"CREDENTIALS": "7777"
}
Running snagsby would output this to stdout:
snagsby s3://my-config/myapp/production/config.json
export API_KEY="123"
export CREDENTIALS="7777"
Snagsby also reads its list of sources from the SNAGSBY_SOURCE
environment
variables and will merge together multiple sources in order.
# s3://my-config/one.json
{
"SETTING": "FIRST",
"ONE": "1"
}
# s3://my-config/two.json
{
"SETTING": "SECOND",
"TWO": "2"
}
Running snagsby against both sources would merge the output in order
SNAGSBY_SOURCE="s3://my-config/one.json, s3://my-config/two.json" snagsby
export SETTING="SECOND"
export ONE="1"
export TWO="2"
How We Use Snagsby In AWS ECS (EC2 Container Service)
We use ECS task iam roles to allow our application to s3:GetObject
and
kms:Decrypt
the relevant key containing the configuration needed by the app.
We then set the SNAGSBY_SOURCE
environment variable in our ecs task
definition. This allows us to use an entrypoint script in our containers that
evaluates snagsby before executing the docker CMD
.
Example docker entrypoing script:
#!/bin/bash
set -e
# Snagsby will by default read sources found in the SNAGSBY_SOURCE env var. If
# the env var is not set or is empty snagsby exits 0 and outputs nothing
eval $(snagsby)
# Execute what is passed to the entrypoint scrip, which will be the
# Dockerfile CMD
exec "$@"
This entrypoint is safe for the container even if SNAGSBY_SOURCE
isn’t set as
snagsby will just output nothing to stdout.
Using Snagsby in Python (snagsby-py)
We’ve also released a python library for reading snagsby JSON. It’s called
snagsby-py. This can be used to
read snagsby configuration directly in python apps, including python lambda
functions.
Example setting.py file in a python app
import os
import snagsby
# By default snagsby.load() reads from the SNAGSBY_SOURCE env var and injects
# the contents in os.environ. This is safe to call if no SNAGSBY_SOURCE is set.
snagsby.load()
print(os.environ['SETTING'])
Summary
Snagsby has worked well for us so far. It’s a fairly straightforward solution,
simple and somewhat limited by design, and didn’t require adding any additional
infrastructure.