diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index ed9bd575c7144..83180ec460199 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -18,27 +18,25 @@ import { BaseTargetTrackingProps, PredefinedMetric, TargetTrackingScalingPolicy const NAME_TAG: string = 'Name'; /** - * Properties of a Fleet + * Basic properties of an AutoScalingGroup, except the exact machines to run and where they should run + * + * Constructs that want to create AutoScalingGroups can inherit + * this interface and specialize the essential parts in various ways. */ -export interface AutoScalingGroupProps { - /** - * Type of instance to launch - */ - instanceType: ec2.InstanceType; - +export interface CommonAutoScalingGroupProps { /** * Minimum number of instances in the fleet * * @default 1 */ - minSize?: number; + minCapacity?: number; /** * Maximum number of instances in the fleet * * @default desiredCapacity */ - maxSize?: number; + maxCapacity?: number; /** * Initial amount of instances in the fleet @@ -52,16 +50,6 @@ export interface AutoScalingGroupProps { */ keyName?: string; - /** - * AMI to launch - */ - machineImage: ec2.IMachineImageSource; - - /** - * VPC to launch these instances in. - */ - vpc: ec2.IVpcNetwork; - /** * Where to place instances within the VPC */ @@ -153,6 +141,26 @@ export interface AutoScalingGroupProps { associatePublicIpAddress?: boolean; } +/** + * Properties of a Fleet + */ +export interface AutoScalingGroupProps extends CommonAutoScalingGroupProps { + /** + * VPC to launch these instances in. + */ + vpc: ec2.IVpcNetwork; + + /** + * Type of instance to launch + */ + instanceType: ec2.InstanceType; + + /** + * AMI to launch + */ + machineImage: ec2.IMachineImageSource; +} + /** * A Fleet represents a managed set of EC2 instances * @@ -236,19 +244,19 @@ export class AutoScalingGroup extends cdk.Construct implements IAutoScalingGroup const desiredCapacity = (props.desiredCapacity !== undefined ? props.desiredCapacity : - (props.minSize !== undefined ? props.minSize : - (props.maxSize !== undefined ? props.maxSize : 1))); - const minSize = props.minSize !== undefined ? props.minSize : 1; - const maxSize = props.maxSize !== undefined ? props.maxSize : desiredCapacity; + (props.minCapacity !== undefined ? props.minCapacity : + (props.maxCapacity !== undefined ? props.maxCapacity : 1))); + const minCapacity = props.minCapacity !== undefined ? props.minCapacity : 1; + const maxCapacity = props.maxCapacity !== undefined ? props.maxCapacity : desiredCapacity; - if (desiredCapacity < minSize || desiredCapacity > maxSize) { - throw new Error(`Should have minSize (${minSize}) <= desiredCapacity (${desiredCapacity}) <= maxSize (${maxSize})`); + if (desiredCapacity < minCapacity || desiredCapacity > maxCapacity) { + throw new Error(`Should have minCapacity (${minCapacity}) <= desiredCapacity (${desiredCapacity}) <= maxCapacity (${maxCapacity})`); } const asgProps: CfnAutoScalingGroupProps = { cooldown: props.cooldownSeconds !== undefined ? `${props.cooldownSeconds}` : undefined, - minSize: minSize.toString(), - maxSize: maxSize.toString(), + minSize: minCapacity.toString(), + maxSize: maxCapacity.toString(), desiredCapacity: desiredCapacity.toString(), launchConfigurationName: launchConfig.ref, loadBalancerNames: new cdk.Token(() => this.loadBalancerNames.length > 0 ? this.loadBalancerNames : undefined), diff --git a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts index 78de5fbe217a2..fca1ce567bbf6 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts @@ -126,7 +126,7 @@ export = { test.done(); }, - 'can set minSize, maxSize, desiredCapacity to 0'(test: Test) { + 'can set minCapacity, maxCapacity, desiredCapacity to 0'(test: Test) { const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); const vpc = mockVpc(stack); @@ -134,8 +134,8 @@ export = { instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.Micro), machineImage: new ec2.AmazonLinuxImage(), vpc, - minSize: 0, - maxSize: 0, + minCapacity: 0, + maxCapacity: 0, desiredCapacity: 0 }); @@ -159,7 +159,7 @@ export = { instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.Micro), machineImage: new ec2.AmazonLinuxImage(), vpc, - minSize: 10 + minCapacity: 10 }); // THEN @@ -183,7 +183,7 @@ export = { instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.Micro), machineImage: new ec2.AmazonLinuxImage(), vpc, - maxSize: 10 + maxCapacity: 10 }); // THEN @@ -415,8 +415,8 @@ export = { instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.Micro), machineImage: new ec2.AmazonLinuxImage(), vpc, - minSize: 0, - maxSize: 0, + minCapacity: 0, + maxCapacity: 0, desiredCapacity: 0, associatePublicIpAddress: true, }); @@ -438,8 +438,8 @@ export = { instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.Micro), machineImage: new ec2.AmazonLinuxImage(), vpc, - minSize: 0, - maxSize: 0, + minCapacity: 0, + maxCapacity: 0, desiredCapacity: 0, associatePublicIpAddress: false, }); @@ -461,8 +461,8 @@ export = { instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.Micro), machineImage: new ec2.AmazonLinuxImage(), vpc, - minSize: 0, - maxSize: 0, + minCapacity: 0, + maxCapacity: 0, desiredCapacity: 0, }); diff --git a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts index ee4e9cb6a928f..2e457030d2682 100644 --- a/packages/@aws-cdk/aws-ec2/lib/machine-image.ts +++ b/packages/@aws-cdk/aws-ec2/lib/machine-image.ts @@ -190,7 +190,7 @@ export class GenericLinuxImage implements IMachineImageSource { public getImage(scope: Construct): MachineImage { const stack = Stack.find(scope); const region = stack.requireRegion('AMI cannot be determined'); - const ami = this.amiMap[region]; + const ami = region !== 'test-region' ? this.amiMap[region] : 'ami-12345'; if (!ami) { throw new Error(`Unable to find AMI in AMI map: no AMI specified for region '${region}'`); } diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index b913f8756c623..2e1a42a32a40a 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -21,7 +21,7 @@ const cluster = new ecs.Cluster(this, 'Cluster', { }); // Add capacity to it -cluster.addDefaultAutoScalingGroupCapacity({ +cluster.addDefaultAutoScalingGroupCapacity('Capacity', { instanceType: new ec2.InstanceType("t2.xlarge"), instanceCount: 3, }); diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index 190284b67165a..fd5ea83151a53 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -74,15 +74,13 @@ export class Cluster extends cdk.Construct implements ICluster { * * Returns the AutoScalingGroup so you can add autoscaling settings to it. */ - public addDefaultAutoScalingGroupCapacity(options: AddDefaultAutoScalingGroupOptions): autoscaling.AutoScalingGroup { - const autoScalingGroup = new autoscaling.AutoScalingGroup(this, 'DefaultAutoScalingGroup', { + public addDefaultAutoScalingGroupCapacity(id: string, options: AddDefaultAutoScalingGroupOptions): autoscaling.AutoScalingGroup { + const autoScalingGroup = new autoscaling.AutoScalingGroup(this, id, { + ...options, vpc: this.vpc, - instanceType: options.instanceType, machineImage: new EcsOptimizedAmi(), - updateType: autoscaling.UpdateType.ReplacingUpdate, - minSize: options.minCapacity, - maxSize: options.maxCapacity, - desiredCapacity: options.instanceCount, + updateType: options.updateType || autoscaling.UpdateType.ReplacingUpdate, + instanceType: options.instanceType, }); this.addAutoScalingGroupCapacity(autoScalingGroup, options); @@ -375,31 +373,9 @@ export interface AddAutoScalingGroupCapacityOptions { /** * Properties for adding autoScalingGroup */ -export interface AddDefaultAutoScalingGroupOptions extends AddAutoScalingGroupCapacityOptions { - +export interface AddDefaultAutoScalingGroupOptions extends AddAutoScalingGroupCapacityOptions, autoscaling.CommonAutoScalingGroupProps { /** * The type of EC2 instance to launch into your Autoscaling Group */ instanceType: ec2.InstanceType; - - /** - * Number of container instances registered in your ECS Cluster - * - * @default 1 - */ - instanceCount?: number; - - /** - * Maximum number of instances - * - * @default Same as instanceCount - */ - maxCapacity?: number; - - /** - * Minimum number of instances - * - * @default Same as instanceCount - */ - minCapacity?: number; } diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.ts index 0683524d92eb7..5cabaf5b02c46 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.ts @@ -12,7 +12,7 @@ class EventStack extends cdk.Stack { const vpc = new ec2.VpcNetwork(this, 'Vpc', { maxAZs: 1 }); const cluster = new ecs.Cluster(this, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts index eb658343bf09b..2e0cebef27a56 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts @@ -10,7 +10,7 @@ const stack = new cdk.Stack(app, 'aws-ecs-integ'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); -cluster.addDefaultAutoScalingGroupCapacity({ +cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts index 50492fa437e05..49e8a29da2aeb 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/integ.lb-bridge-nw.ts @@ -10,7 +10,7 @@ const stack = new cdk.Stack(app, 'aws-ecs-integ-ecs'); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 2 }); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); -cluster.addDefaultAutoScalingGroupCapacity({ +cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); @@ -44,4 +44,4 @@ listener.addTargets('ECS', { new cdk.Output(stack, 'LoadBalancerDNS', { value: lb.dnsName, }); -app.run(); \ No newline at end of file +app.run(); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-event-rule-target.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-event-rule-target.ts index cafb405ffd62e..f4515489a0c68 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-event-rule-target.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-event-rule-target.ts @@ -11,7 +11,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'Vpc', { maxAZs: 1 }); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); @@ -58,4 +58,4 @@ export = { test.done(); } -}; \ No newline at end of file +}; diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts index e3eb27442a3db..760c2d3a462ab 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts @@ -13,7 +13,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -54,7 +54,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer('BaseContainer', { image: ecs.ContainerImage.fromDockerHub('test'), @@ -79,7 +79,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer('BaseContainer', { image: ecs.ContainerImage.fromDockerHub('test'), @@ -124,7 +124,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -152,7 +152,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { networkMode: NetworkMode.Bridge }); @@ -184,7 +184,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { networkMode: NetworkMode.AwsVpc }); @@ -235,7 +235,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { networkMode: NetworkMode.AwsVpc }); @@ -263,7 +263,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -292,7 +292,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -323,7 +323,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -354,7 +354,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -381,7 +381,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc'); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -411,7 +411,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc'); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -438,7 +438,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -469,7 +469,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef'); taskDefinition.addContainer("web", { @@ -498,7 +498,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'VPC'); const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'TD', { networkMode: ecs.NetworkMode.Host }); const container = taskDefinition.addContainer('web', { image: ecs.ContainerImage.fromDockerHub('test'), diff --git a/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts index 8748b7cadbc87..17fb38eb6fa3d 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.ecs-cluster.ts @@ -15,7 +15,7 @@ export = { vpc, }); - cluster.addDefaultAutoScalingGroupCapacity({ + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); @@ -164,7 +164,7 @@ export = { }); // WHEN - cluster.addDefaultAutoScalingGroupCapacity({ + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); @@ -188,7 +188,7 @@ export = { const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new InstanceType("m3.large") }); @@ -206,9 +206,9 @@ export = { const vpc = new ec2.VpcNetwork(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro'), - instanceCount: 3 + desiredCapacity: 3 }); // THEN diff --git a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts index 70413e120e296..112cdabfba764 100644 --- a/packages/@aws-cdk/aws-ecs/test/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs/test/test.l3s.ts @@ -12,7 +12,7 @@ export = { const stack = new cdk.Stack(); const vpc = new ec2.VpcNetwork(stack, 'VPC'); const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); - cluster.addDefaultAutoScalingGroupCapacity({ instanceType: new ec2.InstanceType('t2.micro') }); + cluster.addDefaultAutoScalingGroupCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); // WHEN new ecs.LoadBalancedEc2Service(stack, 'Service', { diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index 6bf0898a242df..281d5abe1015d 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -1,2 +1,32 @@ -## The CDK Construct Library for AWS EKS -This module is part of the [AWS Cloud Development Kit](https://github.com/awslabs/aws-cdk) project. +## AWS Elastic Container Service for Kubernetes (EKS) Construct Library + +This construct library allows you to define and create [Amazon Elastic Container Service for Kubernetes (EKS)](https://aws.amazon.com/eks/) clusters programmatically. + +### Example + +The following example shows how to start an EKS cluster and how to +add worker nodes to it: + +[starting a cluster example](test/integ.eks-cluster.lit.ts) + +After deploying the previous CDK app you still need to configure `kubectl` +and manually add the nodes to your cluster, as described [in the EKS user +guide](https://docs.aws.amazon.com/eks/latest/userguide/launch-workers.html). + +### SSH into your nodes + +If you want to be able to SSH into your worker nodes, you must already +have an SSH key in the region you're connecting to and pass it, and you must +be able to connect to the hosts (meaning they must have a public IP and you +should be allowed to connect to them on port 22): + +[ssh into nodes example](test/example.ssh-into-nodes.lit.ts) + +If you want to SSH into nodes in a private subnet, you should set up a +bastion host in a public subnet. That setup is recommended, but is +unfortunately beyond the scope of this documentation. + +### Roadmap + +- [ ] Add ability to start tasks on clusters using CDK (similar to ECS's "`Service`" concept). +- [ ] Describe how to set up AutoScaling (how to combine EC2 and Kubernetes scaling) \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/lib/ami.ts b/packages/@aws-cdk/aws-eks/lib/ami.ts new file mode 100644 index 0000000000000..52c7516f09083 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/lib/ami.ts @@ -0,0 +1,124 @@ +import ec2 = require('@aws-cdk/aws-ec2'); + +/** + * Properties for EksOptimizedAmi + */ +export interface EksOptimizedAmiProps { + /** + * What instance type to retrieve the image for (normal or GPU-optimized) + * + * @default Normal + */ + nodeType?: NodeType; + + /** + * The Kubernetes version to use + * + * @default The latest version + */ + kubernetesVersion?: string; +} + +/** + * Source for EKS optimized AMIs + */ +export class EksOptimizedAmi extends ec2.GenericLinuxImage implements ec2.IMachineImageSource { + constructor(props: EksOptimizedAmiProps = {}) { + const version = props.kubernetesVersion || LATEST_KUBERNETES_VERSION; + if (!(version in EKS_AMI)) { + throw new Error(`We don't have an AMI for kubernetes version ${version}`); + } + super(EKS_AMI[version][props.nodeType || NodeType.Normal]); + } +} + +const LATEST_KUBERNETES_VERSION = '1.11'; + +/** + * Whether the worker nodes should support GPU or just normal instances + */ +export const enum NodeType { + /** + * Normal instances + */ + Normal = 'Normal', + + /** + * GPU instances + */ + GPU = 'GPU', +} + +export function nodeTypeForInstanceType(instanceType: ec2.InstanceType) { + return instanceType.toString().startsWith('p2') || instanceType.toString().startsWith('p3') ? NodeType.GPU : NodeType.Normal; +} + +/** + * Select AMI to use based on the AWS Region being deployed + * + * TODO: Create dynamic mappign by searching SSM Store + * + * @see https://docs.aws.amazon.com/eks/latest/userguide/eks-optimized-ami.html + */ +const EKS_AMI: {[version: string]: {[type: string]: {[region: string]: string}}} = { + '1.10': parseTable(` + US West (Oregon) (us-west-2) ami-09e1df3bad220af0b ami-0ebf0561e61a2be02 + US East (N. Virginia) (us-east-1) ami-04358410d28eaab63 ami-0131c0ca222183def + US East (Ohio) (us-east-2) ami-0b779e8ab57655b4b ami-0abfb3be33c196cbf + EU (Frankfurt) (eu-central-1) ami-08eb700778f03ea94 ami-000622b1016d2a5bf + EU (Stockholm) (eu-north-1) ami-068b8a1efffd30eda ami-cc149ab2 + EU (Ireland) (eu-west-1) ami-0de10c614955da932 ami-0dafd3a1dc43781f7 + Asia Pacific (Tokyo) (ap-northeast-1) ami-06398bdd37d76571d ami-0afc9d14b2fe11ad9 + Asia Pacific (Seoul) (ap-northeast-2) ami-08a87e0a7c32fa649 ami-0d75b9ab57bfc8c9a + Asia Pacific (Singapore) (ap-southeast-1) ami-0ac3510e44b5bf8ef ami-0ecce0670cb66d17b + Asia Pacific (Sydney) (ap-southeast-2) ami-0d2c929ace88cfebe ami-03b048bd9d3861ce9 + `), + '1.11': parseTable(` + US West (Oregon) (us-west-2) ami-0a2abab4107669c1b ami-0c9e5e2d8caa9fb5e + US East (N. Virginia) (us-east-1) ami-0c24db5df6badc35a ami-0ff0241c02b279f50 + US East (Ohio) (us-east-2) ami-0c2e8d28b1f854c68 ami-006a12f54eaafc2b1 + EU (Frankfurt) (eu-central-1) ami-010caa98bae9a09e2 ami-0d6f0554fd4743a9d + EU (Stockholm) (eu-north-1) ami-06ee67302ab7cf838 ami-0b159b75 + EU (Ireland) (eu-west-1) ami-01e08d22b9439c15a ami-097978e7acde1fd7c + Asia Pacific (Tokyo) (ap-northeast-1) ami-0f0e8066383e7a2cb ami-036b3969c5eb8d3cf + Asia Pacific (Seoul) (ap-northeast-2) ami-0b7baa90de70f683f ami-0b7f163f7194396f7 + Asia Pacific (Singapore) (ap-southeast-1) ami-019966ed970c18502 ami-093f742654a955ee6 + Asia Pacific (Sydney) (ap-southeast-2) ami-06ade0abbd8eca425 ami-05e09575123ff498b + `), +}; + +/** + * Helper function which makes it easier to copy/paste the HTML AMI table into this source. + * + * I can't stress enough how much of a temporary solution this should be, but until we + * have a proper discovery mechanism, this is easier than converting the table into + * nested dicts by hand. + */ +function parseTable(contents: string): {[type: string]: {[region: string]: string}} { + const normalTable: {[region: string]: string} = {}; + const gpuTable: {[region: string]: string} = {}; + + // Last parenthesized expression that looks like a region + const extractRegion = /\(([a-z]+-[a-z]+-[0-9]+)\)\s*$/; + + for (const line of contents.split('\n')) { + if (line.trim() === '') { continue; } + + const parts = line.split('\t'); + if (parts.length !== 3) { + throw new Error(`Line lost its TABs: "${line}"`); + } + + const m = extractRegion.exec(parts[0]); + if (!m) { throw new Error(`Like doesn't seem to contain a region: "${line}"`); } + const region = m[1]; + + normalTable[region] = parts[1].trim(); + gpuTable[region] = parts[2].trim(); + } + + return { + [NodeType.GPU]: gpuTable, + [NodeType.Normal]: normalTable + }; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-base.ts b/packages/@aws-cdk/aws-eks/lib/cluster-base.ts new file mode 100644 index 0000000000000..9de91498e56c4 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/lib/cluster-base.ts @@ -0,0 +1,98 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); + +/** + * An EKS cluster + */ +export interface ICluster extends cdk.IConstruct, ec2.IConnectable { + /** + * The VPC in which this Cluster was created + */ + readonly vpc: ec2.IVpcNetwork; + + /** + * The physical name of the Cluster + */ + readonly clusterName: string; + + /** + * The unique ARN assigned to the service by AWS + * in the form of arn:aws:eks: + */ + readonly clusterArn: string; + + /** + * The API Server endpoint URL + */ + readonly clusterEndpoint: string; + + /** + * The certificate-authority-data for your cluster. + */ + readonly clusterCertificateAuthorityData: string; + + /** + * Export cluster references to use in other stacks + */ + export(): ClusterImportProps; +} + +/** + * A SecurityGroup Reference, object not created with this template. + */ +export abstract class ClusterBase extends cdk.Construct implements ICluster { + public abstract readonly connections: ec2.Connections; + public abstract readonly vpc: ec2.IVpcNetwork; + public abstract readonly clusterName: string; + public abstract readonly clusterArn: string; + public abstract readonly clusterEndpoint: string; + public abstract readonly clusterCertificateAuthorityData: string; + + /** + * Export cluster references to use in other stacks + */ + public export(): ClusterImportProps { + return { + vpc: this.vpc.export(), + clusterName: this.makeOutput('ClusterNameExport', this.clusterName), + clusterArn: this.makeOutput('ClusterArn', this.clusterArn), + clusterEndpoint: this.makeOutput('ClusterEndpoint', this.clusterEndpoint), + clusterCertificateAuthorityData: this.makeOutput('ClusterCAData', this.clusterCertificateAuthorityData), + securityGroups: this.connections.securityGroups.map(sg => sg.export()), + }; + } + + private makeOutput(name: string, value: any): string { + return new cdk.Output(this, name, { value }).makeImportValue().toString(); + } +} + +export interface ClusterImportProps { + /** + * The VPC in which this Cluster was created + */ + readonly vpc: ec2.VpcNetworkImportProps; + + /** + * The physical name of the Cluster + */ + readonly clusterName: string; + + /** + * The unique ARN assigned to the service by AWS + * in the form of arn:aws:eks: + */ + readonly clusterArn: string; + + /** + * The API Server endpoint URL + */ + readonly clusterEndpoint: string; + + /** + * The certificate-authority-data for your cluster. + */ + readonly clusterCertificateAuthorityData: string; + + readonly securityGroups: ec2.SecurityGroupImportProps[]; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts new file mode 100644 index 0000000000000..406d30bd704aa --- /dev/null +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -0,0 +1,342 @@ +import autoscaling = require('@aws-cdk/aws-autoscaling'); +import ec2 = require('@aws-cdk/aws-ec2'); +import iam = require('@aws-cdk/aws-iam'); +import cdk = require('@aws-cdk/cdk'); +import { EksOptimizedAmi, nodeTypeForInstanceType } from './ami'; +import { ClusterBase, ClusterImportProps, ICluster } from './cluster-base'; +import { CfnCluster } from './eks.generated'; +import { maxPodsForInstanceType } from './instance-data'; + +/** + * Properties to instantiate the Cluster + */ +export interface ClusterProps { + /** + * The VPC in which to create the Cluster + */ + vpc: ec2.IVpcNetwork; + + /** + * Where to place EKS Control Plane ENIs + * + * If you want to create public load balancers, this must include public subnets. + * + * For example, to only select private subnets, supply the following: + * + * ``` + * vpcPlacements: [ + * { subnetsToUse: ec2.SubnetType.Private } + * ] + * ``` + * + * @default All public and private subnets + */ + vpcPlacements?: ec2.VpcPlacementStrategy[]; + + /** + * Role that provides permissions for the Kubernetes control plane to make calls to AWS API operations on your behalf. + * + * @default A role is automatically created for you + */ + role?: iam.IRole; + + /** + * Name for the cluster. + * + * @default Automatically generated name + */ + clusterName?: string; + + /** + * Security Group to use for Control Plane ENIs + * + * @default A security group is automatically created + */ + securityGroup?: ec2.ISecurityGroup; + + /** + * The Kubernetes version to run in the cluster + * + * @default If not supplied, will use Amazon default version + */ + version?: string; +} + +/** + * A Cluster represents a managed Kubernetes Service (EKS) + * + * This is a fully managed cluster of API Servers (control-plane) + * The user is still required to create the worker nodes. + */ +export class Cluster extends ClusterBase { + /** + * Import an existing cluster + * + * @param scope the construct scope, in most cases 'this' + * @param id the id or name to import as + * @param props the cluster properties to use for importing information + */ + public static import(scope: cdk.Construct, id: string, props: ClusterImportProps): ICluster { + return new ImportedCluster(scope, id, props); + } + + /** + * The VPC in which this Cluster was created + */ + public readonly vpc: ec2.IVpcNetwork; + + /** + * The Name of the created EKS Cluster + */ + public readonly clusterName: string; + + /** + * The AWS generated ARN for the Cluster resource + * + * @example arn:aws:eks:us-west-2:666666666666:cluster/prod + */ + public readonly clusterArn: string; + + /** + * The endpoint URL for the Cluster + * + * This is the URL inside the kubeconfig file to use with kubectl + * + * @example https://5E1D0CEXAMPLEA591B746AFC5AB30262.yl4.us-west-2.eks.amazonaws.com + */ + public readonly clusterEndpoint: string; + + /** + * The certificate-authority-data for your cluster. + */ + public readonly clusterCertificateAuthorityData: string; + + /** + * Manages connection rules (Security Group Rules) for the cluster + * + * @type {ec2.Connections} + * @memberof Cluster + */ + public readonly connections: ec2.Connections; + + /** + * IAM role assumed by the EKS Control Plane + */ + public readonly role: iam.IRole; + + private readonly version: string | undefined; + + /** + * Initiates an EKS Cluster with the supplied arguments + * + * @param scope a Construct, most likely a cdk.Stack created + * @param name the name of the Construct to create + * @param props properties in the IClusterProps interface + */ + constructor(scope: cdk.Construct, id: string, props: ClusterProps) { + super(scope, id); + + this.vpc = props.vpc; + this.version = props.version; + + this.tagSubnets(); + + this.role = props.role || new iam.Role(this, 'ClusterRole', { + assumedBy: new iam.ServicePrincipal('eks.amazonaws.com'), + managedPolicyArns: [ + new iam.AwsManagedPolicy('AmazonEKSClusterPolicy', this).policyArn, + new iam.AwsManagedPolicy('AmazonEKSServicePolicy', this).policyArn, + ], + }); + + const securityGroup = props.securityGroup || new ec2.SecurityGroup(this, 'ControlPlaneSecurityGroup', { + vpc: props.vpc, + description: 'EKS Control Plane Security Group', + }); + + this.connections = new ec2.Connections({ + securityGroups: [securityGroup], + defaultPortRange: new ec2.TcpPort(443), // Control Plane has an HTTPS API + }); + + // Get subnetIds for all selected subnets + const placements = props.vpcPlacements || [{ subnetsToUse: ec2.SubnetType.Public }, { subnetsToUse: ec2.SubnetType.Private }]; + const subnetIds = flatMap(placements, p => this.vpc.subnets(p)).map(s => s.subnetId); + + const resource = new CfnCluster(this, 'Resource', { + name: props.clusterName, + roleArn: this.role.roleArn, + version: props.version, + resourcesVpcConfig: { + securityGroupIds: [securityGroup.securityGroupId], + subnetIds + } + }); + + this.clusterName = resource.clusterName; + this.clusterArn = resource.clusterArn; + this.clusterEndpoint = resource.clusterEndpoint; + this.clusterCertificateAuthorityData = resource.clusterCertificateAuthorityData; + + new cdk.Output(this, 'ClusterName', { value: this.clusterName, disableExport: true }); + } + + /** + * Add nodes to this EKS cluster + * + * The nodes will automatically be configured with the right VPC and AMI + * for the instance type and Kubernetes version. + */ + public addCapacity(id: string, options: AddWorkerNodesOptions): autoscaling.AutoScalingGroup { + const asg = new autoscaling.AutoScalingGroup(this, id, { + ...options, + vpc: this.vpc, + machineImage: new EksOptimizedAmi({ + nodeType: nodeTypeForInstanceType(options.instanceType), + kubernetesVersion: this.version, + }), + updateType: options.updateType || autoscaling.UpdateType.RollingUpdate, + instanceType: options.instanceType, + }); + + this.addAutoScalingGroup(asg, { + maxPods: maxPodsForInstanceType(options.instanceType), + }); + + return asg; + } + + /** + * Add compute capacity to this EKS cluster in the form of an AutoScalingGroup + * + * The AutoScalingGroup must be running an EKS-optimized AMI containing the + * /etc/eks/bootstrap.sh script. This method will configure Security Groups, + * add the right policies to the instance role, apply the right tags, and add + * the required user data to the instance's launch configuration. + * + * Prefer to use `addCapacity` if possible, it will automatically configure + * the right AMI and the `maxPods` number based on your instance type. + * + * @see https://docs.aws.amazon.com/eks/latest/userguide/launch-workers.html + */ + public addAutoScalingGroup(autoScalingGroup: autoscaling.AutoScalingGroup, options: AddAutoScalingGroupOptions) { + // self rules + autoScalingGroup.connections.allowInternally(new ec2.AllTraffic()); + + // Cluster to:nodes rules + autoScalingGroup.connections.allowFrom(this, new ec2.TcpPort(443)); + autoScalingGroup.connections.allowFrom(this, new ec2.TcpPortRange(1025, 65535)); + + // Allow HTTPS from Nodes to Cluster + autoScalingGroup.connections.allowTo(this, new ec2.TcpPort(443)); + + // Allow all node outbound traffic + autoScalingGroup.connections.allowToAnyIPv4(new ec2.TcpAllPorts()); + autoScalingGroup.connections.allowToAnyIPv4(new ec2.UdpAllPorts()); + autoScalingGroup.connections.allowToAnyIPv4(new ec2.IcmpAllTypesAndCodes()); + + autoScalingGroup.addUserData( + 'set -o xtrace', + `/etc/eks/bootstrap.sh ${this.clusterName} --use-max-pods ${options.maxPods}`, + ); + // FIXME: Add a cfn-signal call once we've sorted out UserData and can write reliable + // signaling scripts: https://github.com/awslabs/aws-cdk/issues/623 + + autoScalingGroup.role.attachManagedPolicy(new iam.AwsManagedPolicy('AmazonEKSWorkerNodePolicy', this).policyArn); + autoScalingGroup.role.attachManagedPolicy(new iam.AwsManagedPolicy('AmazonEKS_CNI_Policy', this).policyArn); + autoScalingGroup.role.attachManagedPolicy(new iam.AwsManagedPolicy('AmazonEC2ContainerRegistryReadOnly', this).policyArn); + + // EKS Required Tags + autoScalingGroup.apply(new cdk.Tag(`kubernetes.io/cluster/${this.clusterName}`, 'owned', { applyToLaunchedInstances: true })); + + // Create an Output for the Instance Role ARN (need to paste it into aws-auth-cm.yaml) + new cdk.Output(autoScalingGroup, 'InstanceRoleARN', { + disableExport: true, + value: autoScalingGroup.role.roleArn + }); + } + + /** + * Opportunistically tag subnets with the required tags. + * + * If no subnets could be found (because this is an imported VPC), add a warning. + * + * @see https://docs.aws.amazon.com/eks/latest/userguide/network_reqs.html + */ + private tagSubnets() { + const privates = this.vpc.subnets({ subnetsToUse: ec2.SubnetType.Private }); + + for (const subnet of privates) { + if (!isRealSubnetConstruct(subnet)) { + // Just give up, all of them will be the same. + this.node.addWarning('Could not auto-tag private subnets with "kubernetes.io/role/internal-elb=1", please remember to do this manually'); + return; + } + + subnet.apply(new cdk.Tag("kubernetes.io/role/internal-elb", "1")); + } + } +} + +function isRealSubnetConstruct(subnet: ec2.IVpcSubnet): subnet is ec2.VpcSubnet { + return (subnet as any).addDefaultRouteToIGW !== undefined; +} + +/** + * Options for adding worker nodes + */ +export interface AddWorkerNodesOptions extends autoscaling.CommonAutoScalingGroupProps { + /** + * Instance type of the instances to start + */ + instanceType: ec2.InstanceType; +} + +/** + * Options for adding an AutoScalingGroup as capacity + */ +export interface AddAutoScalingGroupOptions { + /** + * How many pods to allow on this instance. + * + * Should be at most equal to the maximum number of IP addresses available to + * the instance type less one. + */ + maxPods: number; +} + +/** + * Import a cluster to use in another stack + */ +class ImportedCluster extends ClusterBase { + public readonly vpc: ec2.IVpcNetwork; + public readonly clusterCertificateAuthorityData: string; + public readonly clusterName: string; + public readonly clusterArn: string; + public readonly clusterEndpoint: string; + public readonly connections = new ec2.Connections(); + + constructor(scope: cdk.Construct, id: string, props: ClusterImportProps) { + super(scope, id); + + this.vpc = ec2.VpcNetwork.import(this, "VPC", props.vpc); + this.clusterName = props.clusterName; + this.clusterEndpoint = props.clusterEndpoint; + this.clusterArn = props.clusterArn; + this.clusterCertificateAuthorityData = props.clusterCertificateAuthorityData; + + let i = 1; + for (const sgProps of props.securityGroups) { + this.connections.addSecurityGroup(ec2.SecurityGroup.import(this, `SecurityGroup${i}`, sgProps)); + i++; + } + } +} + +function flatMap(xs: T[], f: (x: T) => U[]): U[] { + const ret = new Array(); + for (const x of xs) { + ret.push(...f(x)); + } + return ret; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/lib/index.ts b/packages/@aws-cdk/aws-eks/lib/index.ts index 815ecf6ab62fc..07f3f07aa8fff 100644 --- a/packages/@aws-cdk/aws-eks/lib/index.ts +++ b/packages/@aws-cdk/aws-eks/lib/index.ts @@ -1,2 +1,6 @@ +export * from "./cluster-base"; +export * from "./cluster"; +export * from "./ami"; + // AWS::EKS CloudFormation Resources: -export * from './eks.generated'; +export * from "./eks.generated"; diff --git a/packages/@aws-cdk/aws-eks/lib/instance-data.ts b/packages/@aws-cdk/aws-eks/lib/instance-data.ts new file mode 100644 index 0000000000000..137bcbdd6cab0 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/lib/instance-data.ts @@ -0,0 +1,74 @@ +import ec2 = require('@aws-cdk/aws-ec2'); + +/** + * Used internally to bootstrap the worker nodes + * This sets the max pods based on the instanceType created + * ref: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html#AvailableIpPerENI + */ +const MAX_PODS = Object.freeze( + new Map([ + ['c4.large', 29], + ['c4.xlarge', 58], + ['c4.2xlarge', 58], + ['c4.4xlarge', 234], + ['c4.8xlarge', 234], + ['c5.large', 29], + ['c5.xlarge', 58], + ['c5.2xlarge', 58], + ['c5.4xlarge', 234], + ['c5.9xlarge', 234], + ['c5.18xlarge', 737], + ['i3.large', 29], + ['i3.xlarge', 58], + ['i3.2xlarge', 58], + ['i3.4xlarge', 234], + ['i3.8xlarge', 234], + ['i3.16xlarge', 737], + ['m3.medium', 12], + ['m3.large', 29], + ['m3.xlarge', 58], + ['m3.2xlarge', 118], + ['m4.large', 20], + ['m4.xlarge', 58], + ['m4.2xlarge', 58], + ['m4.4xlarge', 234], + ['m4.10xlarge', 234], + ['m5.large', 29], + ['m5.xlarge', 58], + ['m5.2xlarge', 58], + ['m5.4xlarge', 234], + ['m5.12xlarge', 234], + ['m5.24xlarge', 737], + ['p2.xlarge', 58], + ['p2.8xlarge', 234], + ['p2.16xlarge', 234], + ['p3.2xlarge', 58], + ['p3.8xlarge', 234], + ['p3.16xlarge', 234], + ['r3.xlarge', 58], + ['r3.2xlarge', 58], + ['r3.4xlarge', 234], + ['r3.8xlarge', 234], + ['r4.large', 29], + ['r4.xlarge', 58], + ['r4.2xlarge', 58], + ['r4.4xlarge', 234], + ['r4.8xlarge', 234], + ['r4.16xlarge', 735], + ['t2.small', 8], + ['t2.medium', 17], + ['t2.large', 35], + ['t2.xlarge', 44], + ['t2.2xlarge', 44], + ['x1.16xlarge', 234], + ['x1.32xlarge', 234], + ]), +); + +export function maxPodsForInstanceType(instanceType: ec2.InstanceType) { + const num = MAX_PODS.get(instanceType.toString()); + if (num === undefined) { + throw new Error(`Instance type not supported for EKS: ${instanceType.toString()}. Please pick a different instance type.`); + } + return num; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index aa5444bfa6e6e..2daa69fdbfcd0 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -56,14 +56,21 @@ "devDependencies": { "@aws-cdk/assert": "^0.24.1", "cdk-build-tools": "^0.24.1", + "cdk-integ-tools": "^0.24.1", "cfn2ts": "^0.24.1", "pkglint": "^0.24.1" }, "dependencies": { + "@aws-cdk/aws-ec2": "^0.24.1", + "@aws-cdk/aws-iam": "^0.24.1", + "@aws-cdk/aws-autoscaling": "^0.24.1", "@aws-cdk/cdk": "^0.24.1" }, "homepage": "https://github.com/awslabs/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-ec2": "^0.24.1", + "@aws-cdk/aws-iam": "^0.24.1", + "@aws-cdk/aws-autoscaling": "^0.24.1", "@aws-cdk/cdk": "^0.24.1" }, "engines": { diff --git a/packages/@aws-cdk/aws-eks/test/MANUAL_TEST.md b/packages/@aws-cdk/aws-eks/test/MANUAL_TEST.md new file mode 100644 index 0000000000000..463e4c5f2a0b3 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/test/MANUAL_TEST.md @@ -0,0 +1,58 @@ +# Manual verification + +Following https://docs.aws.amazon.com/eks/latest/userguide/getting-started.html + +After starting the cluster and installing `kubectl` and `aws-iam-authenticator`: + +``` +apiVersion: v1 +kind: ConfigMap +metadata: + name: aws-auth + namespace: kube-system +data: + mapRoles: | + - rolearn: + username: system:node:{{EC2PrivateDNSName}} + groups: + - system:bootstrappers + - system:nodes +``` + +``` +aws eks update-kubeconfig --name {{ClusterName}} + +# File above, with substitutions +kubectl apply -f aws-auth-cm.yaml + +# Check that nodes joined (may take a while) +kubectl get nodes + +# Start services (will autocreate a load balancer) +kubectl apply -f https://raw.githubusercontent.com/kubernetes/examples/master/guestbook-go/redis-master-controller.json +kubectl apply -f https://raw.githubusercontent.com/kubernetes/examples/master/guestbook-go/redis-master-service.json +kubectl apply -f https://raw.githubusercontent.com/kubernetes/examples/master/guestbook-go/redis-slave-controller.json +kubectl apply -f https://raw.githubusercontent.com/kubernetes/examples/master/guestbook-go/redis-slave-service.json +kubectl apply -f https://raw.githubusercontent.com/kubernetes/examples/master/guestbook-go/guestbook-controller.json +kubectl apply -f https://raw.githubusercontent.com/kubernetes/examples/master/guestbook-go/guestbook-service.json + +# Check up on service status +kubectl get services -o wide +``` + +Visit the website that appears under LoadBalancer on port 3000. The Amazon corporate network will block this +port, in which case you add this: + +``` +ssh -L 3000::3000 ssh-box-somewhere.example.com + +# Visit http://localhost:3000/ +``` + +Clean the services before you stop the cluster to get rid of the load balancer +(otherwise you won't be able to delet the stack): + +``` +kubectl delete --all services + +``` diff --git a/packages/@aws-cdk/aws-eks/test/example.ssh-into-nodes.lit.ts b/packages/@aws-cdk/aws-eks/test/example.ssh-into-nodes.lit.ts new file mode 100644 index 0000000000000..0784d1aca03f5 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/test/example.ssh-into-nodes.lit.ts @@ -0,0 +1,32 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import eks = require('../lib'); + +class EksClusterStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const vpc = new ec2.VpcNetwork(this, 'VPC'); + + const cluster = new eks.Cluster(this, 'EKSCluster', { + vpc + }); + + /// !show + const asg = cluster.addCapacity('Nodes', { + instanceType: new ec2.InstanceType('t2.medium'), + vpcPlacement: { subnetsToUse: ec2.SubnetType.Public }, + keyName: 'my-key-name', + }); + + // Replace with desired IP + asg.connections.allowFrom(new ec2.CidrIPv4('1.2.3.4/32'), new ec2.TcpPort(22)); + /// !hide + } +} + +const app = new cdk.App(); + +new EksClusterStack(app, 'eks-integ-test'); + +app.run(); diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.lit.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.lit.expected.json new file mode 100644 index 0000000000000..68c3ef23cd3d0 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.lit.expected.json @@ -0,0 +1,919 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/VPC/PublicSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableAssociation0B0896DC": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/VPC/PublicSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTableAssociation5A808732": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2DefaultRouteB7481BBA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet2EIP4947BC00": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet3Subnet631C5E25": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/VPC/PublicSubnet3" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + } + ] + } + }, + "VPCPublicSubnet3RouteTable98AE0E14": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3RouteTableAssociation427FE0C6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + } + } + }, + "VPCPublicSubnet3DefaultRouteA0D29D46": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet3EIPAD4BC883": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc" + } + }, + "VPCPublicSubnet3NATGatewayD3048F5C": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet3EIPAD4BC883", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + }, + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/VPC/PrivateSubnet1" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/VPC/PrivateSubnet1" + }, + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableAssociation347902D1": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/VPC/PrivateSubnet2" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/VPC/PrivateSubnet2" + }, + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + } + ] + } + }, + "VPCPrivateSubnet2RouteTableAssociation0C73D413": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "VPCPrivateSubnet3Subnet3EDCD457": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/VPC/PrivateSubnet3" + }, + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + } + ] + } + }, + "VPCPrivateSubnet3RouteTable192186F8": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/VPC/PrivateSubnet3" + }, + { + "Key": "kubernetes.io/role/internal-elb", + "Value": "1" + } + ] + } + }, + "VPCPrivateSubnet3RouteTableAssociationC28D144E": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + } + }, + "VPCPrivateSubnet3DefaultRoute27F311AE": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet3NATGatewayD3048F5C" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "EKSClusterClusterRoleB72F3251": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "eks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKSClusterPolicy" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKSServicePolicy" + ] + ] + } + ] + } + }, + "EKSClusterControlPlaneSecurityGroup580AD1FE": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "EKS Control Plane Security Group", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "EKSClusterControlPlaneSecurityGroupfromeksintegtestEKSClusterNodesInstanceSecurityGroup1F94DB4244376AEF332": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "from eksintegtestEKSClusterNodesInstanceSecurityGroup1F94DB42:443", + "FromPort": 443, + "GroupId": { + "Fn::GetAtt": [ + "EKSClusterControlPlaneSecurityGroup580AD1FE", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "EKSClusterNodesInstanceSecurityGroup460A275E", + "GroupId" + ] + }, + "ToPort": 443 + } + }, + "EKSClusterBA6ECF8F": { + "Type": "AWS::EKS::Cluster", + "Properties": { + "ResourcesVpcConfig": { + "SecurityGroupIds": [ + { + "Fn::GetAtt": [ + "EKSClusterControlPlaneSecurityGroup580AD1FE", + "GroupId" + ] + } + ], + "SubnetIds": [ + { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + }, + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + }, + { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "EKSClusterClusterRoleB72F3251", + "Arn" + ] + } + } + }, + "EKSClusterNodesInstanceSecurityGroup460A275E": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "eks-integ-test/EKSCluster/Nodes/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "SecurityGroupIngress": [], + "Tags": [ + { + "Key": "Name", + "Value": "eks-integ-test/EKSCluster/Nodes" + }, + { + "Key": { + "Fn::Join": [ + "", + [ + "kubernetes.io/cluster/", + { + "Ref": "EKSClusterBA6ECF8F" + } + ] + ] + }, + "Value": "owned" + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "EKSClusterNodesInstanceSecurityGroupfromeksintegtestEKSClusterNodesInstanceSecurityGroup1F94DB42ALLTRAFFIC8DF6EC00": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "-1", + "Description": "from eksintegtestEKSClusterNodesInstanceSecurityGroup1F94DB42:ALL TRAFFIC", + "GroupId": { + "Fn::GetAtt": [ + "EKSClusterNodesInstanceSecurityGroup460A275E", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "EKSClusterNodesInstanceSecurityGroup460A275E", + "GroupId" + ] + } + } + }, + "EKSClusterNodesInstanceSecurityGroupfromeksintegtestEKSClusterControlPlaneSecurityGroup99328DC644383C2D9E9": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "from eksintegtestEKSClusterControlPlaneSecurityGroup99328DC6:443", + "FromPort": 443, + "GroupId": { + "Fn::GetAtt": [ + "EKSClusterNodesInstanceSecurityGroup460A275E", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "EKSClusterControlPlaneSecurityGroup580AD1FE", + "GroupId" + ] + }, + "ToPort": 443 + } + }, + "EKSClusterNodesInstanceSecurityGroupfromeksintegtestEKSClusterControlPlaneSecurityGroup99328DC61025655350D985847": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "from eksintegtestEKSClusterControlPlaneSecurityGroup99328DC6:1025-65535", + "FromPort": 1025, + "GroupId": { + "Fn::GetAtt": [ + "EKSClusterNodesInstanceSecurityGroup460A275E", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "EKSClusterControlPlaneSecurityGroup580AD1FE", + "GroupId" + ] + }, + "ToPort": 65535 + } + }, + "EKSClusterNodesInstanceRoleEE5595D6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKSWorkerNodePolicy" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKS_CNI_Policy" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + ] + ] + } + ] + } + }, + "EKSClusterNodesInstanceProfile0F2DB3B9": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "EKSClusterNodesInstanceRoleEE5595D6" + } + ] + } + }, + "EKSClusterNodesLaunchConfig921F1106": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": "ami-12345", + "InstanceType": "t2.medium", + "IamInstanceProfile": { + "Ref": "EKSClusterNodesInstanceProfile0F2DB3B9" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "EKSClusterNodesInstanceSecurityGroup460A275E", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ", + { + "Ref": "EKSClusterBA6ECF8F" + }, + " --use-max-pods 17" + ] + ] + } + } + }, + "DependsOn": [ + "EKSClusterNodesInstanceRoleEE5595D6" + ] + }, + "EKSClusterNodesASGC2597E34": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "1", + "MinSize": "1", + "DesiredCapacity": "1", + "LaunchConfigurationName": { + "Ref": "EKSClusterNodesLaunchConfig921F1106" + }, + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "eks-integ-test/EKSCluster/Nodes" + }, + { + "Key": { + "Fn::Join": [ + "", + [ + "kubernetes.io/cluster/", + { + "Ref": "EKSClusterBA6ECF8F" + } + ] + ] + }, + "PropagateAtLaunch": true, + "Value": "owned" + } + ], + "VPCZoneIdentifier": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + }, + { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + ] + }, + "UpdatePolicy": { + "AutoScalingRollingUpdate": { + "WaitOnResourceSignals": false, + "PauseTime": "PT0S", + "SuspendProcesses": [ + "HealthCheck", + "ReplaceUnhealthy", + "AZRebalance", + "AlarmNotification", + "ScheduledActions" + ] + }, + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + } + }, + "Outputs": { + "EKSClusterClusterName2B056109": { + "Value": { + "Ref": "EKSClusterBA6ECF8F" + } + }, + "EKSClusterNodesInstanceRoleARN10992C84": { + "Value": { + "Fn::GetAtt": [ + "EKSClusterNodesInstanceRoleEE5595D6", + "Arn" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.lit.ts b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.lit.ts new file mode 100644 index 0000000000000..611bab4f7cce3 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.lit.ts @@ -0,0 +1,28 @@ +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import eks = require('../lib'); + +class EksClusterStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + /// !show + const vpc = new ec2.VpcNetwork(this, 'VPC'); + + const cluster = new eks.Cluster(this, 'EKSCluster', { + vpc + }); + + cluster.addCapacity('Nodes', { + instanceType: new ec2.InstanceType('t2.medium'), + desiredCapacity: 1, // Raise this number to add more nodes + }); + /// !hide + } +} + +const app = new cdk.App(); + +new EksClusterStack(app, 'eks-integ-test'); + +app.run(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts new file mode 100644 index 0000000000000..38f0d8924f547 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -0,0 +1,133 @@ +import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import ec2 = require('@aws-cdk/aws-ec2'); +import cdk = require('@aws-cdk/cdk'); +import { Test } from 'nodeunit'; +import eks = require('../lib'); + +export = { + 'a default cluster spans all subnets'(test: Test) { + // GIVEN + const [stack, vpc] = testFixture(); + + // WHEN + new eks.Cluster(stack, 'Cluster', { vpc }); + + // THEN + expect(stack).to(haveResourceLike('AWS::EKS::Cluster', { + ResourcesVpcConfig: { + SubnetIds: [ + { Ref: "VPCPublicSubnet1SubnetB4246D30" }, + { Ref: "VPCPublicSubnet2Subnet74179F39" }, + { Ref: "VPCPublicSubnet3Subnet631C5E25" }, + { Ref: "VPCPrivateSubnet1Subnet8BCA10E0" }, + { Ref: "VPCPrivateSubnet2SubnetCFCDAA7A" }, + { Ref: "VPCPrivateSubnet3Subnet3EDCD457" } + ] + } + })); + + test.done(); + }, + + 'creating a cluster tags the private VPC subnets'(test: Test) { + // GIVEN + const [stack, vpc] = testFixture(); + + // WHEN + new eks.Cluster(stack, 'Cluster', { vpc }); + + // THEN + expect(stack).to(haveResource('AWS::EC2::Subnet', { + Tags: [ + { Key: "Name", Value: "VPC/PrivateSubnet1" }, + { Key: "aws-cdk:subnet-name", Value: "Private" }, + { Key: "aws-cdk:subnet-type", Value: "Private" }, + { Key: "kubernetes.io/role/internal-elb", Value: "1" } + ] + })); + + test.done(); + }, + + 'adding capacity creates an ASG with tags'(test: Test) { + // GIVEN + const [stack, vpc] = testFixture(); + const cluster = new eks.Cluster(stack, 'Cluster', { vpc }); + + // WHEN + cluster.addCapacity('Default', { + instanceType: new ec2.InstanceType('t2.medium'), + }); + + // THEN + expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + Tags: [ + { + Key: "Name", + PropagateAtLaunch: true, + Value: "Cluster/Default" + }, + { + Key: { "Fn::Join": [ "", [ "kubernetes.io/cluster/", { Ref: "ClusterEB0386A7" } ] ] }, + PropagateAtLaunch: true, + Value: "owned" + } + ] + })); + + test.done(); + }, + + 'adding capacity correctly deduces maxPods and adds userdata'(test: Test) { + // GIVEN + const [stack, vpc] = testFixture(); + const cluster = new eks.Cluster(stack, 'Cluster', { vpc }); + + // WHEN + cluster.addCapacity('Default', { + instanceType: new ec2.InstanceType('t2.medium'), + }); + + // THEN + expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + UserData: { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ", + { Ref: "ClusterEB0386A7" }, + " --use-max-pods 17" + ] + ] + } + } + })); + + test.done(); + }, + + 'exercise export/import'(test: Test) { + // GIVEN + const [stack1, vpc] = testFixture(); + const stack2 = new cdk.Stack(); + const cluster = new eks.Cluster(stack1, 'Cluster', { vpc }); + + // WHEN + const imported = eks.Cluster.import(stack2, 'Imported', cluster.export()); + + // THEN + test.deepEqual(stack2.node.resolve(imported.clusterArn), { + 'Fn::ImportValue': 'Stack:ClusterClusterArn00DCA0E0' + }); + + test.done(); + }, +}; + +function testFixture(): [cdk.Stack, ec2.VpcNetwork] { + const stack = new cdk.Stack(undefined, 'Stack', { env: { region: 'us-east-1' }}); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + + return [stack, vpc]; +} diff --git a/packages/@aws-cdk/aws-eks/test/test.eks.ts b/packages/@aws-cdk/aws-eks/test/test.eks.ts deleted file mode 100644 index 820f6b467f38f..0000000000000 --- a/packages/@aws-cdk/aws-eks/test/test.eks.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Test, testCase } from 'nodeunit'; - -export = testCase({ - notTested(test: Test) { - test.ok(true, 'No tests are specified for this package.'); - test.done(); - } -});