diff --git a/packages/@aws-cdk/aws-batch/README.md b/packages/@aws-cdk/aws-batch/README.md index 48d5b7edf65d8..77557e5ea15d1 100644 --- a/packages/@aws-cdk/aws-batch/README.md +++ b/packages/@aws-cdk/aws-batch/README.md @@ -37,7 +37,7 @@ For more information on **AWS Batch** visit the [AWS Docs for Batch](https://doc ## Compute Environment -At the core of AWS Batch is the compute environment. All batch jobs are processed within a compute environment, which uses resource like OnDemand or Spot EC2 instances. +At the core of AWS Batch is the compute environment. All batch jobs are processed within a compute environment, which uses resource like OnDemand/Spot EC2 instances or Fargate. In **MANAGED** mode, AWS will handle the provisioning of compute resources to accommodate the demand. Otherwise, in **UNMANAGED** mode, you will need to manage the provisioning of those resources. @@ -74,6 +74,21 @@ const spotEnvironment = new batch.ComputeEnvironment(stack, 'MySpotEnvironment', }); ``` +### Fargate Compute Environment + +It is possible to have AWS Batch submit jobs to be run on Fargate compute resources. Below is an example of how this can be done: + +```ts +const vpc = new ec2.Vpc(this, 'VPC'); + +const spotEnvironment = new batch.ComputeEnvironment(stack, 'MyFargateEnvironment', { + computeResources: { + type: batch.ComputeResourceType.FARGATE_SPOT, + vpc, + }, +}); +``` + ### Understanding Progressive Allocation Strategies AWS Batch uses an [allocation strategy](https://docs.aws.amazon.com/batch/latest/userguide/allocation-strategies.html) to determine what compute resource will efficiently handle incoming job requests. By default, **BEST_FIT** will pick an available compute instance based on vCPU requirements. If none exist, the job will wait until resources become available. However, with this strategy, you may have jobs waiting in the queue unnecessarily despite having more powerful instances available. Below is an example of how that situation might look like: diff --git a/packages/@aws-cdk/aws-batch/lib/compute-environment.ts b/packages/@aws-cdk/aws-batch/lib/compute-environment.ts index 18a2d1a446325..c0bc605e22995 100644 --- a/packages/@aws-cdk/aws-batch/lib/compute-environment.ts +++ b/packages/@aws-cdk/aws-batch/lib/compute-environment.ts @@ -18,6 +18,16 @@ export enum ComputeResourceType { * Resources will be EC2 SpotFleet resources. */ SPOT = 'SPOT', + + /** + * Resources will be Fargate resources. + */ + FARGATE = 'FARGATE', + + /** + * Resources will be Fargate resources. + */ + FARGATE_SPOT = 'FARGATE_SPOT', } /** @@ -427,41 +437,100 @@ export class ComputeEnvironment extends Resource implements IComputeEnvironment throw new Error('computeResources is missing but required on a managed compute environment'); } - // Setting a bid percentage is only allowed on SPOT resources + - // Cannot use SPOT_CAPACITY_OPTIMIZED when using ON_DEMAND if (props.computeResources) { - if (props.computeResources.type === ComputeResourceType.ON_DEMAND) { - // VALIDATE FOR ON_DEMAND + if (props.computeResources.type === ComputeResourceType.FARGATE || props.computeResources.type === ComputeResourceType.FARGATE_SPOT) { + // VALIDATE FOR FARGATE - // Bid percentage is not allowed + // Bid percentage cannot be set for Fargate evnvironments if (props.computeResources.bidPercentage !== undefined) { - throw new Error('Setting the bid percentage is only allowed for SPOT type resources on a batch compute environment'); + throw new Error('Bid percentage must not be set for Fargate compute environments'); } - // SPOT_CAPACITY_OPTIMIZED allocation is not allowed - if (props.computeResources.allocationStrategy && props.computeResources.allocationStrategy === AllocationStrategy.SPOT_CAPACITY_OPTIMIZED) { - throw new Error('The SPOT_CAPACITY_OPTIMIZED allocation strategy is only allowed if the environment is a SPOT type compute environment'); + // Allocation strategy cannot be set for Fargate evnvironments + if (props.computeResources.allocationStrategy !== undefined) { + throw new Error('Allocation strategy must not be set for Fargate compute environments'); } - } else { - // VALIDATE FOR SPOT - // Bid percentage must be from 0 - 100 - if (props.computeResources.bidPercentage !== undefined && - (props.computeResources.bidPercentage < 0 || props.computeResources.bidPercentage > 100)) { - throw new Error('Bid percentage can only be a value between 0 and 100'); + // Desired vCPUs cannot be set for Fargate evnvironments + if (props.computeResources.desiredvCpus !== undefined) { + throw new Error('Desired vCPUs must not be set for Fargate compute environments'); + } + + // Image ID cannot be set for Fargate evnvironments + if (props.computeResources.image !== undefined) { + throw new Error('Image must not be set for Fargate compute environments'); + } + + // Instance types cannot be set for Fargate evnvironments + if (props.computeResources.instanceTypes !== undefined) { + throw new Error('Instance types must not be set for Fargate compute environments'); } - } - if (props.computeResources.minvCpus) { - // minvCpus cannot be less than 0 - if (props.computeResources.minvCpus < 0) { - throw new Error('Minimum vCpus for a batch compute environment cannot be less than 0'); + // EC2 key pair cannot be set for Fargate evnvironments + if (props.computeResources.ec2KeyPair !== undefined) { + throw new Error('EC2 key pair must not be set for Fargate compute environments'); } - // minvCpus cannot exceed max vCpus - if (props.computeResources.maxvCpus && - props.computeResources.minvCpus > props.computeResources.maxvCpus) { - throw new Error('Minimum vCpus cannot be greater than the maximum vCpus'); + // Instance role cannot be set for Fargate evnvironments + if (props.computeResources.instanceRole !== undefined) { + throw new Error('Instance role must not be set for Fargate compute environments'); + } + + // Launch template cannot be set for Fargate evnvironments + if (props.computeResources.launchTemplate !== undefined) { + throw new Error('Launch template must not be set for Fargate compute environments'); + } + + // Min vCPUs cannot be set for Fargate evnvironments + if (props.computeResources.minvCpus !== undefined) { + throw new Error('Min vCPUs must not be set for Fargate compute environments'); + } + + // Placement group cannot be set for Fargate evnvironments + if (props.computeResources.placementGroup !== undefined) { + throw new Error('Placement group must not be set for Fargate compute environments'); + } + + // Spot fleet role cannot be set for Fargate evnvironments + if (props.computeResources.spotFleetRole !== undefined) { + throw new Error('Spot fleet role must not be set for Fargate compute environments'); + } + } else { + // VALIDATE FOR ON_DEMAND AND SPOT + if (props.computeResources.minvCpus) { + // minvCpus cannot be less than 0 + if (props.computeResources.minvCpus < 0) { + throw new Error('Minimum vCpus for a batch compute environment cannot be less than 0'); + } + + // minvCpus cannot exceed max vCpus + if (props.computeResources.maxvCpus && + props.computeResources.minvCpus > props.computeResources.maxvCpus) { + throw new Error('Minimum vCpus cannot be greater than the maximum vCpus'); + } + } + // Setting a bid percentage is only allowed on SPOT resources + + // Cannot use SPOT_CAPACITY_OPTIMIZED when using ON_DEMAND + if (props.computeResources.type === ComputeResourceType.ON_DEMAND) { + // VALIDATE FOR ON_DEMAND + + // Bid percentage is not allowed + if (props.computeResources.bidPercentage !== undefined) { + throw new Error('Setting the bid percentage is only allowed for SPOT type resources on a batch compute environment'); + } + + // SPOT_CAPACITY_OPTIMIZED allocation is not allowed + if (props.computeResources.allocationStrategy && props.computeResources.allocationStrategy === AllocationStrategy.SPOT_CAPACITY_OPTIMIZED) { + throw new Error('The SPOT_CAPACITY_OPTIMIZED allocation strategy is only allowed if the environment is a SPOT type compute environment'); + } + } else if (props.computeResources.type === ComputeResourceType.SPOT) { + // VALIDATE FOR SPOT + + // Bid percentage must be from 0 - 100 + if (props.computeResources.bidPercentage !== undefined && + (props.computeResources.bidPercentage < 0 || props.computeResources.bidPercentage > 100)) { + throw new Error('Bid percentage can only be a value between 0 and 100'); + } } } } diff --git a/packages/@aws-cdk/aws-batch/lib/job-definition.ts b/packages/@aws-cdk/aws-batch/lib/job-definition.ts index 88107b0266615..b8ffdbb58f20c 100644 --- a/packages/@aws-cdk/aws-batch/lib/job-definition.ts +++ b/packages/@aws-cdk/aws-batch/lib/job-definition.ts @@ -52,6 +52,14 @@ export enum LogDriver { SYSLOG = 'syslog' } +/** + * Platform capabilities + */ +export enum PlatformCapabilities { + EC2 = 'EC2', + FARGATE = 'FARGATE' +} + /** * Log configuration options to send to a custom log driver for the container. */ @@ -77,6 +85,17 @@ export interface LogConfiguration { readonly secretOptions?: ExposedSecret[]; } + +/** + * Fargate platform configuration + */ +export interface FargatePlatformConfiguration { + /** + * Fargate platform version + */ + readonly platformVersion: ecs.FargatePlatformVersion +} + /** * Properties of a job definition container. */ @@ -197,6 +216,16 @@ export interface JobDefinitionContainer { * @default - No data volumes will be used. */ readonly volumes?: ecs.Volume[]; + + /** + * The platform configuration for jobs that are running on Fargate resources. + */ + readonly fargatePlatformConfiguration?: FargatePlatformConfiguration; + + /** + * The IAM role that AWS Batch can assume. + */ + readonly executionRole?: iam.IRole; } /** @@ -252,6 +281,12 @@ export interface JobDefinitionProps { * @default - undefined */ readonly timeout?: Duration; + + /** + * The platform capabilities required by the job definition. + */ + readonly platformCapabilities?: PlatformCapabilities[]; + } /** @@ -382,6 +417,8 @@ export class JobDefinition extends Resource implements IJobDefinition { physicalName: props.jobDefinitionName, }); + this.validateProps(props); + this.imageConfig = new JobDefinitionImageConfig(this, props.container); const jobDef = new CfnJobDefinition(this, 'Resource', { @@ -402,6 +439,7 @@ export class JobDefinition extends Resource implements IJobDefinition { timeout: { attemptDurationSeconds: props.timeout ? props.timeout.toSeconds() : undefined, }, + platformCapabilities: props.platformCapabilities || undefined, }); this.jobDefinitionArn = this.getResourceArnAttribute(jobDef.ref, { @@ -426,6 +464,20 @@ export class JobDefinition extends Resource implements IJobDefinition { return vars; } + /** + * Validates the properties provided for a new job definition. + */ + private validateProps(props: JobDefinitionProps) { + if (props === undefined) { + return; + } + + if (props.platformCapabilities !== undefined && props.platformCapabilities.includes(PlatformCapabilities.FARGATE) + && props.container.executionRole === undefined) { + throw new Error('Fargate job must have executionRole set'); + } + } + private buildJobContainer(container?: JobDefinitionContainer): CfnJobDefinition.ContainerPropertiesProperty | undefined { if (container === undefined) { return undefined; @@ -437,6 +489,7 @@ export class JobDefinition extends Resource implements IJobDefinition { image: this.imageConfig.imageName, instanceType: container.instanceType && container.instanceType.toString(), jobRoleArn: container.jobRole && container.jobRole.roleArn, + executionRoleArn: container.executionRole && container.executionRole.roleArn, linuxParameters: container.linuxParams ? { devices: container.linuxParams.renderLinuxParameters().devices } : undefined, @@ -458,6 +511,9 @@ export class JobDefinition extends Resource implements IJobDefinition { user: container.user, vcpus: container.vcpus || 1, volumes: container.volumes, + fargatePlatformConfiguration: container.fargatePlatformConfiguration ? { + platformVersion: container.fargatePlatformConfiguration.platformVersion, + } : undefined, }; } diff --git a/packages/@aws-cdk/aws-batch/test/compute-environment.test.ts b/packages/@aws-cdk/aws-batch/test/compute-environment.test.ts index 153eb932eec37..7419adbb470aa 100644 --- a/packages/@aws-cdk/aws-batch/test/compute-environment.test.ts +++ b/packages/@aws-cdk/aws-batch/test/compute-environment.test.ts @@ -7,7 +7,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as batch from '../lib'; -describe('Batch Compute Evironment', () => { +describe('Batch Compute Environment', () => { let expectedManagedDefaultComputeProps: any; let defaultServiceRole: any; @@ -82,6 +82,164 @@ describe('Batch Compute Evironment', () => { }); }); }); + describe('using fargate resources', () => { + test('should deny setting bid percentage', () => { + // THEN + throws(() => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: true, + computeResources: { + vpc, + type: batch.ComputeResourceType.FARGATE, + bidPercentage: -1, + }, + }); + }); + }); + test('should deny setting allocation strategy', () => { + // THEN + throws(() => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: true, + computeResources: { + vpc, + type: batch.ComputeResourceType.FARGATE, + allocationStrategy: batch.AllocationStrategy.BEST_FIT, + }, + }); + }); + }); + test('should deny setting desired vCPUs', () => { + // THEN + throws(() => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: true, + computeResources: { + vpc, + type: batch.ComputeResourceType.FARGATE, + desiredvCpus: 1, + }, + }); + }); + }); + test('should deny setting min vCPUs', () => { + // THEN + throws(() => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: true, + computeResources: { + vpc, + type: batch.ComputeResourceType.FARGATE, + minvCpus: 1, + }, + }); + }); + }); + test('should deny setting image', () => { + // THEN + throws(() => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: true, + computeResources: { + vpc, + type: batch.ComputeResourceType.FARGATE, + image: ec2.MachineImage.latestAmazonLinux(), + }, + }); + }); + }); + test('should deny setting instance types', () => { + // THEN + throws(() => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: true, + computeResources: { + vpc, + type: batch.ComputeResourceType.FARGATE, + instanceTypes: [], + }, + }); + }); + }); + test('should deny setting EC2 key pair', () => { + // THEN + throws(() => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: true, + computeResources: { + vpc, + type: batch.ComputeResourceType.FARGATE, + ec2KeyPair: 'test', + }, + }); + }); + }); + test('should deny setting instance role', () => { + // THEN + throws(() => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: true, + computeResources: { + vpc, + type: batch.ComputeResourceType.FARGATE, + instanceRole: 'test', + }, + }); + }); + }); + test('should deny setting launch template', () => { + // THEN + throws(() => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: true, + computeResources: { + vpc, + type: batch.ComputeResourceType.FARGATE, + launchTemplate: { + launchTemplateName: 'test-template', + }, + }, + }); + }); + }); + test('should deny setting placement group', () => { + // THEN + throws(() => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: true, + computeResources: { + vpc, + type: batch.ComputeResourceType.FARGATE, + placementGroup: 'test', + }, + }); + }); + }); + test('should deny setting spot fleet role', () => { + // THEN + throws(() => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: true, + computeResources: { + vpc, + type: batch.ComputeResourceType.FARGATE, + spotFleetRole: iam.Role.fromRoleArn(stack, 'test-role-arn', 'test-role'), + }, + }); + }); + }); + }); describe('using spot resources', () => { test('should provide a spot fleet role if one is not given and allocationStrategy is BEST_FIT', () => { diff --git a/packages/@aws-cdk/aws-batch/test/job-definition.test.ts b/packages/@aws-cdk/aws-batch/test/job-definition.test.ts index 1658be4c8e22e..70b7d21dff95c 100644 --- a/packages/@aws-cdk/aws-batch/test/job-definition.test.ts +++ b/packages/@aws-cdk/aws-batch/test/job-definition.test.ts @@ -1,4 +1,5 @@ import '@aws-cdk/assert/jest'; +import { throws } from 'assert'; import { ResourcePart } from '@aws-cdk/assert/lib/assertions/have-resource'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecr from '@aws-cdk/aws-ecr'; @@ -62,6 +63,7 @@ describe('Batch Job Definition', () => { }, retryAttempts: 2, timeout: cdk.Duration.seconds(30), + platformCapabilities: [batch.PlatformCapabilities.EC2], }; }); @@ -113,6 +115,7 @@ describe('Batch Job Definition', () => { AttemptDurationSeconds: jobDefProps.timeout ? jobDefProps.timeout.toSeconds() : -1, }, Type: 'container', + PlatformCapabilities: ['EC2'], }, ResourcePart.Properties); }); test('can use an ecr image', () => { @@ -287,4 +290,43 @@ describe('Batch Job Definition', () => { }, }, ResourcePart.Properties); }); + describe('using fargate job definition', () => { + test('can configure platform configuration properly', () => { + // GIVEN + const platformConfiguration: batch.FargatePlatformConfiguration = { + platformVersion: ecs.FargatePlatformVersion.LATEST, + }; + const executionRole = new iam.Role(stack, 'execution-role', { + assumedBy: new iam.ServicePrincipal('batch.amazonaws.com'), + }); + // WHEN + new batch.JobDefinition(stack, 'job-def', { + platformCapabilities: [batch.PlatformCapabilities.FARGATE], + container: { + image: ecs.EcrImage.fromRegistry('docker/whalesay'), + fargatePlatformConfiguration: platformConfiguration, + executionRole: executionRole, + }, + }); + // THEN + expect(stack).toHaveResourceLike('AWS::Batch::JobDefinition', { + ContainerProperties: { + FargatePlatformConfiguration: { + PlatformVersion: 'LATEST', + }, + }, + }, ResourcePart.Properties); + }); + test('must require executionRole', () => { + throws(() => { + // WHEN + new batch.JobDefinition(stack, 'job-def', { + platformCapabilities: [batch.PlatformCapabilities.FARGATE], + container: { + image: ecs.EcrImage.fromRegistry('docker/whalesay'), + }, + }); + }); + }); + }); });