diff --git a/packages/@aws-cdk/aws-batch/lib/compute-environment.ts b/packages/@aws-cdk/aws-batch/lib/compute-environment.ts new file mode 100644 index 0000000000000..663d806f0fff7 --- /dev/null +++ b/packages/@aws-cdk/aws-batch/lib/compute-environment.ts @@ -0,0 +1,458 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; +import { Aws, Construct, IResource, Resource, Stack, Tag } from '@aws-cdk/core'; +import { CfnComputeEnvironment } from './batch.generated'; + +/** + * Property to specify if the compute environment + * uses On-Demand or SpotFleet compute resources. + */ +export enum ComputeResourceType { + /** + * Resources will be EC2 On-Demand resources. + */ + ON_DEMAND = 'EC2', + + /** + * Resources will be EC2 SpotFleet resources. + */ + SPOT = 'SPOT', +} + +/** + * Properties for how to prepare compute resources + * that are provisioned for a compute environment. + */ +export enum AllocationStrategy { + /** + * Batch will use the best fitting instance type will be used + * when assigning a batch job in this compute environment. + */ + BEST_FIT = 'BEST_FIT', + + /** + * Batch will select additional instance types that are large enough to + * meet the requirements of the jobs in the queue, with a preference for + * instance types with a lower cost per unit vCPU. + */ + BEST_FIT_PROGRESSIVE = 'BEST_FIT_PROGRESSIVE', + + /** + * This is only available for Spot Instance compute resources and will select + * additional instance types that are large enough to meet the requirements of + * the jobs in the queue, with a preference for instance types that are less + * likely to be interrupted. + */ + SPOT_CAPACITY_OPTIMIZED = 'SPOT_CAPACITY_OPTIMIZED', +} + +/** + * Properties for defining the structure of the batch compute cluster. + */ +export interface ComputeResources { + /** + * The IAM role applied to EC2 resources in the compute environment. + * + * @default - a new role will be created. + */ + readonly instanceRole?: iam.IRole; + + /** + * The types of EC2 instances that may be launched in the compute environment. You can specify instance + * families to launch any instance type within those families (for example, c4 or p3), or you can specify + * specific sizes within a family (such as c4.8xlarge). You can also choose optimal to pick instance types + * (from the C, M, and R instance families) on the fly that match the demand of your job queues. + * + * @default optimal + */ + readonly instanceTypes?: ec2.InstanceType[]; + + /** + * The EC2 security group(s) associated with instances launched in the compute environment. + * + * @default AWS default security group. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + + /** + * The VPC network that all compute resources will be connected to. + */ + readonly vpc: ec2.IVpc; + + /** + * The VPC subnets into which the compute resources are launched. + * + * @default - private subnets of the supplied VPC. + */ + readonly vpcSubnets?: ec2.SubnetSelection; + + /** + * The type of compute environment: ON_DEMAND or SPOT. + * + * @default ON_DEMAND + */ + readonly type?: ComputeResourceType; + + /** + * This property will be ignored if you set the environment type to ON_DEMAND. + * + * The maximum percentage that a Spot Instance price can be when compared with the On-Demand price for + * that instance type before instances are launched. For example, if your maximum percentage is 20%, + * then the Spot price must be below 20% of the current On-Demand price for that EC2 instance. You always + * pay the lowest (market) price and never more than your maximum percentage. If you leave this field empty, + * the default value is 100% of the On-Demand price. + * + * @default 100 + */ + readonly bidPercentage?: number; + + /** + * The desired number of EC2 vCPUS in the compute environment. + * + * @default - no desired vcpu value will be used. + */ + readonly desiredvCpus?: number; + + /** + * The maximum number of EC2 vCPUs that an environment can reach. Each vCPU is equivalent to + * 1,024 CPU shares. You must specify at least one vCPU. + * + * @default 256 + */ + readonly maxvCpus?: number; + + /** + * The minimum number of EC2 vCPUs that an environment should maintain (even if the compute environment state is DISABLED). + * Each vCPU is equivalent to 1,024 CPU shares. You must specify at least one vCPU. + * + * @default 1 + */ + readonly minvCpus?: number; + + /** + * The EC2 key pair that is used for instances launched in the compute environment. + * If no key is defined, then SSH access is not allowed to provisioned compute resources. + * + * @default - No SSH access will be possible. + */ + readonly ec2KeyPair?: string; + + /** + * The Amazon Machine Image (AMI) ID used for instances launched in the compute environment. + * + * @default - no image will be used. + */ + readonly image?: ec2.IMachineImage; + + /** + * This property will be ignored if you set the environment type to ON_DEMAND. + * + * The Amazon Resource Name (ARN) of the Amazon EC2 Spot Fleet IAM role applied to a SPOT compute environment. + * For more information, see Amazon EC2 Spot Fleet Role in the AWS Batch User Guide. + * + * @link https://docs.aws.amazon.com/batch/latest/userguide/spot_fleet_IAM_role.html + * @default - no fleet role will be used. + */ + readonly spotFleetRole?: iam.IRole; + + /** + * Key-value pair tags to be applied to resources that are launched in the compute environment. + * For AWS Batch, these take the form of "String1": "String2", where String1 is the tag key and + * String2 is the tag value—for example, { "Name": "AWS Batch Instance - C4OnDemand" }. + * + * @default - no tags will be assigned on compute resources. + */ + readonly computeResourcesTags?: Tag; +} + +/** + * Properties for creating a new Compute Environment + */ +export interface ComputeEnvironmentProps { + /** + * The allocation strategy to use for the compute resource in case not enough instances ofthe best + * fitting instance type can be allocated. This could be due to availability of the instance type in + * the region or Amazon EC2 service limits. If this is not specified, the default is BEST_FIT, which + * will use only the best fitting instance type, waiting for additional capacity if it's not available. + * This allocation strategy keeps costs lower but can limit scaling. If you are using Spot Fleets with + * BEST_FIT then the Spot Fleet IAM Role must be specified. BEST_FIT_PROGRESSIVE will select an additional + * instance type that is large enough to meet the requirements of the jobs in the queue, with a preference + * for an instance type with a lower cost. SPOT_CAPACITY_OPTIMIZED is only available for Spot Instance + * compute resources and will select an additional instance type that is large enough to meet the requirements + * of the jobs in the queue, with a preference for an instance type that is less likely to be interrupted. + * + * @default AllocationStrategy.BEST_FIT + */ + readonly allocationStrategy?: AllocationStrategy; + + /** + * A name for the compute environment. + * + * Up to 128 letters (uppercase and lowercase), numbers, hyphens, and underscores are allowed. + * + * @default Cloudformation-generated name + */ + readonly computeEnvironmentName?: string; + + /** + * The details of the compute resources managed by this environment. + * + * If specified, and this is an managed compute environment, the property will be ignored. + * + * By default, AWS Batch managed compute environments use a recent, approved version of the + * Amazon ECS-optimized AMI for compute resources. + * + * @default - AWS-managed compute resources + */ + readonly computeResources?: ComputeResources; + + /** + * The state of the compute environment. If the state is set to true, then the compute + * environment accepts jobs from a queue and can scale out automatically based on queues. + * + * @default true + */ + readonly enabled?: boolean; + + /** + * The IAM role used by Batch to make calls to other AWS services on your behalf for managing + * the resources that you use with the service. By default, this role is created for you using + * the AWS managed service policy for Batch. + * + * @link https://docs.aws.amazon.com/batch/latest/userguide/service_IAM_role.html + * + * @default - Role using the 'service-role/AWSBatchServiceRole' policy. + */ + readonly serviceRole?: iam.IRole, + + /** + * Determines if AWS should manage the allocation of compute resources for processing jobs. + * If set to false, then you are in charge of providing the compute resource details. + * + * @default true + */ + readonly managed?: boolean; +} + +/** + * Properties of a compute environment. + */ +export interface IComputeEnvironment extends IResource { + /** + * The ARN of this compute environment. + * + * @attribute + */ + readonly computeEnvironmentArn: string; + + /** + * The name of this compute environment. + * + * @attribute + */ + readonly computeEnvironmentName: string; +} + +/** + * Batch Compute Environment. + * + * Defines a batch compute environment to run batch jobs on. + */ +export class ComputeEnvironment extends Resource implements IComputeEnvironment { + /** + * Fetches an existing batch compute environment by its amazon resource name. + * + * @param scope + * @param id + * @param computeEnvironmentArn + */ + public static fromComputeEnvironmentArn(scope: Construct, id: string, computeEnvironmentArn: string): IComputeEnvironment { + const stack = Stack.of(scope); + const computeEnvironmentName = stack.parseArn(computeEnvironmentArn).resourceName!; + + class Import extends Resource implements IComputeEnvironment { + public readonly computeEnvironmentArn = computeEnvironmentArn; + public readonly computeEnvironmentName = computeEnvironmentName; + } + + return new Import(scope, id); + } + + /** + * The ARN of this compute environment. + * + * @attribute + */ + public readonly computeEnvironmentArn: string; + + /** + * The name of this compute environment. + * + * @attribute + */ + public readonly computeEnvironmentName: string; + + constructor(scope: Construct, id: string, props: ComputeEnvironmentProps = { enabled: true, managed: true }) { + super(scope, id, { + physicalName: props.computeEnvironmentName, + }); + + this.validateProps(props); + + const spotFleetRole = this.getSpotFleetRole(props); + let computeResources: CfnComputeEnvironment.ComputeResourcesProperty | undefined; + + // Only allow compute resources to be set when using UNMANAGED type + if (props.computeResources && !this.isManaged(props)) { + computeResources = { + allocationStrategy: props.allocationStrategy || AllocationStrategy.BEST_FIT, + bidPercentage: props.computeResources.bidPercentage, + desiredvCpus: props.computeResources.desiredvCpus, + ec2KeyPair: props.computeResources.ec2KeyPair, + imageId: props.computeResources.image && props.computeResources.image.getImage(this).imageId, + instanceRole: props.computeResources.instanceRole + ? props.computeResources.instanceRole.roleArn + : new iam.Role(this, 'Resource-Instance-Role', { + assumedBy: new iam.ServicePrincipal('batch.amazonaws.com'), + }).roleArn, + instanceTypes: this.buildInstanceTypes(props.computeResources.instanceTypes), + maxvCpus: props.computeResources.maxvCpus || 256, + minvCpus: props.computeResources.minvCpus || 0, + securityGroupIds: this.buildSecurityGroupIds(props.computeResources.vpc, props.computeResources.securityGroups), + spotIamFleetRole: spotFleetRole ? spotFleetRole.roleArn : undefined, + subnets: props.computeResources.vpc.selectSubnets(props.computeResources.vpcSubnets).subnetIds, + tags: props.computeResources.computeResourcesTags, + type: props.computeResources.type || ComputeResourceType.ON_DEMAND, + }; + } + + const computeEnvironment = new CfnComputeEnvironment(this, 'Resource', { + computeEnvironmentName: this.physicalName, + computeResources, + serviceRole: props.serviceRole + ? props.serviceRole.roleArn + : new iam.Role(this, 'Resource-Service-Instance-Role', { + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSBatchServiceRole'), + ], + assumedBy: new iam.ServicePrincipal('batch.amazonaws.com'), + }).roleArn, + state: props.enabled === undefined ? 'ENABLED' : (props.enabled ? 'ENABLED' : 'DISABLED'), + type: this.isManaged(props) ? 'UNMANAGED' : 'MANAGED', + }); + + if (props.computeResources && props.computeResources.vpc) { + this.node.addDependency(props.computeResources.vpc); + } + + this.computeEnvironmentArn = this.getResourceArnAttribute(computeEnvironment.ref, { + service: 'batch', + resource: 'compute-environment', + resourceName: this.physicalName, + }); + this.computeEnvironmentName = this.getResourceNameAttribute(computeEnvironment.ref); + } + + private isManaged(props: ComputeEnvironmentProps): boolean { + return props.managed === undefined ? true : props.managed; + } + + /** + * Validates the properties provided for a new batch compute environment. + */ + private validateProps(props: ComputeEnvironmentProps) { + if (props === undefined) { + return; + } + + if (this.isManaged(props) && props.computeResources !== undefined) { + throw new Error('It is not allowed to set computeResources on an AWS managed compute environment'); + } + + if (!this.isManaged(props) && props.computeResources === undefined) { + throw new Error('computeResources is missing but required on an unmanaged 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 + + // 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.allocationStrategy && props.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 { + // 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'); + } + } + + 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'); + } + } + } + } + + private buildInstanceTypes(instanceTypes?: ec2.InstanceType[]): string[] { + if (instanceTypes === undefined) { + return [ + 'optimal', + ]; + } + + return instanceTypes.map((type: ec2.InstanceType) => type.toString()); + } + + private buildSecurityGroupIds(vpc: ec2.IVpc, securityGroups?: ec2.ISecurityGroup[]): string[] | undefined { + if (securityGroups === undefined) { + return [ + new ec2.SecurityGroup(this, 'Resource-Security-Group', { vpc }).securityGroupId, + ]; + } + + return securityGroups.map((group: ec2.ISecurityGroup) => group.securityGroupId); + } + + /** + * Generates an AWS IAM role for provisioning spotfleet resources + * if the allocation strategy is set to BEST_FIT or not defined. + * + * @param props - the compute environment construct properties + */ + private getSpotFleetRole(props: ComputeEnvironmentProps): iam.IRole | undefined { + if (props.allocationStrategy && props.allocationStrategy !== AllocationStrategy.BEST_FIT) { + return undefined; + } + + if (props.computeResources) { + if (props.computeResources.spotFleetRole) { + return props.computeResources.spotFleetRole; + } else if (props.computeResources.type === ComputeResourceType.SPOT) { + return iam.Role.fromRoleArn(this, 'Resource-SpotFleet-Role', + `arn${Aws.PARTITION}iam::${this.stack.account}:role/aws-service-role/spotfleet.amazonaws.com/AWSServiceRoleForEC2SpotFleet`); + } + } + + return undefined; + } +} diff --git a/packages/@aws-cdk/aws-batch/lib/index.ts b/packages/@aws-cdk/aws-batch/lib/index.ts index 5f0b6784e8e2a..251be8901a3ea 100644 --- a/packages/@aws-cdk/aws-batch/lib/index.ts +++ b/packages/@aws-cdk/aws-batch/lib/index.ts @@ -1,2 +1,5 @@ // AWS::Batch CloudFormation Resources: export * from './batch.generated'; +export * from './compute-environment'; +export * from './job-definition'; +export * from './job-queue'; diff --git a/packages/@aws-cdk/aws-batch/lib/job-definition-image-config.ts b/packages/@aws-cdk/aws-batch/lib/job-definition-image-config.ts new file mode 100644 index 0000000000000..6d8a4b18bca43 --- /dev/null +++ b/packages/@aws-cdk/aws-batch/lib/job-definition-image-config.ts @@ -0,0 +1,86 @@ +import * as ecs from '@aws-cdk/aws-ecs'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { JobDefinitionContainer } from './job-definition'; + +/** + * TaskDefinitionRole + * + * Defines the required properties of a Batch Job Definition. + */ +interface TaskDefinitionProps { + /** + * Defines the IAM role used when executing this task definition + */ + readonly executionRole: iam.IRole; +} + +/** + * Batch Job Task Definition + * + * Defines a Batch Job Task Definition. The properties of this task definition mirrors + * those of an {@link ecs.ContainerDefinition}. This class is a wrapper on that structure. + */ +class TaskDefinition { + /** + * The IAM role used during execution of the task definition. This IAM role should + * contain the relevant access required to interact with resources your application needs to perform. + */ + public readonly executionRole: iam.IRole; + + constructor(props: TaskDefinitionProps) { + this.executionRole = props.executionRole; + } + + /** + * Internal function to allow the Batch Job task defintion + * to match the CDK requirements of an EC2 task definition. + * + * @internal + */ + // tslint:disable-next-line: no-empty + public _linkContainer() {} + + /** + * Retrieves the execution role for this task definition + */ + public obtainExecutionRole(): iam.IRole { + return this.executionRole; + } +} + +/** + * The configuration for creating a batch container image. + */ +export class JobDefinitionImageConfig { + /** + * Specifies the name of the container image + */ + public readonly imageName: string; + + constructor(scope: cdk.Construct, container: JobDefinitionContainer) { + const config = this.bindImageConfig(scope, container); + + this.imageName = config.imageName; + } + + private bindImageConfig(scope: cdk.Construct, container: JobDefinitionContainer): ecs.ContainerImageConfig { + return container.image.bind(scope, new ecs.ContainerDefinition(scope, 'Resource-Batch-Job-Container-Definition', { + command: container.command, + cpu: container.vcpus, + image: container.image, + environment: container.environment, + linuxParameters: container.linuxParams, + memoryLimitMiB: container.memoryLimitMiB, + privileged: container.privileged, + readonlyRootFilesystem: container.readOnly, + gpuCount: container.gpuCount, + user: container.user, + taskDefinition: new TaskDefinition({ + executionRole: container.jobRole || new iam.LazyRole(scope, 'Resource-Batch-Task-Definition-Role', { + assumedBy: new iam.ServicePrincipal('batch.amazonaws.com') + }), + }) as unknown as ecs.TaskDefinition, + })); + } +} diff --git a/packages/@aws-cdk/aws-batch/lib/job-definition.ts b/packages/@aws-cdk/aws-batch/lib/job-definition.ts new file mode 100644 index 0000000000000..4dad6363e50b5 --- /dev/null +++ b/packages/@aws-cdk/aws-batch/lib/job-definition.ts @@ -0,0 +1,360 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as iam from '@aws-cdk/aws-iam'; +import { Construct, Duration, IResource, Resource, Stack } from '@aws-cdk/core'; +import { CfnJobDefinition } from './batch.generated'; +import { JobDefinitionImageConfig } from './job-definition-image-config'; + +/** + * Properties of a job definition container. + */ +export interface JobDefinitionContainer { + /** + * The command that is passed to the container. + * + * If you provide a shell command as a single string, you have to quote command-line arguments. + * + * @default - CMD value built into container image. + */ + readonly command?: string[]; + + /** + * The environment variables to pass to the container. + * + * @default none + */ + readonly environment?: { [key: string]: string }; + + /** + * The image used to start a container. + */ + readonly image: ecs.ContainerImage; + + /** + * The instance type to use for a multi-node parallel job. Currently all node groups in a + * multi-node parallel job must use the same instance type. This parameter is not valid + * for single-node container jobs. + * + * @default - None + */ + readonly instanceType?: ec2.InstanceType; + + /** + * The IAM role that the container can assume for AWS permissions. + * + * @default - An IAM role will created. + */ + readonly jobRole?: iam.IRole; + + /** + * Linux-specific modifications that are applied to the container, such as details for device mappings. + * For now, only the `devices` property is supported. + * + * @default - None will be used. + */ + readonly linuxParams?: ecs.LinuxParameters; + + /** + * The hard limit (in MiB) of memory to present to the container. If your container attempts to exceed + * the memory specified here, the container is killed. You must specify at least 4 MiB of memory for a job. + * + * @default 4 + */ + readonly memoryLimitMiB?: number; + + /** + * The mount points for data volumes in your container. + * + * @default - No mount points will be used. + */ + readonly mountPoints?: ecs.MountPoint[]; + + /** + * When this parameter is true, the container is given elevated privileges on the host container instance (similar to the root user). + * @default false + */ + readonly privileged?: boolean; + + /** + * When this parameter is true, the container is given read-only access to its root file system. + * + * @default false + */ + readonly readOnly?: boolean; + + /** + * The number of physical GPUs to reserve for the container. The number of GPUs reserved for all + * containers in a job should not exceed the number of available GPUs on the compute resource that the job is launched on. + * + * @default - No GPU reservation. + */ + readonly gpuCount?: number; + + /** + * A list of ulimits to set in the container. + * + * @default - No limits. + */ + readonly ulimits?: ecs.Ulimit[]; + + /** + * The user name to use inside the container. + * + * @default - None will be used. + */ + readonly user?: string; + + /** + * The number of vCPUs reserved for the container. Each vCPU is equivalent to + * 1,024 CPU shares. You must specify at least one vCPU. + * + * @default 1 + */ + readonly vcpus?: number; + + /** + * A list of data volumes used in a job. + * + * @default - No data volumes will be used. + */ + readonly volumes?: ecs.Volume[]; +} + +/** + * Construction properties of the {@link JobDefinition} construct. + */ +export interface JobDefinitionProps { + /** + * The name of the job definition. + * + * Up to 128 letters (uppercase and lowercase), numbers, hyphens, and underscores are allowed. + * + * @default Cloudformation-generated name + */ + readonly jobDefinitionName?: string; + + /** + * An object with various properties specific to container-based jobs. + */ + readonly container: JobDefinitionContainer; + + /** + * An object with various properties specific to multi-node parallel jobs. + * + * @default - undefined + */ + readonly nodeProps?: IMultiNodeProps; + + /** + * When you submit a job, you can specify parameters that should replace the + * placeholders or override the default job definition parameters. Parameters + * in job submission requests take precedence over the defaults in a job definition. + * This allows you to use the same job definition for multiple jobs that use the same + * format, and programmatically change values in the command at submission time. + * + * @link https://docs.aws.amazon.com/batch/latest/userguide/job_definition_parameters.html + * @default - undefined + */ + readonly parameters?: { [key: string]: string }; + + /** + * The number of times to move a job to the RUNNABLE status. You may specify between 1 and + * 10 attempts. If the value of attempts is greater than one, the job is retried on failure + * the same number of attempts as the value. + * + * @default 1 + */ + readonly retryAttempts?: number; + + /** + * The timeout configuration for jobs that are submitted with this job definition. You can specify + * a timeout duration after which AWS Batch terminates your jobs if they have not finished. + * + * @default - undefined + */ + readonly timeout?: Duration; +} + +/** + * Properties for specifying multi-node properties for compute resources. + */ +export interface IMultiNodeProps { + /** + * Specifies the node index for the main node of a multi-node parallel job. This node index value must be fewer than the number of nodes. + */ + mainNode: number; + + /** + * A list of node ranges and their properties associated with a multi-node parallel job. + */ + rangeProps: INodeRangeProps[]; + + /** + * The number of nodes associated with a multi-node parallel job. + */ + count: number; +} + +/** + * Properties for a multi-node batch job. + */ +export interface INodeRangeProps { + /** + * The container details for the node range. + */ + container: JobDefinitionContainer; + + /** + * The minimum node index value to apply this container definition against. + * + * You may nest node ranges, for example 0:10 and 4:5, in which case the 4:5 range properties override the 0:10 properties. + * + * @default 0 + */ + fromNodeIndex?: number; + + /** + * The maximum node index value to apply this container definition against. If omitted, the highest value is used relative. + * + * to the number of nodes associated with the job. You may nest node ranges, for example 0:10 and 4:5, + * in which case the 4:5 range properties override the 0:10 properties. + * + * @default {@link IMultiNodeprops.count} + */ + toNodeIndex?: number; +} + +/** + * An interface representing a job definition - either a new one, created with the CDK, *using the + * {@link JobDefinition} class, or existing ones, referenced using the {@link JobDefinition.fromJobDefinitionArn} method. + */ +export interface IJobDefinition extends IResource { + /** + * The ARN of this batch job definition. + * + * @attribute + */ + readonly jobDefinitionArn: string; + + /** + * The name of the batch job definition. + * + * @attribute + */ + readonly jobDefinitionName: string; +} + +/** + * Batch Job Definition. + * + * Defines a batch job definition to execute a specific batch job. + */ +export class JobDefinition extends Resource implements IJobDefinition { + /** + * Imports an existing batch job definition by its amazon resource name. + * + * @param scope + * @param id + * @param jobDefinitionArn + */ + public static fromJobDefinitionArn(scope: Construct, id: string, jobDefinitionArn: string): IJobDefinition { + const stack = Stack.of(scope); + const jobDefName = stack.parseArn(jobDefinitionArn).resourceName!; + + class Import extends Resource implements IJobDefinition { + public readonly jobDefinitionArn = jobDefinitionArn; + public readonly jobDefinitionName = jobDefName; + } + + return new Import(scope, id); + } + + public readonly jobDefinitionArn: string; + public readonly jobDefinitionName: string; + private readonly imageConfig: JobDefinitionImageConfig; + + constructor(scope: Construct, id: string, props: JobDefinitionProps) { + super(scope, id, { + physicalName: props.jobDefinitionName, + }); + + this.imageConfig = new JobDefinitionImageConfig(this, props.container); + + const jobDef = new CfnJobDefinition(this, 'Resource', { + jobDefinitionName: props.jobDefinitionName, + containerProperties: this.buildJobContainer(props.container), + type: 'container', + nodeProperties: props.nodeProps + ? { mainNode: props.nodeProps.mainNode, + nodeRangeProperties: this.buildNodeRangeProps(props.nodeProps), + numNodes: props.nodeProps.count } + : undefined, + parameters: props.parameters, + retryStrategy: { + attempts: props.retryAttempts || 1, + }, + timeout: { + attemptDurationSeconds: props.timeout ? props.timeout.toSeconds() : undefined, + }, + }); + + this.jobDefinitionArn = this.getResourceArnAttribute(jobDef.ref, { + service: 'batch', + resource: 'job-definition', + resourceName: this.physicalName, + }); + this.jobDefinitionName = this.getResourceNameAttribute(jobDef.ref); + } + + private deserializeEnvVariables(env?: { [name: string]: string}): CfnJobDefinition.EnvironmentProperty[] | undefined { + const vars = new Array(); + + if (env === undefined) { + return undefined; + } + + Object.keys(env).map((name: string) => { + vars.push({ name, value: env[name] }); + }); + + return vars; + } + + private buildJobContainer(container?: JobDefinitionContainer): CfnJobDefinition.ContainerPropertiesProperty | undefined { + if (container === undefined) { + return undefined; + } + + return { + command: container.command, + environment: this.deserializeEnvVariables(container.environment), + image: this.imageConfig.imageName, + instanceType: container.instanceType && container.instanceType.toString(), + jobRoleArn: container.jobRole && container.jobRole.roleArn, + linuxParameters: container.linuxParams + ? { devices: container.linuxParams.renderLinuxParameters().devices } + : undefined, + memory: container.memoryLimitMiB || 4, + mountPoints: container.mountPoints, + privileged: container.privileged || false, + readonlyRootFilesystem: container.readOnly || false, + ulimits: container.ulimits, + user: container.user, + vcpus: container.vcpus || 1, + volumes: container.volumes, + }; + } + + private buildNodeRangeProps(multiNodeProps: IMultiNodeProps): CfnJobDefinition.NodeRangePropertyProperty[] { + const rangeProps = new Array(); + + for (const prop of multiNodeProps.rangeProps) { + rangeProps.push({ + container: this.buildJobContainer(prop.container), + targetNodes: `${prop.fromNodeIndex || 0}:${prop.toNodeIndex || multiNodeProps.count}`, + }); + } + + return rangeProps; + } +} diff --git a/packages/@aws-cdk/aws-batch/lib/job-queue.ts b/packages/@aws-cdk/aws-batch/lib/job-queue.ts new file mode 100644 index 0000000000000..9e285e45c0ed8 --- /dev/null +++ b/packages/@aws-cdk/aws-batch/lib/job-queue.ts @@ -0,0 +1,139 @@ +import { Construct, IResource, Resource, Stack } from '@aws-cdk/core'; +import { CfnJobQueue } from './batch.generated'; +import { ComputeEnvironment, IComputeEnvironment } from './compute-environment'; + +/** + * Properties for mapping a compute environment to a job queue. + */ +export interface JobQueueComputeEnvironment { + /** + * The batch compute environment to use for processing submitted jobs to this queue. + */ + readonly computeEnvironment: IComputeEnvironment; + + /** + * The order in which this compute environment will be selected for dynamic allocation of resources to process submitted jobs. + */ + readonly order: number; +} + +/** + * Properties of a batch job queue. + */ +export interface JobQueueProps { + /** + * A name for the job queue. + * + * Up to 128 letters (uppercase and lowercase), numbers, hyphens, and underscores are allowed. + * + * @default Cloudformation-generated name + */ + readonly jobQueueName?: string; + + /** + * The set of compute environments mapped to a job queue and their order relative to each other. The job scheduler uses this parameter to + * determine which compute environment should execute a given job. Compute environments must be in the VALID state before you can associate them + * with a job queue. You can associate up to three compute environments with a job queue. + * + * @default Default-Compute-Environment + */ + readonly computeEnvironments?: JobQueueComputeEnvironment[]; + + /** + * The priority of the job queue. Job queues with a higher priority (or a higher integer value for the priority parameter) are evaluated first + * when associated with the same compute environment. Priority is determined in descending order, for example, a job queue with a priority value + * of 10 is given scheduling preference over a job queue with a priority value of 1. + * + * @default 1 + */ + readonly priority?: number; + + /** + * The state of the job queue. If set to true, it is able to accept jobs. + * + * @default true + */ + readonly enabled?: boolean; +} + +/** + * Properties of a Job Queue. + */ +export interface IJobQueue extends IResource { + /** + * The ARN of this batch job queue. + * + * @attribute + */ + readonly jobQueueArn: string; + + /** + * A name for the job queue. + * + * Up to 128 letters (uppercase and lowercase), numbers, hyphens, and underscores are allowed. + * + * @attribute + */ + readonly jobQueueName: string; +} + +/** + * Batch Job Queue. + * + * Defines a batch job queue to define how submitted batch jobs + * should be ran based on specified batch compute environments. + */ +export class JobQueue extends Resource implements IJobQueue { + /** + * Fetches an existing batch job queue by its amazon resource name. + * + * @param scope + * @param id + * @param jobQueueArn + */ + public static fromJobQueueArn(scope: Construct, id: string, jobQueueArn: string): IJobQueue { + const stack = Stack.of(scope); + const jobQueueName = stack.parseArn(jobQueueArn).resourceName!; + + class Import extends Resource implements IJobQueue { + public readonly jobQueueArn = jobQueueArn; + public readonly jobQueueName = jobQueueName; + } + + return new Import(scope, id); + } + + public readonly jobQueueArn: string; + public readonly jobQueueName: string; + + constructor(scope: Construct, id: string, props: JobQueueProps = {}) { + super(scope, id, { + physicalName: props.jobQueueName, + }); + + const jobQueue = new CfnJobQueue(this, 'Resource', { + computeEnvironmentOrder: props.computeEnvironments + ? props.computeEnvironments.map(cp => ({ + computeEnvironment: cp.computeEnvironment.computeEnvironmentArn, + order: cp.order, + } as CfnJobQueue.ComputeEnvironmentOrderProperty)) + : [ + { + // Get an AWS Managed Compute Environment + computeEnvironment: new ComputeEnvironment(this, 'Resource-Batch-Compute-Environment').computeEnvironmentArn, + order: 1, + }, + ], + jobQueueName: this.physicalName, + priority: props.priority || 1, + state: props.enabled === undefined ? 'ENABLED' : (props.enabled ? 'ENABLED' : 'DISABLED'), + }); + + this.jobQueueArn = this.getResourceArnAttribute(jobQueue.ref, { + service: 'batch', + resource: 'job-queue', + resourceName: this.physicalName, + }); + this.jobQueueName = this.getResourceNameAttribute(jobQueue.ref); + } +} diff --git a/packages/@aws-cdk/aws-batch/package.json b/packages/@aws-cdk/aws-batch/package.json index 1aebac3aa7875..71d7c2bc75790 100644 --- a/packages/@aws-cdk/aws-batch/package.json +++ b/packages/@aws-cdk/aws-batch/package.json @@ -67,7 +67,7 @@ "coverageThreshold": { "global": { "branches": 60, - "statements": 80 + "statements": 60 } }, "collectCoverage": true, @@ -81,18 +81,28 @@ "devDependencies": { "@aws-cdk/assert": "999.0.0", "cdk-build-tools": "999.0.0", + "cdk-integ-tools": "999.0.0", "cfn2ts": "999.0.0", + "jest": "^24.9.0", "pkglint": "999.0.0" }, "dependencies": { - "@aws-cdk/core": "999.0.0" + "@aws-cdk/core": "999.0.0", + "@aws-cdk/aws-ec2": "999.0.0", + "@aws-cdk/aws-ecr": "999.0.0", + "@aws-cdk/aws-ecs": "999.0.0", + "@aws-cdk/aws-iam": "999.0.0" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { - "@aws-cdk/core": "999.0.0" + "@aws-cdk/core": "999.0.0", + "@aws-cdk/aws-ec2": "999.0.0", + "@aws-cdk/aws-ecr": "999.0.0", + "@aws-cdk/aws-ecs": "999.0.0", + "@aws-cdk/aws-iam": "999.0.0" }, "engines": { "node": ">= 10.3.0" }, "stability": "experimental" -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-batch/test/compute-environment.test.ts b/packages/@aws-cdk/aws-batch/test/compute-environment.test.ts new file mode 100644 index 0000000000000..18cd77e5f3b15 --- /dev/null +++ b/packages/@aws-cdk/aws-batch/test/compute-environment.test.ts @@ -0,0 +1,395 @@ +import { expect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import { throws } from 'assert'; +import * as batch from '../lib'; + +describe('Batch Compute Evironment', () => { + let expectedManagedDefaultComputeProps: any; + let defaultServiceRole: any; + + let stack: cdk.Stack; + let vpc: ec2.Vpc; + + beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + vpc = new ec2.Vpc(stack, 'test-vpc'); + + defaultServiceRole = { + ServiceRole: { + 'Fn::GetAtt': [ + 'testcomputeenvResourceServiceInstanceRole105069A5', + 'Arn' + ], + }, + }; + + expectedManagedDefaultComputeProps = (overrides: any) => { + return { + ComputeResources: { + AllocationStrategy: batch.AllocationStrategy.BEST_FIT, + InstanceRole: { + 'Fn::GetAtt': [ + 'testcomputeenvResourceInstanceRole7FD819B9', + 'Arn' + ] + }, + InstanceTypes: [ + 'optimal' + ], + MaxvCpus: 256, + MinvCpus: 0, + Subnets: [ + { + Ref: 'testvpcPrivateSubnet1Subnet865FB50A' + }, + { + Ref: 'testvpcPrivateSubnet2Subnet23D3396F' + } + ], + Type: batch.ComputeResourceType.ON_DEMAND, + ...overrides, + } + }; + }; + }); + + describe('when validating props', () => { + test('should deny setting compute resources when using type managed', () => { + // THEN + throws(() => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + computeResources: { + vpc, + }, + }); + }); + }); + + test('should deny if creating an unmanged environment with no provided compute resource props', () => { + // THEN + throws(() => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: false, + }); + }); + }); + }); + + describe('using spot resources', () => { + test('should provide a spotfleet role if one is not given', () => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: false, + computeResources: { + type: batch.ComputeResourceType.SPOT, + vpc, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Batch::ComputeEnvironment', { + Type: 'MANAGED', + ...expectedManagedDefaultComputeProps({ + Type: batch.ComputeResourceType.SPOT, + SpotIamFleetRole: { + 'Fn::Join': [ + '', + [ + 'arn', + { + Ref: 'AWS::Partition' + }, + 'iam::', + { + Ref: 'AWS::AccountId' + }, + ':role/aws-service-role/spotfleet.amazonaws.com/AWSServiceRoleForEC2SpotFleet', + ], + ], + }, + }), + }, ResourcePart.Properties)); + }); + + describe('with a bid percentage', () => { + test('should deny my bid if set below 0', () => { + // THEN + throws(() => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: false, + computeResources: { + vpc, + type: batch.ComputeResourceType.SPOT, + bidPercentage: -1, + }, + }); + }); + }); + + test('should deny my bid if above 100', () => { + // THEN + throws(() => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: false, + computeResources: { + vpc, + type: batch.ComputeResourceType.SPOT, + bidPercentage: 101, + }, + }); + }); + }); + }); + }); + + describe('with properties specified', () => { + test('renders the correct cloudformation properties', () => { + // WHEN + const props = { + allocationStrategy: batch.AllocationStrategy.BEST_FIT, + computeEnvironmentName: 'my-test-compute-env', + computeResources: { + vpc, + computeResourcesTags: new cdk.Tag('foo', 'bar'), + desiredvCpus: 1, + ec2KeyPair: 'my-key-pair', + image: new ecs.EcsOptimizedAmi({ + generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, + hardwareType: ecs.AmiHardwareType.STANDARD, + }), + instanceRole: new iam.Role(stack, 'test-compute-env-instance-role', { + assumedBy: new iam.ServicePrincipal('batch.amazonaws.com'), + }), + instanceTypes: [ + ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO), + ], + maxvCpus: 4, + minvCpus: 1, + securityGroups: [ + new ec2.SecurityGroup(stack, 'test-sg', { + vpc, + allowAllOutbound: true, + }), + ], + type: batch.ComputeResourceType.ON_DEMAND, + vpcSubnets: { + subnetType: ec2.SubnetType.PRIVATE, + }, + } as batch.ComputeResources, + enabled: false, + managed: false, + }; + + new batch.ComputeEnvironment(stack, 'test-compute-env', props); + + // THEN + expect(stack).to(haveResourceLike('AWS::Batch::ComputeEnvironment', { + ComputeEnvironmentName: 'my-test-compute-env', + Type: 'MANAGED', + State: 'DISABLED', + ServiceRole: { + 'Fn::GetAtt': [ + 'testcomputeenvResourceServiceInstanceRole105069A5', + 'Arn' + ], + }, + ComputeResources: { + AllocationStrategy: batch.AllocationStrategy.BEST_FIT, + DesiredvCpus: props.computeResources.desiredvCpus, + Ec2KeyPair: props.computeResources.ec2KeyPair, + ImageId: { + Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter' + }, + InstanceRole: { + 'Fn::GetAtt': [ + props.computeResources.instanceRole ? `${props.computeResources.instanceRole.node.uniqueId}F3B86D94` : '', + 'Arn' + ] + }, + InstanceTypes: [ + props.computeResources.instanceTypes ? props.computeResources.instanceTypes[0].toString() : '', + ], + MaxvCpus: props.computeResources.maxvCpus, + MinvCpus: props.computeResources.minvCpus, + SecurityGroupIds: [ + { + 'Fn::GetAtt': [ + 'testsg872EB48A', + 'GroupId' + ] + } + ], + Subnets: [ + { + Ref: `${vpc.node.uniqueId}PrivateSubnet1Subnet865FB50A` + }, + { + Ref: `${vpc.node.uniqueId}PrivateSubnet2Subnet23D3396F` + } + ], + Tags: { + key: 'foo', + props: {}, + defaultPriority: 100, + value: 'bar' + }, + Type: 'EC2' + }, + }, ResourcePart.Properties)); + }); + + describe('with no allocation strategy specified', () => { + test('should default to a best_fit strategy', () => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: false, + computeResources: { + vpc, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Batch::ComputeEnvironment', { + Type: 'MANAGED', + ServiceRole: { + 'Fn::GetAtt': [ + 'testcomputeenvResourceServiceInstanceRole105069A5', + 'Arn' + ], + }, + }, ResourcePart.Properties)); + }); + }); + + describe('with a min vcpu value', () => { + test('should deny less than 0', () => { + // THEN + throws(() => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + computeResources: { + vpc, + minvCpus: -1, + }, + }); + }); + }); + + test('cannot be greater than the max vcpu value', () => { + // THEN + throws(() => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + computeResources: { + vpc, + minvCpus: 2, + maxvCpus: 1, + }, + }); + }); + }); + }); + + describe('with no min vcpu value provided', () => { + test('should default to 0', () => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: false, + computeResources: { + vpc, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Batch::ComputeEnvironment', { + ...defaultServiceRole, + ...expectedManagedDefaultComputeProps({ + MinvCpus: 0, + }), + }, ResourcePart.Properties)); + }); + }); + + describe('with no max vcpu value provided', () => { + test('should default to 256', () => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: false, + computeResources: { + vpc, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Batch::ComputeEnvironment', { + ...expectedManagedDefaultComputeProps({ + MaxvCpus: 256, + }), + }, ResourcePart.Properties)); + }); + }); + + describe('with no instance role specified', () => { + test('should generate a role for me', () => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: false, + computeResources: { + vpc, + }, + }); + + // THEN + expect(stack).to(haveResource('AWS::Batch::ComputeEnvironment')); + expect(stack).to(haveResource('AWS::IAM::Role')); + }); + }); + + describe('with no instance type defined', () => { + test('should default to optimal matching', () => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: false, + computeResources: { + vpc, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Batch::ComputeEnvironment', { + ...expectedManagedDefaultComputeProps({ + InstanceTypes: [ 'optimal' ], + }), + }, ResourcePart.Properties)); + }); + }); + + describe('with no type specified', () => { + test('should default to EC2', () => { + // WHEN + new batch.ComputeEnvironment(stack, 'test-compute-env', { + managed: false, + computeResources: { + vpc, + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Batch::ComputeEnvironment', { + ...expectedManagedDefaultComputeProps({ + Type: batch.ComputeResourceType.ON_DEMAND, + }), + }, ResourcePart.Properties)); + }); + }); + }); +}); diff --git a/packages/@aws-cdk/aws-batch/test/integ.batch.expected.json b/packages/@aws-cdk/aws-batch/test/integ.batch.expected.json new file mode 100644 index 0000000000000..7c367e0d37a08 --- /dev/null +++ b/packages/@aws-cdk/aws-batch/test/integ.batch.expected.json @@ -0,0 +1,1195 @@ +{ + "Resources": { + "vpcA2121C38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc" + } + ] + } + }, + "vpcPublicSubnet1Subnet2E65531E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PublicSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "vpcPublicSubnet1RouteTable48A2DF9B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet1RouteTableAssociation5D3F4579": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet1RouteTable48A2DF9B" + }, + "SubnetId": { + "Ref": "vpcPublicSubnet1Subnet2E65531E" + } + } + }, + "vpcPublicSubnet1DefaultRoute10708846": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet1RouteTable48A2DF9B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + }, + "DependsOn": [ + "vpcVPCGW7984C166" + ] + }, + "vpcPublicSubnet1EIPDA49DCBE": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet1NATGateway9C16659E": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "vpcPublicSubnet1EIPDA49DCBE", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "vpcPublicSubnet1Subnet2E65531E" + }, + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PublicSubnet1" + } + ] + } + }, + "vpcPublicSubnet2Subnet009B674F": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PublicSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "vpcPublicSubnet2RouteTableEB40D4CB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPublicSubnet2RouteTableAssociation21F81B59": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet2RouteTableEB40D4CB" + }, + "SubnetId": { + "Ref": "vpcPublicSubnet2Subnet009B674F" + } + } + }, + "vpcPublicSubnet2DefaultRouteA1EC0F60": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet2RouteTableEB40D4CB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + }, + "DependsOn": [ + "vpcVPCGW7984C166" + ] + }, + "vpcPublicSubnet2EIP9B3743B1": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPublicSubnet2NATGateway9B8AE11A": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "vpcPublicSubnet2EIP9B3743B1", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "vpcPublicSubnet2Subnet009B674F" + }, + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PublicSubnet2" + } + ] + } + }, + "vpcPublicSubnet3Subnet11B92D7C": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PublicSubnet3" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "vpcPublicSubnet3RouteTableA3C00665": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PublicSubnet3" + } + ] + } + }, + "vpcPublicSubnet3RouteTableAssociationD102D1C4": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet3RouteTableA3C00665" + }, + "SubnetId": { + "Ref": "vpcPublicSubnet3Subnet11B92D7C" + } + } + }, + "vpcPublicSubnet3DefaultRoute3F356A11": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPublicSubnet3RouteTableA3C00665" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + }, + "DependsOn": [ + "vpcVPCGW7984C166" + ] + }, + "vpcPublicSubnet3EIP2C3B9D91": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PublicSubnet3" + } + ] + } + }, + "vpcPublicSubnet3NATGateway82F6CA9E": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "vpcPublicSubnet3EIP2C3B9D91", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "vpcPublicSubnet3Subnet11B92D7C" + }, + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PublicSubnet3" + } + ] + } + }, + "vpcPrivateSubnet1Subnet934893E8": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PrivateSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "vpcPrivateSubnet1RouteTableB41A48CC": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PrivateSubnet1" + } + ] + } + }, + "vpcPrivateSubnet1RouteTableAssociation67945127": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet1RouteTableB41A48CC" + }, + "SubnetId": { + "Ref": "vpcPrivateSubnet1Subnet934893E8" + } + } + }, + "vpcPrivateSubnet1DefaultRoute1AA8E2E5": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet1RouteTableB41A48CC" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "vpcPublicSubnet1NATGateway9C16659E" + } + } + }, + "vpcPrivateSubnet2Subnet7031C2BA": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PrivateSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "vpcPrivateSubnet2RouteTable7280F23E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PrivateSubnet2" + } + ] + } + }, + "vpcPrivateSubnet2RouteTableAssociation007E94D3": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet2RouteTable7280F23E" + }, + "SubnetId": { + "Ref": "vpcPrivateSubnet2Subnet7031C2BA" + } + } + }, + "vpcPrivateSubnet2DefaultRouteB0E07F99": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet2RouteTable7280F23E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "vpcPublicSubnet2NATGateway9B8AE11A" + } + } + }, + "vpcPrivateSubnet3Subnet985AC459": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "vpcA2121C38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PrivateSubnet3" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + } + ] + } + }, + "vpcPrivateSubnet3RouteTable24DA79A0": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc/PrivateSubnet3" + } + ] + } + }, + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet3RouteTable24DA79A0" + }, + "SubnetId": { + "Ref": "vpcPrivateSubnet3Subnet985AC459" + } + } + }, + "vpcPrivateSubnet3DefaultRoute30C45F47": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "vpcPrivateSubnet3RouteTable24DA79A0" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "vpcPublicSubnet3NATGateway82F6CA9E" + } + } + }, + "vpcIGWE57CBDCA": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "batch-stack/vpc" + } + ] + } + }, + "vpcVPCGW7984C166": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "vpcA2121C38" + }, + "InternetGatewayId": { + "Ref": "vpcIGWE57CBDCA" + } + } + }, + "batchmanagedcomputeenvResourceServiceInstanceRole3A9DC7D6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "batch.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSBatchServiceRole" + ] + ] + } + ] + } + }, + "batchmanagedcomputeenv1AA975A9": { + "Type": "AWS::Batch::ComputeEnvironment", + "Properties": { + "ServiceRole": { + "Fn::GetAtt": [ + "batchmanagedcomputeenvResourceServiceInstanceRole3A9DC7D6", + "Arn" + ] + }, + "Type": "UNMANAGED", + "State": "ENABLED" + } + }, + "batchdemandcomputeenvResourceInstanceRole8989496E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "batch.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "batchdemandcomputeenvResourceSecurityGroup64711D4D": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "batch-stack/batch-demand-compute-env/Resource-Security-Group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "vpcA2121C38" + } + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "batchdemandcomputeenvResourceServiceInstanceRole8DF7CB96": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "batch.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSBatchServiceRole" + ] + ] + } + ] + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "batchdemandcomputeenv6131030A": { + "Type": "AWS::Batch::ComputeEnvironment", + "Properties": { + "ServiceRole": { + "Fn::GetAtt": [ + "batchdemandcomputeenvResourceServiceInstanceRole8DF7CB96", + "Arn" + ] + }, + "Type": "MANAGED", + "ComputeResources": { + "AllocationStrategy": "BEST_FIT", + "InstanceRole": { + "Fn::GetAtt": [ + "batchdemandcomputeenvResourceInstanceRole8989496E", + "Arn" + ] + }, + "InstanceTypes": [ + "optimal" + ], + "MaxvCpus": 256, + "MinvCpus": 0, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "batchdemandcomputeenvResourceSecurityGroup64711D4D", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "vpcPrivateSubnet1Subnet934893E8" + }, + { + "Ref": "vpcPrivateSubnet2Subnet7031C2BA" + }, + { + "Ref": "vpcPrivateSubnet3Subnet985AC459" + } + ], + "Type": "EC2" + }, + "State": "ENABLED" + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "batchspotcomputeenvResourceInstanceRoleF6188F15": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "batch.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "batchspotcomputeenvResourceSecurityGroup07B09BF9": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "batch-stack/batch-spot-compute-env/Resource-Security-Group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "vpcA2121C38" + } + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "batchspotcomputeenvResourceServiceInstanceRole8B0DF5A7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "batch.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSBatchServiceRole" + ] + ] + } + ] + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "batchspotcomputeenv2CE4DFD9": { + "Type": "AWS::Batch::ComputeEnvironment", + "Properties": { + "ServiceRole": { + "Fn::GetAtt": [ + "batchspotcomputeenvResourceServiceInstanceRole8B0DF5A7", + "Arn" + ] + }, + "Type": "MANAGED", + "ComputeResources": { + "AllocationStrategy": "BEST_FIT", + "BidPercentage": 80, + "InstanceRole": { + "Fn::GetAtt": [ + "batchspotcomputeenvResourceInstanceRoleF6188F15", + "Arn" + ] + }, + "InstanceTypes": [ + "optimal" + ], + "MaxvCpus": 256, + "MinvCpus": 0, + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "batchspotcomputeenvResourceSecurityGroup07B09BF9", + "GroupId" + ] + } + ], + "SpotIamFleetRole": { + "Fn::Join": [ + "", + [ + "arn", + { + "Ref": "AWS::Partition" + }, + "iam::", + { + "Ref": "AWS::AccountId" + }, + ":role/aws-service-role/spotfleet.amazonaws.com/AWSServiceRoleForEC2SpotFleet" + ] + ] + }, + "Subnets": [ + { + "Ref": "vpcPrivateSubnet1Subnet934893E8" + }, + { + "Ref": "vpcPrivateSubnet2Subnet7031C2BA" + }, + { + "Ref": "vpcPrivateSubnet3Subnet985AC459" + } + ], + "Type": "SPOT" + }, + "State": "ENABLED" + }, + "DependsOn": [ + "vpcIGWE57CBDCA", + "vpcPrivateSubnet1DefaultRoute1AA8E2E5", + "vpcPrivateSubnet1RouteTableB41A48CC", + "vpcPrivateSubnet1RouteTableAssociation67945127", + "vpcPrivateSubnet1Subnet934893E8", + "vpcPrivateSubnet2DefaultRouteB0E07F99", + "vpcPrivateSubnet2RouteTable7280F23E", + "vpcPrivateSubnet2RouteTableAssociation007E94D3", + "vpcPrivateSubnet2Subnet7031C2BA", + "vpcPrivateSubnet3DefaultRoute30C45F47", + "vpcPrivateSubnet3RouteTable24DA79A0", + "vpcPrivateSubnet3RouteTableAssociationC58B3C2C", + "vpcPrivateSubnet3Subnet985AC459", + "vpcPublicSubnet1DefaultRoute10708846", + "vpcPublicSubnet1EIPDA49DCBE", + "vpcPublicSubnet1NATGateway9C16659E", + "vpcPublicSubnet1RouteTable48A2DF9B", + "vpcPublicSubnet1RouteTableAssociation5D3F4579", + "vpcPublicSubnet1Subnet2E65531E", + "vpcPublicSubnet2DefaultRouteA1EC0F60", + "vpcPublicSubnet2EIP9B3743B1", + "vpcPublicSubnet2NATGateway9B8AE11A", + "vpcPublicSubnet2RouteTableEB40D4CB", + "vpcPublicSubnet2RouteTableAssociation21F81B59", + "vpcPublicSubnet2Subnet009B674F", + "vpcPublicSubnet3DefaultRoute3F356A11", + "vpcPublicSubnet3EIP2C3B9D91", + "vpcPublicSubnet3NATGateway82F6CA9E", + "vpcPublicSubnet3RouteTableA3C00665", + "vpcPublicSubnet3RouteTableAssociationD102D1C4", + "vpcPublicSubnet3Subnet11B92D7C", + "vpcA2121C38", + "vpcVPCGW7984C166" + ] + }, + "batchjobqueueE3C528F2": { + "Type": "AWS::Batch::JobQueue", + "Properties": { + "ComputeEnvironmentOrder": [ + { + "ComputeEnvironment": { + "Ref": "batchmanagedcomputeenv1AA975A9" + }, + "Order": 1 + }, + { + "ComputeEnvironment": { + "Ref": "batchdemandcomputeenv6131030A" + }, + "Order": 2 + }, + { + "ComputeEnvironment": { + "Ref": "batchspotcomputeenv2CE4DFD9" + }, + "Order": 3 + } + ], + "Priority": 1, + "State": "ENABLED" + } + }, + "batchjobrepo4C508C51": { + "Type": "AWS::ECR::Repository", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "batchjobdeffromecrE0E30DAD": { + "Type": "AWS::Batch::JobDefinition", + "Properties": { + "Type": "container", + "ContainerProperties": { + "Image": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 4, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "batchjobrepo4C508C51", + "Arn" + ] + } + ] + } + ] + }, + ".dkr.ecr.", + { + "Fn::Select": [ + 3, + { + "Fn::Split": [ + ":", + { + "Fn::GetAtt": [ + "batchjobrepo4C508C51", + "Arn" + ] + } + ] + } + ] + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "batchjobrepo4C508C51" + }, + ":latest" + ] + ] + }, + "Memory": 4, + "Privileged": false, + "ReadonlyRootFilesystem": false, + "Vcpus": 1 + }, + "RetryStrategy": { + "Attempts": 1 + }, + "Timeout": {} + } + }, + "batchjobdeffrom4007378D": { + "Type": "AWS::Batch::JobDefinition", + "Properties": { + "Type": "container", + "ContainerProperties": { + "Image": "docker/whalesay", + "Memory": 4, + "Privileged": false, + "ReadonlyRootFilesystem": false, + "Vcpus": 1 + }, + "RetryStrategy": { + "Attempts": 1 + }, + "Timeout": {} + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-batch/test/integ.batch.ts b/packages/@aws-cdk/aws-batch/test/integ.batch.ts new file mode 100644 index 0000000000000..c28d7d4f2e159 --- /dev/null +++ b/packages/@aws-cdk/aws-batch/test/integ.batch.ts @@ -0,0 +1,55 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as ecr from '@aws-cdk/aws-ecr'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as cdk from '@aws-cdk/core'; +import * as batch from '../lib/'; + +export const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'batch-stack'); + +const vpc = new ec2.Vpc(stack, 'vpc'); + +new batch.JobQueue(stack, 'batch-job-queue', { + computeEnvironments: [ + { + computeEnvironment: new batch.ComputeEnvironment(stack, 'batch-managed-compute-env'), + order: 1, + }, + { + computeEnvironment: new batch.ComputeEnvironment(stack, 'batch-demand-compute-env', { + managed: false, + computeResources: { + type: batch.ComputeResourceType.ON_DEMAND, + vpc, + }, + }), + order: 2, + }, + { + computeEnvironment: new batch.ComputeEnvironment(stack, 'batch-spot-compute-env', { + managed: false, + computeResources: { + type: batch.ComputeResourceType.SPOT, + vpc, + bidPercentage: 80, + }, + }), + order: 3, + }, + ] +}); + +const repo = new ecr.Repository(stack, 'batch-job-repo'); + +new batch.JobDefinition(stack, 'batch-job-def-from-ecr', { + container: { + image: new ecs.EcrImage(repo, 'latest'), + }, +}); + +new batch.JobDefinition(stack, 'batch-job-def-from-', { + container: { + image: ecs.ContainerImage.fromRegistry('docker/whalesay'), + }, +}); diff --git a/packages/@aws-cdk/aws-batch/test/job-definition.test.ts b/packages/@aws-cdk/aws-batch/test/job-definition.test.ts new file mode 100644 index 0000000000000..45cc0c0418ef7 --- /dev/null +++ b/packages/@aws-cdk/aws-batch/test/job-definition.test.ts @@ -0,0 +1,202 @@ +import '@aws-cdk/assert/jest'; +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'; +import * as ecs from '@aws-cdk/aws-ecs'; +import * as iam from '@aws-cdk/aws-iam'; +import * as cdk from '@aws-cdk/core'; +import * as batch from '../lib'; + +describe('Batch Job Definition', () => { + let stack: cdk.Stack; + let jobDefProps: batch.JobDefinitionProps; + + beforeEach(() => { + stack = new cdk.Stack(); + + const role = new iam.Role(stack, 'job-role', { + assumedBy: new iam.ServicePrincipal('batch.amazonaws.com'), + }); + + const linuxParams = new ecs.LinuxParameters(stack, 'job-linux-params', { + initProcessEnabled: true, + sharedMemorySize: 1, + }); + + jobDefProps = { + jobDefinitionName: 'test-job', + container: { + command: [ 'echo "Hello World"' ], + environment: { + foo: 'bar', + }, + jobRole: role, + gpuCount: 1, + image: ecs.EcrImage.fromRegistry('docker/whalesay'), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO), + linuxParams, + memoryLimitMiB: 1, + mountPoints: new Array(), + privileged: true, + readOnly: true, + ulimits: new Array(), + user: 'root', + vcpus: 2, + volumes: new Array(), + }, + nodeProps: { + count: 2, + mainNode: 1, + rangeProps: new Array(), + }, + parameters: { + foo: 'bar', + }, + retryAttempts: 2, + timeout: cdk.Duration.seconds(30), + }; + }); + + test('renders the correct cloudformation properties', () => { + // WHEN + new batch.JobDefinition(stack, 'job-def', jobDefProps); + + // THEN + expect(stack).toHaveResourceLike('AWS::Batch::JobDefinition', { + JobDefinitionName: jobDefProps.jobDefinitionName, + ContainerProperties: jobDefProps.container ? { + Command: jobDefProps.container.command, + Environment: [ + { + Name: 'foo', + Value: 'bar', + }, + ], + InstanceType: jobDefProps.container.instanceType ? jobDefProps.container.instanceType.toString() : '', + LinuxParameters: {}, + Memory: jobDefProps.container.memoryLimitMiB, + MountPoints: [], + Privileged: jobDefProps.container.privileged, + ReadonlyRootFilesystem: jobDefProps.container.readOnly, + Ulimits: [], + User: jobDefProps.container.user, + Vcpus: jobDefProps.container.vcpus, + Volumes: [], + } : undefined, + NodeProperties: jobDefProps.nodeProps ? { + MainNode: jobDefProps.nodeProps.mainNode, + NodeRangeProperties: [], + NumNodes: jobDefProps.nodeProps.count, + } : undefined, + Parameters: { + foo: 'bar', + }, + RetryStrategy: { + Attempts: jobDefProps.retryAttempts, + }, + Timeout: { + AttemptDurationSeconds: jobDefProps.timeout ? jobDefProps.timeout.toSeconds() : -1, + }, + Type: 'container', + }, ResourcePart.Properties); + }); + test('can use an ecr image', () => { + // WHEN + const repo = new ecr.Repository(stack, 'image-repo'); + + new batch.JobDefinition(stack, 'job-def', { + container: { + image: ecs.ContainerImage.fromEcrRepository(repo), + }, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Batch::JobDefinition', { + ContainerProperties: { + Image: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 4, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'imagerepoD116FAF0', + 'Arn' + ] + } + ] + } + ] + }, + '.dkr.ecr.', + { + 'Fn::Select': [ + 3, + { + 'Fn::Split': [ + ':', + { + 'Fn::GetAtt': [ + 'imagerepoD116FAF0', + 'Arn' + ] + } + ] + } + ] + }, + '.', + { + Ref: 'AWS::URLSuffix' + }, + '/', + { + Ref: 'imagerepoD116FAF0' + }, + ':latest' + ] + ] + }, + Memory: 4, + Privileged: false, + ReadonlyRootFilesystem: false, + Vcpus: 1 + } + }, ResourcePart.Properties); + }); + + test('can use a registry image', () => { + // WHEN + new batch.JobDefinition(stack, 'job-def', { + container: { + image: ecs.ContainerImage.fromRegistry('docker/whalesay'), + }, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Batch::JobDefinition', { + ContainerProperties: { + Image: 'docker/whalesay', + Memory: 4, + Privileged: false, + ReadonlyRootFilesystem: false, + Vcpus: 1, + }, + }, ResourcePart.Properties); + }); + + test('can be imported from an ARN', () => { + // WHEN + const importedJob = batch.JobDefinition.fromJobDefinitionArn(stack, 'job-def-clone', + 'arn:aws:batch:us-east-1:123456789012:job-definition/job-def-name:1'); + + // THEN + expect(importedJob.jobDefinitionName).toEqual('job-def-name:1'); + expect(importedJob.jobDefinitionArn).toEqual('arn:aws:batch:us-east-1:123456789012:job-definition/job-def-name:1'); + }); +}); diff --git a/packages/@aws-cdk/aws-batch/test/job-queue.test.ts b/packages/@aws-cdk/aws-batch/test/job-queue.test.ts new file mode 100644 index 0000000000000..d99a6ad82e8fe --- /dev/null +++ b/packages/@aws-cdk/aws-batch/test/job-queue.test.ts @@ -0,0 +1,81 @@ +import { ResourcePart } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; +import * as cdk from '@aws-cdk/core'; +import * as batch from '../lib'; + +describe('Batch Job Queue', () => { + let stack: cdk.Stack; + let computeEnvironment: batch.ComputeEnvironment; + + beforeEach(() => { + stack = new cdk.Stack(); + computeEnvironment = new batch.ComputeEnvironment(stack, 'test-compute-env'); + }); + + it('can be imported from an ARN', () => { + // WHEN + const existingJobQ = new batch.JobQueue(stack, 'test-job-queue', { + priority: 1, + enabled: false, + computeEnvironments: [ + { + computeEnvironment, + order: 1, + } + ], + }); + const jobQFromArn = batch.JobQueue.fromJobQueueArn(stack, 'test-job-queue-from-arn', existingJobQ.jobQueueArn); + + // THEN + expect(jobQFromArn.jobQueueArn).toEqual(existingJobQ.jobQueueArn); + }); + + it('renders the correct cloudformation properties', () => { + // WHEN + const props: batch.JobQueueProps = { + priority: 1, + enabled: false, + computeEnvironments: [ + { + computeEnvironment, + order: 1, + }, + ], + jobQueueName: 'test-job-queue-name' + }; + new batch.JobQueue(stack, 'test-job-queue', props); + + // THEN + expect(stack).toHaveResourceLike('AWS::Batch::JobQueue', { + JobQueueName: props.jobQueueName, + State: props.enabled ? 'ENABLED' : 'DISABLED', + Priority: props.priority, + ComputeEnvironmentOrder: [ + { + ComputeEnvironment: { + Ref: 'testcomputeenv547FFD1A' + }, + Order: 1, + } + ], + }, ResourcePart.Properties); + }); + + it('should have a default queue priority of 1', () => { + // WHEN + new batch.JobQueue(stack, 'test-job-queue', { + enabled: false, + computeEnvironments: [ + { + computeEnvironment, + order: 1, + } + ], + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::Batch::JobQueue', { + Priority: 1, + }, ResourcePart.Properties); + }); +});