|
| 1 | +import { ISchedule, IScheduleTarget, ScheduleTargetConfig } from '@aws-cdk/aws-scheduler-alpha'; |
| 2 | +import { Lazy, ValidationError } from 'aws-cdk-lib'; |
| 3 | +import * as ec2 from 'aws-cdk-lib/aws-ec2'; |
| 4 | +import * as ecs from 'aws-cdk-lib/aws-ecs'; |
| 5 | +import { IRole, PolicyStatement } from 'aws-cdk-lib/aws-iam'; |
| 6 | +import { ScheduleTargetBase, ScheduleTargetBaseProps } from './target'; |
| 7 | + |
| 8 | +/** |
| 9 | + * 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. |
| 10 | + */ |
| 11 | +export interface Tag { |
| 12 | + /** |
| 13 | + * Key is the name of the tag |
| 14 | + */ |
| 15 | + readonly key: string; |
| 16 | + /** |
| 17 | + * Value is the metadata contents of the tag |
| 18 | + */ |
| 19 | + readonly value: string; |
| 20 | +} |
| 21 | + |
| 22 | +/** |
| 23 | + * Parameters for scheduling ECS Run Task (common to EC2 and Fargate launch types). |
| 24 | + */ |
| 25 | +export interface EcsRunTaskBaseProps extends ScheduleTargetBaseProps { |
| 26 | + /** |
| 27 | + * The task definition to use for scheduled tasks. |
| 28 | + * |
| 29 | + * Note: this must be TaskDefinition, and not ITaskDefinition, |
| 30 | + * as it requires properties that are not known for imported task definitions |
| 31 | + * If you want to run a RunTask with an imported task definition, |
| 32 | + * consider using a Universal target. |
| 33 | + */ |
| 34 | + readonly taskDefinition: ecs.TaskDefinition; |
| 35 | + |
| 36 | + /** |
| 37 | + * The capacity provider strategy to use for the task. |
| 38 | + * |
| 39 | + * @default - No capacity provider strategy |
| 40 | + */ |
| 41 | + readonly capacityProviderStrategies?: ecs.CapacityProviderStrategy[]; |
| 42 | + |
| 43 | + /** |
| 44 | + * The subnets associated with the task. These subnets must all be in the same VPC. |
| 45 | + * The task will be launched in these subnets. |
| 46 | + * |
| 47 | + * @default - all private subnets of the VPC are selected. |
| 48 | + */ |
| 49 | + readonly vpcSubnets?: ec2.SubnetSelection; |
| 50 | + |
| 51 | + /** |
| 52 | + * The security groups associated with the task. These security groups must all be in the same VPC. |
| 53 | + * Controls inbound and outbound network access for the task. |
| 54 | + * |
| 55 | + * @default - The security group for the VPC is used. |
| 56 | + */ |
| 57 | + readonly securityGroups?: ec2.ISecurityGroup[]; |
| 58 | + |
| 59 | + /** |
| 60 | + * Specifies whether to enable Amazon ECS managed tags for the task. |
| 61 | + * @default - false |
| 62 | + */ |
| 63 | + readonly enableEcsManagedTags?: boolean; |
| 64 | + |
| 65 | + /** |
| 66 | + * Whether to enable execute command functionality for the containers in this task. |
| 67 | + * If true, this enables execute command functionality on all containers in the task. |
| 68 | + * |
| 69 | + * @default - false |
| 70 | + */ |
| 71 | + readonly enableExecuteCommand?: boolean; |
| 72 | + |
| 73 | + /** |
| 74 | + * Specifies an ECS task group for the task. |
| 75 | + * |
| 76 | + * @default - No group |
| 77 | + */ |
| 78 | + readonly group?: string; |
| 79 | + |
| 80 | + /** |
| 81 | + * Specifies whether to propagate the tags from the task definition to the task. |
| 82 | + * If no value is specified, the tags are not propagated. |
| 83 | + * |
| 84 | + * @default - No tag propagation |
| 85 | + */ |
| 86 | + readonly propagateTags?: boolean; |
| 87 | + |
| 88 | + /** |
| 89 | + * The reference ID to use for the task. |
| 90 | + * |
| 91 | + * @default - No reference ID. |
| 92 | + */ |
| 93 | + readonly referenceId?: string; |
| 94 | + |
| 95 | + /** |
| 96 | + * The metadata that you apply to the task to help you categorize and organize them. |
| 97 | + * Each tag consists of a key and an optional value, both of which you define. |
| 98 | + * |
| 99 | + * @default - No tags |
| 100 | + */ |
| 101 | + readonly tags?: Tag[]; |
| 102 | + |
| 103 | + /** |
| 104 | + * The number of tasks to create based on TaskDefinition. |
| 105 | + * |
| 106 | + * @default 1 |
| 107 | + */ |
| 108 | + readonly taskCount?: number; |
| 109 | + |
| 110 | +} |
| 111 | + |
| 112 | +/** |
| 113 | + * Properties for scheduling an ECS Fargate Task. |
| 114 | + */ |
| 115 | +export interface FargateTaskProps extends EcsRunTaskBaseProps { |
| 116 | + /** |
| 117 | + * Specifies whether the task's elastic network interface receives a public IP address. |
| 118 | + * If true, the task will receive a public IP address and be accessible from the internet. |
| 119 | + * Should only be set to true when using public subnets. |
| 120 | + * |
| 121 | + * @default - true if the subnet type is PUBLIC, otherwise false |
| 122 | + */ |
| 123 | + readonly assignPublicIp?: boolean; |
| 124 | + |
| 125 | + /** |
| 126 | + * Specifies the platform version for the task. |
| 127 | + * Specify only the numeric portion of the platform version, such as 1.1.0. |
| 128 | + * Platform versions determine the underlying runtime environment for the task. |
| 129 | + * |
| 130 | + * @default - LATEST |
| 131 | + */ |
| 132 | + readonly platformVersion?: ecs.FargatePlatformVersion; |
| 133 | +} |
| 134 | + |
| 135 | +/** |
| 136 | + * Properties for scheduling an ECS Task on EC2. |
| 137 | + */ |
| 138 | +export interface Ec2TaskProps extends EcsRunTaskBaseProps { |
| 139 | + /** |
| 140 | + * The rules that must be met in order to place a task on a container instance. |
| 141 | + * |
| 142 | + * @default - No placement constraints. |
| 143 | + */ |
| 144 | + readonly placementConstraints?: ecs.PlacementConstraint[]; |
| 145 | + |
| 146 | + /** |
| 147 | + * The algorithm for selecting container instances for task placement. |
| 148 | + * |
| 149 | + * @default - No placement strategies. |
| 150 | + */ |
| 151 | + readonly placementStrategies?: ecs.PlacementStrategy[]; |
| 152 | +} |
| 153 | + |
| 154 | +/** |
| 155 | + * Schedule an ECS Task using AWS EventBridge Scheduler. |
| 156 | + */ |
| 157 | +export abstract class EcsRunTask extends ScheduleTargetBase implements IScheduleTarget { |
| 158 | + constructor( |
| 159 | + protected readonly cluster: ecs.ICluster, |
| 160 | + protected readonly props: EcsRunTaskBaseProps, |
| 161 | + ) { |
| 162 | + super(props, cluster.clusterArn); |
| 163 | + } |
| 164 | + |
| 165 | + protected addTargetActionToRole(role: IRole): void { |
| 166 | + // grantRun already adds the necessary PassRole permissions for both task role and execution role |
| 167 | + this.props.taskDefinition.grantRun(role); |
| 168 | + |
| 169 | + // Add permissions for tagging if needed |
| 170 | + if (this.props.propagateTags === true || this.props.tags) { |
| 171 | + role.addToPrincipalPolicy(new PolicyStatement({ |
| 172 | + actions: ['ecs:TagResource'], |
| 173 | + resources: [`arn:${this.cluster.stack.partition}:ecs:${this.cluster.env.region}:${this.props.taskDefinition.env.account}:task/${this.cluster.clusterName}/*`], |
| 174 | + })); |
| 175 | + } |
| 176 | + } |
| 177 | + |
| 178 | + protected bindBaseTargetConfig(_schedule: ISchedule): ScheduleTargetConfig { |
| 179 | + return { |
| 180 | + ...super.bindBaseTargetConfig(_schedule), |
| 181 | + ecsParameters: { |
| 182 | + taskDefinitionArn: this.props.taskDefinition.taskDefinitionArn, |
| 183 | + capacityProviderStrategy: this.props.capacityProviderStrategies, |
| 184 | + taskCount: this.props.taskCount, |
| 185 | + tags: this.props.tags, |
| 186 | + propagateTags: this.props.propagateTags ? ecs.PropagatedTagSource.TASK_DEFINITION : undefined, |
| 187 | + enableEcsManagedTags: this.props.enableEcsManagedTags, |
| 188 | + enableExecuteCommand: this.props.enableExecuteCommand, |
| 189 | + group: this.props.group, |
| 190 | + referenceId: this.props.referenceId, |
| 191 | + }, |
| 192 | + }; |
| 193 | + } |
| 194 | +} |
| 195 | + |
| 196 | +/** |
| 197 | + * Schedule an ECS Task on Fargate using AWS EventBridge Scheduler. |
| 198 | + */ |
| 199 | +export class EcsRunFargateTask extends EcsRunTask { |
| 200 | + private readonly subnetSelection?: ec2.SubnetSelection; |
| 201 | + private readonly assignPublicIp?: boolean; |
| 202 | + private readonly platformVersion?: string; |
| 203 | + private readonly capacityProviderStrategies?: ecs.CapacityProviderStrategy[]; |
| 204 | + |
| 205 | + constructor( |
| 206 | + cluster: ecs.ICluster, |
| 207 | + props: FargateTaskProps, |
| 208 | + ) { |
| 209 | + super(cluster, props); |
| 210 | + this.subnetSelection = props.vpcSubnets; |
| 211 | + this.assignPublicIp = props.assignPublicIp; |
| 212 | + this.platformVersion = props.platformVersion; |
| 213 | + this.capacityProviderStrategies = props.capacityProviderStrategies; |
| 214 | + } |
| 215 | + |
| 216 | + protected bindBaseTargetConfig(_schedule: ISchedule): ScheduleTargetConfig { |
| 217 | + if (!this.props.taskDefinition.isFargateCompatible) { |
| 218 | + throw new ValidationError('TaskDefinition is not compatible with Fargate launch type.', _schedule); |
| 219 | + } |
| 220 | + |
| 221 | + const subnetSelection = this.subnetSelection || { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }; |
| 222 | + |
| 223 | + // Throw an error if assignPublicIp is true and the subnet type is not public |
| 224 | + if (this.assignPublicIp && subnetSelection.subnetType !== ec2.SubnetType.PUBLIC) { |
| 225 | + throw new ValidationError('assignPublicIp should be set to true only for public subnets', _schedule); |
| 226 | + } |
| 227 | + |
| 228 | + const assignPublicIp = this.assignPublicIp !== undefined |
| 229 | + ? (this.assignPublicIp ? 'ENABLED' : 'DISABLED') |
| 230 | + : (subnetSelection.subnetType === ec2.SubnetType.PUBLIC ? 'ENABLED' : 'DISABLED'); |
| 231 | + |
| 232 | + // Only one of capacityProviderStrategies or launchType can be set |
| 233 | + // See https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_RunTask.html#ECS-RunTask-request-launchType |
| 234 | + const launchType = this.capacityProviderStrategies ? undefined : ecs.LaunchType.FARGATE; |
| 235 | + |
| 236 | + const bindBaseTargetConfigParameters = super.bindBaseTargetConfig(_schedule).ecsParameters!; |
| 237 | + |
| 238 | + return { |
| 239 | + ...super.bindBaseTargetConfig(_schedule), |
| 240 | + ecsParameters: { |
| 241 | + ...bindBaseTargetConfigParameters, |
| 242 | + launchType, |
| 243 | + platformVersion: this.platformVersion, |
| 244 | + networkConfiguration: { |
| 245 | + awsvpcConfiguration: { |
| 246 | + assignPublicIp, |
| 247 | + subnets: this.cluster.vpc.selectSubnets(subnetSelection).subnetIds, |
| 248 | + securityGroups: (this.props.securityGroups && this.props.securityGroups.length > 0) |
| 249 | + ? |
| 250 | + this.props.securityGroups?.map((sg) => sg.securityGroupId) |
| 251 | + : undefined, |
| 252 | + }, |
| 253 | + }, |
| 254 | + }, |
| 255 | + }; |
| 256 | + } |
| 257 | +} |
| 258 | + |
| 259 | +/** |
| 260 | + * Schedule an ECS Task on EC2 using AWS EventBridge Scheduler. |
| 261 | + */ |
| 262 | +export class EcsRunEc2Task extends EcsRunTask { |
| 263 | + private readonly capacityProviderStrategies?: ecs.CapacityProviderStrategy[]; |
| 264 | + private readonly placementConstraints?: ecs.PlacementConstraint[]; |
| 265 | + private readonly placementStrategies?: ecs.PlacementStrategy[]; |
| 266 | + |
| 267 | + constructor( |
| 268 | + cluster: ecs.ICluster, |
| 269 | + props: Ec2TaskProps, |
| 270 | + ) { |
| 271 | + super(cluster, props); |
| 272 | + this.placementConstraints = props.placementConstraints; |
| 273 | + this.placementStrategies = props.placementStrategies; |
| 274 | + this.capacityProviderStrategies = props.capacityProviderStrategies; |
| 275 | + } |
| 276 | + |
| 277 | + protected bindBaseTargetConfig(_schedule: ISchedule): ScheduleTargetConfig { |
| 278 | + if (this.props.taskDefinition.compatibility === ecs.Compatibility.FARGATE) { |
| 279 | + throw new ValidationError('TaskDefinition is not compatible with EC2 launch type', _schedule); |
| 280 | + } |
| 281 | + |
| 282 | + // Only one of capacityProviderStrategy or launchType can be set |
| 283 | + // See https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_RunTask.html#ECS-RunTask-request-launchType |
| 284 | + const launchType = this.capacityProviderStrategies ? undefined : ecs.LaunchType.EC2; |
| 285 | + |
| 286 | + const taskDefinitionUsesAwsVpc = this.props.taskDefinition.networkMode === ecs.NetworkMode.AWS_VPC; |
| 287 | + |
| 288 | + // Security groups are only configurable with the "awsvpc" network mode. |
| 289 | + // See https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_RunTask.html#ECS-RunTask-request-networkConfiguration |
| 290 | + if (!taskDefinitionUsesAwsVpc && (this.props.securityGroups || this.props.vpcSubnets)) { |
| 291 | + throw new ValidationError('Security groups and subnets can only be used with awsvpc network mode', _schedule); |
| 292 | + } |
| 293 | + |
| 294 | + const subnetSelection = |
| 295 | + taskDefinitionUsesAwsVpc ? this.props.vpcSubnets || { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS } |
| 296 | + : undefined; |
| 297 | + |
| 298 | + const bindBaseTargetConfigParameters = super.bindBaseTargetConfig(_schedule).ecsParameters!; |
| 299 | + |
| 300 | + return { |
| 301 | + ...super.bindBaseTargetConfig(_schedule), |
| 302 | + ecsParameters: { |
| 303 | + ...bindBaseTargetConfigParameters, |
| 304 | + launchType, |
| 305 | + placementConstraints: Lazy.any({ |
| 306 | + produce: () => { |
| 307 | + // Only map if placementConstraints is defined and has items |
| 308 | + return this.placementConstraints?.length |
| 309 | + ? this.placementConstraints?.map((constraint) => constraint.toJson()).flat() |
| 310 | + : undefined; |
| 311 | + }, |
| 312 | + }), |
| 313 | + placementStrategy: Lazy.any({ |
| 314 | + produce: () => { |
| 315 | + return this.placementStrategies?.length |
| 316 | + ? this.placementStrategies?.map((strategy) => strategy.toJson()).flat() |
| 317 | + : undefined; |
| 318 | + }, |
| 319 | + }, { omitEmptyArray: true }), |
| 320 | + ... (taskDefinitionUsesAwsVpc && { |
| 321 | + networkConfiguration: { |
| 322 | + awsvpcConfiguration: { |
| 323 | + subnets: this.cluster.vpc.selectSubnets(subnetSelection).subnetIds, |
| 324 | + securityGroups: (this.props.securityGroups && this.props.securityGroups.length > 0) |
| 325 | + ? |
| 326 | + this.props.securityGroups.map((sg) => sg.securityGroupId) |
| 327 | + : undefined, |
| 328 | + }, |
| 329 | + }, |
| 330 | + }), |
| 331 | + }, |
| 332 | + }; |
| 333 | + } |
| 334 | +} |
0 commit comments