AWS Fargate Secrets

10 Dec 2020

If you want to inject a password (or other secret) as an env var into a Docker container in AWS Fargate, here's how. (Note that the make-believe AWS account number 999999999999 is used throughout.)

First, let's create a Docker image whose only job it is to print its env vars, to prove that we have figured out how to set the env vars we want to set.

Here is the Dockerfile (literally named Dockerfile:

FROM alpine
CMD ["env"]

Here's how we build and run the docker image:

$ docker build --tag envprinter:1.0 .
$ docker container run --rm -e FOO envprinter:1.0
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOSTNAME=d02bb7e3585d
FOO=bar
HOME=/root

Let's create an ECR repository to hold this custom docker image that will run in Fargate. (You'll need IAM: AmazonEC2ContainerRegistryFullAccess)

$ aws ecr create-repository \
	--repository-name environmentprinter

$ aws --output text ecr describe-repositories \
	--query 'repositories[*].[repositoryUri]'
999999999999.dkr.ecr.us-east-1.amazonaws.com/environmentprinter

Time to tag our Docker image and push it to our new repository.

$ docker tag \
	envprinter:1.0 \
	999999999999.dkr.ecr.us-east-1.amazonaws.com/environmentprinter:1.0

$ aws ecr get-login-password \
	| docker login \
	--username AWS \
	--password-stdin \
	https://999999999999.dkr.ecr.us-east-1.amazonaws.com

$ docker push \
	999999999999.dkr.ecr.us-east-1.amazonaws.com/environmentprinter:1.0

Let's create a secret that we will store in Systems Manager / Parameter Store. (You'll need IAM: AmazonSSMFullAccess)

$ aws ssm put-parameter \
	--name MY_PASSWORD \
	--value PoorlyChosen \
	--type SecureString

Let's create an ECR cluster to run our Docker image as a Fargate job in. (You'll need IAM: AmazonECS_FullAccess)

$ aws ecs create-cluster \
	--cluster-name foo

Let's create a task execution role that our Farget task will run as.

$ cat printenvironmentRole_trust_policy.json 
{
  "Version": "2008-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

$ aws iam create-role \
	--role-name printenvironmentRole \
	--assume-role-policy-document file://printenvironmentRole_trust_policy.json

Let's create a managed policy named read-my-password that will allow reading MY_PASSWORD from Systems Manager / Parameter Store.

$ aws --output text ssm get-parameter \
	--name MY_PASSWORD \
	--query 'Parameter.[ARN]'
arn:aws:ssm:us-east-1:999999999999:parameter/MY_PASSWORD

$ cat read-my-password-policy-document.json 
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ssm:GetParameters",
        "secretsmanager:GetSecretValue",
        "kms:Decrypt"
      ],
      "Resource": [
        "arn:aws:ssm:us-east-1:999999999999:parameter/MY_PASSWORD"
      ]
    }
  ]
}

$ aws iam create-policy \
	--policy-name read-my-password \
	--policy-document file://read-my-password-policy-document.json

Let's attach read-my-password managed policy to printenvironmentRole

$ aws --output text iam list-policies \
	--scope Local \
	--query 'Policies[*].[Arn]'
arn:aws:iam::994899087112:policy/read-my-password

$ aws iam attach-role-policy \
	--role-name printenvironmentRole \
	--policy-arn arn:aws:iam::999999999999:policy/read-my-password

We also need to attach AmazonECSTaskExecutionRolePolicy managed policy to printenvironmentRole, or else our Fargate task won't have the permissions to run.

$ aws --output text iam list-policies \
	--scope AWS\
	--query 'Policies[?ends_with(Arn, `AmazonECSTaskExecutionRolePolicy`)]|[*][Arn]'
arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

$ aws iam attach-role-policy \
	--role-name printenvironmentRole \
	--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

We need to check the logs of our Fargate task after it is finished, so let's create a CloudWatch log group for that.

$ aws logs create-log-group \
	--log-group-name /ecs/printenvironment

Register a task definition named printenvironment

$ cat printenvironment-container-definitions.json 
[
  {
    "name": "envprintercontainer",
    "image": "999999999999.dkr.ecr.us-east-1.amazonaws.com/environmentprinter:1.0",
    "essential": true,
    "secrets": [
      {
        "valueFrom": "arn:aws:ssm:us-east-1:999999999999:parameter/MY_PASSWORD",
        "name": "MY_PASSWORD"
      }
    ],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/printenvironment",
        "awslogs-region": "us-east-1",
        "awslogs-stream-prefix": "ecs"
      }
    }
  }
]

$ aws --output text iam list-roles \
	--query 'Roles[?RoleName==`printenvironmentRole`]|[*][Arn]'
arn:aws:iam::994899087112:role/printenvironmentRole

$ aws ecs register-task-definition \
	--family printenvironment \
	--execution-role-arn arn:aws:iam::999999999999:role/printenvironmentRole \
	--container-definitions file://printenvironment-container-definitions.json \
	--network-mode awsvpc \
	--requires-compatibilities FARGATE \
	--cpu 256 \
	--memory 512

List available subnets and security groups so we can pick which ones we want to run our task with:

$ aws --output text ec2 describe-subnets \
	--query 'Subnets[*].[AvailabilityZone,SubnetId,VpcId,CidrBlock]'
us-east-1a	subnet-09a50bcae65469211	vpc-04bdba9cb846c9f97	10.0.0.0/24

$ aws --output json ec2 describe-security-groups
### (returns blob of json too large to show)

Manually kick off the task to be sure it works.

$ cat run-task-network-configuration.json 
{
  "awsvpcConfiguration": {
    "subnets": [
      "subnet-09a50bcae65469211"
    ],
    "securityGroups": [
      "sg-03816181f609da88b"
    ],
    "assignPublicIp": "ENABLED"
  }
}

$ aws ecs run-task \
	--cluster foo \
	--task-definition printenvironment:1 \
	--launch-type FARGATE \
	--network-configuration file://run-task-network-configuration.json

Check the task's logs to see that the env vars were printed.

$ aws --output text logs describe-log-streams \
	--log-group-name /ecs/printenvironment \
	--order-by LastEventTime \
	--descending \
	--max-items 5 \
	--query 'logStreams[*].[creationTime,logStreamName]'
1607623756054	ecs/envprintercontainer/09f914e84e274c6c92ad25e97e869622

$ aws --output text logs get-log-events \
	--log-group-name /ecs/printenvironment \
	--log-stream-name ecs/envprintercontainer/09f914e84e274c6c92ad25e97e869622 \
	--query 'events[*].[timestamp,message]'
1607623756859	PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
1607623756859	HOSTNAME=ip-10-0-0-79.ec2.internal
1607623756859	ECS_CONTAINER_METADATA_URI_V4=http://169.254.170.2/v4/96293374-3fd1-4cfa-a3d2-8498bf4b545e
1607623756859	MY_PASSWORD=PoorlyChosenPassword
1607623756859	AWS_DEFAULT_REGION=us-east-1
1607623756859	AWS_EXECUTION_ENV=AWS_ECS_FARGATE
1607623756859	AWS_REGION=us-east-1
1607623756859	ECS_CONTAINER_METADATA_URI=http://169.254.170.2/v3/96293374-3fd1-4cfa-a3d2-8498bf4b545e
1607623756859	HOME=/root

And Lo and Behold, there is MY_PASSWORD, correctly injected into the Fargate container!

FOOTNOTES

These links were very handy in helping me figure this out: