Injecting Secrets Manager values into Fargate tasks through the task definition, the execution role permissions it needs, why rotated values do not reach running tasks, and when to read secrets at runtime instead.
Getting a secret into a Fargate container takes three things that live in different places: the secret in Secrets Manager, a secrets block in the task definition, and a permission on the task execution role. Miss the permission and the task fails to start with an access error. Get all three right and the next surprise is rotation: the secret changes in Secrets Manager, and the running tasks keep using the old value.
This guide covers the task definition setup, the IAM it needs, what happens on rotation, and the alternative of reading secrets at runtime, starting with the option where the secret is declared in code: Encore.
Each option is covered below, starting with Encore.
Encore combines an open source infrastructure SDK for TypeScript and Go with a platform that provisions and deploys into your own AWS or GCP account. A secret is a field in a secrets struct, and Encore loads it before your code runs:
// email/email.go
package email
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"encore.dev/beta/errs"
)
// Encore loads these at startup. On AWS the values live in Secrets Manager.
var secrets struct {
PostmarkServerToken string
}
type SendParams struct {
To string
Subject string
Body string
}
//encore:api private method=POST path=/email/send
func Send(ctx context.Context, p *SendParams) error {
payload, _ := json.Marshal(map[string]string{
"From": "app@example.com",
"To": p.To,
"Subject": p.Subject,
"TextBody": p.Body,
})
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.postmarkapp.com/email", bytes.NewReader(payload))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Postmark-Server-Token", secrets.PostmarkServerToken)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return &errs.Error{Code: errs.Unavailable, Message: fmt.Sprintf("postmark returned %d", resp.StatusCode)}
}
return nil
}
Run in your terminal to get started locally.
Set the value per environment type from the CLI. The values are stored per environment, and on AWS Encore replicates them into Secrets Manager in your account and injects them into the container as secret environment variables:
The secret is declared where it is used, with no task definition JSON or ARN to copy. Encore provisions an IAM role for the service, so there is no execution role policy to write. And a cloud deploy fails if a secret the code declares has no value for that environment, so a forgotten secret stops the release instead of a task that starts with an empty variable.
Local development uses the same declaration: encore run makes the local values available, and a .secrets.local.cue file can override one for your machine only. Shared values are never stored in the repository or in a .env file, so an AI coding agent working in the codebase sees which secrets a service declares but never sees the values in the files it works with, and encore secret list only shows which environments have a value; a .secrets.local.cue override is the exception, since it is a plaintext file in the app directory. Teams whose secrets must stay in an existing store can reference them through external vaults.
secrets blockThe standard way to inject a secret as an environment variable. Create the secret:
aws secretsmanager create-secret \ --name prod/email \ --secret-string file://secret.json
secret.json holds the JSON value, for example {"postmark_token":"..."}. Reading it from a file keeps the token out of shell history and the process list. Create the file outside the repository with owner-only permissions (umask 077), and delete it once the secret exists.
Reference it in the container definition. The key name after the ARN selects one field of a JSON secret, and the two trailing colons leave the version stage and version id at their defaults (the current version):
"secrets": [
{
"name": "POSTMARK_SERVER_TOKEN",
"valueFrom": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:prod/email-AbCdEf:postmark_token::"
}
]
Then grant the task execution role access. The execution role is the one ECS uses to start the task, not the task role your code runs with:
{
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:prod/email-*"
}
Add kms:Decrypt on the key if the secret uses a customer managed KMS key instead of the default aws/secretsmanager key. The wildcard after the name covers the six random characters Secrets Manager appends to every secret ARN.
Rotation is the limitation, because ECS resolves the value once, when the task starts. After a rotation, the running tasks keep the old value until you launch new ones, typically with aws ecs update-service --force-new-deployment. For database passwords rotated by Secrets Manager, that means a window where running tasks hold a password that may stop working, unless the rotation strategy keeps the previous one valid.
For secrets that rotate on their own schedule, read them from the application with the AWS SDK and cache the value for a few minutes. The task role, not the execution role, then needs secretsmanager:GetSecretValue. AWS also publishes caching clients for several languages that handle the refresh.
The cost is more code in every service, a dependency on Secrets Manager being reachable at runtime, an API charge per refresh, and AWS credentials on every developer laptop to run the service locally.
environment entries in the task definition are free and easier to review.Use Encore when you want secrets declared next to the code that uses them, with the IAM role created for each service, deploys that fail on a missing value, and the same declaration working locally and in your own AWS account. Use the task definition secrets block for an existing ECS setup, and runtime reads with caching when a service must follow rotations without a restart.
Add a secrets entry to the container definition with the environment variable name and the secret ARN in valueFrom. To pick one key from a JSON secret, append it: arn:aws:secretsmanager:region:account:secret:name-AbCdEf:key::. The task execution role needs secretsmanager:GetSecretValue on that secret, plus kms:Decrypt if it is encrypted with a customer managed key.
ECS reads secrets referenced in the task definition once, when the task starts, and sets them as environment variables. A rotated value only reaches new tasks, so launch new tasks or update the service with force new deployment. If a service must pick up rotations without restarting, read the secret at runtime with the AWS SDK and cache it.
$0.40 per secret per month and $0.05 per 10,000 API calls. Injecting secrets through the task definition costs one GetSecretValue call per task start, while reading at runtime costs a call per cache refresh.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.