chore(pulumi): add snippet about ecs service with containers with cascade start

This commit is contained in:
Michele Cereda
2025-10-06 23:41:10 +02:00
parent 600124c782
commit f4189c317c
2 changed files with 458 additions and 0 deletions

View File

@@ -26,6 +26,7 @@
1. [Assume role with MFA enabled but AssumeRoleTokenProvider session option not set](#assume-role-with-mfa-enabled-but-assumeroletokenprovider-session-option-not-set)
1. [Attempting to deploy or update resources with pending operations from previous deployment](#attempting-to-deploy-or-update-resources-with-pending-operations-from-previous-deployment)
1. [Change your program back to the original providers](#change-your-program-back-to-the-original-providers)
1. [RangeError: Invalid string length](#rangeerror-invalid-string-length)
1. [Stack init fails because the stack supposedly already exists](#stack-init-fails-because-the-stack-supposedly-already-exists)
1. [Stack init fails due to missing scheme](#stack-init-fails-due-to-missing-scheme)
1. [Stack init fails due to invalid key identifier](#stack-init-fails-due-to-invalid-key-identifier)
@@ -1466,6 +1467,35 @@ Solution:
1. Run `pulumi install` to gather the required version.
1. Try the action again now.
### RangeError: Invalid string length
Error message example:
```plaintext
Diagnostics:
pulumi:pulumi:Stack (someStack-dev):
error: Running program '/path/to/pulumi/project/index.ts' failed with an unhandled exception:
RangeError: Invalid string length
at markNodeModules (node:internal/util/inspect:1601:21)
at formatError (node:internal/util/inspect:1691:18)
at formatRaw (node:internal/util/inspect:1084:14)
at formatValue (node:internal/util/inspect:932:10)
at Object.inspect (node:internal/util/inspect:409:10)
at process.uncaughtHandler (/path/to/pulumi/project/node_modules/@pulumi/cmd/run/run.ts:302:48)
at process.emit (node:events:520:35)
at process.emit (/path/to/pulumi/project/node_modules/@pulumi/pulumi/vendor/ts-node@7.0.1/index.js:2204:25)
at process.emit (/path/to/pulumi/project/node_modules/source-map-support/source-map-support.js:516:21)
at emitUnhandledRejection (node:internal/process/promises:252:13)
error: an unhandled error occurred: Program exited with non-zero exit code: 1
```
Root cause: something is wrong in the stack's configuration; most likely, the code tries to load a key from it and fails
because it is missing in the file.
Solution: add all required missing keys to the stack's configuration file.
</details>
### Stack init fails because the stack supposedly already exists
Context: a stack fails to initialize.

View File

@@ -0,0 +1,428 @@
import {
getListenerOutput, GetListenerResult,
getLoadBalancerOutput, GetLoadBalancerResult,
ListenerRule,
TargetGroup,
} from '@pulumi/aws/alb';
import { LogGroup } from '@pulumi/aws/cloudwatch';
import {
getSecurityGroupOutput, GetSecurityGroupResult,
getSubnetsOutput, GetSubnetsResult,
getVpcOutput, GetVpcResult,
SecurityGroup,
} from '@pulumi/aws/ec2';
import { getImageOutput, GetImageResult } from '@pulumi/aws/ecr';
import {
getClusterOutput, GetClusterResult,
Service,
TaskDefinition,
} from '@pulumi/aws/ecs';
import { getRoleOutput, GetRoleResult } from '@pulumi/aws/iam';
import { Secret, SecretVersion } from '@pulumi/aws/secretsmanager';
import { SecurityGroupEgressRule, SecurityGroupIngressRule } from '@pulumi/aws/vpc';
import {
Config as PulumiConfig,
Input as PulumiInput,
Output as PulumiOutput,
interpolate as pulumiInterpolate,
jsonStringify as pulumiJsonStringify,
} from '@pulumi/pulumi';
const pulumiConfig = new PulumiConfig();
const dbImage: string = pulumiConfig.get('dbImage')
|| '012345678901.dkr.ecr.eu-west-1.amazonaws.com/cache/dockerHub/library/postgres:16.8';
const dbPort: number = pulumiConfig.getNumber('dbPort') || 5432;
const dbName: string = pulumiConfig.get('dbName') || 'postgres';
const dbUser: string = pulumiConfig.get('dbUser') || 'postgres';
const dbPassword: string = pulumiConfig.requireSecret('dbPassword');
const vpc: PulumiOutput<GetVpcResult> = getVpcOutput({ default: true });
const subnets: PulumiOutput<GetSubnetsResult> = getSubnetsOutput({
region: vpc.region,
filters: [{
name: 'tag:Name',
values: [
'private-a',
'private-b',
'private-c',
],
}],
});
const ecsCluster: PulumiOutput<GetClusterResult> = getClusterOutput({
region: vpc.region,
clusterName: 'development',
});
const ecsExecutionRole: PulumiOutput<GetRoleResult> = getRoleOutput({ name: 'mainApp-dev-ecsExecutionRole' });
const ecsTaskRole: PulumiOutput<GetRoleResult> = getRoleOutput({ name: 'mainApp-dev-ecsTaskRole' });
const vpn_securityGroup: PulumiOutput<GetSecurityGroupResult> = getSecurityGroupOutput({
vpcId: vpc.id,
name: 'vpn',
});
const loadBalancer: PulumiOutput<GetLoadBalancerResult> = getLoadBalancerOutput({
region: vpc.region,
name: 'mainApp-dev',
});
const listener: PulumiOutput<GetListenerResult> = getListenerOutput({
loadBalancerArn: loadBalancer.arn,
port: 443,
});
const mainApp_imageTag: string = 'latest';
const mainApp_image: PulumiOutput<GetImageResult> = getImageOutput({
repositoryName: 'mainApp',
imageTag: mainApp_imageTag,
});
const mainApp_imageReference: PulumiOutput<string> = mainApp_image.imageUri;
const commonTags: PulumiInput<{ [key: string]: PulumiInput<string> }> = {
Team: 'Infrastructure',
Owner: 'infrastructure@example.org',
Application: 'MainApp',
Component: 'Service',
ManagedByPulumi: 'true',
PulumiProject: 'mainApp/infra/dev',
};
const dbPassword_secret: Secret = new Secret(
'dbPassword',
{
name: 'mainApp/dev/db/password',
description: 'Password',
tags: {
Application: 'MainApp',
Environment: 'Development',
Component: 'Service',
},
},
{ protect: true },
);
new SecretVersion(
'dbPassword',
{
secretId: dbPassword_secret.id,
secretString: dbPassword,
versionStages: [
'INITIAL',
'AWSCURRENT',
],
},
{ parent: dbPassword_secret },
);
const logGroup: LogGroup = new LogGroup(
'mainApp',
{
name: '/ecs/mainApp/dev',
tags: {
...commonTags,
},
retentionInDays: 14,
},
);
const containerDefinitions: unknown = [
{
name: 'db',
essential: true,
image: dbImage,
environment: [
// server config
{ name: 'POSTGRES_PORT', value: dbPort.toString() },
{ name: 'POSTGRES_DB', value: dbName },
{ name: 'POSTGRES_USER', value: dbUser },
{ name: 'POSTGRES_PASSWORD', value: dbPassword },
// needed by the health check
{ name: 'PGPORT', value: dbPort.toString() },
],
portMappings: [{
protocol: 'tcp',
appProtocol: 'http',
containerPort: dbPort,
hostPort: dbPort,
}],
healthCheck: {
command: [ 'CMD-SHELL', "pg_isready -h 'localhost' || exit 1" ],
interval: 30,
timeout: 5,
retries: 2,
startPeriod: 15,
},
logConfiguration: {
logDriver: 'awslogs',
options: {
'awslogs-region': 'eu-west-1',
'awslogs-group': logGroup.name,
'awslogs-stream-prefix': 'db',
},
},
},
{
name: 'db-init',
essential: false, // cannot be essential if others need to depend on it being in the COMPLETE state
dependsOn: [
{
containerName: 'db',
condition: 'HEALTHY',
},
],
image: mainApp_imageReference,
environment: [
{
name: 'PGHOST',
value: 'localhost', // ecs does *not* resolve container names to ip addresses like docker and k8s do
},
{ name: 'PGPORT', value: dbPort.toString() },
{ name: 'PGDATABASE', value: dbName },
{ name: 'PGUSER', value: dbUser },
],
secrets: [
{ name: 'PGPASSWORD', valueFrom: dbPassword_secret.arn },
],
workingDirectory: '/opt/src',
command: [ 'alembic', 'upgrade', 'head' ],
logConfiguration: {
logDriver: 'awslogs',
options: {
'awslogs-region': 'eu-west-1',
'awslogs-group': logGroup.name,
'awslogs-stream-prefix': 'db',
},
},
},
{
name: 'mainApp',
essential: true,
dependsOn: [{
containerName: 'db-init',
condition: 'COMPLETE',
}],
image: mainApp_imageReference,
environment: [
{
name: 'PGHOST',
value: 'localhost', // ecs does *not* resolve container names to ip addresses like docker and k8s do
},
{ name: 'PGPORT', value: dbPort.toString() },
{ name: 'PGDATABASE', value: dbName },
{ name: 'PGUSER', value: dbUser },
],
secrets: [
{ name: 'PGPASSWORD', valueFrom: dbPassword_secret.arn },
],
healthCheck: {
command: [ 'CMD-SHELL', 'curl -f http://localhost:8080/ || exit 1' ],
interval: 30,
timeout: 5,
retries: 2,
startPeriod: 15,
},
portMappings: [{
protocol: 'tcp',
appProtocol: 'http',
containerPort: 8080,
hostPort: 8080,
}],
logConfiguration: {
logDriver: 'awslogs',
options: {
'awslogs-region': 'eu-west-1',
'awslogs-group': logGroup.name,
'awslogs-stream-prefix': 'mainApp',
},
},
},
];
const taskDefinition: TaskDefinition = new TaskDefinition(
'mainApp',
{
family: 'mainApp',
tags: {
...commonTags,
},
executionRoleArn: ecsExecutionRole.arn,
taskRoleArn: ecsTaskRole.arn,
containerDefinitions: pulumiJsonStringify(containerDefinitions),
cpu: '2048',
memory: '4096',
networkMode: 'awsvpc',
requiresCompatibilities: [
'FARGATE',
],
runtimePlatform: {
cpuArchitecture: 'X86_64',
operatingSystemFamily: 'LINUX',
},
},
);
const securityGroup: SecurityGroup = new SecurityGroup(
'mainApp',
{
name: 'mainApp-dev',
description: 'Controls the network perimeter for MainApp in development',
tags: {
...commonTags,
Name: 'MainApp Dev',
},
vpcId: vpc.id,
},
);
new SecurityGroupEgressRule(
'mainApp-allowAll:ipv4',
{
securityGroupId: securityGroup.id,
description: 'All IPv4 connections',
tags: {
Name: 'All IPv4',
},
cidrIpv4: '0.0.0.0/0',
ipProtocol: '-1',
},
{ parent: securityGroup },
);
new SecurityGroupEgressRule(
'mainApp-allowAll:ipv6',
{
securityGroupId: securityGroup.id,
description: 'All IPv6 connections',
tags: {
Name: 'All IPv6',
},
cidrIpv6: '::/0',
ipProtocol: '-1',
},
{ parent: securityGroup },
);
new SecurityGroupIngressRule(
'mainApp-internalTraffic',
{
securityGroupId: securityGroup.id,
description: 'Traffic between members of this Security Group',
tags: {
Name: 'Internal traffic',
},
referencedSecurityGroupId: securityGroup.id,
ipProtocol: '-1',
},
{ parent: securityGroup },
);
new SecurityGroupIngressRule(
'mainApp-vpn',
{
securityGroupId: securityGroup.id,
description: 'Traffic from the VPN',
tags: {
Name: 'VPN',
},
referencedSecurityGroupId: vpn_securityGroup.id,
ipProtocol: '-1',
},
{ parent: securityGroup },
);
const targetGroup: TargetGroup = new TargetGroup(
'mainApp',
{
name: 'MainApp',
tags: {
...commonTags,
},
vpcId: vpc.id,
ipAddressType: 'ipv4',
targetType: 'ip',
protocol: 'HTTP',
protocolVersion: 'HTTP2',
port: 80,
healthCheck: {
healthyThreshold: 5,
matcher: '200',
path: '/',
timeout: 5,
unhealthyThreshold: 2,
},
loadBalancingAlgorithmType: 'round_robin',
loadBalancingCrossZoneEnabled: 'use_load_balancer_configuration',
stickiness: {
enabled: true,
type: 'lb_cookie',
},
deregistrationDelay: 30,
},
);
new ListenerRule(
'mainApp',
{
tags: {
...commonTags,
Name: 'MainApp',
},
listenerArn: listener.arn,
actions: [{
type: 'forward',
targetGroupArn: targetGroup.arn,
}],
conditions: [{
hostHeader: {
values: [
'main-app.dev.example.org',
],
},
}],
},
);
new Service(
'mainApp',
{
name: 'mainApp',
tags: {
...commonTags,
},
cluster: ecsCluster.arn,
taskDefinition: pulumiInterpolate`${taskDefinition.family}:${taskDefinition.revision}`,
capacityProviderStrategies: [{
capacityProvider: 'FARGATE_SPOT',
weight: 1,
}],
platformVersion: 'LATEST',
desiredCount: 1,
healthCheckGracePeriodSeconds: 5,
loadBalancers: [{
containerName: 'mainApp',
containerPort: 9000,
targetGroupArn: targetGroup.arn,
}],
networkConfiguration: {
subnets: subnets.ids,
securityGroups: [
securityGroup.id,
],
},
forceNewDeployment: true,
deploymentCircuitBreaker: {
enable: true,
rollback: true,
},
enableEcsManagedTags: true,
propagateTags: 'SERVICE',
enableExecuteCommand: true,
waitForSteadyState: true,
},
);