@fjall/components-infrastructure 39.0.5 → 39.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/patterns/aws/computeEcs.d.ts +65 -21
- package/dist/lib/patterns/aws/computeEcs.js +243 -95
- package/dist/lib/patterns/aws/computeEcsTypes.d.ts +49 -25
- package/dist/lib/resources/aws/compute/ecs.d.ts +3 -0
- package/dist/lib/resources/aws/compute/ecs.js +4 -0
- package/dist/lib/resources/aws/compute/ecsLifecycleHookMigration.d.ts +32 -62
- package/dist/lib/resources/aws/compute/ecsLifecycleHookMigration.js +96 -50
- package/dist/lib/resources/aws/compute/ecsRolloutBarrier.d.ts +61 -0
- package/dist/lib/resources/aws/compute/ecsRolloutBarrier.js +185 -0
- package/dist/lib/resources/aws/compute/ecsRolloutBarrierLambda.source.cjs +475 -0
- package/dist/lib/resources/aws/compute/ecsTaskDefinition.d.ts +19 -8
- package/dist/lib/resources/aws/compute/ecsTaskDefinition.js +16 -7
- package/dist/lib/resources/aws/compute/ecsValidation.d.ts +3 -3
- package/dist/lib/resources/aws/compute/ecsValidation.js +3 -3
- package/dist/lib/resources/aws/compute/lambda.d.ts +6 -0
- package/dist/lib/resources/aws/compute/lambda.js +18 -12
- package/dist/lib/resources/aws/compute/lifecycleHookLambda.source.cjs +329 -82
- package/package.json +5 -4
|
@@ -320,10 +320,13 @@ export interface EcsInitContainerMigrationsConfig {
|
|
|
320
320
|
* deployment lifecycle hook (PRE_SCALE_UP). The service rollout BLOCKS until
|
|
321
321
|
* the migration succeeds; one task per deploy.
|
|
322
322
|
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
*
|
|
323
|
+
* The migration runs from a task definition of its own, which the service's
|
|
324
|
+
* task definition names in a tag. The hook reads that tag from the revision
|
|
325
|
+
* each deployment targets, so it always runs the migration that deployment's
|
|
326
|
+
* code was built against. With `separateTaskDef` that task definition has
|
|
327
|
+
* its own IAM roles, log group and size; without it, it runs as the
|
|
328
|
+
* service's. Use `separateTaskDef` when migrations need permissions or
|
|
329
|
+
* credentials the service doesn't.
|
|
327
330
|
*/
|
|
328
331
|
export interface EcsLifecycleHookMigrationsConfig {
|
|
329
332
|
/** Required for the lifecycle-hook variant. */
|
|
@@ -333,8 +336,7 @@ export interface EcsLifecycleHookMigrationsConfig {
|
|
|
333
336
|
/** Override timeout in seconds. Defaults to 300 (5 min). Capped at 900s per Lambda invocation; longer migrations use the IN_PROGRESS callback chain. */
|
|
334
337
|
timeoutSeconds?: number;
|
|
335
338
|
/**
|
|
336
|
-
* Override image URI (string only)
|
|
337
|
-
* JSON-serialised into the runner Lambda's `MIGRATE_CONFIG` env var; synth
|
|
339
|
+
* Override image URI (string only): the migration container's image. Synth
|
|
338
340
|
* throws if a `Repository` is passed in lifecycle-hook mode.
|
|
339
341
|
*/
|
|
340
342
|
image?: string;
|
|
@@ -344,7 +346,7 @@ export interface EcsLifecycleHookMigrationsConfig {
|
|
|
344
346
|
secretsImport?: Record<string, SecretImport>;
|
|
345
347
|
/** SSM Parameter Store secret keys. */
|
|
346
348
|
secrets?: string[];
|
|
347
|
-
/**
|
|
349
|
+
/** The migration container's name. Defaults to `"migrate"`. */
|
|
348
350
|
name?: string;
|
|
349
351
|
/**
|
|
350
352
|
* Container entrypoint. Defaults to inheriting from the image. Set to
|
|
@@ -353,14 +355,19 @@ export interface EcsLifecycleHookMigrationsConfig {
|
|
|
353
355
|
*/
|
|
354
356
|
entryPoint?: string[];
|
|
355
357
|
/**
|
|
356
|
-
* When present,
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
358
|
+
* When present, the migration's task definition has its own IAM roles,
|
|
359
|
+
* log group and size, and with `egressTo` its own security group. Required
|
|
360
|
+
* when migrations need permissions the service doesn't (e.g.
|
|
361
|
+
* `rds:CreateDBSnapshot`, cross-service Secrets Manager reads), or a
|
|
362
|
+
* connected database's contributed credentials and policies, such as its
|
|
363
|
+
* schema administrator's — preserves least-privilege on the service task
|
|
364
|
+
* role.
|
|
361
365
|
*
|
|
362
|
-
* When absent, the
|
|
363
|
-
*
|
|
366
|
+
* When absent, the migration's task definition runs as the service's own
|
|
367
|
+
* roles, at its size, in its log group, from its image, with its primary
|
|
368
|
+
* container's environment, entry point and secrets beneath the
|
|
369
|
+
* migration's own. A connected database's contributed secrets and
|
|
370
|
+
* policies are not added: they would widen the service's roles.
|
|
364
371
|
*/
|
|
365
372
|
separateTaskDef?: EcsLifecycleHookMigrationsSeparateTaskDef;
|
|
366
373
|
}
|
|
@@ -394,7 +401,7 @@ export interface EcsPostDeployMigrationsConfig {
|
|
|
394
401
|
secretsImport?: Record<string, SecretImport>;
|
|
395
402
|
/** SSM Parameter Store secret keys. */
|
|
396
403
|
secrets?: string[];
|
|
397
|
-
/**
|
|
404
|
+
/** The migration container's name. Defaults to `"migrate"`. */
|
|
398
405
|
name?: string;
|
|
399
406
|
/** Container entrypoint. Defaults to inheriting from the image. */
|
|
400
407
|
entryPoint?: string[];
|
|
@@ -910,14 +917,28 @@ export interface EcsServiceConfig {
|
|
|
910
917
|
* Names another service in the same cluster whose **lifecycle-hook** (or
|
|
911
918
|
* post-deploy) migrations must finish before this service rolls out.
|
|
912
919
|
*
|
|
913
|
-
* Implemented as
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
920
|
+
* Implemented as the named service's rollout barrier: a custom resource
|
|
921
|
+
* that completes only once ECS reports the named service's deployment of
|
|
922
|
+
* its task definition finished, its migrate hook included, and fails once
|
|
923
|
+
* ECS rolled that deployment back. This service's CloudFormation
|
|
924
|
+
* `DependsOn` names the barrier, so CloudFormation does not begin creating
|
|
925
|
+
* or updating this service until the named service has rolled out. Use it
|
|
926
|
+
* for DB-connecting workers that must not start until the migrate-owning
|
|
927
|
+
* service has applied role / grant / schema migrations; without it, a
|
|
928
|
+
* worker can attempt RDS-IAM auth before the migrate task has run
|
|
929
|
+
* `GRANT rds_iam` (the race that aborts a migration-bearing deploy).
|
|
930
|
+
*
|
|
931
|
+
* A `DependsOn` on the named service itself would not order this:
|
|
932
|
+
* CloudFormation may start a service's dependants at its
|
|
933
|
+
* eventual-consistency check, long before its ECS deployment has finished
|
|
934
|
+
* (measured in us-east-1 from 2026-10-01).
|
|
935
|
+
*
|
|
936
|
+
* The barrier waits only when the named service's task definition changes,
|
|
937
|
+
* which a new image or schema version does, so a deploy that changes
|
|
938
|
+
* neither orders nothing. It holds only for deploys CloudFormation runs:
|
|
939
|
+
* Fjall's code-only deploy and its rollback register a task definition and
|
|
940
|
+
* update the service directly. It fails a rollout that takes longer than
|
|
941
|
+
* an hour, CloudFormation's limit for a custom resource.
|
|
921
942
|
*
|
|
922
943
|
* Works for EC2-capacity services — it is a resource-ordering edge, not a
|
|
923
944
|
* worker-side lifecycle hook, so the FARGATE-only restriction on hook
|
|
@@ -928,8 +949,11 @@ export interface EcsServiceConfig {
|
|
|
928
949
|
* single completion point to await. Validated at synth (target exists,
|
|
929
950
|
* declares hook migrations, no self-reference, no cycle).
|
|
930
951
|
*
|
|
931
|
-
* Escape hatch for orderings this field can't express:
|
|
932
|
-
*
|
|
952
|
+
* Escape hatch for orderings this field can't express: depend on the
|
|
953
|
+
* service's rollout barrier,
|
|
954
|
+
* `compute.getService("x")?.node.addDependency(compute.getRolloutBarrier("y"))`.
|
|
955
|
+
* A `DependsOn` between the services themselves does not order their
|
|
956
|
+
* rollouts.
|
|
933
957
|
*
|
|
934
958
|
* @example
|
|
935
959
|
* awaitMigrationsFrom: "app"
|
|
@@ -4,6 +4,7 @@ import { Construct } from "constructs";
|
|
|
4
4
|
import type { StackBuilder } from "../base/awsStack.js";
|
|
5
5
|
import { Grant, type IGrantable } from "aws-cdk-lib/aws-iam";
|
|
6
6
|
import type { ApplicationListener, ApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
|
7
|
+
import type { ILogGroup } from "aws-cdk-lib/aws-logs";
|
|
7
8
|
import { StackEncryptionKey } from "../secrets/stackEncryptionKey.js";
|
|
8
9
|
import { type EcsClusterProps } from "./ecsTypes.js";
|
|
9
10
|
import { type EcsIngressProfile } from "./ingressProfile.js";
|
|
@@ -96,6 +97,8 @@ export default class EcsCluster extends Construct implements IConnectable {
|
|
|
96
97
|
getService(name: string): FargateService | Ec2Service | undefined;
|
|
97
98
|
/** Get all services in this cluster. */
|
|
98
99
|
getServices(): Map<string, FargateService | Ec2Service>;
|
|
100
|
+
/** The log group every container of the named service writes to. */
|
|
101
|
+
getServiceLogGroup(serviceName: string): ILogGroup | undefined;
|
|
99
102
|
/**
|
|
100
103
|
* Get the task definition for a service or a registered scheduled task.
|
|
101
104
|
* Used by Schedule / Subscription targets to construct the EcsTask adapter
|
|
@@ -226,6 +226,10 @@ export default class EcsCluster extends Construct {
|
|
|
226
226
|
}
|
|
227
227
|
return result;
|
|
228
228
|
}
|
|
229
|
+
/** The log group every container of the named service writes to. */
|
|
230
|
+
getServiceLogGroup(serviceName) {
|
|
231
|
+
return this.services.get(serviceName)?.logGroup;
|
|
232
|
+
}
|
|
229
233
|
/**
|
|
230
234
|
* Get the task definition for a service or a registered scheduled task.
|
|
231
235
|
* Used by Schedule / Subscription targets to construct the EcsTask adapter
|
|
@@ -1,77 +1,38 @@
|
|
|
1
|
-
import { type BaseService, DeploymentLifecycleStage } from "aws-cdk-lib/aws-ecs";
|
|
1
|
+
import { type BaseService, type ContainerDefinition, DeploymentLifecycleStage, type TaskDefinition } from "aws-cdk-lib/aws-ecs";
|
|
2
2
|
import { Construct } from "constructs";
|
|
3
3
|
import { LambdaFunction } from "./lambda.js";
|
|
4
|
-
/**
|
|
5
|
-
* Awsvpc network configuration consumed by the runner's `RunTask` call.
|
|
6
|
-
*/
|
|
7
|
-
export interface LifecycleHookNetworkConfiguration {
|
|
8
|
-
subnetIds: string[];
|
|
9
|
-
securityGroupIds: string[];
|
|
10
|
-
assignPublicIp: boolean;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Optional nested block describing a separate migration task definition.
|
|
14
|
-
*
|
|
15
|
-
* Three lockstep fields enforced by the type system — present-as-a-group or
|
|
16
|
-
* absent-as-a-group. This shape is the resources-layer mirror of
|
|
17
|
-
* `EcsLifecycleHookMigrationsSeparateTaskDef` at the patterns layer.
|
|
18
|
-
*/
|
|
19
|
-
export interface MigrationTaskDef {
|
|
20
|
-
/** ARN of the migration task definition (different from the service task def). */
|
|
21
|
-
definitionArn: string;
|
|
22
|
-
/** Family of the migration task definition — used to scope `ecs:RunTask`. */
|
|
23
|
-
family: string;
|
|
24
|
-
/** Task role ARN for the migration task. */
|
|
25
|
-
taskRoleArn: string;
|
|
26
|
-
/** Execution role ARN for the migration task. */
|
|
27
|
-
executionRoleArn: string;
|
|
28
|
-
}
|
|
29
4
|
export interface EcsLifecycleHookMigrationProps {
|
|
30
5
|
/**
|
|
31
6
|
* The ECS service this hook fires for.
|
|
32
7
|
*/
|
|
33
8
|
service: BaseService;
|
|
9
|
+
/**
|
|
10
|
+
* The container whose definition carries the migration's label: the
|
|
11
|
+
* service's primary container. Defaults to the task definition's default
|
|
12
|
+
* container, its first essential one.
|
|
13
|
+
*/
|
|
14
|
+
primaryContainer?: ContainerDefinition;
|
|
15
|
+
/** The service's physical name, as a literal. */
|
|
16
|
+
serviceName: string;
|
|
17
|
+
/** The physical name of the service's cluster, as a literal. */
|
|
18
|
+
clusterName: string;
|
|
34
19
|
/** Cluster ARN — used for both `ecs:RunTask`'s cluster condition and ListTasks scoping. */
|
|
35
20
|
clusterArn: string;
|
|
36
|
-
/** ARN of the task definition the migrate task launches from. */
|
|
37
|
-
taskDefinitionArn: string;
|
|
38
|
-
/** Family of the task definition — used in the `ecs:RunTask` resource ARN. */
|
|
39
|
-
taskDefinitionFamily: string;
|
|
40
|
-
/** Task execution role ARN (the role pulling the image, writing logs). */
|
|
41
|
-
taskExecutionRoleArn: string;
|
|
42
|
-
/** Task role ARN (the role the running container assumes). */
|
|
43
|
-
taskRoleArn: string;
|
|
44
|
-
/** Migration command, passed through to the runner. */
|
|
45
|
-
command: string[];
|
|
46
|
-
/** Synthetic container name used in `RunTask` overrides. */
|
|
47
|
-
containerName: string;
|
|
48
|
-
/** Optional image override (else the task definition's image is used). */
|
|
49
|
-
image?: string;
|
|
50
21
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
22
|
+
* The task definition the migration runs: its container carries the
|
|
23
|
+
* command, entry point and environment. The service's task definition
|
|
24
|
+
* names it in its `fjall:migration-task-definition` tag, which the hook
|
|
25
|
+
* reads from the revision each deployment targets, and in its primary
|
|
26
|
+
* container's docker label, which the hook reads when that revision
|
|
27
|
+
* carries no tag.
|
|
54
28
|
*/
|
|
55
|
-
|
|
56
|
-
/** Optional environment override. */
|
|
57
|
-
environment?: Record<string, string>;
|
|
29
|
+
migrationTaskDefinition: TaskDefinition;
|
|
58
30
|
/**
|
|
59
31
|
* Lambda timeout in seconds. Defaults to 300, capped at 900 (Lambda max).
|
|
60
32
|
* Should comfortably exceed the migration command's expected runtime
|
|
61
33
|
* including a few re-invocation cycles.
|
|
62
34
|
*/
|
|
63
35
|
timeoutSeconds?: number;
|
|
64
|
-
/** Awsvpc network configuration for the migrate task. */
|
|
65
|
-
networkConfiguration: LifecycleHookNetworkConfiguration;
|
|
66
|
-
/**
|
|
67
|
-
* Optional separate migration task definition. When present, `RunTask`
|
|
68
|
-
* targets THIS task def instead of the service's, and the Lambda's IAM
|
|
69
|
-
* policy expands to cover both task-def families + both role pairs.
|
|
70
|
-
*
|
|
71
|
-
* When absent, behaviour is unchanged — the migration runs in the service
|
|
72
|
-
* task def via `ContainerOverrides`.
|
|
73
|
-
*/
|
|
74
|
-
migrationTaskDef?: MigrationTaskDef;
|
|
75
36
|
/**
|
|
76
37
|
* Which deployment lifecycle stage to fire at. Defaults to
|
|
77
38
|
* `PRE_SCALE_UP` (migration runs BEFORE the new revision scales up;
|
|
@@ -84,11 +45,20 @@ export interface EcsLifecycleHookMigrationProps {
|
|
|
84
45
|
lifecycleStage?: DeploymentLifecycleStage;
|
|
85
46
|
}
|
|
86
47
|
/**
|
|
87
|
-
* ECS deployment lifecycle hook
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
48
|
+
* ECS deployment lifecycle hook that runs a service's migration as a one-off
|
|
49
|
+
* `RunTask` of its migration task definition, before the new revision scales
|
|
50
|
+
* up (or after, for post-deploy work). Self-wires: synthesises the Lambda +
|
|
51
|
+
* IAM + LogGroup and the role ECS assumes to invoke the hook, stamps the
|
|
52
|
+
* service's task definition with the migration task definition it runs, and
|
|
53
|
+
* attaches the hook to the service via the typed CDK API.
|
|
54
|
+
*
|
|
55
|
+
* The hook reads everything it runs from the deployment it serves (FJL-901):
|
|
56
|
+
* the target revision's cluster, task definition and network, and the
|
|
57
|
+
* migration the task definition's tag names, or its primary container's
|
|
58
|
+
* docker label where a copy carries no tag. Its function has no
|
|
59
|
+
* environment, so it changes only when its code does, never on an image
|
|
60
|
+
* deploy, and its own update can never pair one release's migration with
|
|
61
|
+
* another's code.
|
|
92
62
|
*/
|
|
93
63
|
export declare class EcsLifecycleHookMigration extends Construct {
|
|
94
64
|
readonly lambda: LambdaFunction;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import {
|
|
4
|
+
import { MIGRATION_TASK_DEFINITION_LABEL_KEY, MIGRATION_TASK_DEFINITION_TAG_KEY } from "@fjall/util/migration";
|
|
5
|
+
import { ArnFormat, Stack, Tags } from "aws-cdk-lib";
|
|
5
6
|
import { Code, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
6
|
-
import { PolicyStatement, Effect } from "aws-cdk-lib/aws-iam";
|
|
7
7
|
import { RetentionDays } from "aws-cdk-lib/aws-logs";
|
|
8
|
+
import { Effect, PolicyStatement, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
|
|
8
9
|
import { DeploymentLifecycleLambdaTarget, DeploymentLifecycleStage } from "aws-cdk-lib/aws-ecs";
|
|
9
10
|
import { Construct } from "constructs";
|
|
10
11
|
import { LambdaFunction } from "./lambda.js";
|
|
@@ -13,11 +14,20 @@ const LAMBDA_SOURCE_FILE = "lifecycleHookLambda.source.cjs";
|
|
|
13
14
|
const LAMBDA_TIMEOUT_DEFAULT_SECONDS = 300;
|
|
14
15
|
const LAMBDA_TIMEOUT_MAX_SECONDS = 900;
|
|
15
16
|
/**
|
|
16
|
-
* ECS deployment lifecycle hook
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
17
|
+
* ECS deployment lifecycle hook that runs a service's migration as a one-off
|
|
18
|
+
* `RunTask` of its migration task definition, before the new revision scales
|
|
19
|
+
* up (or after, for post-deploy work). Self-wires: synthesises the Lambda +
|
|
20
|
+
* IAM + LogGroup and the role ECS assumes to invoke the hook, stamps the
|
|
21
|
+
* service's task definition with the migration task definition it runs, and
|
|
22
|
+
* attaches the hook to the service via the typed CDK API.
|
|
23
|
+
*
|
|
24
|
+
* The hook reads everything it runs from the deployment it serves (FJL-901):
|
|
25
|
+
* the target revision's cluster, task definition and network, and the
|
|
26
|
+
* migration the task definition's tag names, or its primary container's
|
|
27
|
+
* docker label where a copy carries no tag. Its function has no
|
|
28
|
+
* environment, so it changes only when its code does, never on an image
|
|
29
|
+
* deploy, and its own update can never pair one release's migration with
|
|
30
|
+
* another's code.
|
|
21
31
|
*/
|
|
22
32
|
export class EcsLifecycleHookMigration extends Construct {
|
|
23
33
|
lambda;
|
|
@@ -25,62 +35,81 @@ export class EcsLifecycleHookMigration extends Construct {
|
|
|
25
35
|
super(scope, id);
|
|
26
36
|
const stack = Stack.of(scope);
|
|
27
37
|
const timeoutSeconds = Math.min(props.timeoutSeconds ?? LAMBDA_TIMEOUT_DEFAULT_SECONDS, LAMBDA_TIMEOUT_MAX_SECONDS);
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
const migration = props.migrationTaskDefinition;
|
|
39
|
+
const ecsArn = (resource, resourceName) => stack.formatArn({
|
|
40
|
+
service: "ecs",
|
|
41
|
+
resource,
|
|
42
|
+
resourceName,
|
|
43
|
+
arnFormat: ArnFormat.SLASH_RESOURCE_NAME
|
|
44
|
+
});
|
|
45
|
+
// Literal names, never the service's attributes: the service names this
|
|
46
|
+
// function as its hook target, so a policy that referenced the service
|
|
47
|
+
// would close a cycle.
|
|
48
|
+
const serviceKey = `${props.clusterName}/${props.serviceName}`;
|
|
49
|
+
const familyRevisions = (family) => `arn:${stack.partition}:ecs:${stack.region}:${stack.account}:task-definition/${family}:*`;
|
|
50
|
+
// The tag is a reference, so the service's task definition registers
|
|
51
|
+
// after the migration's. The hook reads the tag, and the label on a
|
|
52
|
+
// revision that carries no tag. A tag alone updates in place, on
|
|
53
|
+
// CloudFormation's own revision only, so a revision a restart or a
|
|
54
|
+
// code-only deploy copied from it would go on naming a migration task
|
|
55
|
+
// definition CloudFormation then deregisters. The label carries the same
|
|
56
|
+
// reference in the container definitions, which no revision can change:
|
|
57
|
+
// a change to the migration alone registers a new revision of the
|
|
58
|
+
// service's task definition, the deploy rolls the service onto it, and
|
|
59
|
+
// every lane that copies a revision copies the live one's label and tag.
|
|
60
|
+
Tags.of(props.service.taskDefinition).add(MIGRATION_TASK_DEFINITION_TAG_KEY, migration.taskDefinitionArn, { includeResourceTypes: ["AWS::ECS::TaskDefinition"] });
|
|
61
|
+
const primary = props.primaryContainer ?? props.service.taskDefinition.defaultContainer;
|
|
62
|
+
if (primary === undefined) {
|
|
63
|
+
throw new Error(`EcsLifecycleHookMigration: the task definition of service "${props.serviceName}" has no essential container to carry its migration's label`);
|
|
37
64
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
65
|
+
primary.addDockerLabel(MIGRATION_TASK_DEFINITION_LABEL_KEY, migration.taskDefinitionArn);
|
|
66
|
+
const passRoleResources = [migration.taskRole.roleArn];
|
|
67
|
+
if (migration.executionRole !== undefined) {
|
|
68
|
+
passRoleResources.push(migration.executionRole.roleArn);
|
|
41
69
|
}
|
|
42
|
-
// ECS RunTask awsvpcConfiguration uses subnets/securityGroups and
|
|
43
|
-
// assignPublicIp as "ENABLED"/"DISABLED" string, not boolean.
|
|
44
|
-
const awsvpcConfiguration = {
|
|
45
|
-
subnets: props.networkConfiguration.subnetIds,
|
|
46
|
-
securityGroups: props.networkConfiguration.securityGroupIds,
|
|
47
|
-
assignPublicIp: props.networkConfiguration.assignPublicIp
|
|
48
|
-
? "ENABLED"
|
|
49
|
-
: "DISABLED"
|
|
50
|
-
};
|
|
51
70
|
this.lambda = new LambdaFunction(this, `${id}Fn`, {
|
|
52
71
|
runtime: Runtime.NODEJS_24_X,
|
|
53
72
|
handler: "index.handler",
|
|
54
|
-
code: Code.fromInline(
|
|
73
|
+
code: Code.fromInline(readFileSync(path.resolve(__dirname, LAMBDA_SOURCE_FILE), "utf-8")),
|
|
55
74
|
lambdaDescription: `${id} ECS deployment lifecycle hook for migration`,
|
|
56
75
|
roleDescription: `Execution role for ${id} migration lifecycle-hook runner`,
|
|
57
76
|
timeout: timeoutSeconds,
|
|
58
77
|
memorySize: 256,
|
|
59
78
|
logGroupRetention: RetentionDays.ONE_MONTH,
|
|
60
|
-
environment: {
|
|
61
|
-
MIGRATE_CONFIG: JSON.stringify({
|
|
62
|
-
clusterArn: props.clusterArn,
|
|
63
|
-
taskDefinitionArn: targetTaskDefinitionArn,
|
|
64
|
-
name: props.containerName,
|
|
65
|
-
command: props.command,
|
|
66
|
-
...(props.image !== undefined ? { image: props.image } : {}),
|
|
67
|
-
...(props.entryPoint !== undefined
|
|
68
|
-
? { entryPoint: props.entryPoint }
|
|
69
|
-
: {}),
|
|
70
|
-
...(props.environment !== undefined
|
|
71
|
-
? { environment: props.environment }
|
|
72
|
-
: {}),
|
|
73
|
-
networkConfiguration: awsvpcConfiguration,
|
|
74
|
-
...(migrationTaskDef !== undefined
|
|
75
|
-
? { hasSeparateMigrationTaskDef: true }
|
|
76
|
-
: {})
|
|
77
|
-
})
|
|
78
|
-
},
|
|
79
79
|
inlinePolicy: [
|
|
80
|
+
new PolicyStatement({
|
|
81
|
+
effect: Effect.ALLOW,
|
|
82
|
+
actions: ["ecs:DescribeServiceRevisions"],
|
|
83
|
+
resources: [
|
|
84
|
+
ecsArn("service", serviceKey),
|
|
85
|
+
ecsArn("service-revision", `${serviceKey}/*`)
|
|
86
|
+
]
|
|
87
|
+
}),
|
|
88
|
+
new PolicyStatement({
|
|
89
|
+
effect: Effect.ALLOW,
|
|
90
|
+
actions: ["ecs:ListTagsForResource"],
|
|
91
|
+
resources: [
|
|
92
|
+
familyRevisions(props.service.taskDefinition.family),
|
|
93
|
+
familyRevisions(migration.family)
|
|
94
|
+
]
|
|
95
|
+
}),
|
|
96
|
+
// The label fallback reads a copied revision's container definitions.
|
|
97
|
+
// DescribeTaskDefinition takes no resource type (AWS service reference,
|
|
98
|
+
// ecs.json, read 2026-10-03), so it cannot be scoped to the family.
|
|
99
|
+
// The revision ECS hands the hook is in the stack's own Region, so the
|
|
100
|
+
// read is held to it.
|
|
101
|
+
new PolicyStatement({
|
|
102
|
+
effect: Effect.ALLOW,
|
|
103
|
+
actions: ["ecs:DescribeTaskDefinition"],
|
|
104
|
+
resources: ["*"],
|
|
105
|
+
conditions: {
|
|
106
|
+
StringEquals: { "aws:RequestedRegion": stack.region }
|
|
107
|
+
}
|
|
108
|
+
}),
|
|
80
109
|
new PolicyStatement({
|
|
81
110
|
effect: Effect.ALLOW,
|
|
82
111
|
actions: ["ecs:RunTask"],
|
|
83
|
-
resources:
|
|
112
|
+
resources: [familyRevisions(migration.family)],
|
|
84
113
|
conditions: {
|
|
85
114
|
ArnEquals: { "ecs:cluster": props.clusterArn }
|
|
86
115
|
}
|
|
@@ -105,9 +134,26 @@ export class EcsLifecycleHookMigration extends Construct {
|
|
|
105
134
|
})
|
|
106
135
|
]
|
|
107
136
|
});
|
|
137
|
+
// Without a role the target creates one while the service renders, after
|
|
138
|
+
// every aspect has run, so no tag and no removal marker ever reach it. The
|
|
139
|
+
// scope and id are the target's own, so the logical id does not move.
|
|
140
|
+
const hookRole = new Role(props.service, `${id}Role`, {
|
|
141
|
+
assumedBy: new ServicePrincipal("ecs.amazonaws.com")
|
|
142
|
+
});
|
|
143
|
+
this.lambda.grantInvoke(hookRole);
|
|
144
|
+
// The hook names the role, which orders the service after the role but
|
|
145
|
+
// not after the grant in the role's own policy, so a create could start
|
|
146
|
+
// the service's first deployment, and ECS invoke the hook, before the
|
|
147
|
+
// grant exists (FJL-1029).
|
|
148
|
+
const serviceResource = props.service.node.defaultChild;
|
|
149
|
+
if (serviceResource === undefined) {
|
|
150
|
+
throw new Error(`EcsLifecycleHookMigration: service "${props.serviceName}" has no CloudFormation resource to order after its hook role's grant`);
|
|
151
|
+
}
|
|
152
|
+
serviceResource.node.addDependency(hookRole);
|
|
108
153
|
const lifecycleStage = props.lifecycleStage ?? DeploymentLifecycleStage.PRE_SCALE_UP;
|
|
109
154
|
props.service.addLifecycleHook(new DeploymentLifecycleLambdaTarget(this.lambda, id, {
|
|
110
|
-
lifecycleStages: [lifecycleStage]
|
|
155
|
+
lifecycleStages: [lifecycleStage],
|
|
156
|
+
role: hookRole
|
|
111
157
|
}));
|
|
112
158
|
}
|
|
113
159
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { CustomResource } from "aws-cdk-lib";
|
|
2
|
+
import type { BaseService } from "aws-cdk-lib/aws-ecs";
|
|
3
|
+
import { Construct, type IDependable } from "constructs";
|
|
4
|
+
/**
|
|
5
|
+
* The stack's one rollout-barrier provider: the `onEvent` and `isComplete`
|
|
6
|
+
* handlers, from one inline source, and the provider framework that asks
|
|
7
|
+
* `isComplete` every 15 s for at most an hour. Each barrier grants the
|
|
8
|
+
* `isComplete` role the reads of its own service only; the role also reads
|
|
9
|
+
* its own stack's status, to complete at once on a rollback.
|
|
10
|
+
*
|
|
11
|
+
* The framework's waiter state machine logs at FATAL, without execution
|
|
12
|
+
* data, to a group named under `/aws/vendedlogs/states/`
|
|
13
|
+
* (`waiterLogGroupName`). Being named, the group is destroyed with its stack
|
|
14
|
+
* whatever the stage: a group a rollback retained would collide with the one
|
|
15
|
+
* the next upgrade creates under the same name.
|
|
16
|
+
*/
|
|
17
|
+
export declare class EcsRolloutBarrierProvider extends Construct {
|
|
18
|
+
/** The stack's provider, made on first use. */
|
|
19
|
+
static of(scope: Construct): EcsRolloutBarrierProvider;
|
|
20
|
+
readonly serviceToken: string;
|
|
21
|
+
private readonly isCompleteHandler;
|
|
22
|
+
private readonly granted;
|
|
23
|
+
private constructor();
|
|
24
|
+
/**
|
|
25
|
+
* The reads `isComplete` makes of one service, scoped to it: its
|
|
26
|
+
* description, its deployments and their revisions (AWS service reference,
|
|
27
|
+
* ecs.json, 2026-10-01). Granted once a service.
|
|
28
|
+
*
|
|
29
|
+
* The ARNs are built from the literal cluster and service names, never from
|
|
30
|
+
* the service's attributes: the grant lands on the one role every barrier in
|
|
31
|
+
* the stack shares, so a reference to a service would make each barrier
|
|
32
|
+
* wait for every owner, and an owner that itself awaits another owner would
|
|
33
|
+
* close a cycle.
|
|
34
|
+
*/
|
|
35
|
+
grantRolloutReads(clusterName: string, serviceName: string): void;
|
|
36
|
+
}
|
|
37
|
+
export interface EcsRolloutBarrierProps {
|
|
38
|
+
/** The service whose rollout the barrier waits for. */
|
|
39
|
+
readonly service: BaseService;
|
|
40
|
+
/** The service's physical name, as a literal. */
|
|
41
|
+
readonly serviceName: string;
|
|
42
|
+
/** The physical name of the service's cluster, as a literal. */
|
|
43
|
+
readonly clusterName: string;
|
|
44
|
+
/**
|
|
45
|
+
* The task definition the service deploys. It changes whenever the image
|
|
46
|
+
* or the expected schema version does, which is exactly when CloudFormation
|
|
47
|
+
* invokes the barrier.
|
|
48
|
+
*/
|
|
49
|
+
readonly taskDefinitionArn: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A custom resource that completes only once ECS reports the service's
|
|
53
|
+
* deployment of its task definition finished, and fails once ECS rolled it
|
|
54
|
+
* back (FJL-901). Its dependants therefore wait for the rollout, PRE_SCALE_UP
|
|
55
|
+
* migrate hook included, whatever CloudFormation's own stabilisation of the
|
|
56
|
+
* service reports. Delete only acknowledges.
|
|
57
|
+
*/
|
|
58
|
+
export declare class EcsRolloutBarrier extends Construct implements IDependable {
|
|
59
|
+
readonly resource: CustomResource;
|
|
60
|
+
constructor(scope: Construct, id: string, props: EcsRolloutBarrierProps);
|
|
61
|
+
}
|