From 70c31085cbaec851c6827a5c528bf393b9ffb2a1 Mon Sep 17 00:00:00 2001 From: Kishan Gajjar Date: Mon, 6 Oct 2025 15:45:15 -0400 Subject: [PATCH] fix(ecs): update task definition validations for managed instances --- .../aws-ecs/lib/base/task-definition.ts | 37 +- .../aws-ecs/test/task-definition.test.ts | 361 +++++++++++++++++- 2 files changed, 386 insertions(+), 12 deletions(-) diff --git a/packages/aws-cdk-lib/aws-ecs/lib/base/task-definition.ts b/packages/aws-cdk-lib/aws-ecs/lib/base/task-definition.ts index 8d5d68e48a1ce..c50ba5dde5f56 100644 --- a/packages/aws-cdk-lib/aws-ecs/lib/base/task-definition.ts +++ b/packages/aws-cdk-lib/aws-ecs/lib/base/task-definition.ts @@ -462,7 +462,8 @@ export class TaskDefinition extends TaskDefinitionBase { props.volumes.forEach(v => this.addVolume(v)); } - this.networkMode = props.networkMode ?? (this.isFargateCompatible ? NetworkMode.AWS_VPC : NetworkMode.BRIDGE); + this.networkMode = props.networkMode ?? + (this.isFargateCompatible || this.isManagedInstancesCompatible ? NetworkMode.AWS_VPC : NetworkMode.BRIDGE); if (this.isFargateCompatible && this.networkMode !== NetworkMode.AWS_VPC) { throw new ValidationError(`Fargate tasks can only have AwsVpc network mode, got: ${this.networkMode}`, this); } @@ -492,19 +493,29 @@ export class TaskDefinition extends TaskDefinitionBase { throw new ValidationError(`Managed Instances tasks can only have AwsVpc or Host network mode, got: ${this.networkMode}`, this); } - // Managed Instances require both CPU and memory specifications - if (!props.cpu || !props.memoryMiB) { - throw new ValidationError(`Managed Instances-compatible tasks require both CPU (${props.cpu}) and memory (${props.memoryMiB}) specifications`, this); - } - // Managed Instances don't support inference accelerators if (props.inferenceAccelerators && props.inferenceAccelerators.length > 0) { throw new ValidationError('Cannot use inference accelerators on tasks that run on Managed Instances', this); } - // Managed Instances don't support placement constraints - if (props.placementConstraints && props.placementConstraints.length > 0) { - throw new ValidationError('Cannot set placement constraints on tasks that run on Managed Instances', this); + // Managed Instances don't support ephemeral storage + if (props.ephemeralStorageGiB) { + throw new ValidationError('Ephemeral storage is not supported for tasks running on Managed Instances', this); + } + + // Managed Instances don't support IPC mode + if (props.ipcMode) { + throw new ValidationError('IPC mode is not supported for tasks running on Managed Instances', this); + } + + // Managed Instances don't support proxy configuration + if (props.proxyConfiguration) { + throw new ValidationError('Proxy configuration is not supported for tasks running on Managed Instances', this); + } + + // Managed Instances only support LINUX operating system family + if (props.runtimePlatform?.operatingSystemFamily && !props.runtimePlatform.operatingSystemFamily.isLinux()) { + throw new ValidationError(`Managed Instances tasks only support LINUX operating system family, got: ${props.runtimePlatform.operatingSystemFamily._operatingSystemFamily}`, this); } } @@ -556,7 +567,7 @@ export class TaskDefinition extends TaskDefinitionBase { networkMode: this.renderNetworkMode(this.networkMode), placementConstraints: Lazy.any({ produce: () => - !isFargateCompatible(this.compatibility) ? this.placementConstraints : undefined, + !isFargateCompatible(this.compatibility) && !isManagedInstancesCompatible(this.compatibility) ? this.placementConstraints : undefined, }, { omitEmptyArray: true }), proxyConfiguration: props.proxyConfiguration ? props.proxyConfiguration.bind(this.stack, this) : undefined, cpu: props.cpu, @@ -566,7 +577,7 @@ export class TaskDefinition extends TaskDefinitionBase { ephemeralStorage: this.ephemeralStorageGiB ? { sizeInGiB: this.ephemeralStorageGiB, } : undefined, - runtimePlatform: this.isFargateCompatible && this.runtimePlatform ? { + runtimePlatform: (this.isFargateCompatible || this.isManagedInstancesCompatible) && this.runtimePlatform ? { cpuArchitecture: this.runtimePlatform?.cpuArchitecture?._cpuArchitecture, operatingSystemFamily: this.runtimePlatform?.operatingSystemFamily?._operatingSystemFamily, } : undefined, @@ -751,6 +762,10 @@ export class TaskDefinition extends TaskDefinitionBase { private validateVolume(volume: Volume): void { if (volume.configuredAtLaunch !== true) { + // Validate DockerVolumeConfiguration is not used with Managed Instances + if (this.isManagedInstancesCompatible && volume.dockerVolumeConfiguration) { + throw new ValidationError(`DockerVolumeConfiguration is not supported for tasks running on Managed Instances. Volume '${volume.name}' cannot use dockerVolumeConfiguration`, this); + } return; } diff --git a/packages/aws-cdk-lib/aws-ecs/test/task-definition.test.ts b/packages/aws-cdk-lib/aws-ecs/test/task-definition.test.ts index ee4fecfecb044..a1703793e4ce2 100644 --- a/packages/aws-cdk-lib/aws-ecs/test/task-definition.test.ts +++ b/packages/aws-cdk-lib/aws-ecs/test/task-definition.test.ts @@ -1,4 +1,4 @@ -import { Template } from '../../assertions'; +import { Match, Template } from '../../assertions'; import { EbsDeviceVolumeType } from '../../aws-ec2'; import * as ecr from '../../aws-ecr'; import * as iam from '../../aws-iam'; @@ -707,3 +707,362 @@ describe('task definition revision', () => { }); }); }); + +describe('Managed Instances compatibility', () => { + describe('When creating a task definition with Managed Instances compatibility', () => { + test('A task definition with Managed Instances compatibility defaults to networkmode AwsVpc', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.MANAGED_INSTANCES, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + NetworkMode: 'awsvpc', + RequiresCompatibilities: ['MANAGED_INSTANCES'], + }); + }); + + test('allows Host network mode for Managed Instances', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.MANAGED_INSTANCES, + networkMode: ecs.NetworkMode.HOST, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + NetworkMode: 'host', + }); + }); + + test('throws when using Bridge network mode with Managed Instances', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => { + new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.MANAGED_INSTANCES, + networkMode: ecs.NetworkMode.BRIDGE, + }); + }).toThrow('Managed Instances tasks can only have AwsVpc or Host network mode, got: bridge'); + }); + + test('throws when using None network mode with Managed Instances', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => { + new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.MANAGED_INSTANCES, + networkMode: ecs.NetworkMode.NONE, + }); + }).toThrow('Managed Instances tasks can only have AwsVpc or Host network mode, got: none'); + }); + + test('throws when using inference accelerators with Managed Instances', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => { + new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.MANAGED_INSTANCES, + inferenceAccelerators: [{ + deviceName: 'device1', + deviceType: 'eia2.medium', + }], + }); + }).toThrow('Cannot use inference accelerators on tasks that run on Managed Instances'); + }); + + test('throws when using ephemeral storage with Managed Instances', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => { + new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.MANAGED_INSTANCES, + ephemeralStorageGiB: 30, + }); + }).toThrow('Ephemeral storage is not supported for tasks running on Managed Instances'); + }); + + test('throws when using IPC mode with Managed Instances', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => { + new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.MANAGED_INSTANCES, + ipcMode: ecs.IpcMode.HOST, + }); + }).toThrow('IPC mode is not supported for tasks running on Managed Instances'); + }); + + test('throws when using proxy configuration with Managed Instances', () => { + // GIVEN + const stack = new cdk.Stack(); + const proxyConfig = new ecs.AppMeshProxyConfiguration({ + containerName: 'envoy', + properties: { + appPorts: [8080], + proxyEgressPort: 15001, + proxyIngressPort: 15000, + ignoredUID: 1337, + egressIgnoredIPs: ['169.254.170.2', '169.254.169.254'], + }, + }); + + // THEN + expect(() => { + new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.MANAGED_INSTANCES, + proxyConfiguration: proxyConfig, + }); + }).toThrow('Proxy configuration is not supported for tasks running on Managed Instances'); + }); + + test('throws when using non-Linux operating system family with Managed Instances', () => { + // GIVEN + const stack = new cdk.Stack(); + + // THEN + expect(() => { + new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.MANAGED_INSTANCES, + runtimePlatform: { + operatingSystemFamily: ecs.OperatingSystemFamily.WINDOWS_SERVER_2019_CORE, + }, + }); + }).toThrow('Managed Instances tasks only support LINUX operating system family, got: WINDOWS_SERVER_2019_CORE'); + }); + + test('allows Linux operating system family with Managed Instances', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.MANAGED_INSTANCES, + runtimePlatform: { + operatingSystemFamily: ecs.OperatingSystemFamily.LINUX, + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + RuntimePlatform: { + OperatingSystemFamily: 'LINUX', + }, + }); + }); + + test('allows runtime platform with Managed Instances', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.MANAGED_INSTANCES, + runtimePlatform: { + cpuArchitecture: ecs.CpuArchitecture.ARM64, + operatingSystemFamily: ecs.OperatingSystemFamily.LINUX, + }, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + RuntimePlatform: { + CpuArchitecture: 'ARM64', + OperatingSystemFamily: 'LINUX', + }, + }); + }); + + test('throws when using DockerVolumeConfiguration with Managed Instances', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.MANAGED_INSTANCES, + }); + + // THEN + expect(() => { + taskDefinition.addVolume({ + name: 'my-volume', + dockerVolumeConfiguration: { + driver: 'local', + scope: ecs.Scope.TASK, + }, + }); + }).toThrow("DockerVolumeConfiguration is not supported for tasks running on Managed Instances. Volume 'my-volume' cannot use dockerVolumeConfiguration"); + }); + + test('isManagedInstancesCompatible returns true for MANAGED_INSTANCES compatibility', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.MANAGED_INSTANCES, + }); + + // THEN + expect(taskDefinition.isManagedInstancesCompatible).toBe(true); + expect(taskDefinition.isEc2Compatible).toBe(false); + expect(taskDefinition.isFargateCompatible).toBe(false); + expect(taskDefinition.isExternalCompatible).toBe(false); + }); + + test('isManagedInstancesCompatible returns true for EC2_AND_MANAGED_INSTANCES compatibility', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.EC2_AND_MANAGED_INSTANCES, + }); + + // THEN + expect(taskDefinition.isManagedInstancesCompatible).toBe(true); + expect(taskDefinition.isEc2Compatible).toBe(true); + expect(taskDefinition.isFargateCompatible).toBe(false); + expect(taskDefinition.isExternalCompatible).toBe(false); + }); + + test('isManagedInstancesCompatible returns true for FARGATE_AND_MANAGED_INSTANCES compatibility', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.FARGATE_AND_MANAGED_INSTANCES, + }); + + // THEN + expect(taskDefinition.isManagedInstancesCompatible).toBe(true); + expect(taskDefinition.isEc2Compatible).toBe(false); + expect(taskDefinition.isFargateCompatible).toBe(true); + expect(taskDefinition.isExternalCompatible).toBe(false); + }); + + test('placement constraints are not rendered for Managed Instances compatible tasks', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.MANAGED_INSTANCES, + }); + + // WHEN + taskDefinition.addPlacementConstraint(ecs.PlacementConstraint.memberOf('attribute:ecs.instance-type =~ t2.*')); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::ECS::TaskDefinition', { + PlacementConstraints: Match.absent(), + }); + }); + }); + + describe('Volume validation with configuredAtLaunch', () => { + test('throws when volume with configuredAtLaunch has other configurations', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.FARGATE, + }); + + // THEN + expect(() => { + taskDefinition.addVolume({ + name: 'my-volume', + configuredAtLaunch: true, + host: { + sourcePath: '/path/to/source', + }, + }); + }).toThrow("Volume Configurations must not be specified for 'my-volume' when 'configuredAtLaunch' is set to true"); + }); + + test('throws when volume with configuredAtLaunch has dockerVolumeConfiguration', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.EC2, + }); + + // THEN + expect(() => { + taskDefinition.addVolume({ + name: 'my-volume', + configuredAtLaunch: true, + dockerVolumeConfiguration: { + driver: 'local', + scope: ecs.Scope.TASK, + }, + }); + }).toThrow("Volume Configurations must not be specified for 'my-volume' when 'configuredAtLaunch' is set to true"); + }); + + test('throws when volume with configuredAtLaunch has efsVolumeConfiguration', () => { + // GIVEN + const stack = new cdk.Stack(); + const taskDefinition = new ecs.TaskDefinition(stack, 'TD', { + cpu: '512', + memoryMiB: '512', + compatibility: ecs.Compatibility.FARGATE, + }); + + // THEN + expect(() => { + taskDefinition.addVolume({ + name: 'my-volume', + configuredAtLaunch: true, + efsVolumeConfiguration: { + fileSystemId: 'fs-12345678', + }, + }); + }).toThrow("Volume Configurations must not be specified for 'my-volume' when 'configuredAtLaunch' is set to true"); + }); + }); +});