diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/TaskDefinitionContainerRestartPolicyDefaultTestDeployAssert2196C660.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/TaskDefinitionContainerRestartPolicyDefaultTestDeployAssert2196C660.assets.json new file mode 100644 index 0000000000000..6f873f91f3dc9 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/TaskDefinitionContainerRestartPolicyDefaultTestDeployAssert2196C660.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.5", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "TaskDefinitionContainerRestartPolicyDefaultTestDeployAssert2196C660.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/TaskDefinitionContainerRestartPolicyDefaultTestDeployAssert2196C660.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/TaskDefinitionContainerRestartPolicyDefaultTestDeployAssert2196C660.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/TaskDefinitionContainerRestartPolicyDefaultTestDeployAssert2196C660.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/aws-ecs-task-definition-container-restart-policy.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/aws-ecs-task-definition-container-restart-policy.assets.json new file mode 100644 index 0000000000000..0a36b186a379d --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/aws-ecs-task-definition-container-restart-policy.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.5", + "files": { + "97915316dcb8359e5f057ea5fcf4f55ce45085f3ffe49edde0737f593ef80c7d": { + "source": { + "path": "aws-ecs-task-definition-container-restart-policy.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "97915316dcb8359e5f057ea5fcf4f55ce45085f3ffe49edde0737f593ef80c7d.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/aws-ecs-task-definition-container-restart-policy.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/aws-ecs-task-definition-container-restart-policy.template.json new file mode 100644 index 0000000000000..000e8aa4c0573 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/aws-ecs-task-definition-container-restart-policy.template.json @@ -0,0 +1,88 @@ +{ + "Resources": { + "TaskDefTaskRole1EDB4A67": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "TaskDef54694570": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Essential": true, + "Image": "public.ecr.aws/ecs-sample-image/amazon-ecs-sample:latest", + "Name": "Container", + "RestartPolicy": { + "Enabled": true, + "IgnoredExitCodes": [ + 0, + 127 + ], + "RestartAttemptPeriod": 360 + } + } + ], + "Cpu": "256", + "Family": "awsecstaskdefinitioncontainerrestartpolicyTaskDef3A6394C1", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + } + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/cdk.out new file mode 100644 index 0000000000000..bd5311dc372de --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"36.0.5"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/integ.json new file mode 100644 index 0000000000000..ab004c3c26f32 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "36.0.5", + "testCases": { + "TaskDefinitionContainerRestartPolicy/DefaultTest": { + "stacks": [ + "aws-ecs-task-definition-container-restart-policy" + ], + "assertionStack": "TaskDefinitionContainerRestartPolicy/DefaultTest/DeployAssert", + "assertionStackName": "TaskDefinitionContainerRestartPolicyDefaultTestDeployAssert2196C660" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/manifest.json new file mode 100644 index 0000000000000..d15a60ddd4315 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/manifest.json @@ -0,0 +1,119 @@ +{ + "version": "36.0.5", + "artifacts": { + "aws-ecs-task-definition-container-restart-policy.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-ecs-task-definition-container-restart-policy.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-ecs-task-definition-container-restart-policy": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-ecs-task-definition-container-restart-policy.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/97915316dcb8359e5f057ea5fcf4f55ce45085f3ffe49edde0737f593ef80c7d.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-ecs-task-definition-container-restart-policy.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-ecs-task-definition-container-restart-policy.assets" + ], + "metadata": { + "/aws-ecs-task-definition-container-restart-policy/TaskDef/TaskRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TaskDefTaskRole1EDB4A67" + } + ], + "/aws-ecs-task-definition-container-restart-policy/TaskDef/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "TaskDef54694570" + } + ], + "/aws-ecs-task-definition-container-restart-policy/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-ecs-task-definition-container-restart-policy/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-ecs-task-definition-container-restart-policy" + }, + "TaskDefinitionContainerRestartPolicyDefaultTestDeployAssert2196C660.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "TaskDefinitionContainerRestartPolicyDefaultTestDeployAssert2196C660.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "TaskDefinitionContainerRestartPolicyDefaultTestDeployAssert2196C660": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "TaskDefinitionContainerRestartPolicyDefaultTestDeployAssert2196C660.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "TaskDefinitionContainerRestartPolicyDefaultTestDeployAssert2196C660.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "TaskDefinitionContainerRestartPolicyDefaultTestDeployAssert2196C660.assets" + ], + "metadata": { + "/TaskDefinitionContainerRestartPolicy/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/TaskDefinitionContainerRestartPolicy/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "TaskDefinitionContainerRestartPolicy/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/tree.json new file mode 100644 index 0000000000000..3315ebafbd590 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.js.snapshot/tree.json @@ -0,0 +1,203 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-ecs-task-definition-container-restart-policy": { + "id": "aws-ecs-task-definition-container-restart-policy", + "path": "aws-ecs-task-definition-container-restart-policy", + "children": { + "TaskDef": { + "id": "TaskDef", + "path": "aws-ecs-task-definition-container-restart-policy/TaskDef", + "children": { + "TaskRole": { + "id": "TaskRole", + "path": "aws-ecs-task-definition-container-restart-policy/TaskDef/TaskRole", + "children": { + "ImportTaskRole": { + "id": "ImportTaskRole", + "path": "aws-ecs-task-definition-container-restart-policy/TaskDef/TaskRole/ImportTaskRole", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-ecs-task-definition-container-restart-policy/TaskDef/TaskRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-ecs-task-definition-container-restart-policy/TaskDef/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ECS::TaskDefinition", + "aws:cdk:cloudformation:props": { + "containerDefinitions": [ + { + "essential": true, + "image": "public.ecr.aws/ecs-sample-image/amazon-ecs-sample:latest", + "name": "Container", + "restartPolicy": { + "enabled": true, + "ignoredExitCodes": [ + 0, + 127 + ], + "restartAttemptPeriod": 360 + } + } + ], + "cpu": "256", + "family": "awsecstaskdefinitioncontainerrestartpolicyTaskDef3A6394C1", + "memory": "512", + "networkMode": "awsvpc", + "requiresCompatibilities": [ + "FARGATE" + ], + "taskRoleArn": { + "Fn::GetAtt": [ + "TaskDefTaskRole1EDB4A67", + "Arn" + ] + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "Container": { + "id": "Container", + "path": "aws-ecs-task-definition-container-restart-policy/TaskDef/Container", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-ecs-task-definition-container-restart-policy/BootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-ecs-task-definition-container-restart-policy/CheckBootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "TaskDefinitionContainerRestartPolicy": { + "id": "TaskDefinitionContainerRestartPolicy", + "path": "TaskDefinitionContainerRestartPolicy", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "TaskDefinitionContainerRestartPolicy/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "TaskDefinitionContainerRestartPolicy/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "TaskDefinitionContainerRestartPolicy/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "TaskDefinitionContainerRestartPolicy/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "TaskDefinitionContainerRestartPolicy/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.ts new file mode 100644 index 0000000000000..b3b295f64bf43 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ecs/test/base/integ.task-definition-container-restart-policy.ts @@ -0,0 +1,21 @@ +import * as cdk from 'aws-cdk-lib'; +import * as ecs from 'aws-cdk-lib/aws-ecs'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-ecs-task-definition-container-restart-policy'); + +const taskDefinition = new ecs.FargateTaskDefinition(stack, 'TaskDef', {}); + +taskDefinition.addContainer('Container', { + image: ecs.ContainerImage.fromRegistry('public.ecr.aws/ecs-sample-image/amazon-ecs-sample:latest'), + enableRestartPolicy: true, + restartIgnoredExitCodes: [0, 127], + restartAttemptPeriod: cdk.Duration.seconds(360), +}); + +new IntegTest(app, 'TaskDefinitionContainerRestartPolicy', { + testCases: [stack], +}); + +app.synth(); \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-ecs/README.md b/packages/aws-cdk-lib/aws-ecs/README.md index f0669e3adcc66..61fbaae1adab9 100644 --- a/packages/aws-cdk-lib/aws-ecs/README.md +++ b/packages/aws-cdk-lib/aws-ecs/README.md @@ -619,6 +619,22 @@ taskDefinition.addContainer('container', { }); ``` +### Restart policy + +To enable a restart policy for the container, set `enableRestartPolicy` to true and also specify +`restartIgnoredExitCodes` and `restartAttemptPeriod` if necessary. + +```ts +declare const taskDefinition: ecs.TaskDefinition; + +taskDefinition.addContainer('container', { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + enableRestartPolicy: true, + restartIgnoredExitCodes: [0, 127], + restartAttemptPeriod: Duration.seconds(360), +}); +``` + ## Docker labels You can add labels to the container with the `dockerLabels` property or with the `addDockerLabel` method: diff --git a/packages/aws-cdk-lib/aws-ecs/lib/container-definition.ts b/packages/aws-cdk-lib/aws-ecs/lib/container-definition.ts index a620a3510ef3b..ba028210e0bae 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/container-definition.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/container-definition.ts @@ -384,6 +384,42 @@ export interface ContainerDefinitionOptions { * An array of ulimits to set in the container. */ readonly ulimits?: Ulimit[]; + + /** + * Enable a restart policy for a container. + * + * When you set up a restart policy, Amazon ECS can restart the container without needing to replace the task. + * + * @default - false unless `restartIgnoredExitCodes` or `restartAttemptPeriod` is set. + * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/container-restart-policy.html + */ + readonly enableRestartPolicy?: boolean; + + /** + * A list of exit codes that Amazon ECS will ignore and not attempt a restart on. + * + * This property can't be used if `enableRestartPolicy` is set to false. + * + * You can specify a maximum of 50 container exit codes. + * + * @default - No exit codes are ignored. + */ + readonly restartIgnoredExitCodes?: number[]; + + /** + * A period of time that the container must run for before a restart can be attempted. + * + * A container can be restarted only once every `restartAttemptPeriod` seconds. + * If a container isn't able to run for this time period and exits early, it will not be restarted. + * + * This property can't be used if `enableRestartPolicy` is set to false. + * + * You can set a minimum `restartAttemptPeriod` of 60 seconds and a maximum `restartAttemptPeriod` + * of 1800 seconds. + * + * @default - Duration.seconds(300) if `enableRestartPolicy` is true, otherwise no period. + */ + readonly restartAttemptPeriod?: cdk.Duration; } /** @@ -590,6 +626,8 @@ export class ContainerDefinition extends Construct { if (props.ulimits) { this.addUlimits(...props.ulimits); } + + this.validateRestartPolicy(props.enableRestartPolicy, props.restartIgnoredExitCodes, props.restartAttemptPeriod); } /** @@ -771,6 +809,18 @@ export class ContainerDefinition extends Construct { }; } + private validateRestartPolicy(enableRestartPolicy?: boolean, restartIgnoredExitCodes?: number[], restartAttemptPeriod?: cdk.Duration) { + if (enableRestartPolicy === false && (restartIgnoredExitCodes !== undefined || restartAttemptPeriod !== undefined)) { + throw new Error('The restartIgnoredExitCodes and restartAttemptPeriod cannot be specified if enableRestartPolicy is false'); + } + if (restartIgnoredExitCodes && restartIgnoredExitCodes.length > 50) { + throw new Error(`Only up to 50 can be specified for restartIgnoredExitCodes, got: ${restartIgnoredExitCodes.length}`); + } + if (restartAttemptPeriod && (restartAttemptPeriod.toSeconds() < 60 || restartAttemptPeriod.toSeconds() > 1800)) { + throw new Error(`The restartAttemptPeriod must be between 60 seconds and 1800 seconds, got ${restartAttemptPeriod.toSeconds()} seconds`); + } + } + /** * Whether this container definition references a specific JSON field of a secret * stored in Secrets Manager. @@ -873,6 +923,7 @@ export class ContainerDefinition extends Construct { resourceRequirements: (!this.props.gpuCount && this.inferenceAcceleratorResources.length == 0 ) ? undefined : renderResourceRequirements(this.props.gpuCount, this.inferenceAcceleratorResources), systemControls: this.props.systemControls && renderSystemControls(this.props.systemControls), + restartPolicy: renderRestartPolicy(this.props.enableRestartPolicy, this.props.restartIgnoredExitCodes, this.props.restartAttemptPeriod), }; } } @@ -1495,3 +1546,23 @@ function renderSystemControls(systemControls: SystemControl[]): CfnTaskDefinitio value: sc.value, })); } + +function renderRestartPolicy( + enableRestartPolicy?: boolean, + restartIgnoredExitCodes?: number[], + restartAttemptPeriod?: cdk.Duration, +): CfnTaskDefinition.RestartPolicyProperty | undefined { + if (enableRestartPolicy === undefined && restartIgnoredExitCodes === undefined && restartAttemptPeriod === undefined) { + return; + } + + return { + // If `enableRestartPolicy` is undefined, we know that `restartIgnoredExitCodes` or restartAttemptPeriod is specified + // according to the above branch, so we treat `enableRestartPolicy` as true. + // The `validateRestartPolicy` function also ensures that `enableRestartPolicy` is not false if `restartIgnoredExitCodes` + // or `restartAttemptPeriod` is specified, so there is no conflict. + enabled: enableRestartPolicy ?? true, + ignoredExitCodes: restartIgnoredExitCodes, // always undefined if `enabled` is false + restartAttemptPeriod: restartAttemptPeriod?.toSeconds(), // always undefined if `enabled` is false + }; +} diff --git a/packages/aws-cdk-lib/aws-ecs/test/container-definition.test.ts b/packages/aws-cdk-lib/aws-ecs/test/container-definition.test.ts index 96d357fba7242..191f687089dc8 100644 --- a/packages/aws-cdk-lib/aws-ecs/test/container-definition.test.ts +++ b/packages/aws-cdk-lib/aws-ecs/test/container-definition.test.ts @@ -2758,4 +2758,207 @@ describe('container definition', () => { // THEN expect(() => new ecs.ContainerDefinition(stack, 'Container', containerDefinitionProps)).toThrow(/Only one credential spec is allowed per container definition/); }); + + test('can specify restart policy', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // WHEN + new ecs.ContainerDefinition(stack, 'Container', { + image: ecs.ContainerImage.fromRegistry('/aws/aws-example-app'), + taskDefinition, + memoryLimitMiB: 2048, + enableRestartPolicy: true, + restartIgnoredExitCodes: [0, 127], + restartAttemptPeriod: cdk.Duration.seconds(360), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Image: '/aws/aws-example-app', + Name: 'Container', + Memory: 2048, + RestartPolicy: { + Enabled: true, + IgnoredExitCodes: [0, 127], + RestartAttemptPeriod: 360, + }, + }, + ], + }); + }); + + test('restart policy will not be set if not specified', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // WHEN + new ecs.ContainerDefinition(stack, 'Container', { + image: ecs.ContainerImage.fromRegistry('/aws/aws-example-app'), + taskDefinition, + memoryLimitMiB: 2048, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Image: '/aws/aws-example-app', + Name: 'Container', + Memory: 2048, + RestartPolicy: Match.absent(), + }, + ], + }); + }); + + test('enable restart policy when enableRestartPolicy is not specified but restartIgnoredExitCodes is specified', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // WHEN + new ecs.ContainerDefinition(stack, 'Container', { + image: ecs.ContainerImage.fromRegistry('/aws/aws-example-app'), + taskDefinition, + memoryLimitMiB: 2048, + restartIgnoredExitCodes: [0, 127], + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Image: '/aws/aws-example-app', + Name: 'Container', + Memory: 2048, + RestartPolicy: { + Enabled: true, + IgnoredExitCodes: [0, 127], + }, + }, + ], + }); + }); + + test('enable restart policy when enableRestartPolicy is not specified but restartAttemptPeriod is specified', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // WHEN + new ecs.ContainerDefinition(stack, 'Container', { + image: ecs.ContainerImage.fromRegistry('/aws/aws-example-app'), + taskDefinition, + memoryLimitMiB: 2048, + restartAttemptPeriod: cdk.Duration.seconds(360), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + ContainerDefinitions: [ + { + Image: '/aws/aws-example-app', + Name: 'Container', + Memory: 2048, + RestartPolicy: { + Enabled: true, + RestartAttemptPeriod: 360, + }, + }, + ], + }); + }); + + test('throws when enableRestartPolicy is set to false but restartIgnoredExitCodes is specified', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // THEN + expect(() => { + new ecs.ContainerDefinition(stack, 'Container', { + image: ecs.ContainerImage.fromRegistry('/aws/aws-example-app'), + taskDefinition, + memoryLimitMiB: 2048, + enableRestartPolicy: false, + restartIgnoredExitCodes: [0, 127], + }); + }).toThrow(/The restartIgnoredExitCodes and restartAttemptPeriod cannot be specified if enableRestartPolicy is false/); + }); + + test('throws when enableRestartPolicy is set to false but restartAttemptPeriod is specified', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // THEN + expect(() => { + new ecs.ContainerDefinition(stack, 'Container', { + image: ecs.ContainerImage.fromRegistry('/aws/aws-example-app'), + taskDefinition, + memoryLimitMiB: 2048, + enableRestartPolicy: false, + restartAttemptPeriod: cdk.Duration.seconds(360), + }); + }).toThrow(/The restartIgnoredExitCodes and restartAttemptPeriod cannot be specified if enableRestartPolicy is false/); + }); + + test('throws when there are more than 50 in restartIgnoredExitCodes', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // WHEN + const restartIgnoredExitCodes = Array.from({ length: 51 }, (_, i) => i); + + // THEN + expect(() => { + new ecs.ContainerDefinition(stack, 'Container', { + image: ecs.ContainerImage.fromRegistry('/aws/aws-example-app'), + taskDefinition, + memoryLimitMiB: 2048, + enableRestartPolicy: true, + restartIgnoredExitCodes, + }); + }).toThrow(/Only up to 50 can be specified for restartIgnoredExitCodes, got: 51/); + }); + + test('throws when restartAttemptPeriod is greater than 1800 seconds', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // THEN + expect(() => { + new ecs.ContainerDefinition(stack, 'Container', { + image: ecs.ContainerImage.fromRegistry('/aws/aws-example-app'), + taskDefinition, + memoryLimitMiB: 2048, + enableRestartPolicy: true, + restartAttemptPeriod: cdk.Duration.seconds(1801), + }); + }).toThrow(/The restartAttemptPeriod must be between 60 seconds and 1800 seconds, got 1801 seconds/); + }); + + test('throws when restartAttemptPeriod is less than 60 seconds', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TaskDef'); + + // THEN + expect(() => { + new ecs.ContainerDefinition(stack, 'Container', { + image: ecs.ContainerImage.fromRegistry('/aws/aws-example-app'), + taskDefinition, + memoryLimitMiB: 2048, + enableRestartPolicy: true, + restartAttemptPeriod: cdk.Duration.seconds(59), + }); + }).toThrow(/The restartAttemptPeriod must be between 60 seconds and 1800 seconds, got 59 seconds/); + }); });