chore(aws/ecs): populate environment variables from secret manager secrets

This commit is contained in:
Michele Cereda
2025-11-21 23:05:00 +01:00
parent 2165853277
commit febb2695f8
3 changed files with 277 additions and 1 deletions

View File

@@ -62,6 +62,9 @@ By default, containers behave like other Linux processes with respect to access
Unless explicitly protected and guaranteed, all containers running on the same host share CPU, memory, and other
resources much like normal processes running on that host share those very same resources.
Specify the _execution role_ to allow ECS components to call AWS services when starting tasks.<br/>
Specify the _task role_ to allow the app running in a task to call AWS services.
<details>
<summary>Usage</summary>
@@ -172,6 +175,57 @@ Whatever the [launch type] or [capacity provider][capacity providers]:
> [!important]
> Task definition's parameters differ depending on the launch type.
Specifying the _Execution Role_ in a task definition grants that IAM Role's permissions **to the ECS container
agent**, allowing it to call AWS when starting tasks.<br/>
This is required when ECS (and **not** the app in the task's container) needs to make calls to, i.e., read a value from
Secrets Manager.<br/>
This IAM Role must allow `ecs.amazonaws.com` to assume it.
<details style='padding: 0 0 1rem 1rem'>
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowECSToAssumeThisVeryRole",
"Effect": "Allow",
"Principal": {
"Service": "ecs.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
```
</details>
Specifying the _Task Role_ in a task definition grants that IAM Role's permissions **to the task's container**.<br/>
This is required when the app in the task's container (and **not** ECS) needs to make calls to, i.e., recover a file
from S3.<br/>
This IAM Role must allow `ecs-tasks.amazonaws.com` to assume it.
<details style='padding: 0 0 1rem 1rem'>
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowECSTasksToAssumeThisVeryRole",
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
```
</details>
## Standalone tasks
Refer [Amazon ECS standalone tasks].
@@ -1527,6 +1581,8 @@ Options:
- [Pass Secrets Manager secrets through Amazon ECS environment variables].
Use Secrets Manager in environment variables
When setting environment variables to secrets from Secrets Manager, it is the **execution** role (and **not** the task
role) that must have the permissions required to access them.

View File

@@ -50,7 +50,9 @@ When replicating a secret, Secrets Manager creates a copy of the original (A.K.A
as a _replica_ secret.<br/>
The replica secret remains linked to the primary secret, and is updated when a new version of the primary is created.
Secrets Manager uses [IAM] to allow only authorized users to access or modify a secret.
Secrets Manager uses [IAM] to allow only authorized users to access or modify a secret.<br/>
Permissions for them can be set in IAM Policies that are _identity-based_ (the usual ones, granted to IAM Identities),
or _resource-based_ (secret-specific).
_Managed_ secrets are created and managed by the AWS service that created them.<br/>
The managing service might also restrict users from updating secrets, or deleting them without a recovery period.<br/>
@@ -64,6 +66,8 @@ Managed secrets use a naming convention that includes the ID of the service mana
### Sources
- [Authentication and access control for AWS Secrets Manager]
<!--
Reference
═╬═Time══
@@ -76,5 +80,7 @@ Managed secrets use a naming convention that includes the ID of the service mana
[Secrets management]: ../../secrets%20management.md
<!-- Upstream -->
[Authentication and access control for AWS Secrets Manager]: https://docs.aws.amazon.com/secretsmanager/latest/userguide/auth-and-access.html
<!-- Others -->
[JSON structure of AWS Secrets Manager secrets]: https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_secret_json_structure.html

View File

@@ -0,0 +1,214 @@
import {
Output as PulumiOutput,
interpolate as pulumiInterpolate, jsonStringify as pulumiJsonStringify,
} from "@pulumi/pulumi";
import { Secret, SecretPolicy, SecretVersion } from "@pulumi/aws/secretsmanager";
import {
GetRoleResult,
getRoleOutput,
} from "@pulumi/aws/iam";
import {
GetClusterResult, Service, TaskDefinition,
getClusterOutput,
} from "@pulumi/aws/ecs";
import {
GetSecurityGroupResult, GetSubnetResult,
getSecurityGroupOutput, getSubnetOutput,
} from "@pulumi/aws/ec2";
const executionRole: PulumiOutput<GetRoleResult> = getRoleOutput({ name: "ecsExecutionRole" });
const composite_secret: Secret = new Secret(
"smSecretsInEnv-composite",
{
name: "composite-secret",
description: "Some value-only secret",
tags: {},
},
);
new SecretVersion(
"smSecretsInEnv-composite",
{
secretId: composite_secret.id,
secretString: pulumiJsonStringify({
someField: "someValue",
}),
versionStages: [
"AWSCURRENT",
],
},
{ parent: composite_secret },
);
new SecretPolicy(
"smSecretsInEnv-composite",
{
secretArn: composite_secret.arn,
policy: pulumiJsonStringify({
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Principal: {
AWS: executionRole.arn,
},
Action: "secretsmanager:GetSecretValue",
Resource: composite_secret.arn,
},
],
}),
},
{ parent: composite_secret },
);
const valueOnly_secret: Secret = new Secret(
"smSecretsInEnv-valueOnly",
{
name: "valueOnly-secret",
description: "Some value-only secret",
tags: {},
},
);
new SecretVersion(
"smSecretsInEnv-valueOnly",
{
secretId: valueOnly_secret.id,
secretString: "value-only secret",
versionStages: [
"AWSCURRENT",
],
},
{ parent: valueOnly_secret },
);
new SecretPolicy(
"smSecretsInEnv-valueOnly",
{
secretArn: valueOnly_secret.arn,
policy: pulumiJsonStringify({
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Principal: {
AWS: executionRole.arn,
},
Action: "secretsmanager:GetSecretValue",
Resource: valueOnly_secret.arn,
},
],
}),
},
{ parent: valueOnly_secret },
);
const containerDefinitions = [
{
name: "echo-server",
image: "mendhak/http-https-echo:38@sha256:c73e039e883944a38e37eaba829eb9a67641cd03eff868827683951feceef96e",
essential: true,
environment: [
{
name: "WHATEVER",
value: "whatever",
},
],
secrets: [
// loaded as environment variables, but their value is taken from Secrets Manager
{
name: "VALUE_ONLY_SECRET",
valueFrom: valueOnly_secret.arn,
},
{
// requires specifying the field name, prefixed by ':' and followed by '::'
name: "COMPOSITE_SECRET",
valueFrom: pulumiInterpolate`${composite_secret.arn}:someField::`,
},
],
healthCheck: {
command: [
"CMD-SHELL",
`nc -vz -w1 localhost 8080 || nc -vz -w1 localhost 8443`,
],
interval: 6,
retries: 3,
startPeriod: 3,
timeout: 5,
},
portMappings: [
{
name: "http",
protocol: "tcp",
appProtocol: "http",
containerPort: 8080,
hostPort: 8080,
},
{
name: "https",
protocol: "tcp",
appProtocol: "http",
containerPort: 8443,
hostPort: 8443,
},
],
mountPoints: [],
systemControls: [],
volumesFrom: [],
},
];
const taskDefinition = new TaskDefinition(
"smSecretsInEnv",
{
family: "SmSecretsInEnv",
tags: {},
containerDefinitions: pulumiJsonStringify(containerDefinitions),
cpu: "256",
memory: "512",
executionRoleArn: executionRole.arn,
networkMode: "awsvpc",
requiresCompatibilities: [
"FARGATE",
],
runtimePlatform: {
cpuArchitecture: "X86_64",
operatingSystemFamily: "LINUX",
},
},
);
const cluster: PulumiOutput<GetClusterResult> = getClusterOutput({ clusterName: "dev" });
const securityGroup: PulumiOutput<GetSecurityGroupResult> = getSecurityGroupOutput({
vpcId: "vpc-01234567",
name: "default",
});
const subnet: PulumiOutput<GetSubnetResult> = getSubnetOutput({
availabilityZone: "eu-west-1a",
state: "available",
filters: [{
name: "tag:Name",
values: [
"priv-*",
],
}],
});
new Service(
"smSecretsInEnv",
{
name: "SmSecretsInEnv",
cluster: cluster.arn,
taskDefinition: taskDefinition.arn,
tags: {},
desiredCount: 1,
launchType: "FARGATE",
networkConfiguration: {
subnets: [
subnet.id,
],
securityGroups: [
securityGroup.id,
],
},
},
);