packages/aws-cdk-lib/aws-ecs/lib/base/task-definition.ts (543 lines of code) (raw):

import { Construct } from 'constructs'; import { ImportedTaskDefinition } from './_imported-task-definition'; import * as ec2 from '../../../aws-ec2'; import * as iam from '../../../aws-iam'; import { IResource, Lazy, Names, PhysicalName, Resource } from '../../../core'; import { addConstructMetadata, MethodMetadata } from '../../../core/lib/metadata-resource'; import { ContainerDefinition, ContainerDefinitionOptions, PortMapping, Protocol } from '../container-definition'; import { CfnTaskDefinition } from '../ecs.generated'; import { FirelensLogRouter, FirelensLogRouterDefinitionOptions, FirelensLogRouterType, obtainDefaultFluentBitECRImage } from '../firelens-log-router'; import { AwsLogDriver } from '../log-drivers/aws-log-driver'; import { PlacementConstraint } from '../placement'; import { ProxyConfiguration } from '../proxy-configuration/proxy-configuration'; import { RuntimePlatform } from '../runtime-platform'; /** * The interface for all task definitions. */ export interface ITaskDefinition extends IResource { /** * ARN of this task definition * @attribute */ readonly taskDefinitionArn: string; /** * Execution role for this task definition */ readonly executionRole?: iam.IRole; /** * What launch types this task definition should be compatible with. */ readonly compatibility: Compatibility; /** * Return true if the task definition can be run on an EC2 cluster */ readonly isEc2Compatible: boolean; /** * Return true if the task definition can be run on a Fargate cluster */ readonly isFargateCompatible: boolean; /** * Return true if the task definition can be run on a ECS Anywhere cluster */ readonly isExternalCompatible: boolean; /** * The networking mode to use for the containers in the task. */ readonly networkMode: NetworkMode; /** * The name of the IAM role that grants containers in the task permission to call AWS APIs on your behalf. */ readonly taskRole: iam.IRole; } /** * The common properties for all task definitions. For more information, see * [Task Definition Parameters](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html). */ export interface CommonTaskDefinitionProps { /** * The name of a family that this task definition is registered to. A family groups multiple versions of a task definition. * * @default - Automatically generated name. */ readonly family?: string; /** * The name of the IAM task execution role that grants the ECS agent permission to call AWS APIs on your behalf. * * The role will be used to retrieve container images from ECR and create CloudWatch log groups. * * @default - An execution role will be automatically created if you use ECR images in your task definition. */ readonly executionRole?: iam.IRole; /** * The name of the IAM role that grants containers in the task permission to call AWS APIs on your behalf. * * @default - A task role is automatically created for you. */ readonly taskRole?: iam.IRole; /** * The configuration details for the App Mesh proxy. * * @default - No proxy configuration. */ readonly proxyConfiguration?: ProxyConfiguration; /** * The list of volume definitions for the task. For more information, see * [Task Definition Parameter Volumes](https://docs.aws.amazon.com/AmazonECS/latest/developerguide//task_definition_parameters.html#volumes). * * @default - No volumes are passed to the Docker daemon on a container instance. */ readonly volumes?: Volume[]; /** * Enables fault injection and allows for fault injection requests to be accepted from the task's containers. * * Fault injection only works with tasks using the {@link NetworkMode.AWS_VPC} or {@link NetworkMode.HOST} network modes. * * @default undefined - ECS default setting is false */ readonly enableFaultInjection?: boolean; } /** * The properties for task definitions. */ export interface TaskDefinitionProps extends CommonTaskDefinitionProps { /** * The networking mode to use for the containers in the task. * * On Fargate, the only supported networking mode is AwsVpc. * * @default - NetworkMode.Bridge for EC2 & External tasks, AwsVpc for Fargate tasks. */ readonly networkMode?: NetworkMode; /** * The placement constraints to use for tasks in the service. * * You can specify a maximum of 10 constraints per task (this limit includes * constraints in the task definition and those specified at run time). * * Not supported in Fargate. * * @default - No placement constraints. */ readonly placementConstraints?: PlacementConstraint[]; /** * The task launch type compatibility requirement. */ readonly compatibility: Compatibility; /** * The number of cpu units used by the task. * * If you are using the EC2 launch type, this field is optional and any value can be used. * If you are using the Fargate launch type, this field is required and you must use one of the following values, * which determines your range of valid values for the memory parameter: * * 256 (.25 vCPU) - Available memory values: 512 (0.5 GB), 1024 (1 GB), 2048 (2 GB) * * 512 (.5 vCPU) - Available memory values: 1024 (1 GB), 2048 (2 GB), 3072 (3 GB), 4096 (4 GB) * * 1024 (1 vCPU) - Available memory values: 2048 (2 GB), 3072 (3 GB), 4096 (4 GB), 5120 (5 GB), 6144 (6 GB), 7168 (7 GB), 8192 (8 GB) * * 2048 (2 vCPU) - Available memory values: Between 4096 (4 GB) and 16384 (16 GB) in increments of 1024 (1 GB) * * 4096 (4 vCPU) - Available memory values: Between 8192 (8 GB) and 30720 (30 GB) in increments of 1024 (1 GB) * * 8192 (8 vCPU) - Available memory values: Between 16384 (16 GB) and 61440 (60 GB) in increments of 4096 (4 GB) * * 16384 (16 vCPU) - Available memory values: Between 32768 (32 GB) and 122880 (120 GB) in increments of 8192 (8 GB) * * @default - CPU units are not specified. */ readonly cpu?: string; /** * The amount (in MiB) of memory used by the task. * * If using the EC2 launch type, this field is optional and any value can be used. * If using the Fargate launch type, this field is required and you must use one of the following values, * which determines your range of valid values for the cpu parameter: * * 512 (0.5 GB), 1024 (1 GB), 2048 (2 GB) - Available cpu values: 256 (.25 vCPU) * * 1024 (1 GB), 2048 (2 GB), 3072 (3 GB), 4096 (4 GB) - Available cpu values: 512 (.5 vCPU) * * 2048 (2 GB), 3072 (3 GB), 4096 (4 GB), 5120 (5 GB), 6144 (6 GB), 7168 (7 GB), 8192 (8 GB) - Available cpu values: 1024 (1 vCPU) * * Between 4096 (4 GB) and 16384 (16 GB) in increments of 1024 (1 GB) - Available cpu values: 2048 (2 vCPU) * * Between 8192 (8 GB) and 30720 (30 GB) in increments of 1024 (1 GB) - Available cpu values: 4096 (4 vCPU) * * Between 16384 (16 GB) and 61440 (60 GB) in increments of 4096 (4 GB) - Available cpu values: 8192 (8 vCPU) * * Between 32768 (32 GB) and 122880 (120 GB) in increments of 8192 (8 GB) - Available cpu values: 16384 (16 vCPU) * * @default - Memory used by task is not specified. */ readonly memoryMiB?: string; /** * The IPC resource namespace to use for the containers in the task. * * Not supported in Fargate and Windows containers. * * @default - IpcMode used by the task is not specified */ readonly ipcMode?: IpcMode; /** * The process namespace to use for the containers in the task. * * Only supported for tasks that are hosted on AWS Fargate if the tasks * are using platform version 1.4.0 or later (Linux). Only the TASK option * is supported for Linux-based Fargate containers. Not supported in Windows * containers. If pidMode is specified for a Fargate task, then * runtimePlatform.operatingSystemFamily must also be specified. For more * information, see [Task Definition Parameters](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#task_definition_pidmode). * * @default - PidMode used by the task is not specified */ readonly pidMode?: PidMode; /** * The inference accelerators to use for the containers in the task. * * Not supported in Fargate. * * @default - No inference accelerators. * @deprecated ECS TaskDefinition's inferenceAccelerator is EOL since April 2024 */ readonly inferenceAccelerators?: InferenceAccelerator[]; /** * The amount (in GiB) of ephemeral storage to be allocated to the task. * * Only supported in Fargate platform version 1.4.0 or later. * * @default - Undefined, in which case, the task will receive 20GiB ephemeral storage. */ readonly ephemeralStorageGiB?: number; /** * The operating system that your task definitions are running on. * * A runtimePlatform is supported only for tasks using the Fargate launch type. * * @default - Undefined. */ readonly runtimePlatform?: RuntimePlatform; } /** * The common task definition attributes used across all types of task definitions. */ export interface CommonTaskDefinitionAttributes { /** * The arn of the task definition */ readonly taskDefinitionArn: string; /** * The networking mode to use for the containers in the task. * * @default Network mode cannot be provided to the imported task. */ readonly networkMode?: NetworkMode; /** * The name of the IAM role that grants containers in the task permission to call AWS APIs on your behalf. * * @default Permissions cannot be granted to the imported task. */ readonly taskRole?: iam.IRole; /** * The IAM role that grants containers and Fargate agents permission to make AWS API calls on your behalf. * * Some tasks do not have an execution role. * * @default - undefined */ readonly executionRole?: iam.IRole; } /** * A reference to an existing task definition */ export interface TaskDefinitionAttributes extends CommonTaskDefinitionAttributes { /** * What launch types this task definition should be compatible with. * * @default Compatibility.EC2_AND_FARGATE */ readonly compatibility?: Compatibility; } abstract class TaskDefinitionBase extends Resource implements ITaskDefinition { public abstract readonly compatibility: Compatibility; public abstract readonly networkMode: NetworkMode; public abstract readonly taskDefinitionArn: string; public abstract readonly taskRole: iam.IRole; public abstract readonly executionRole?: iam.IRole; /** * Return true if the task definition can be run on an EC2 cluster */ public get isEc2Compatible(): boolean { return isEc2Compatible(this.compatibility); } /** * Return true if the task definition can be run on a Fargate cluster */ public get isFargateCompatible(): boolean { return isFargateCompatible(this.compatibility); } /** * Return true if the task definition can be run on a ECS anywhere cluster */ public get isExternalCompatible(): boolean { return isExternalCompatible(this.compatibility); } } /** * The base class for all task definitions. */ export class TaskDefinition extends TaskDefinitionBase { /** * Imports a task definition from the specified task definition ARN. * * The task will have a compatibility of EC2+Fargate. */ public static fromTaskDefinitionArn(scope: Construct, id: string, taskDefinitionArn: string): ITaskDefinition { return new ImportedTaskDefinition(scope, id, { taskDefinitionArn: taskDefinitionArn }); } /** * Create a task definition from a task definition reference */ public static fromTaskDefinitionAttributes(scope: Construct, id: string, attrs: TaskDefinitionAttributes): ITaskDefinition { return new ImportedTaskDefinition(scope, id, { taskDefinitionArn: attrs.taskDefinitionArn, compatibility: attrs.compatibility, networkMode: attrs.networkMode, taskRole: attrs.taskRole, executionRole: attrs.executionRole, }); } /** * The name of a family that this task definition is registered to. * A family groups multiple versions of a task definition. */ public readonly family: string; /** * The full Amazon Resource Name (ARN) of the task definition. * @attribute */ public readonly taskDefinitionArn: string; /** * The name of the IAM role that grants containers in the task permission to call AWS APIs on your behalf. */ public readonly taskRole: iam.IRole; /** * The networking mode to use for the containers in the task. */ public readonly networkMode: NetworkMode; /** * Default container for this task * * Load balancers will send traffic to this container. The first * essential container that is added to this task will become the default * container. */ public defaultContainer?: ContainerDefinition; /** * The task launch type compatibility requirement. */ public readonly compatibility: Compatibility; /** * The amount (in GiB) of ephemeral storage to be allocated to the task. * * Only supported in Fargate platform version 1.4.0 or later. */ public readonly ephemeralStorageGiB?: number; /** * The process namespace to use for the containers in the task. * * Only supported for tasks that are hosted on AWS Fargate if the tasks * are using platform version 1.4.0 or later (Linux). Not supported in * Windows containers. If pidMode is specified for a Fargate task, * then runtimePlatform.operatingSystemFamily must also be specified. For more * information, see [Task Definition Parameters](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#task_definition_pidmode). */ public readonly pidMode?: PidMode; /** * The container definitions. */ protected readonly containers = new Array<ContainerDefinition>(); /** * All volumes */ private readonly volumes: Volume[] = []; /** * Placement constraints for task instances */ private readonly placementConstraints = new Array<CfnTaskDefinition.TaskDefinitionPlacementConstraintProperty>(); /** * Inference accelerators for task instances */ private readonly _inferenceAccelerators: InferenceAccelerator[] = []; private _executionRole?: iam.IRole; private _passRoleStatement?: iam.PolicyStatement; private runtimePlatform?: RuntimePlatform; private readonly _cpu?: string; private readonly _memory?: string; /** * Constructs a new instance of the TaskDefinition class. */ constructor(scope: Construct, id: string, props: TaskDefinitionProps) { super(scope, id); // Enhanced CDK Analytics Telemetry addConstructMetadata(this, props); this.family = props.family || Names.uniqueId(this); this.compatibility = props.compatibility; if (props.volumes) { props.volumes.forEach(v => this.addVolume(v)); } this.networkMode = props.networkMode ?? (this.isFargateCompatible ? NetworkMode.AWS_VPC : NetworkMode.BRIDGE); if (this.isFargateCompatible && this.networkMode !== NetworkMode.AWS_VPC) { throw new Error(`Fargate tasks can only have AwsVpc network mode, got: ${this.networkMode}`); } if (props.proxyConfiguration && this.networkMode !== NetworkMode.AWS_VPC) { throw new Error(`ProxyConfiguration can only be used with AwsVpc network mode, got: ${this.networkMode}`); } if (props.placementConstraints && props.placementConstraints.length > 0 && this.isFargateCompatible) { throw new Error('Cannot set placement constraints on tasks that run on Fargate'); } if (this.isFargateCompatible && (!props.cpu || !props.memoryMiB)) { throw new Error(`Fargate-compatible tasks require both CPU (${props.cpu}) and memory (${props.memoryMiB}) specifications`); } if (props.inferenceAccelerators && props.inferenceAccelerators.length > 0 && this.isFargateCompatible) { throw new Error('Cannot use inference accelerators on tasks that run on Fargate'); } if (this.isExternalCompatible && ![NetworkMode.BRIDGE, NetworkMode.HOST, NetworkMode.NONE].includes(this.networkMode)) { throw new Error(`External tasks can only have Bridge, Host or None network mode, got: ${this.networkMode}`); } if (!this.isFargateCompatible && props.runtimePlatform) { throw new Error('Cannot specify runtimePlatform in non-Fargate compatible tasks'); } // https://docs.aws.amazon.com/AmazonECS/latest/developerguide/fault-injection.html if (props.enableFaultInjection && ![NetworkMode.AWS_VPC, NetworkMode.HOST].includes(this.networkMode)) { throw new Error(`Only AWS_VPC and HOST Network Modes are supported for enabling Fault Injection, got ${this.networkMode} mode.`); } this._executionRole = props.executionRole; this.taskRole = props.taskRole || new iam.Role(this, 'TaskRole', { assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), }); if (props.inferenceAccelerators) { props.inferenceAccelerators.forEach(ia => this.addInferenceAccelerator(ia)); } this.ephemeralStorageGiB = props.ephemeralStorageGiB; this.pidMode = props.pidMode; // validate the cpu and memory size for the Windows operation system family. if (props.runtimePlatform?.operatingSystemFamily?.isWindows()) { // We know that props.cpu and props.memoryMiB are defined because an error would have been thrown previously if they were not. // But, typescript is not able to figure this out, so using the `!` operator here to let the type-checker know they are defined. this.checkFargateWindowsBasedTasksSize(props.cpu!, props.memoryMiB!, props.runtimePlatform!); } this.runtimePlatform = props.runtimePlatform; this._cpu = props.cpu; this._memory = props.memoryMiB; const taskDef = new CfnTaskDefinition(this, 'Resource', { containerDefinitions: Lazy.any({ produce: () => this.renderContainers() }, { omitEmptyArray: true }), volumes: Lazy.any({ produce: () => this.renderVolumes() }, { omitEmptyArray: true }), executionRoleArn: Lazy.string({ produce: () => this.executionRole && this.executionRole.roleArn }), family: this.family, taskRoleArn: this.taskRole.roleArn, requiresCompatibilities: [ ...(isEc2Compatible(props.compatibility) ? ['EC2'] : []), ...(isFargateCompatible(props.compatibility) ? ['FARGATE'] : []), ...(isExternalCompatible(props.compatibility) ? ['EXTERNAL'] : []), ], networkMode: this.renderNetworkMode(this.networkMode), placementConstraints: Lazy.any({ produce: () => !isFargateCompatible(this.compatibility) ? this.placementConstraints : undefined, }, { omitEmptyArray: true }), proxyConfiguration: props.proxyConfiguration ? props.proxyConfiguration.bind(this.stack, this) : undefined, cpu: props.cpu, memory: props.memoryMiB, ipcMode: props.ipcMode, pidMode: this.pidMode, inferenceAccelerators: Lazy.any({ produce: () => !isFargateCompatible(this.compatibility) ? this.renderInferenceAccelerators() : undefined, }, { omitEmptyArray: true }), ephemeralStorage: this.ephemeralStorageGiB ? { sizeInGiB: this.ephemeralStorageGiB, } : undefined, runtimePlatform: this.isFargateCompatible && this.runtimePlatform ? { cpuArchitecture: this.runtimePlatform?.cpuArchitecture?._cpuArchitecture, operatingSystemFamily: this.runtimePlatform?.operatingSystemFamily?._operatingSystemFamily, } : undefined, enableFaultInjection: props.enableFaultInjection, }); if (props.placementConstraints) { props.placementConstraints.forEach(pc => this.addPlacementConstraint(pc)); } this.taskDefinitionArn = taskDef.ref; this.node.addValidation({ validate: () => this.validateTaskDefinition() }); } public get executionRole(): iam.IRole | undefined { return this._executionRole; } /** * Public getter method to access list of inference accelerators attached to the instance. */ public get inferenceAccelerators(): InferenceAccelerator[] { return this._inferenceAccelerators; } private renderVolumes(): CfnTaskDefinition.VolumeProperty[] { return this.volumes.map(renderVolume); function renderVolume(spec: Volume): CfnTaskDefinition.VolumeProperty { return { host: spec.host, name: spec.name, configuredAtLaunch: spec.configuredAtLaunch, dockerVolumeConfiguration: spec.dockerVolumeConfiguration && { autoprovision: spec.dockerVolumeConfiguration.autoprovision, driver: spec.dockerVolumeConfiguration.driver, driverOpts: spec.dockerVolumeConfiguration.driverOpts, labels: spec.dockerVolumeConfiguration.labels, scope: spec.dockerVolumeConfiguration.scope, }, efsVolumeConfiguration: spec.efsVolumeConfiguration && { filesystemId: spec.efsVolumeConfiguration.fileSystemId, authorizationConfig: spec.efsVolumeConfiguration.authorizationConfig, rootDirectory: spec.efsVolumeConfiguration.rootDirectory, transitEncryption: spec.efsVolumeConfiguration.transitEncryption, transitEncryptionPort: spec.efsVolumeConfiguration.transitEncryptionPort, }, }; } } private renderInferenceAccelerators(): CfnTaskDefinition.InferenceAcceleratorProperty[] { return this._inferenceAccelerators.map(renderInferenceAccelerator); function renderInferenceAccelerator(inferenceAccelerator: InferenceAccelerator): CfnTaskDefinition.InferenceAcceleratorProperty { return { deviceName: inferenceAccelerator.deviceName, deviceType: inferenceAccelerator.deviceType, }; } } /** * Validate the existence of the input target and set default values. * * @internal */ public _validateTarget(options: LoadBalancerTargetOptions): LoadBalancerTarget { const targetContainer = this.findContainer(options.containerName); if (targetContainer === undefined) { throw new Error(`No container named '${options.containerName}'. Did you call "addContainer()"?`); } const targetProtocol = options.protocol || Protocol.TCP; const targetContainerPort = options.containerPort || targetContainer.containerPort; const portMapping = targetContainer.findPortMapping(targetContainerPort, targetProtocol); if (portMapping === undefined) { // eslint-disable-next-line max-len throw new Error(`Container '${targetContainer}' has no mapping for port ${options.containerPort} and protocol ${targetProtocol}. Did you call "container.addPortMappings()"?`); } return { containerName: options.containerName, portMapping, }; } /** * Returns the port range to be opened that match the provided container name and container port. * * @internal */ public _portRangeFromPortMapping(portMapping: PortMapping): ec2.Port { if (portMapping.hostPort !== undefined && portMapping.hostPort !== 0) { return portMapping.protocol === Protocol.UDP ? ec2.Port.udp(portMapping.hostPort) : ec2.Port.tcp(portMapping.hostPort); } if (this.networkMode === NetworkMode.BRIDGE || this.networkMode === NetworkMode.NAT) { return EPHEMERAL_PORT_RANGE; } if (portMapping.containerPort !== ContainerDefinition.CONTAINER_PORT_USE_RANGE) { return portMapping.protocol === Protocol.UDP ? ec2.Port.udp(portMapping.containerPort) : ec2.Port.tcp(portMapping.containerPort); } const [startPort, endPort] = portMapping.containerPortRange!.split('-', 2).map(v => Number(v)); return portMapping.protocol === Protocol.UDP ? ec2.Port.udpRange(startPort, endPort) : ec2.Port.tcpRange(startPort, endPort); } /** * Adds a policy statement to the task IAM role. */ @MethodMetadata() public addToTaskRolePolicy(statement: iam.PolicyStatement) { this.taskRole.addToPrincipalPolicy(statement); } /** * Adds a policy statement to the task execution IAM role. */ @MethodMetadata() public addToExecutionRolePolicy(statement: iam.PolicyStatement) { this.obtainExecutionRole().addToPrincipalPolicy(statement); } /** * Adds a new container to the task definition. */ @MethodMetadata() public addContainer(id: string, props: ContainerDefinitionOptions) { return new ContainerDefinition(this, id, { taskDefinition: this, ...props }); } /** * Adds a firelens log router to the task definition. */ @MethodMetadata() public addFirelensLogRouter(id: string, props: FirelensLogRouterDefinitionOptions) { // only one firelens log router is allowed in each task. if (this.containers.find(x => x instanceof FirelensLogRouter)) { throw new Error('Firelens log router is already added in this task.'); } return new FirelensLogRouter(this, id, { taskDefinition: this, ...props }); } /** * Links a container to this task definition. * @internal */ public _linkContainer(container: ContainerDefinition) { if (this._cpu) { const taskCpu = Number(this._cpu); const sumOfContainerCpu = [...this.containers, container].map(c => c.cpu).filter((cpu): cpu is number => typeof cpu === 'number').reduce((a, c) => a + c, 0); if (taskCpu < sumOfContainerCpu) { throw new Error('The sum of all container cpu values cannot be greater than the value of the task cpu'); } } this.containers.push(container); if (this.defaultContainer === undefined && container.essential) { this.defaultContainer = container; } } /** * Adds a volume to the task definition. */ @MethodMetadata() public addVolume(volume: Volume) { this.validateVolume(volume); this.volumes.push(volume); } private validateVolume(volume: Volume): void { if (volume.configuredAtLaunch !== true) { return; } // Other volume configurations must not be specified. if (volume.host || volume.dockerVolumeConfiguration || volume.efsVolumeConfiguration) { throw new Error(`Volume Configurations must not be specified for '${volume.name}' when 'configuredAtLaunch' is set to true`); } } /** * Adds the specified placement constraint to the task definition. */ @MethodMetadata() public addPlacementConstraint(constraint: PlacementConstraint) { if (isFargateCompatible(this.compatibility)) { throw new Error('Cannot set placement constraints on tasks that run on Fargate'); } this.placementConstraints.push(...constraint.toJson()); } /** * Adds the specified extension to the task definition. * * Extension can be used to apply a packaged modification to * a task definition. */ @MethodMetadata() public addExtension(extension: ITaskDefinitionExtension) { extension.extend(this); } /** * Adds an inference accelerator to the task definition. * @deprecated ECS TaskDefinition's inferenceAccelerator is EOL since April 2024 */ @MethodMetadata() public addInferenceAccelerator(inferenceAccelerator: InferenceAccelerator) { if (isFargateCompatible(this.compatibility)) { throw new Error('Cannot use inference accelerators on tasks that run on Fargate'); } this._inferenceAccelerators.push(inferenceAccelerator); } /** * Grants permissions to run this task definition * * This will grant the following permissions: * * - ecs:RunTask * - iam:PassRole * * @param grantee Principal to grant consume rights to */ @MethodMetadata() public grantRun(grantee: iam.IGrantable) { grantee.grantPrincipal.addToPrincipalPolicy(this.passRoleStatement); return iam.Grant.addToPrincipal({ grantee, actions: ['ecs:RunTask'], resourceArns: [this.taskDefinitionArn], }); } /** * Creates the task execution IAM role if it doesn't already exist. */ @MethodMetadata() public obtainExecutionRole(): iam.IRole { if (!this._executionRole) { this._executionRole = new iam.Role(this, 'ExecutionRole', { assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), // needed for cross-account access with TagParameterContainerImage roleName: PhysicalName.GENERATE_IF_NEEDED, }); this.passRoleStatement.addResources(this._executionRole.roleArn); } return this._executionRole; } /** * Whether this task definition has at least a container that references a * specific JSON field of a secret stored in Secrets Manager. */ public get referencesSecretJsonField(): boolean | undefined { for (const container of this.containers) { if (container.referencesSecretJsonField) { return true; } } return false; } /** * Validates the task definition. */ private validateTaskDefinition(): string[] { const ret = new Array<string>(); if (isEc2Compatible(this.compatibility)) { // EC2 mode validations // Container sizes if (!this._memory) { for (const container of this.containers) { if (!container.memoryLimitSpecified) { ret.push(`ECS Container ${container.containerName} must have at least one of 'memoryLimitMiB' or 'memoryReservationMiB' specified`); } } } } // Validate that there are no named port mapping conflicts for Service Connect. const portMappingNames = new Map<string, string>(); // Map from port mapping name to most recent container it appears in. this.containers.forEach(container => { for (const pm of container.portMappings) { if (pm.name) { if (portMappingNames.has(pm.name)) { ret.push(`Port mapping name '${pm.name}' cannot appear in both '${container.containerName}' and '${portMappingNames.get(pm.name)}'`); } portMappingNames.set(pm.name, container.containerName); } } }); // Validate if multiple volumes configured with configuredAtLaunch. const runtimeVolumes = this.volumes.filter(vol => vol.configuredAtLaunch); if (runtimeVolumes.length > 1) { const volumeNames = runtimeVolumes.map(vol => vol.name).join(','); ret.push(`More than one volume is configured at launch: [${volumeNames}]`); } // Validate that volume with configuredAtLaunch set to true is mounted by at least one container. for (const volume of this.volumes) { if (volume.configuredAtLaunch) { const isVolumeMounted = this.containers.some(container => { return container.mountPoints.some(mp => mp.sourceVolume === volume.name); }); if (!isVolumeMounted) { ret.push(`Volume '${volume.name}' should be mounted by at least one container when 'configuredAtLaunch' is true`); } } } return ret; } /** * Determine the existing port mapping for the provided name. * @param name: port mapping name * @returns PortMapping for the provided name, if it exists. */ @MethodMetadata() public findPortMappingByName(name: string): PortMapping | undefined { let portMapping; this.containers.forEach(container => { const pm = container.findPortMappingByName(name); if (pm) { portMapping = pm; } }); return portMapping; } /** * Returns the container that match the provided containerName. */ @MethodMetadata() public findContainer(containerName: string): ContainerDefinition | undefined { return this.containers.find(c => c.containerName === containerName); } private get passRoleStatement() { if (!this._passRoleStatement) { this._passRoleStatement = new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['iam:PassRole'], resources: this.executionRole ? [this.taskRole.roleArn, this.executionRole.roleArn] : [this.taskRole.roleArn], conditions: { StringLike: { 'iam:PassedToService': 'ecs-tasks.amazonaws.com' }, }, }); } return this._passRoleStatement; } private renderNetworkMode(networkMode: NetworkMode): string | undefined { return (networkMode === NetworkMode.NAT) ? undefined : networkMode; } private renderContainers() { // add firelens log router container if any application container is using firelens log driver, // also check if already created log router container for (const container of this.containers) { if (container.logDriverConfig && container.logDriverConfig.logDriver === 'awsfirelens' && !this.containers.find(x => x instanceof FirelensLogRouter)) { this.addFirelensLogRouter('log-router', { image: obtainDefaultFluentBitECRImage(this, container.logDriverConfig), firelensConfig: { type: FirelensLogRouterType.FLUENTBIT, }, logging: new AwsLogDriver({ streamPrefix: 'firelens' }), memoryReservationMiB: 50, }); break; } } return this.containers.map(x => x.renderContainerDefinition()); } private checkFargateWindowsBasedTasksSize(cpu: string, memory: string, runtimePlatform: RuntimePlatform) { if (Number(cpu) === 1024) { if (Number(memory) < 1024 || Number(memory) > 8192 || (Number(memory) % 1024 !== 0)) { throw new Error(`If provided cpu is ${cpu}, then memoryMiB must have a min of 1024 and a max of 8192, in 1024 increments. Provided memoryMiB was ${Number(memory)}.`); } } else if (Number(cpu) === 2048) { if (Number(memory) < 4096 || Number(memory) > 16384 || (Number(memory) % 1024 !== 0)) { throw new Error(`If provided cpu is ${cpu}, then memoryMiB must have a min of 4096 and max of 16384, in 1024 increments. Provided memoryMiB ${Number(memory)}.`); } } else if (Number(cpu) === 4096) { if (Number(memory) < 8192 || Number(memory) > 30720 || (Number(memory) % 1024 !== 0)) { throw new Error(`If provided cpu is ${cpu}, then memoryMiB must have a min of 8192 and a max of 30720, in 1024 increments.Provided memoryMiB was ${Number(memory)}.`); } } else { throw new Error(`If operatingSystemFamily is ${runtimePlatform.operatingSystemFamily!._operatingSystemFamily}, then cpu must be in 1024 (1 vCPU), 2048 (2 vCPU), or 4096 (4 vCPU). Provided value was: ${cpu}`); } } } /** * The port range to open up for dynamic port mapping */ const EPHEMERAL_PORT_RANGE = ec2.Port.tcpRange(32768, 65535); /** * The networking mode to use for the containers in the task. */ export enum NetworkMode { /** * The task's containers do not have external connectivity and port mappings can't be specified in the container definition. */ NONE = 'none', /** * The task utilizes Docker's built-in virtual network which runs inside each container instance. */ BRIDGE = 'bridge', /** * The task is allocated an elastic network interface. */ AWS_VPC = 'awsvpc', /** * The task bypasses Docker's built-in virtual network and maps container ports directly to the EC2 instance's network interface directly. * * In this mode, you can't run multiple instantiations of the same task on a * single container instance when port mappings are used. */ HOST = 'host', /** * The task utilizes NAT network mode required by Windows containers. * * This is the only supported network mode for Windows containers. For more information, see * [Task Definition Parameters](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#network_mode). */ NAT = 'nat', } /** * The IPC resource namespace to use for the containers in the task. */ export enum IpcMode { /** * If none is specified, then IPC resources within the containers of a task are private and not * shared with other containers in a task or on the container instance */ NONE = 'none', /** * If host is specified, then all containers within the tasks that specified the host IPC mode on * the same container instance share the same IPC resources with the host Amazon EC2 instance. */ HOST = 'host', /** * If task is specified, all containers within the specified task share the same IPC resources. */ TASK = 'task', } /** * The process namespace to use for the containers in the task. */ export enum PidMode { /** * If host is specified, then all containers within the tasks that specified the host PID mode * on the same container instance share the same process namespace with the host Amazon EC2 instance. */ HOST = 'host', /** * If task is specified, all containers within the specified task share the same process namespace. */ TASK = 'task', } /** * Elastic Inference Accelerator. * For more information, see [Elastic Inference Basics](https://docs.aws.amazon.com/elastic-inference/latest/developerguide/basics.html) */ export interface InferenceAccelerator { /** * The Elastic Inference accelerator device name. * @default - empty */ readonly deviceName?: string; /** * The Elastic Inference accelerator type to use. The allowed values are: eia2.medium, eia2.large and eia2.xlarge. * @default - empty */ readonly deviceType?: string; } /** * A data volume used in a task definition. * * For tasks that use a Docker volume, specify a DockerVolumeConfiguration. * For tasks that use a bind mount host volume, specify a host and optional sourcePath. * * For more information, see [Using Data Volumes in Tasks](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/using_data_volumes.html). */ export interface Volume { /** * This property is specified when you are using bind mount host volumes. * * Bind mount host volumes are supported when you are using either the EC2 or Fargate launch types. * The contents of the host parameter determine whether your bind mount host volume persists on the * host container instance and where it is stored. If the host parameter is empty, then the Docker * daemon assigns a host path for your data volume. However, the data is not guaranteed to persist * after the containers associated with it stop running. */ readonly host?: Host; /** * The name of the volume. * * Up to 255 letters (uppercase and lowercase), numbers, and hyphens are allowed. * This name is referenced in the sourceVolume parameter of container definition mountPoints. */ readonly name: string; /** * Indicates if the volume should be configured at launch. * * @default false */ readonly configuredAtLaunch?: boolean; /** * This property is specified when you are using Docker volumes. * * Docker volumes are only supported when you are using the EC2 launch type. * Windows containers only support the use of the local driver. * To use bind mounts, specify a host instead. */ readonly dockerVolumeConfiguration?: DockerVolumeConfiguration; /** * This property is specified when you are using Amazon EFS. * * When specifying Amazon EFS volumes in tasks using the Fargate launch type, * Fargate creates a supervisor container that is responsible for managing the Amazon EFS volume. * The supervisor container uses a small amount of the task's memory. * The supervisor container is visible when querying the task metadata version 4 endpoint, * but is not visible in CloudWatch Container Insights. * * @default No Elastic FileSystem is setup */ readonly efsVolumeConfiguration?: EfsVolumeConfiguration; } /** * The details on a container instance bind mount host volume. */ export interface Host { /** * Specifies the path on the host container instance that is presented to the container. * If the sourcePath value does not exist on the host container instance, the Docker daemon creates it. * If the location does exist, the contents of the source path folder are exported. * * This property is not supported for tasks that use the Fargate launch type. */ readonly sourcePath?: string; } /** * Properties for an ECS target. * * @internal */ export interface LoadBalancerTarget { /** * The name of the container. */ readonly containerName: string; /** * The port mapping of the target. */ readonly portMapping: PortMapping; } /** * Properties for defining an ECS target. The port mapping for it must already have been created through addPortMapping(). */ export interface LoadBalancerTargetOptions { /** * The name of the container. */ readonly containerName: string; /** * The port number of the container. Only applicable when using application/network load balancers. * * @default - Container port of the first added port mapping. */ readonly containerPort?: number; /** * The protocol used for the port mapping. Only applicable when using application load balancers. * * @default Protocol.TCP */ readonly protocol?: Protocol; } /** * The configuration for a Docker volume. Docker volumes are only supported when you are using the EC2 launch type. */ export interface DockerVolumeConfiguration { /** * Specifies whether the Docker volume should be created if it does not already exist. * If true is specified, the Docker volume will be created for you. * * @default false */ readonly autoprovision?: boolean; /** * The Docker volume driver to use. */ readonly driver: string; /** * A map of Docker driver-specific options passed through. * * @default No options */ readonly driverOpts?: { [key: string]: string }; /** * Custom metadata to add to your Docker volume. * * @default No labels */ readonly labels?: { [key: string]: string }; /** * The scope for the Docker volume that determines its lifecycle. */ readonly scope: Scope; } /** * The authorization configuration details for the Amazon EFS file system. */ export interface AuthorizationConfig { /** * The access point ID to use. * If an access point is specified, the root directory value will be * relative to the directory set for the access point. * If specified, transit encryption must be enabled in the EFSVolumeConfiguration. * * @default No id */ readonly accessPointId?: string; /** * Whether or not to use the Amazon ECS task IAM role defined * in a task definition when mounting the Amazon EFS file system. * If enabled, transit encryption must be enabled in the EFSVolumeConfiguration. * * Valid values: ENABLED | DISABLED * * @default If this parameter is omitted, the default value of DISABLED is used. */ readonly iam?: string; } /** * The configuration for an Elastic FileSystem volume. */ export interface EfsVolumeConfiguration { /** * The Amazon EFS file system ID to use. */ readonly fileSystemId: string; /** * The directory within the Amazon EFS file system to mount as the root directory inside the host. * Specifying / will have the same effect as omitting this parameter. * * @default The root of the Amazon EFS volume */ readonly rootDirectory?: string; /** * Whether or not to enable encryption for Amazon EFS data in transit between * the Amazon ECS host and the Amazon EFS server. * Transit encryption must be enabled if Amazon EFS IAM authorization is used. * * Valid values: ENABLED | DISABLED * * @default DISABLED */ readonly transitEncryption?: string; /** * The port to use when sending encrypted data between * the Amazon ECS host and the Amazon EFS server. EFS mount helper uses. * * @default Port selection strategy that the Amazon EFS mount helper uses. */ readonly transitEncryptionPort?: number; /** * The authorization configuration details for the Amazon EFS file system. * * @default No configuration. */ readonly authorizationConfig?: AuthorizationConfig; } /** * The scope for the Docker volume that determines its lifecycle. * Docker volumes that are scoped to a task are automatically provisioned when the task starts and destroyed when the task stops. * Docker volumes that are scoped as shared persist after the task stops. */ export enum Scope { /** * Docker volumes that are scoped to a task are automatically provisioned when the task starts and destroyed when the task stops. */ TASK = 'task', /** * Docker volumes that are scoped as shared persist after the task stops. */ SHARED = 'shared', } /** * The task launch type compatibility requirement. */ export enum Compatibility { /** * The task should specify the EC2 launch type. */ EC2, /** * The task should specify the Fargate launch type. */ FARGATE, /** * The task can specify either the EC2 or Fargate launch types. */ EC2_AND_FARGATE, /** * The task should specify the External launch type. */ EXTERNAL, } /** * An extension for Task Definitions * * Classes that want to make changes to a TaskDefinition (such as * adding helper containers) can implement this interface, and can * then be "added" to a TaskDefinition like so: * * taskDefinition.addExtension(new MyExtension("some_parameter")); */ export interface ITaskDefinitionExtension { /** * Apply the extension to the given TaskDefinition * * @param taskDefinition [disable-awslint:ref-via-interface] */ extend(taskDefinition: TaskDefinition): void; } /** * Return true if the given task definition can be run on an EC2 cluster */ export function isEc2Compatible(compatibility: Compatibility): boolean { return [Compatibility.EC2, Compatibility.EC2_AND_FARGATE].includes(compatibility); } /** * Return true if the given task definition can be run on a Fargate cluster */ export function isFargateCompatible(compatibility: Compatibility): boolean { return [Compatibility.FARGATE, Compatibility.EC2_AND_FARGATE].includes(compatibility); } /** * Return true if the given task definition can be run on a ECS Anywhere cluster */ export function isExternalCompatible(compatibility: Compatibility): boolean { return [Compatibility.EXTERNAL].includes(compatibility); } /** * Represents revision of a task definition, either a specific numbered revision or * the `latest` revision */ export class TaskDefinitionRevision { /** * The most recent revision of a task */ public static readonly LATEST = new TaskDefinitionRevision('latest'); /** * Specific revision of a task */ public static of(revision: number) { if (revision < 1) { throw new Error(`A task definition revision must be 'latest' or a positive number, got ${revision}`); } return new TaskDefinitionRevision(revision.toString()); } /** * The string representation of this revision */ public readonly revision: string; private constructor(revision: string) { this.revision = revision; } }