From febb2695f8c06a954bfedd09e44c97e29175bf41 Mon Sep 17 00:00:00 2001 From: Michele Cereda Date: Fri, 21 Nov 2025 23:05:00 +0100 Subject: [PATCH] chore(aws/ecs): populate environment variables from secret manager secrets --- knowledge base/cloud computing/aws/ecs.md | 56 +++++ .../cloud computing/aws/secrets manager.md | 8 +- ... secrets manager secrets in environment.ts | 214 ++++++++++++++++++ 3 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 snippets/pulumi/aws/ecs/use secrets manager secrets in environment.ts diff --git a/knowledge base/cloud computing/aws/ecs.md b/knowledge base/cloud computing/aws/ecs.md index 28d65dd..306e7ac 100644 --- a/knowledge base/cloud computing/aws/ecs.md +++ b/knowledge base/cloud computing/aws/ecs.md @@ -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.
+Specify the _task role_ to allow the app running in a task to call AWS services. +
Usage @@ -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.
+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.
+This IAM Role must allow `ecs.amazonaws.com` to assume it. + +
+ +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowECSToAssumeThisVeryRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} +``` + +
+ +Specifying the _Task Role_ in a task definition grants that IAM Role's permissions **to the task's container**.
+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.
+This IAM Role must allow `ecs-tasks.amazonaws.com` to assume it. + +
+ +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowECSTasksToAssumeThisVeryRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + }, + "Action": "sts:AssumeRole" + } + ] +} +``` + +
+ ## 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. diff --git a/knowledge base/cloud computing/aws/secrets manager.md b/knowledge base/cloud computing/aws/secrets manager.md index 4ea9f02..6a2ee39 100644 --- a/knowledge base/cloud computing/aws/secrets manager.md +++ b/knowledge base/cloud computing/aws/secrets manager.md @@ -50,7 +50,9 @@ When replicating a secret, Secrets Manager creates a copy of the original (A.K.A as a _replica_ secret.
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.
+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.
The managing service might also restrict users from updating secrets, or deleting them without a recovery period.
@@ -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] + +[Authentication and access control for AWS Secrets Manager]: https://docs.aws.amazon.com/secretsmanager/latest/userguide/auth-and-access.html + [JSON structure of AWS Secrets Manager secrets]: https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_secret_json_structure.html diff --git a/snippets/pulumi/aws/ecs/use secrets manager secrets in environment.ts b/snippets/pulumi/aws/ecs/use secrets manager secrets in environment.ts new file mode 100644 index 0000000..83bcd3a --- /dev/null +++ b/snippets/pulumi/aws/ecs/use secrets manager secrets in environment.ts @@ -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 = 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 = getClusterOutput({ clusterName: "dev" }); +const securityGroup: PulumiOutput = getSecurityGroupOutput({ + vpcId: "vpc-01234567", + name: "default", +}); +const subnet: PulumiOutput = 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, + ], + }, + }, +);