Great question. I agree that the SSM Parameter Store or AWS Secrets Manager are the recommended tools for this job, but they are just not that usable in my opinion if you follow the infrastructure as code principle.
Therefore I keep my configuration files right next to the Terraform scripts in the same Git repository and inject the actual sensitive data (passwords, keys, whatever) during runtime from the SSM Parameter Store into the specific target files.
This has several advantages over using the AWS provided solution:
- No size limit (SSM Parameter Store limits you to 4 KB in the free version).
- Non-sensitive configuration is version controlled with all the advantages that come with that.
- I can easily see the current configs used and update them instead of logging into the AWS console and so on.
Here is how I manage configs/secrets with Terraform / AWS ECS
The basic idea is as following: We store configuration files without sensitive data in the repository next to Terraform. Secrets are stored in AWS parameter store. To get the data into our containers at runtime, we modify the entrypoint. We could of course just create modified images, but that creates a big maintenance overhead in my opinion. Using the entrypoint approach, we can keep using vanilla images.
The disadvantage is that I have to create custom entrypoint scripts. That means that I have to find the Dockerfile of the image I'm interested in and extract the commands used to start the actual process running in the image.
I have a git repository like this:
├── files
│ └── promstack
│ ├── grafana
│ │ ├── default-datasources.yml
│ │ ├── grafana.ini
│ │ └── run.sh
│ ├── loki
│ │ └── run.sh
│ ├── nginx
│ │ ├── nginx.conf
│ │ └── run.sh
│ └── prometheus
│ ├── prometheus.yml
│ ├── rules-alerting.yml
│ ├── rules-recording.yml
│ └── run.sh
├── myscript.tf
└── variables.tf
The run.sh scripts represent the entrypoints. Here is an exemplary run.sh:
#!/bin/sh
set -x
require () {
if [ ! "$1" ]; then
echo "ERROR: var not found"
exit 1
fi
}
expand () {
var_name="${1}"
file="${2}"
eval var="\$$var_name"
sed -i "s+\${${var_name}}+${var}+g" ${file}
sed -i "s+\$${var_name}+${var}+g" ${file}
}
require ${GRAFANA_INI}
require ${DEFAULT_DATASOURCES_YML}
require ${DOMAIN}
echo ${GRAFANA_INI} | base64 -d > /etc/grafana/grafana.ini
chmod 666 /etc/grafana/grafana.ini
expand DOMAIN /etc/grafana/grafana.ini
echo ${DEFAULT_DATASOURCES_YML} | base64 -d > /etc/grafana/provisioning/datasources/default.yml
chmod 666 /etc/grafana/provisioning/datasources/default.yml
su -s "/bin/sh" -c "/run.sh" grafana
Here a part from a Terraform script (ECS Container task definition to be exact):
{
name: "grafana",
image: "grafana/grafana:7.0.5",
portMappings: [{
containerPort : 3000,
hostPort: 0,
protocol: "tcp"
}],
user: "0",
entryPoint: [ "/bin/sh", "-c", join(" ", [
"export DEFAULT_DATASOURCES_YML=${base64encode(file("${path.module}/files/promstack/grafana/default-datasources.yml"))};",
"export GRAFANA_INI=${base64encode(file("${path.module}/files/promstack/grafana/grafana.ini"))};",
"echo '${base64encode(file("${path.module}/files/promstack/grafana/run.sh"))}' | base64 -d | sh;"
])],
secrets: [
{
name: "DOMAIN",
valueFrom: "<my ssm parameter>"
}
]
},