packages/aws-cdk-lib/aws-events-targets/lib/ecs-task.ts (187 lines of code) (raw):

import { Construct } from 'constructs'; import { ContainerOverride, EphemeralStorageOverride, InferenceAcceleratorOverride } from './ecs-task-properties'; import { addToDeadLetterQueueResourcePolicy, bindBaseTargetConfig, singletonEventRole, TargetBaseProps } from './util'; import * as ec2 from '../../aws-ec2'; import * as ecs from '../../aws-ecs'; import * as events from '../../aws-events'; import * as iam from '../../aws-iam'; import * as cdk from '../../core'; /** * Metadata that you apply to a resource to help categorize and organize the resource. Each tag consists of a key and an optional value, both of which you define. */ export interface Tag { /** * Key is the name of the tag */ readonly key: string; /** * Value is the metadata contents of the tag */ readonly value: string; } /** * Properties to define an ECS Event Task */ export interface EcsTaskProps extends TargetBaseProps { /** * Cluster where service will be deployed */ readonly cluster: ecs.ICluster; /** * Task Definition of the task that should be started */ readonly taskDefinition: ecs.ITaskDefinition; /** * How many tasks should be started when this event is triggered * * @default 1 */ readonly taskCount?: number; /** * Container setting overrides * * Key is the name of the container to override, value is the * values you want to override. */ readonly containerOverrides?: ContainerOverride[]; /** * The CPU override for the task. * * @default - The task definition's CPU value */ readonly cpu?: string; /** * The ephemeral storage setting override for the task. * * NOTE: This parameter is only supported for tasks hosted on Fargate that use the following platform versions: * - Linux platform version 1.4.0 or later. * - Windows platform version 1.0.0 or later. * * @default - The task definition's ephemeral storage value */ readonly ephemeralStorage?: EphemeralStorageOverride; /** * The execution role for the task. * * The Amazon Resource Name (ARN) of the task execution role override for the task. * * @default - The task definition's execution role */ readonly executionRole?: iam.IRole; /** * The Elastic Inference accelerator override for the task. * * @default - The task definition's inference accelerator overrides */ readonly inferenceAcceleratorOverrides?: InferenceAcceleratorOverride[]; /** * The memory override for the task. * * @default - The task definition's memory value */ readonly memory?: string; /** * The IAM role for the task. * * @default - The task definition's task role */ readonly taskRole?: iam.IRole; /** * In what subnets to place the task's ENIs * * (Only applicable in case the TaskDefinition is configured for AwsVpc networking) * * @default Private subnets */ readonly subnetSelection?: ec2.SubnetSelection; /** * Existing security group to use for the task's ENIs * * (Only applicable in case the TaskDefinition is configured for AwsVpc networking) * * @default A new security group is created * @deprecated use securityGroups instead */ readonly securityGroup?: ec2.ISecurityGroup; /** * Existing security groups to use for the task's ENIs * * (Only applicable in case the TaskDefinition is configured for AwsVpc networking) * * @default A new security group is created */ readonly securityGroups?: ec2.ISecurityGroup[]; /** * Existing IAM role to run the ECS task * * @default A new IAM role is created */ readonly role?: iam.IRole; /** * The platform version on which to run your task * * Unless you have specific compatibility requirements, you don't need to specify this. * * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/platform_versions.html * * @default - ECS will set the Fargate platform version to 'LATEST' */ readonly platformVersion?: ecs.FargatePlatformVersion; /** * Specifies whether the task's elastic network interface receives a public IP address. * You can specify true only when LaunchType is set to FARGATE. * * @default - true if the subnet type is PUBLIC, otherwise false */ readonly assignPublicIp?: boolean; /** * Specifies whether to propagate the tags from the task definition to the task. If no value is specified, the tags are not propagated. * * @default - Tags will not be propagated */ readonly propagateTags?: ecs.PropagatedTagSource; /** * The metadata that you apply to the task to help you categorize and organize them. Each tag consists of a key and an optional value, both of which you define. * * @default - No additional tags are applied to the task */ readonly tags?: Tag[]; /** * Whether or not to enable the execute command functionality for the containers in this task. * If true, this enables execute command functionality on all containers in the task. * * @default - false */ readonly enableExecuteCommand?: boolean; /** * Specifies the launch type on which your task is running. The launch type that you specify here * must match one of the launch type (compatibilities) of the target task. * * @default - 'EC2' if `isEc2Compatible` for the `taskDefinition` is true, otherwise 'FARGATE' */ readonly launchType?: ecs.LaunchType; } /** * Start a task on an ECS cluster */ export class EcsTask implements events.IRuleTarget { // Security group fields are public because we can generate a new security group if none is provided. /** * The security group associated with the task. Only applicable with awsvpc network mode. * * @default - A new security group is created. * @deprecated use securityGroups instead. */ public readonly securityGroup?: ec2.ISecurityGroup; /** * The security groups associated with the task. Only applicable with awsvpc network mode. * * @default - A new security group is created. */ public readonly securityGroups?: ec2.ISecurityGroup[]; private readonly cluster: ecs.ICluster; private readonly taskDefinition: ecs.ITaskDefinition; private readonly taskCount: number; private readonly role: iam.IRole; private readonly platformVersion?: ecs.FargatePlatformVersion; private readonly assignPublicIp?: boolean; private readonly propagateTags?: ecs.PropagatedTagSource; private readonly tags?: Tag[]; private readonly enableExecuteCommand?: boolean; private readonly launchType?: ecs.LaunchType; constructor(private readonly props: EcsTaskProps) { if (props.securityGroup !== undefined && props.securityGroups !== undefined) { throw new Error('Only one of SecurityGroup or SecurityGroups can be populated.'); } this.cluster = props.cluster; this.taskDefinition = props.taskDefinition; this.taskCount = props.taskCount ?? 1; this.platformVersion = props.platformVersion; this.assignPublicIp = props.assignPublicIp; this.enableExecuteCommand = props.enableExecuteCommand; this.launchType = props.launchType; const propagateTagsValidValues = [ecs.PropagatedTagSource.TASK_DEFINITION, ecs.PropagatedTagSource.NONE]; if (props.propagateTags && !propagateTagsValidValues.includes(props.propagateTags)) { throw new Error('When propagateTags is passed, it must be set to TASK_DEFINITION or NONE.'); } this.propagateTags = props.propagateTags; this.role = props.role ?? singletonEventRole(this.taskDefinition); for (const stmt of this.createEventRolePolicyStatements()) { this.role.addToPrincipalPolicy(stmt); } this.tags = props.tags; // Security groups are only configurable with the "awsvpc" network mode. if (this.taskDefinition.networkMode !== ecs.NetworkMode.AWS_VPC) { if (props.securityGroup !== undefined || props.securityGroups !== undefined) { cdk.Annotations.of(this.taskDefinition).addWarningV2('@aws-cdk/aws-events-targets:ecsTaskSecurityGroupIgnored', 'security groups are ignored when network mode is not awsvpc'); } return; } if (props.securityGroups) { this.securityGroups = props.securityGroups; return; } if (!Construct.isConstruct(this.taskDefinition)) { throw new Error('Cannot create a security group for ECS task. ' + 'The task definition in ECS task is not a Construct. ' + 'Please pass a taskDefinition as a Construct in EcsTaskProps.'); } let securityGroup = props.securityGroup || this.taskDefinition.node.tryFindChild('SecurityGroup') as ec2.ISecurityGroup; securityGroup = securityGroup || new ec2.SecurityGroup(this.taskDefinition, 'SecurityGroup', { vpc: this.props.cluster.vpc }); this.securityGroup = securityGroup; // Maintain backwards-compatibility for customers that read the generated security group. this.securityGroups = [securityGroup]; } /** * Allows using tasks as target of EventBridge events */ public bind(_rule: events.IRule, _id?: string): events.RuleTargetConfig { const arn = this.cluster.clusterArn; const role = this.role; const taskCount = this.taskCount; const taskDefinitionArn = this.taskDefinition.taskDefinitionArn; const propagateTags = this.propagateTags; const tagList = this.tags; const enableExecuteCommand = this.enableExecuteCommand; const input = this.createInput(); const subnetSelection = this.props.subnetSelection || { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }; // throw an error if assignPublicIp is true and the subnet type is not PUBLIC if (this.assignPublicIp && subnetSelection.subnetType !== ec2.SubnetType.PUBLIC) { throw new Error('assignPublicIp should be set to true only for PUBLIC subnets'); } const assignPublicIp = (this.assignPublicIp ?? subnetSelection.subnetType === ec2.SubnetType.PUBLIC) ? 'ENABLED' : 'DISABLED'; const launchType = this.launchType ?? (this.taskDefinition.isEc2Compatible ? 'EC2' : 'FARGATE'); if (assignPublicIp === 'ENABLED' && launchType !== 'FARGATE') { throw new Error('assignPublicIp is only supported for FARGATE tasks'); } const baseEcsParameters = { taskCount, taskDefinitionArn, propagateTags, tagList, enableExecuteCommand }; const ecsParameters: events.CfnRule.EcsParametersProperty = this.taskDefinition.networkMode === ecs.NetworkMode.AWS_VPC ? { ...baseEcsParameters, launchType, platformVersion: this.platformVersion, networkConfiguration: { awsVpcConfiguration: { subnets: this.props.cluster.vpc.selectSubnets(subnetSelection).subnetIds, assignPublicIp, securityGroups: this.securityGroups && this.securityGroups.map(sg => sg.securityGroupId), }, }, } : baseEcsParameters; if (this.props.deadLetterQueue) { addToDeadLetterQueueResourcePolicy(_rule, this.props.deadLetterQueue); } return { ...bindBaseTargetConfig(this.props), arn, role, ecsParameters, input: events.RuleTargetInput.fromObject(input), targetResource: this.taskDefinition, }; } private createInput(): Record<string, any> { const containerOverrides = this.props.containerOverrides && this.props.containerOverrides .map(({ containerName, ...overrides }) => ({ name: containerName, ...overrides })); if (this.props.ephemeralStorage) { const ephemeralStorage = this.props.ephemeralStorage; if (ephemeralStorage.sizeInGiB < 20 || ephemeralStorage.sizeInGiB > 200) { throw new Error('Ephemeral storage size must be between 20 GiB and 200 GiB.'); } } // See https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_TaskOverride.html return { // In prior versions, containerOverrides was passed even when undefined, so we always set it for backward compatibility. containerOverrides, ...(this.props.cpu && { cpu: this.props.cpu }), ...(this.props.ephemeralStorage && { ephemeralStorage: this.props.ephemeralStorage }), ...(this.props.executionRole?.roleArn && { executionRole: this.props.executionRole.roleArn }), ...(this.props.inferenceAcceleratorOverrides && { inferenceAcceleratorOverrides: this.props.inferenceAcceleratorOverrides }), ...(this.props.memory && { memory: this.props.memory }), ...(this.props.taskRole?.roleArn && { taskRole: this.props.taskRole.roleArn }), }; } private createEventRolePolicyStatements(): iam.PolicyStatement[] { // check if there is a taskdefinition revision (arn will end with : followed by digits) included in the arn already let needsRevisionWildcard = false; if (!cdk.Token.isUnresolved(this.taskDefinition.taskDefinitionArn)) { const revisionAtEndPattern = /:[0-9]+$/; const hasRevision = revisionAtEndPattern.test(this.taskDefinition.taskDefinitionArn); needsRevisionWildcard = !hasRevision; } const policyStatements = [ new iam.PolicyStatement({ actions: ['ecs:RunTask'], resources: [`${this.taskDefinition.taskDefinitionArn}${needsRevisionWildcard ? ':*' : ''}`], conditions: { ArnEquals: { 'ecs:cluster': this.cluster.clusterArn }, }, }), new iam.PolicyStatement({ actions: ['ecs:TagResource'], resources: [`arn:${this.cluster.stack.partition}:ecs:${this.cluster.env.region}:*:task/${this.cluster.clusterName}/*`], }), ]; // If it so happens that a Task Execution Role was created for the TaskDefinition, // then the EventBridge Role must have permissions to pass it (otherwise it doesn't). if (this.taskDefinition.executionRole !== undefined) { policyStatements.push(new iam.PolicyStatement({ actions: ['iam:PassRole'], resources: [this.taskDefinition.executionRole.roleArn], })); } // For Fargate task we need permission to pass the task role. if (this.taskDefinition.isFargateCompatible) { policyStatements.push(new iam.PolicyStatement({ actions: ['iam:PassRole'], resources: [this.taskDefinition.taskRole.roleArn], })); } return policyStatements; } }