diff --git a/knowledge base/pulumi.md b/knowledge base/pulumi.md index 3982a79..7880d9c 100644 --- a/knowledge base/pulumi.md +++ b/knowledge base/pulumi.md @@ -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. + + + ### Stack init fails because the stack supposedly already exists Context: a stack fails to initialize. diff --git a/snippets/pulumi/aws/ecs service containers with chained start.ts b/snippets/pulumi/aws/ecs service containers with chained start.ts new file mode 100644 index 0000000..de809c2 --- /dev/null +++ b/snippets/pulumi/aws/ecs service containers with chained start.ts @@ -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 = getVpcOutput({ default: true }); +const subnets: PulumiOutput = getSubnetsOutput({ + region: vpc.region, + filters: [{ + name: 'tag:Name', + values: [ + 'private-a', + 'private-b', + 'private-c', + ], + }], +}); + +const ecsCluster: PulumiOutput = getClusterOutput({ + region: vpc.region, + clusterName: 'development', +}); +const ecsExecutionRole: PulumiOutput = getRoleOutput({ name: 'mainApp-dev-ecsExecutionRole' }); +const ecsTaskRole: PulumiOutput = getRoleOutput({ name: 'mainApp-dev-ecsTaskRole' }); + +const vpn_securityGroup: PulumiOutput = getSecurityGroupOutput({ + vpcId: vpc.id, + name: 'vpn', +}); + +const loadBalancer: PulumiOutput = getLoadBalancerOutput({ + region: vpc.region, + name: 'mainApp-dev', +}); +const listener: PulumiOutput = getListenerOutput({ + loadBalancerArn: loadBalancer.arn, + port: 443, +}); + +const mainApp_imageTag: string = 'latest'; +const mainApp_image: PulumiOutput = getImageOutput({ + repositoryName: 'mainApp', + imageTag: mainApp_imageTag, +}); +const mainApp_imageReference: PulumiOutput = mainApp_image.imageUri; + +const commonTags: PulumiInput<{ [key: string]: PulumiInput }> = { + 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, + }, +);