ECS tasks are a common way to run containerized applications on AWS. When your application needs sensitive information, like API keys or database credentials, you have a few options for getting those secrets into your containers. The most straightforward, but often the riskiest, is to pass them as environment variables directly in your ECS task definition. A safer, more robust approach is to leverage AWS Secrets Manager or AWS Systems Manager Parameter Store.

Let’s see how this plays out in practice. Imagine you have a simple Node.js application that needs a database password.

The Risky Way: Environment Variables

Here’s a snippet of an ECS task definition using environment variables:

{
  "family": "my-app-task",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "networkMode": "awsvpc",
  "containerDefinitions": [
    {
      "name": "my-app-container",
      "image": "my-dockerhub-user/my-node-app:latest",
      "portMappings": [
        {
          "containerPort": 3000,
          "hostPort": 3000,
          "protocol": "tcp"
        }
      ],
      "environment": [
        {
          "name": "DATABASE_PASSWORD",
          "value": "s3cr3tP@ssw0rd!"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/my-app",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ],
  "requiresCompatibilities": [
    "FARGATE"
  ],
  "cpu": "256",
  "memory": "512"
}

In this definition, s3cr3tP@ssw0rd! is hardcoded directly into the value field.

Why is this bad?

  1. Visibility: Task definitions are often stored in source control. You’ve just committed your database password to your Git repository. Even if you encrypt your repository, it’s an unnecessary risk.
  2. Auditing: Who changed the password? When? It’s hard to track changes to environment variables within task definitions without careful auditing of your infrastructure-as-code.
  3. Rotation: If you need to rotate your database password, you have to update the task definition, potentially redeploy your entire ECS service, and ensure no downtime. This is manual and error-prone.
  4. Access Control: Anyone who can view or modify your ECS task definitions can see your secrets. This broadens the attack surface.

The Safer Way: AWS Secrets Manager or Parameter Store

Instead of embedding secrets directly, you can store them in AWS Secrets Manager or AWS Systems Manager Parameter Store. These services are designed for secure secret management.

Let’s look at how you’d integrate with Secrets Manager.

First, store your secret in Secrets Manager:

  1. Navigate to AWS Secrets Manager in the AWS Console.
  2. Click "Store a new secret."
  3. Choose "Credentials for other AWS services" (or "Other type of secrets" if it’s not an AWS service credential).
  4. Enter your database credentials (e.g., username, password, host, port).
  5. Name your secret, for example, my-app/database-credentials.
  6. Complete the wizard.

Now, update your ECS task definition to reference this secret. You do this by using the secrets field within a container definition, which maps a secret from Secrets Manager to an environment variable within your container.

{
  "family": "my-app-task",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "networkMode": "awsvpc",
  "containerDefinitions": [
    {
      "name": "my-app-container",
      "image": "my-dockerhub-user/my-node-app:latest",
      "portMappings": [
        {
          "containerPort": 3000,
          "hostPort": 3000,
          "protocol": "tcp"
        }
      ],
      "secrets": [
        {
          "name": "DATABASE_PASSWORD",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:my-app/database-credentials-XXXXXX:password::"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/my-app",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ],
  "requiresCompatibilities": [
    "FARGATE"
  ],
  "cpu": "256",
  "memory": "512"
}

How this works:

  • The valueFrom field points to the specific secret in Secrets Manager. The format is arn:aws:secretsmanager:<region>:<account-id>:secret:<secret-name>:<key>:<versionStage>. In this case, we’re extracting the password field from the secret named my-app/database-credentials at the AWSCURRENT version.
  • When ECS launches the task, it retrieves the secret value from Secrets Manager before starting your container.
  • The secret’s value is then injected as an environment variable named DATABASE_PASSWORD into your container.
  • Crucially, the secret value itself is never stored in the task definition JSON. Only a reference is stored.

Benefits of this approach:

  1. No Secrets in Code/Config: Your task definition remains clean. The secret value is not exposed in your source control or ECS configuration.
  2. Centralized Management: Secrets are managed in a dedicated service, making it easy to view, update, and audit them.
  3. Automated Rotation: Secrets Manager can be configured to automatically rotate secrets on a schedule, reducing manual effort and improving security.
  4. Fine-grained Access Control: You can use IAM policies to control precisely who or what (e.g., your ECS task role) can access specific secrets. The ECS task execution role needs secretsmanager:GetSecretValue permission for the secret it’s trying to access.
  5. Version Control: Secrets Manager keeps a history of secret versions, allowing you to easily revert if needed.

Parameter Store Integration (SSM Parameter Store)

The process is very similar if you use AWS Systems Manager Parameter Store. You’d store your secret as a SecureString parameter:

  1. Navigate to AWS Systems Manager in the AWS Console.
  2. Go to "Parameter Store" and click "Create parameter."
  3. Name your parameter, e.g., /my-app/database-password.
  4. Set "Type" to SecureString.
  5. Enter your secret value.
  6. Click "Create parameter."

Then, update your ECS task definition:

{
  "family": "my-app-task",
  "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
  "networkMode": "awsvpc",
  "containerDefinitions": [
    {
      "name": "my-app-container",
      "image": "my-dockerhub-user/my-node-app:latest",
      "portMappings": [
        {
          "containerPort": 3000,
          "hostPort": 3000,
          "protocol": "tcp"
        }
      ],
      "environment": [
        {
          "name": "DATABASE_PASSWORD",
          "value": "" // This value is overridden by the SSM parameter
        }
      ],
      "environmentFiles": [], // Not used here, but could be an alternative
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/my-app",
          "awslogs-region": "us-east-1",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ],
  "requiresCompatibilities": [
    "FARGATE"
  ],
  "cpu": "256",
  "memory": "512"
}

Wait, where’s the valueFrom for SSM? ECS task definitions don’t directly support referencing SSM parameters in the secrets field like they do for Secrets Manager. This is a key difference.

Instead, you use the environment field, but you need to configure your ECS task execution role to have permissions to ssm:GetParameter for the specific parameter.

For example, if your task execution role has the following IAM policy statement:

{
    "Effect": "Allow",
    "Action": [
        "ssm:GetParameter"
    ],
    "Resource": "arn:aws:ssm:us-east-1:123456789012:parameter/my-app/database-password"
}

And your application code is written to fetch the environment variable DATABASE_PASSWORD and then use the AWS SDK to retrieve the actual secret value from SSM using the ssm:GetParameter API call. This means your application code needs to be aware of SSM.

A common pattern is to use a sidecar container or an entrypoint script that fetches the SSM parameter and then sets the environment variable before starting the main application process.

Example using an entrypoint script:

Your entrypoint.sh could look like this:

#!/bin/bash
# Fetch the secret from SSM and set it as an environment variable
export DATABASE_PASSWORD=$(aws ssm get-parameter --name "/my-app/database-password" --with-decryption --query "Parameter.Value" --output text)

# Now execute the main application command
exec "$@"

And your Dockerfile would include:

COPY entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
CMD ["node", "your-app.js"]

The ECS task definition would not have DATABASE_PASSWORD in the environment list, as the entrypoint script injects it.

The key takeaway here is that while Secrets Manager integrates more directly with ECS task definitions via the secrets field, Parameter Store (especially for SecureString) often requires your application or an intermediary script/container to fetch the value.

However, for simple values or configuration that isn’t strictly a "secret" but still sensitive, Parameter Store is often more cost-effective and simpler to manage than Secrets Manager.

The most surprising thing about injecting secrets into ECS is how often the valueFrom syntax for Secrets Manager is misunderstood, leading people to believe they are directly embedding secret names rather than secret values when they’re actually pointing to the ARN of the secret and a specific key within it. The password:: part is crucial for extracting a specific field from a JSON-formatted secret.

If you’re looking to manage application configuration alongside secrets, consider using AWS AppConfig, which integrates with Parameter Store and Secrets Manager and provides features like validation and rollbacks.

Want structured learning?

Take the full Ecs course →