From 22fc8b9bbde09660accec7f7ac175e5a4ff0b0ec Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 8 Feb 2019 18:51:00 +0100 Subject: [PATCH] feat(aws-eks): add construct library for EKS (#1655) Construct library to set up an EKS cluster and add nodes to it. Generalizes adding AutoScalingGroup capacity and make it the same between both ECS and EKS clusters. Fixes naming inconsistencies among properties of `AutoScalingGroup`, and between EC2 AutoScaling and Application AutoScaling. This PR takes #991 but reimplements the API in the style of the ECS library. BREAKING CHANGE: For `AutoScalingGroup`, renamed `minSize` => `minCapacity`, `maxSize` => `maxCapacity`, for consistency with `desiredCapacity` and also Application AutoScaling. For ECS's `addDefaultAutoScalingGroupCapacity()`, `instanceCount` => `desiredCapacity` and the function now takes an ID (pass `"DefaultAutoScalingGroup"` to avoid interruption to your deployments). --- .../aws-autoscaling/lib/auto-scaling-group.ts | 62 +- .../test/test.auto-scaling-group.ts | 22 +- .../@aws-cdk/aws-ec2/lib/machine-image.ts | 2 +- packages/@aws-cdk/aws-ecs/README.md | 2 +- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 36 +- .../aws-ecs/test/ec2/integ.event-task.lit.ts | 2 +- .../aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts | 2 +- .../aws-ecs/test/ec2/integ.lb-bridge-nw.ts | 4 +- .../test/ec2/test.ec2-event-rule-target.ts | 4 +- .../aws-ecs/test/ec2/test.ec2-service.ts | 32 +- .../@aws-cdk/aws-ecs/test/test.ecs-cluster.ts | 10 +- packages/@aws-cdk/aws-ecs/test/test.l3s.ts | 2 +- packages/@aws-cdk/aws-eks/README.md | 34 +- packages/@aws-cdk/aws-eks/lib/ami.ts | 124 +++ packages/@aws-cdk/aws-eks/lib/cluster-base.ts | 98 ++ packages/@aws-cdk/aws-eks/lib/cluster.ts | 342 +++++++ packages/@aws-cdk/aws-eks/lib/index.ts | 6 +- .../@aws-cdk/aws-eks/lib/instance-data.ts | 74 ++ packages/@aws-cdk/aws-eks/package.json | 7 + packages/@aws-cdk/aws-eks/test/MANUAL_TEST.md | 58 ++ .../test/example.ssh-into-nodes.lit.ts | 32 + .../test/integ.eks-cluster.lit.expected.json | 919 ++++++++++++++++++ .../aws-eks/test/integ.eks-cluster.lit.ts | 28 + .../@aws-cdk/aws-eks/test/test.cluster.ts | 133 +++ packages/@aws-cdk/aws-eks/test/test.eks.ts | 8 - 25 files changed, 1934 insertions(+), 109 deletions(-) create mode 100644 packages/@aws-cdk/aws-eks/lib/ami.ts create mode 100644 packages/@aws-cdk/aws-eks/lib/cluster-base.ts create mode 100644 packages/@aws-cdk/aws-eks/lib/cluster.ts create mode 100644 packages/@aws-cdk/aws-eks/lib/instance-data.ts create mode 100644 packages/@aws-cdk/aws-eks/test/MANUAL_TEST.md create mode 100644 packages/@aws-cdk/aws-eks/test/example.ssh-into-nodes.lit.ts create mode 100644 packages/@aws-cdk/aws-eks/test/integ.eks-cluster.lit.expected.json create mode 100644 packages/@aws-cdk/aws-eks/test/integ.eks-cluster.lit.ts create mode 100644 packages/@aws-cdk/aws-eks/test/test.cluster.ts delete mode 100644 packages/@aws-cdk/aws-eks/test/test.eks.ts 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(); - } -});