From f1d7c8661c8f8aa1c717efae998ac6e7b6f958cd Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 31 Jan 2019 15:30:25 +0100 Subject: [PATCH 01/10] Work from the branch Original work done by @IPyandy --- .../eks-cluster/index.ts | 99 +++ examples/cdk-examples-typescript/package.json | 1 + packages/@aws-cdk/aws-eks/README.md | 97 ++- packages/@aws-cdk/aws-eks/lib/cluster.ts | 459 +++++++++++++ packages/@aws-cdk/aws-eks/lib/index.ts | 5 +- .../@aws-cdk/aws-eks/lib/instance-data.ts | 112 ++++ packages/@aws-cdk/aws-eks/package.json | 6 +- packages/@aws-cdk/aws-eks/test/test.eks.ts | 620 +++++++++++++++++- 8 files changed, 1389 insertions(+), 10 deletions(-) create mode 100644 examples/cdk-examples-typescript/eks-cluster/index.ts create mode 100644 packages/@aws-cdk/aws-eks/lib/cluster.ts create mode 100644 packages/@aws-cdk/aws-eks/lib/instance-data.ts diff --git a/examples/cdk-examples-typescript/eks-cluster/index.ts b/examples/cdk-examples-typescript/eks-cluster/index.ts new file mode 100644 index 0000000000000..4621cc684fa85 --- /dev/null +++ b/examples/cdk-examples-typescript/eks-cluster/index.ts @@ -0,0 +1,99 @@ +import ec2 = require("@aws-cdk/aws-ec2"); +import eks = require("@aws-cdk/aws-eks"); +import cdk = require("@aws-cdk/cdk"); + +const ENV = "dev"; +const app = new cdk.App(); + +/** + * Ths stack creates the VPC and network for the cluster + * + * @default single public subnet per availability zone (3) + * This creates three (3) total subnets with an Internet Gateway + * The subnets could be private with a Nat Gateway + * they must not be isolated, as instances later need to + * have outbound internet access to contact the API Server + */ +const networkStack = new cdk.Stack(app, "Network"); + +const vpc = new ec2.VpcNetwork(networkStack, "VPC", { + cidr: "10.244.0.0/16", + maxAZs: 3, + natGateways: 0, + subnetConfiguration: [ + { + name: "pub", + cidrMask: 24, + subnetType: ec2.SubnetType.Public + } + ], + tags: { + env: `${ENV}` + } +}); +const vpcExport = vpc.export(); + +/** + * This stack creates the EKS Cluster with the imported VPC + * above, and puts the cluster inside the chosen placement + * + * clusterName can be set (not recommended), let cfn generate + * version can be specified, only 1.10 supported now + * will become useful when more versions are supported + * + * It also creates a group of 3 worker nodes with default types + * and given min, max and sshKeys + */ +const clusterStack = new cdk.Stack(app, "Cluster"); + +const clusterVpc = ec2.VpcNetworkRef.import( + clusterStack, + "ClusterVpc", + vpcExport +); +const cluster = new eks.Cluster(clusterStack, "Cluster", { + vpc: clusterVpc, + vpcPlacement: { + subnetsToUse: ec2.SubnetType.Public + } +}); + +/** + * This is optional and should be more specific to given + * corparate CIDRS for access from the outside, maybe + * even a bastion host inside AWS. + */ +cluster.connections.allowFromAnyIPv4(new ec2.TcpPort(443)); + +const grp1 = new eks.Nodes(clusterStack, "NodeGroup1", { + vpc: clusterVpc, + cluster, + minNodes: 3, + maxNodes: 6, + sshKeyName: "aws-dev-key" +}); +grp1.nodeGroup.connections.allowFromAnyIPv4(new ec2.TcpPort(22)); + +/** + * This adds a second group of worker nodes of different + * InstanceClass and InstanceSize + * This gets pushed into an Array of Nodes + */ +const grp2 = new eks.Nodes(clusterStack, "NodeGroup2", { + vpc: clusterVpc, + cluster, + nodeClass: ec2.InstanceClass.T2, + nodeSize: ec2.InstanceSize.Medium, + nodeType: eks.NodeType.Normal, + minNodes: 2, + maxNodes: 4, + sshKeyName: "aws-dev-key" +}); +/** + * This is optional and should be more specific to given + * corparate CIDRS for access from the outside, maybe + * even a bastion host inside AWS. + */ +grp2.nodeGroup.connections.allowFromAnyIPv4(new ec2.TcpPort(22)); + +app.run(); diff --git a/examples/cdk-examples-typescript/package.json b/examples/cdk-examples-typescript/package.json index 358e5badf0621..450ab04648e60 100644 --- a/examples/cdk-examples-typescript/package.json +++ b/examples/cdk-examples-typescript/package.json @@ -29,6 +29,7 @@ "@aws-cdk/aws-dynamodb": "^0.22.0", "@aws-cdk/aws-ec2": "^0.22.0", "@aws-cdk/aws-ecs": "^0.22.0", + "@aws-cdk/aws-eks": "^0.22.0", "@aws-cdk/aws-elasticloadbalancing": "^0.22.0", "@aws-cdk/aws-elasticloadbalancingv2": "^0.22.0", "@aws-cdk/aws-iam": "^0.22.0", diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index 6bf0898a242df..4b30df1033402 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -1,2 +1,95 @@ -## 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. + +### Define a VPC Network Stack + +The cluster must be placed inside of an `AWS VPC`. + +```ts +import ec2 = require('@aws-cdk/aws-ec2'); +import eks = require('@aws-cdk/aws-eks'); +import cdk = require('@aws-cdk/cdk'); + +const app = new cdk.App(); + +const networkStack = new cdk.Stack(app, 'Network'); + +const vpc = new ec2.VpcNetwork(networkStack, 'VPC'); +``` + +### Import the Network Stack + +This can be done within a single stack and no need to import, but it's best to separate functions as much as possible. + +```ts +const clusterStack = new cdk.Stack(app, 'Cluster'); + +const clusterVpc = ec2.VpcNetworkRef.import(clusterStack, 'ClusterVpc', vpc.export()); +const cluster = new eks.Cluster(clusterStack, 'Cluster', { + vpc: clusterVpc, + vpcPlacement: { + subnetsToUse: ec2.SubnetType.Public, + }, +}); +``` + +This creates the `EKS Control Plane` but no `Worker Nodes` will be created at this step. + +#### Connections + +The `Cluster` class implements the `IConnectable` interface to easily manage security group rules. + +Allowing access to the API server with `kubectl` one would add `cluster.connections.allowFromAnyIPv4(new ec2.TcpPort(443));`. Though for best practices it's best to use the `connections.allowFrom` and allow from a specific connectable peer. Such as a `bastion` host's security group. + +### Create the Worker Nodes + +Here we create a set of worker nodes to run our container workloads on. + +```ts +const grp1 = new eks.Nodes(clusterStack, 'NodeGroup1', { + vpc: clusterVpc, + cluster, + minNodes: 3, + maxNodes: 6, + sshKeyName: 'aws-dev-key', +}); +``` + +This example creates nodes between `minNodes` value and `maxNodes` value with the default parameters for node types (`M5` and `Large`) as that serves decently for most situations in dev and sometimes prod environments. + +The creation of also initializes the security groups with the [Amazon Recommended](https://docs.aws.amazon.com/eks/latest/userguide/sec-group-reqs.html) settings for `cluster:to:nodes and nodes:to:cluster` communication. + +By default even if the "sshKeyName" is provided, not security groups are modified to allow access. You'll need to add the rules using the connections specifier. + +- Allow SSH from any IPv4 `grp1.nodeGroup.connections.allowFromAnyIPv4(new ec2.TcpPort(22));` + +Recommended method (be more explicit) + +Allow only from specified IPv4 (can also be another security group) see [connections documentation](https://awslabs.github.io/aws-cdk/refs/_aws-cdk_aws-ec2.html#connections) `cluster.connections.allowFrom(new ec2.CidrIPv4("X.X.X.X/32"), new ec2.TcpPort(443))` + +### Add More Worker Nodes + +You can also add more worker nodes to the Stack as you see fit. Maybe different sizes? or different class? Or maybe separate worker nodes from production, testing and dev?. + +```ts +const grp2 = new eks.Nodes(clusterStack, 'NodeGroup2', { + vpc: clusterVpc, + cluster, + nodeClass: ec2.InstanceClass.T2, + nodeSize: ec2.InstanceSize.Medium, + nodeType: eks.NodeType.Normal, + minNodes: 2, + maxNodes: 4, + sshKeyName: 'aws-dev-key', +}); +``` + +There's an `Array` of autoscaling groups that tracks the creation of nodes. + +### Roadmap + +- [ ] Add ability to create worker nodes by passing a IClusterProp with creation details (faster for testing) + - More dangerous for production +- [ ] Make worker nodes `connections` implementation details native, so that no need to access through the `autoscaling` group `nodeGroup` public field +- [ ] Make able to create nodes in separate stack 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..0567e61f77f81 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -0,0 +1,459 @@ +import asg = 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 { cloudformation } from './eks.generated'; +import { maxPods, nodeAmi, NodeType } from './instance-data'; + +/** + * Reference properties used when a cluster is exported or imported + * with the const a = ClusterRef.import | export methodes + */ +export interface IClusterRefProps { + /** + * The physical name of the Cluster + */ + clusterName: string; + /** + * The unique ARN assigned to the service by AWS + * in the form of arn:aws:eks: + */ + clusterArn: string; + /** + * The API Server endpoint URL + */ + clusterEndpoint: string; + /** + * Reeference the vpc placement for placing nodes into ASG subnets + */ + vpcPlacement: ec2.VpcPlacementStrategy; + /** + * The security group ID used by the cluster for it's rules + */ + securityGroupId: string; + /** + * The IConnectable interface implementation for updating + * security group rules + */ + connections: ec2.Connections; +} + +/** + * A SecurityGroup Reference, object not created with this template. + */ +export abstract class ClusterRef extends cdk.Construct implements ec2.IConnectable { + /** + * Import an existing cluster + * + * @param parent the construct parent, 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(parent: cdk.Construct, id: string, props: IClusterRefProps): ClusterRef { + return new ImportedCluster(parent, id, props); + } + + public abstract readonly clusterName: string; + public abstract readonly clusterArn: string; + public abstract readonly clusterEndpoint: string; + public abstract readonly vpcPlacement: ec2.VpcPlacementStrategy; + public abstract readonly securityGroupId: string; + public abstract readonly connections: ec2.Connections; + + /** + * Export cluster references to use in other stacks + */ + public export(): IClusterRefProps { + return { + clusterName: this.makeOutput('ClusterName', this.clusterName), + clusterArn: this.makeOutput('ClusterArn', this.clusterArn), + clusterEndpoint: this.makeOutput('ClusterEndpoint', this.clusterEndpoint), + vpcPlacement: this.vpcPlacement, + securityGroupId: this.securityGroupId, + connections: this.connections, + }; + } + + private makeOutput(name: string, value: any): string { + return new cdk.Output(this, name, { value }).makeImportValue().toString(); + } +} + +/** + * Properties to instantiate the Cluster + */ +export interface IClusterProps extends cdk.StackProps { + /** + * The VPC in which to create the Cluster + */ + vpc: ec2.VpcNetworkRef; + /** + * Where to place the cluster within the VPC + * Which SubnetType this placement falls in + * @default If not supplied, defaults to public + * subnets if available otherwise private subnets + */ + vpcPlacement: ec2.VpcPlacementStrategy; + /** + * This provides the physical cfn name of the Cluster. + * It is not recommended to use an explicit name. + * + * @default If you don't specify a clusterName, AWS CloudFormation + * generates a unique physical ID and uses that ID for the name. + */ + clusterName?: string; + /** + * The Kubernetes version to run in the cluster only 1.10 support today + * + * @default If not supplied, will use Amazon default version (1.10.3) + */ + 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 ClusterRef { + public readonly vpc: ec2.VpcNetworkRef; + /** + * The Name of the created EKS Cluster + * + * @type {string} + * @memberof Cluster + */ + public readonly clusterName: string; + /** + * The AWS generated ARN for the Cluster resource + * + * @type {string} + * @memberof Cluster + */ + public readonly clusterArn: string; + /** + * The endpoint URL for the Cluster + * This is the URL inside the kubeconfig file to use with kubectl + * + * @type {string} + * @memberof Cluster + */ + public readonly clusterEndpoint: string; + public readonly clusterCA: string; + /** + * The VPC Placement strategy for the given cluster + * PublicSubnets? PrivateSubnets? + * + * @type {ec2.VpcPlacementStrategy} + * @memberof Cluster + */ + public readonly vpcPlacement: ec2.VpcPlacementStrategy; + /** + * The security group used by the cluster, currently only one supported + * Updating the cluster by adding resources causes a destruction and + * re-creation. This is a limitation of the EKS API itself. + * + * @type {ec2.SecurityGroup} + * @memberof Cluster + */ + public readonly securityGroup: ec2.SecurityGroup; + /** + * The security group ID attached to the security group of the cluster + * Used within the IConnectable implementation + * + * @type {string} + * @memberof Cluster + */ + public readonly securityGroupId: string; + /** + * Manages connection rules (Security Group Rules) for the cluster + * + * @type {ec2.Connections} + * @memberof Cluster + */ + public readonly connections: ec2.Connections; + + private readonly cluster: cloudformation.ClusterResource; + + /** + * Initiates an EKS Cluster with the supplied arguments + * + * @param parent 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(parent: cdk.Construct, name: string, props: IClusterProps) { + super(parent, name); + + this.vpc = props.vpc; + this.vpcPlacement = props.vpcPlacement; + const subnets = this.vpc.subnets(this.vpcPlacement); + const subnetIds: string[] = []; + subnets.map(s => subnetIds.push(s.subnetId)); + + const role = this.addClusterRole(); + + this.securityGroup = this.addSecurityGroup(); + this.securityGroupId = this.securityGroup.securityGroupId; + this.connections = new ec2.Connections({ + securityGroups: [this.securityGroup], + }); + + const clusterProps: cloudformation.ClusterResourceProps = { + name: props.clusterName, + roleArn: role.roleArn, + version: props.version, + resourcesVpcConfig: { + securityGroupIds: new Array(this.securityGroupId), + subnetIds, + }, + }; + this.cluster = this.createCluster(clusterProps); + this.clusterName = this.cluster.clusterName; + this.clusterArn = this.cluster.clusterArn; + this.clusterEndpoint = this.cluster.clusterEndpoint; + this.clusterCA = this.cluster.clusterCertificateAuthorityData; + } + + private createCluster(props: cloudformation.ClusterResourceProps) { + const cluster = new cloudformation.ClusterResource(this, 'Cluster', props); + + return cluster; + } + + /** + * This is private because for now the EKS API is limited + * Once the security groups are assigned, one can modify the groups + * but any additional groups or removal of groups destroys and + * creates a brand new cluster + */ + private addSecurityGroup() { + return new ec2.SecurityGroup(this, 'ClusterSecurityGroup', { + vpc: this.vpc, + description: 'Cluster API Server Security Group.', + tags: { + Name: 'Cluster SecurityGroup', + Description: 'The security group assigned to the cluster', + }, + }); + } + + private addClusterRole() { + const role = new iam.Role(this, 'ClusterRole', { + assumedBy: new iam.ServicePrincipal('eks.amazonaws.com'), + managedPolicyArns: [ + 'arn:aws:iam::aws:policy/AmazonEKSClusterPolicy', + 'arn:aws:iam::aws:policy/AmazonEKSServicePolicy', + ], + }); + + return role; + } +} + +/** + * Properties for instantiating an Autoscaling Group of worker nodes + * The options are limited on purpose, though moe can be added. + * The requirements for Kubernetes scaling and updated configurations + * are a bit different. + * + * More properties will be added to match those in the future. + */ +export interface INodeProps { + vpc: ec2.VpcNetworkRef; + cluster: ClusterRef; + /** + * The ec2 InstanceClass to use on the worker nodes + * Note, not all instance classes are supported + * ref: https://amazon-eks.s3-us-west-2.amazonaws.com/cloudformation/2018-08-30/amazon-eks-nodegroup.yaml + * + * example: ec2.InstanceClass.M5 + * + * @default M5 + */ + nodeClass?: ec2.InstanceClass; + /** + * The size of the chosen instance class. + * Note, not all instancer sizes are supported per class. + * ref: https://amazon-eks.s3-us-west-2.amazonaws.com/cloudformation/2018-08-30/amazon-eks-nodegroup.yaml + * + * example: ec2.InstanceSize.Large + * + * @default Large + */ + nodeSize?: ec2.InstanceSize; + /** + * The instance type for EKS to support + * Whether to support GPU optimized EKS or Normal instances + * + * @default Normal + */ + nodeType?: NodeType; + /** + * Minimum number of instances in the worker group + * + * @default 1 + */ + minNodes?: number; + /** + * Maximum number of instances in the worker group + * + * @default 1 + */ + maxNodes?: number; + /** + * The name of the SSH keypair to grant access to the worker nodes + * This must be created in the AWS Console first + * + * @default No SSH access granted + */ + sshKeyName?: string; + /** + * Additional tags to associate with the worker group + */ + tags?: cdk.Tags; +} +export class Nodes extends cdk.Construct { + /** + * A VPC reference to place the autoscaling group of nodes inside + * + * @type {ec2.VpcNetworkRef} + * @memberof Nodes + */ + public readonly vpc: ec2.VpcNetworkRef; + /** + * The autoscaling group used to setup the worker nodes + * + * @type {asg.AutoScalingGroup} + * @memberof Nodes + */ + public readonly nodeGroup: asg.AutoScalingGroup; + /** + * An array of worker nodes as multiple groups can be deployed + * within a Stack. This is mainly to track and can be read from + * + * @type {asg.AutoScalingGroup[]} + * @memberof Nodes + */ + public readonly nodeGroups: asg.AutoScalingGroup[] = []; + + private readonly vpcPlacement: ec2.VpcPlacementStrategy; + private readonly clusterName: string; + private readonly cluster: ClusterRef; + + /** + * Creates an instance of Nodes. + * + * @param {cdk.Construct} parent + * @param {string} name + * @param {INodeProps} props + * @memberof Nodes + */ + constructor(parent: cdk.Construct, name: string, props: INodeProps) { + super(parent, name); + + this.cluster = props.cluster; + this.clusterName = props.cluster.clusterName; + this.vpc = props.vpc; + this.vpcPlacement = props.cluster.vpcPlacement; + + const nodeClass = props.nodeClass || ec2.InstanceClass.M5; + const nodeSize = props.nodeSize || ec2.InstanceSize.Large; + const nodeType = props.nodeType || NodeType.Normal; + + const type = new ec2.InstanceTypePair(nodeClass, nodeSize); + const nodeProps: asg.AutoScalingGroupProps = { + vpc: this.vpc, + instanceType: type, + machineImage: new ec2.GenericLinuxImage(nodeAmi[nodeType]), + minSize: props.minNodes || 1, + maxSize: props.maxNodes || 1, + desiredCapacity: props.minNodes || 1, + keyName: props.sshKeyName, + vpcPlacement: this.vpcPlacement, + tags: props.tags, + }; + this.nodeGroup = this.addNodes(nodeProps, type); + } + + private addNodes(props: asg.AutoScalingGroupProps, type: ec2.InstanceTypePair) { + const nodes = new asg.AutoScalingGroup(this, `NodeGroup-${type.toString()}`, props); + // EKS Required Tags + nodes.tags.setTag(`kubernetes.io/cluster/${this.clusterName}`, 'owned', { + overwrite: false, + }); + + this.addRole(nodes.role); + + // bootstrap nodes + this.addUserData({ nodes, type: type.toString() }); + this.addDefaultRules({ nodes }); + + this.nodeGroups.push(nodes); + + return nodes; + } + + private addDefaultRules(props: { nodes: asg.AutoScalingGroup }) { + // self rules + props.nodes.connections.allowInternally(new ec2.AllTraffic()); + + // Cluster to:nodes rules + props.nodes.connections.allowFrom(this.cluster, new ec2.TcpPort(443)); + props.nodes.connections.allowFrom(this.cluster, new ec2.TcpPortRange(1025, 65535)); + + // Allow HTTPS from Nodes to Cluster + props.nodes.connections.allowTo(this.cluster, new ec2.TcpPort(443)); + + // Allow all node outbound traffic + props.nodes.connections.allowToAnyIPv4(new ec2.TcpAllPorts()); + props.nodes.connections.allowToAnyIPv4(new ec2.UdpAllPorts()); + props.nodes.connections.allowToAnyIPv4(new ec2.IcmpAllTypesAndCodes()); + } + + private addUserData(props: { nodes: asg.AutoScalingGroup; type: string }) { + const max = maxPods.get(props.type); + props.nodes.addUserData( + 'set -o xtrace', + `/etc/eks/bootstrap.sh ${this.clusterName} --use-max-pods ${max}`, + ); + } + + private addRole(role: iam.Role) { + role.attachManagedPolicy('arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy'); + role.attachManagedPolicy('arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy'); + role.attachManagedPolicy('arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly'); + + return role; + } +} + +/** + * Import a cluster to use in another stack + * This cluster was not created here + * + * @default NO + * + * Cross stack currently runs into an issue with references + * to security groups that are in stacks not yet deployed + */ +class ImportedCluster extends ClusterRef { + public readonly clusterName: string; + public readonly clusterArn: string; + public readonly clusterEndpoint: string; + public readonly vpcPlacement: ec2.VpcPlacementStrategy; + public readonly securityGroupId: string; + public readonly connections: ec2.Connections; + + constructor(parent: cdk.Construct, name: string, props: IClusterRefProps) { + super(parent, name); + + this.clusterName = props.clusterName; + this.clusterEndpoint = props.clusterEndpoint; + this.clusterArn = props.clusterArn; + this.vpcPlacement = props.vpcPlacement; + this.securityGroupId = props.securityGroupId; + this.connections = props.connections; + } +} diff --git a/packages/@aws-cdk/aws-eks/lib/index.ts b/packages/@aws-cdk/aws-eks/lib/index.ts index 815ecf6ab62fc..540045137f19b 100644 --- a/packages/@aws-cdk/aws-eks/lib/index.ts +++ b/packages/@aws-cdk/aws-eks/lib/index.ts @@ -1,2 +1,5 @@ +export * from "./cluster"; +export * from "./instance-data"; + // 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..ea3d7958575a2 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/lib/instance-data.ts @@ -0,0 +1,112 @@ +/** + * 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 + */ +export const maxPods = 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], + ]), +); + +/** + * Whether the worker nodes should support GPU or just normal instances + */ +export const enum NodeType { + Normal = 'Normal', + GPU = 'GPUSupport', +} + +/** + * Select AMI to use based on the AWS Region being deployed + * + * TODO: Create dynamic mappign by searching SSM Store + */ +export const nodeAmi = Object.freeze({ + Normal: { + ['us-east-1']: 'ami-0440e4f6b9713faf6', + ['us-west-2']: 'ami-0a54c984b9f908c81', + ['eu-west-1']: 'ami-0c7a4976cb6fafd3a', + }, + GPUSupport: { + ['us-east-1']: 'ami-058bfb8c236caae89', + ['us-west-2']: 'ami-0731694d53ef9604b', + ['eu-west-1']: 'ami-0706dc8a5eed2eed9', + }, +}); + +/** + * The type of update to perform on instances in this AutoScalingGroup + */ +export enum UpdateType { + /** + * Don't do anything + */ + None = 'None', + + /** + * Replace the entire AutoScalingGroup + * + * Builds a new AutoScalingGroup first, then delete the old one. + */ + ReplacingUpdate = 'Replace', + + /** + * Replace the instances in the AutoScalingGroup. + */ + RollingUpdate = 'RollingUpdate', +} diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index 2528687649fb2..32f4d280cad0d 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -56,10 +56,14 @@ "devDependencies": { "@aws-cdk/assert": "^0.22.0", "cdk-build-tools": "^0.22.0", + "cdk-integ-tools": "^0.22.0", "cfn2ts": "^0.22.0", "pkglint": "^0.22.0" }, "dependencies": { + "@aws-cdk/aws-ec2": "^0.22.0", + "@aws-cdk/aws-iam": "^0.22.0", + "@aws-cdk/aws-autoscaling": "^0.22.0", "@aws-cdk/cdk": "^0.22.0" }, "homepage": "https://github.com/awslabs/aws-cdk", @@ -69,4 +73,4 @@ "engines": { "node": ">= 8.10.0" } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-eks/test/test.eks.ts b/packages/@aws-cdk/aws-eks/test/test.eks.ts index 820f6b467f38f..6f39992c61c69 100644 --- a/packages/@aws-cdk/aws-eks/test/test.eks.ts +++ b/packages/@aws-cdk/aws-eks/test/test.eks.ts @@ -1,8 +1,616 @@ -import { Test, testCase } from 'nodeunit'; +import { expect, haveResource } 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'); + +// tslint:disable:object-literal-key-quotes + +export = { + 'can create default cluster, no worker nodes'(test: Test) { + const stack = new cdk.Stack(undefined, '', { + env: { region: 'us-east-1', account: '123456' }, + }); + + new eks.Cluster(stack, 'MyCluster', { + vpc: exportedVpc(stack), + vpcPlacement: { + subnetsToUse: ec2.SubnetType.Public, + }, + }); + + expect(stack).toMatch({ + Resources: { + MyClusterClusterRole0A67D5C4: { + Type: 'AWS::IAM::Role', + Properties: { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'eks.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + 'arn:aws:iam::aws:policy/AmazonEKSClusterPolicy', + 'arn:aws:iam::aws:policy/AmazonEKSServicePolicy', + ], + }, + }, + MyClusterClusterSecurityGroup17ACDE00: { + Type: 'AWS::EC2::SecurityGroup', + Properties: { + GroupDescription: 'Cluster API Server Security Group.', + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + SecurityGroupIngress: [], + Tags: [ + { + Key: 'Name', + Value: 'Cluster SecurityGroup', + }, + { + Key: 'Description', + Value: 'The security group assigned to the cluster', + }, + ], + VpcId: 'test-vpc-1234', + }, + }, + MyCluster9CF8BB78: { + Type: 'AWS::EKS::Cluster', + Properties: { + ResourcesVpcConfig: { + SecurityGroupIds: [ + { + 'Fn::GetAtt': ['MyClusterClusterSecurityGroup17ACDE00', 'GroupId'], + }, + ], + SubnetIds: ['pub1'], + }, + RoleArn: { + 'Fn::GetAtt': ['MyClusterClusterRole0A67D5C4', 'Arn'], + }, + }, + }, + }, + }); -export = testCase({ - notTested(test: Test) { - test.ok(true, 'No tests are specified for this package.'); test.done(); - } -}); + }, + 'can create worker group, default settings'(test: Test) { + const stack = new cdk.Stack(undefined, '', { + env: { region: 'us-east-1', account: '123456' }, + }); + + const testVpc = exportedVpc(stack); + + const cl = new eks.Cluster(stack, 'MyCluster', { + vpc: testVpc, + vpcPlacement: { + subnetsToUse: ec2.SubnetType.Public, + }, + }); + + new eks.Nodes(stack, 'NodeGroup1', { + vpc: testVpc, + cluster: cl, + }); + + expect(stack).to( + haveResource('AWS::AutoScaling::AutoScalingGroup', { + MaxSize: '1', + MinSize: '1', + DesiredCapacity: '1', + LaunchConfigurationName: { + Ref: 'NodeGroup1NodeGroupm5largeLaunchConfig593791C7', + }, + Tags: [ + { + Key: 'Name', + PropagateAtLaunch: true, + Value: 'NodeGroup1/NodeGroup-m5.large', + }, + { + Key: { + 'Fn::Join': [ + '', + [ + 'kubernetes.io/cluster/', + { + Ref: 'MyCluster9CF8BB78', + }, + ], + ], + }, + PropagateAtLaunch: true, + Value: 'owned', + }, + ], + VPCZoneIdentifier: ['pub1'], + }), + ); + + expect(stack).to( + haveResource('AWS::AutoScaling::LaunchConfiguration', { + ImageId: 'ami-0440e4f6b9713faf6', + InstanceType: 'm5.large', + IamInstanceProfile: { + Ref: 'NodeGroup1NodeGroupm5largeInstanceProfileCED4339B', + }, + SecurityGroups: [ + { + 'Fn::GetAtt': ['NodeGroup1NodeGroupm5largeInstanceSecurityGroup68AD5F49', 'GroupId'], + }, + ], + UserData: { + 'Fn::Base64': { + 'Fn::Join': [ + '', + [ + '#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', + { + Ref: 'MyCluster9CF8BB78', + }, + ' --use-max-pods 29', + ], + ], + }, + }, + }), + ); + + expect(stack).to( + haveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'ec2.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + 'arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy', + 'arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy', + 'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly', + ], + }), + ); + + expect(stack).to( + haveResource('AWS::IAM::InstanceProfile', { + Roles: [ + { + Ref: 'NodeGroup1NodeGroupm5largeInstanceRoleA66DE8CA', + }, + ], + }), + ); + + expect(stack).to( + haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'NodeGroup1/NodeGroup-m5.large/InstanceSecurityGroup', + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + SecurityGroupIngress: [], + Tags: [ + { + Key: 'Name', + Value: 'NodeGroup1/NodeGroup-m5.large', + }, + { + Key: { + 'Fn::Join': [ + '', + [ + 'kubernetes.io/cluster/', + { + Ref: 'MyCluster9CF8BB78', + }, + ], + ], + }, + Value: 'owned', + }, + ], + VpcId: 'test-vpc-1234', + }), + ); + + test.done(); + }, + 'can create second worker group, custom settings'(test: Test) { + const stack = new cdk.Stack(undefined, '', { + env: { region: 'us-east-1', account: '123456' }, + }); + + const testVpc = exportedVpc(stack); + + const cl = new eks.Cluster(stack, 'MyCluster', { + vpc: testVpc, + vpcPlacement: { + subnetsToUse: ec2.SubnetType.Public, + }, + }); + + new eks.Nodes(stack, 'NodeGroup1', { + vpc: testVpc, + cluster: cl, + }); + + new eks.Nodes(stack, 'NodeGroup2', { + vpc: testVpc, + cluster: cl, + nodeClass: ec2.InstanceClass.T2, + nodeSize: ec2.InstanceSize.Medium, + nodeType: eks.NodeType.Normal, + minNodes: 2, + maxNodes: 4, + sshKeyName: 'aws-dev-key', + }); + + expect(stack).to( + haveResource('AWS::AutoScaling::AutoScalingGroup', { + MaxSize: '4', + MinSize: '2', + DesiredCapacity: '2', + LaunchConfigurationName: { + Ref: 'NodeGroup2NodeGroupt2mediumLaunchConfigA973C952', + }, + Tags: [ + { + Key: 'Name', + PropagateAtLaunch: true, + Value: 'NodeGroup2/NodeGroup-t2.medium', + }, + { + Key: { + 'Fn::Join': [ + '', + [ + 'kubernetes.io/cluster/', + { + Ref: 'MyCluster9CF8BB78', + }, + ], + ], + }, + PropagateAtLaunch: true, + Value: 'owned', + }, + ], + VPCZoneIdentifier: ['pub1'], + }), + ); + + expect(stack).to( + haveResource('AWS::AutoScaling::LaunchConfiguration', { + ImageId: 'ami-0440e4f6b9713faf6', + InstanceType: 't2.medium', + IamInstanceProfile: { + Ref: 'NodeGroup2NodeGroupt2mediumInstanceProfile32011E3A', + }, + KeyName: 'aws-dev-key', + SecurityGroups: [ + { + 'Fn::GetAtt': ['NodeGroup2NodeGroupt2mediumInstanceSecurityGroup5F0C1B74', 'GroupId'], + }, + ], + UserData: { + 'Fn::Base64': { + 'Fn::Join': [ + '', + [ + '#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', + { + Ref: 'MyCluster9CF8BB78', + }, + ' --use-max-pods 17', + ], + ], + }, + }, + }), + ); + + expect(stack).to( + haveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'ec2.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + 'arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy', + 'arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy', + 'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly', + ], + }), + ); + + expect(stack).to( + haveResource('AWS::IAM::InstanceProfile', { + Roles: [ + { + Ref: 'NodeGroup2NodeGroupt2mediumInstanceRoleFDAFEE3C', + }, + ], + }), + ); + + expect(stack).to( + haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'NodeGroup1/NodeGroup-m5.large/InstanceSecurityGroup', + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + SecurityGroupIngress: [], + Tags: [ + { + Key: 'Name', + Value: 'NodeGroup1/NodeGroup-m5.large', + }, + { + Key: { + 'Fn::Join': [ + '', + [ + 'kubernetes.io/cluster/', + { + Ref: 'MyCluster9CF8BB78', + }, + ], + ], + }, + Value: 'owned', + }, + ], + VpcId: 'test-vpc-1234', + }), + ); + + expect(stack).to( + haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'NodeGroup2/NodeGroup-t2.medium/InstanceSecurityGroup', + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + SecurityGroupIngress: [], + Tags: [ + { + Key: 'Name', + Value: 'NodeGroup2/NodeGroup-t2.medium', + }, + { + Key: { + 'Fn::Join': [ + '', + [ + 'kubernetes.io/cluster/', + { + Ref: 'MyCluster9CF8BB78', + }, + ], + ], + }, + Value: 'owned', + }, + ], + VpcId: 'test-vpc-1234', + }), + ); + + test.done(); + }, + 'can export cluster'(test: Test) { + const stack = new cdk.Stack(undefined, 'TestStack', { + env: { region: 'us-east-1', account: '123456' }, + }); + + const testVpc = exportedVpc(stack); + + const cl = new eks.Cluster(stack, 'MyCluster', { + vpc: testVpc, + vpcPlacement: { + subnetsToUse: ec2.SubnetType.Public, + }, + }); + + const exportedCluster = cl.export(); + + new eks.Nodes(stack, 'NodeGroup1', { + vpc: testVpc, + cluster: cl, + }); + + const stack2 = new cdk.Stack(undefined, 'TestStack2', { + env: { region: 'us-east-1', account: '123456' }, + }); + + const testVpc2 = exportedVpc(stack2); + const importedCluster = eks.ClusterRef.import(stack2, 'importedCluster', exportedCluster); + + new eks.Nodes(stack2, 'NodeGroup2', { + vpc: testVpc2, + cluster: importedCluster, + nodeClass: ec2.InstanceClass.P3, + nodeSize: ec2.InstanceSize.Large, + nodeType: eks.NodeType.Normal, + minNodes: 4, + maxNodes: 8, + sshKeyName: 'aws-dev-key', + tags: { ['Environment']: 'test' }, + }); + + expect(stack2).to( + haveResource('AWS::AutoScaling::AutoScalingGroup', { + MaxSize: '8', + MinSize: '4', + DesiredCapacity: '4', + LaunchConfigurationName: { + Ref: 'NodeGroup2NodeGroupp3largeLaunchConfigE29DC643', + }, + Tags: [ + { + Key: 'Environment', + Value: 'test', + }, + { + Key: 'Name', + PropagateAtLaunch: true, + Value: 'NodeGroup2/NodeGroup-p3.large', + }, + { + Key: { + 'Fn::Join': [ + '', + [ + 'kubernetes.io/cluster/', + { + 'Fn::ImportValue': 'TestStack:MyClusterClusterName37E9AAAB', + }, + ], + ], + }, + PropagateAtLaunch: true, + Value: 'owned', + }, + ], + VPCZoneIdentifier: ['pub1'], + }), + ); + + expect(stack2).to( + haveResource('AWS::AutoScaling::LaunchConfiguration', { + ImageId: 'ami-0440e4f6b9713faf6', + InstanceType: 'p3.large', + IamInstanceProfile: { + Ref: 'NodeGroup2NodeGroupp3largeInstanceProfileA6647AEB', + }, + KeyName: 'aws-dev-key', + SecurityGroups: [ + { + 'Fn::GetAtt': ['NodeGroup2NodeGroupp3largeInstanceSecurityGroupEDB7AF89', 'GroupId'], + }, + ], + UserData: { + 'Fn::Base64': { + 'Fn::Join': [ + '', + [ + '#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', + { + 'Fn::ImportValue': 'TestStack:MyClusterClusterName37E9AAAB', + }, + ' --use-max-pods undefined', + ], + ], + }, + }, + }), + ); + + expect(stack2).to( + haveResource('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'ec2.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + ManagedPolicyArns: [ + 'arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy', + 'arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy', + 'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly', + ], + }), + ); + + expect(stack2).to( + haveResource('AWS::IAM::InstanceProfile', { + Roles: [ + { + Ref: 'NodeGroup2NodeGroupp3largeInstanceRole480076E3', + }, + ], + }), + ); + + expect(stack).to( + haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Cluster API Server Security Group.', + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + SecurityGroupIngress: [], + Tags: [ + { + Key: 'Name', + Value: 'Cluster SecurityGroup', + }, + { + Key: 'Description', + Value: 'The security group assigned to the cluster', + }, + ], + VpcId: 'test-vpc-1234', + }), + ); + + test.done(); + }, +}; + +function exportedVpc(stack: cdk.Stack) { + return ec2.VpcNetwork.import(stack, 'TestVpc', { + vpcId: 'test-vpc-1234', + availabilityZones: ['us-east-1d'], + publicSubnetIds: ['pub1'], + isolatedSubnetIds: [], + }); +} From 873a19d35be3a859d1e78cd02a3c4db5895ee9e0 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 31 Jan 2019 16:36:06 +0100 Subject: [PATCH 02/10] Rewrite EKS API to make it more in line with ECS API Generalize and conformize adding AutoScalingGroup capacity to both ECS and EKS clusters, as well as naming consistency among variables and with Application AutoScaling. 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 +- packages/@aws-cdk/aws-ecs/README.md | 2 +- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 36 +- .../aws-ecs/test/ec2/integ.lb-awsvpc-nw.ts | 2 +- .../aws-ecs/test/ec2/integ.lb-bridge-nw.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 | 99 +-- packages/@aws-cdk/aws-eks/lib/ami.ts | 122 ++++ packages/@aws-cdk/aws-eks/lib/cluster-base.ts | 93 +++ packages/@aws-cdk/aws-eks/lib/cluster.ts | 533 ++++++--------- packages/@aws-cdk/aws-eks/lib/index.ts | 3 +- .../@aws-cdk/aws-eks/lib/instance-data.ts | 58 +- packages/@aws-cdk/aws-eks/test/MANUAL_TEST.md | 50 ++ .../test/example.ssh-into-nodes.lit.ts | 32 + .../aws-eks/test/integ.eks-cluster.lit.ts | 28 + packages/@aws-cdk/aws-eks/test/test.eks.ts | 616 ------------------ 19 files changed, 636 insertions(+), 1170 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/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.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 ef8bd5b318233..3a37987c49dc4 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 BasicAutoScalingGroupProps { /** * 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 */ @@ -158,6 +146,26 @@ export interface AutoScalingGroupProps { associatePublicIpAddress?: boolean; } +/** + * Properties of a Fleet + */ +export interface AutoScalingGroupProps extends BasicAutoScalingGroupProps { + /** + * 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 * @@ -247,19 +255,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 ce067b7b52fd4..f8700a67f2dd8 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 @@ -414,8 +414,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, }); @@ -437,8 +437,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, }); @@ -460,8 +460,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-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 6b68c2102ea4d..d6570967ac49a 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -10,7 +10,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 dfc6d5cc3fe0f..abad1a6d132df 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); @@ -351,31 +349,9 @@ export interface AddAutoScalingGroupCapacityOptions { /** * Properties for adding autoScalingGroup */ -export interface AddDefaultAutoScalingGroupOptions extends AddAutoScalingGroupCapacityOptions { - +export interface AddDefaultAutoScalingGroupOptions extends AddAutoScalingGroupCapacityOptions, autoscaling.BasicAutoScalingGroupProps { /** * 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.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-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 4b30df1033402..9b61ba6054a74 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -2,94 +2,31 @@ This construct library allows you to define and create [Amazon Elastic Container Service for Kubernetes (EKS)](https://aws.amazon.com/eks/) clusters programmatically. -### Define a VPC Network Stack +### Example -The cluster must be placed inside of an `AWS VPC`. +The following example shows how to start an EKS cluster and how to +add worker nodes to it: -```ts -import ec2 = require('@aws-cdk/aws-ec2'); -import eks = require('@aws-cdk/aws-eks'); -import cdk = require('@aws-cdk/cdk'); +[starting a cluster example](test/integ.eks-cluster.lit.ts) -const app = new cdk.App(); +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). -const networkStack = new cdk.Stack(app, 'Network'); +### SSH into your nodes -const vpc = new ec2.VpcNetwork(networkStack, 'VPC'); -``` +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): -### Import the Network Stack +[ssh into nodes exampled](test/example.ssh-into-nodes.lit.ts) -This can be done within a single stack and no need to import, but it's best to separate functions as much as possible. - -```ts -const clusterStack = new cdk.Stack(app, 'Cluster'); - -const clusterVpc = ec2.VpcNetworkRef.import(clusterStack, 'ClusterVpc', vpc.export()); -const cluster = new eks.Cluster(clusterStack, 'Cluster', { - vpc: clusterVpc, - vpcPlacement: { - subnetsToUse: ec2.SubnetType.Public, - }, -}); -``` - -This creates the `EKS Control Plane` but no `Worker Nodes` will be created at this step. - -#### Connections - -The `Cluster` class implements the `IConnectable` interface to easily manage security group rules. - -Allowing access to the API server with `kubectl` one would add `cluster.connections.allowFromAnyIPv4(new ec2.TcpPort(443));`. Though for best practices it's best to use the `connections.allowFrom` and allow from a specific connectable peer. Such as a `bastion` host's security group. - -### Create the Worker Nodes - -Here we create a set of worker nodes to run our container workloads on. - -```ts -const grp1 = new eks.Nodes(clusterStack, 'NodeGroup1', { - vpc: clusterVpc, - cluster, - minNodes: 3, - maxNodes: 6, - sshKeyName: 'aws-dev-key', -}); -``` - -This example creates nodes between `minNodes` value and `maxNodes` value with the default parameters for node types (`M5` and `Large`) as that serves decently for most situations in dev and sometimes prod environments. - -The creation of also initializes the security groups with the [Amazon Recommended](https://docs.aws.amazon.com/eks/latest/userguide/sec-group-reqs.html) settings for `cluster:to:nodes and nodes:to:cluster` communication. - -By default even if the "sshKeyName" is provided, not security groups are modified to allow access. You'll need to add the rules using the connections specifier. - -- Allow SSH from any IPv4 `grp1.nodeGroup.connections.allowFromAnyIPv4(new ec2.TcpPort(22));` - -Recommended method (be more explicit) - -Allow only from specified IPv4 (can also be another security group) see [connections documentation](https://awslabs.github.io/aws-cdk/refs/_aws-cdk_aws-ec2.html#connections) `cluster.connections.allowFrom(new ec2.CidrIPv4("X.X.X.X/32"), new ec2.TcpPort(443))` - -### Add More Worker Nodes - -You can also add more worker nodes to the Stack as you see fit. Maybe different sizes? or different class? Or maybe separate worker nodes from production, testing and dev?. - -```ts -const grp2 = new eks.Nodes(clusterStack, 'NodeGroup2', { - vpc: clusterVpc, - cluster, - nodeClass: ec2.InstanceClass.T2, - nodeSize: ec2.InstanceSize.Medium, - nodeType: eks.NodeType.Normal, - minNodes: 2, - maxNodes: 4, - sshKeyName: 'aws-dev-key', -}); -``` - -There's an `Array` of autoscaling groups that tracks the creation of nodes. +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 create worker nodes by passing a IClusterProp with creation details (faster for testing) - - More dangerous for production -- [ ] Make worker nodes `connections` implementation details native, so that no need to access through the `autoscaling` group `nodeGroup` public field -- [ ] Make able to create nodes in separate stack +- [ ] 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..182a6566b6569 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/lib/ami.ts @@ -0,0 +1,122 @@ +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 + */ + 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 = 'GPUSupport', +} + +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 + */ +export 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..65ed106f20859 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/lib/cluster-base.ts @@ -0,0 +1,93 @@ +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; +} + +/** + * 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('ClusterName', 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 index 0567e61f77f81..3f712d1d39c9b 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -1,47 +1,66 @@ -import asg = require('@aws-cdk/aws-autoscaling'); +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 { cloudformation } from './eks.generated'; -import { maxPods, nodeAmi, NodeType } from './instance-data'; +import { EksOptimizedAmi, nodeTypeForInstanceType } from './ami'; +import { ClusterBase, ClusterImportProps, ICluster } from './cluster-base'; +import { CfnCluster } from './eks.generated'; +import { maxPodsForInstanceType } from './instance-data'; /** - * Reference properties used when a cluster is exported or imported - * with the const a = ClusterRef.import | export methodes + * Properties to instantiate the Cluster */ -export interface IClusterRefProps { +export interface ClusterProps { /** - * The physical name of the Cluster + * The VPC in which to create the Cluster */ - clusterName: string; + vpc: ec2.IVpcNetwork; + /** - * The unique ARN assigned to the service by AWS - * in the form of arn:aws:eks: + * Where to place EKS Control Plane ENIs + * + * If you want to create public load balancers, this must include public subnets. + * + * @default All public and private subnets */ - clusterArn: string; + vpcPlacements?: ec2.VpcPlacementStrategy[]; + /** - * The API Server endpoint URL + * 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 */ - clusterEndpoint: string; + role?: iam.IRole; + /** - * Reeference the vpc placement for placing nodes into ASG subnets + * Name for the cluster. + * + * @default Automatically generated name */ - vpcPlacement: ec2.VpcPlacementStrategy; + clusterName?: string; + /** - * The security group ID used by the cluster for it's rules + * Security Group to use for Control Plane ENIs + * + * @default A security group is automatically created */ - securityGroupId: string; + securityGroup?: ec2.ISecurityGroup; + /** - * The IConnectable interface implementation for updating - * security group rules + * The Kubernetes version to run in the cluster + * + * @default If not supplied, will use Amazon default version */ - connections: ec2.Connections; + version?: string; } /** - * A SecurityGroup Reference, object not created with this template. + * 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 abstract class ClusterRef extends cdk.Construct implements ec2.IConnectable { +export class Cluster extends ClusterBase { /** * Import an existing cluster * @@ -49,123 +68,41 @@ export abstract class ClusterRef extends cdk.Construct implements ec2.IConnectab * @param id the id or name to import as * @param props the cluster properties to use for importing information */ - public static import(parent: cdk.Construct, id: string, props: IClusterRefProps): ClusterRef { + public static import(parent: cdk.Construct, id: string, props: ClusterImportProps): ICluster { return new ImportedCluster(parent, id, props); } - public abstract readonly clusterName: string; - public abstract readonly clusterArn: string; - public abstract readonly clusterEndpoint: string; - public abstract readonly vpcPlacement: ec2.VpcPlacementStrategy; - public abstract readonly securityGroupId: string; - public abstract readonly connections: ec2.Connections; - /** - * Export cluster references to use in other stacks + * The VPC in which this Cluster was created */ - public export(): IClusterRefProps { - return { - clusterName: this.makeOutput('ClusterName', this.clusterName), - clusterArn: this.makeOutput('ClusterArn', this.clusterArn), - clusterEndpoint: this.makeOutput('ClusterEndpoint', this.clusterEndpoint), - vpcPlacement: this.vpcPlacement, - securityGroupId: this.securityGroupId, - connections: this.connections, - }; - } + public readonly vpc: ec2.IVpcNetwork; - private makeOutput(name: string, value: any): string { - return new cdk.Output(this, name, { value }).makeImportValue().toString(); - } -} - -/** - * Properties to instantiate the Cluster - */ -export interface IClusterProps extends cdk.StackProps { - /** - * The VPC in which to create the Cluster - */ - vpc: ec2.VpcNetworkRef; - /** - * Where to place the cluster within the VPC - * Which SubnetType this placement falls in - * @default If not supplied, defaults to public - * subnets if available otherwise private subnets - */ - vpcPlacement: ec2.VpcPlacementStrategy; - /** - * This provides the physical cfn name of the Cluster. - * It is not recommended to use an explicit name. - * - * @default If you don't specify a clusterName, AWS CloudFormation - * generates a unique physical ID and uses that ID for the name. - */ - clusterName?: string; - /** - * The Kubernetes version to run in the cluster only 1.10 support today - * - * @default If not supplied, will use Amazon default version (1.10.3) - */ - 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 ClusterRef { - public readonly vpc: ec2.VpcNetworkRef; /** * The Name of the created EKS Cluster - * - * @type {string} - * @memberof Cluster */ public readonly clusterName: string; + /** * The AWS generated ARN for the Cluster resource * - * @type {string} - * @memberof Cluster + * @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 * - * @type {string} - * @memberof Cluster + * @example https://5E1D0CEXAMPLEA591B746AFC5AB30262.yl4.us-west-2.eks.amazonaws.com */ public readonly clusterEndpoint: string; - public readonly clusterCA: string; - /** - * The VPC Placement strategy for the given cluster - * PublicSubnets? PrivateSubnets? - * - * @type {ec2.VpcPlacementStrategy} - * @memberof Cluster - */ - public readonly vpcPlacement: ec2.VpcPlacementStrategy; - /** - * The security group used by the cluster, currently only one supported - * Updating the cluster by adding resources causes a destruction and - * re-creation. This is a limitation of the EKS API itself. - * - * @type {ec2.SecurityGroup} - * @memberof Cluster - */ - public readonly securityGroup: ec2.SecurityGroup; + /** - * The security group ID attached to the security group of the cluster - * Used within the IConnectable implementation - * - * @type {string} - * @memberof Cluster + * The certificate-authority-data for your cluster. */ - public readonly securityGroupId: string; + public readonly clusterCertificateAuthorityData: string; + /** * Manages connection rules (Security Group Rules) for the cluster * @@ -174,7 +111,12 @@ export class Cluster extends ClusterRef { */ public readonly connections: ec2.Connections; - private readonly cluster: cloudformation.ClusterResource; + /** + * 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 @@ -183,277 +125,208 @@ export class Cluster extends ClusterRef { * @param name the name of the Construct to create * @param props properties in the IClusterProps interface */ - constructor(parent: cdk.Construct, name: string, props: IClusterProps) { + constructor(parent: cdk.Construct, name: string, props: ClusterProps) { super(parent, name); this.vpc = props.vpc; - this.vpcPlacement = props.vpcPlacement; - const subnets = this.vpc.subnets(this.vpcPlacement); - const subnetIds: string[] = []; - subnets.map(s => subnetIds.push(s.subnetId)); + 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 role = this.addClusterRole(); + const securityGroup = props.securityGroup || new ec2.SecurityGroup(this, 'ControlPlaneSecurityGroup', { + vpc: props.vpc, + description: 'EKS Control Plane Security Group', + }); + // FIXME: Tag Security Group as soon as we can w/: "kubernetes.io/cluster/${ClusterName}": "owned" - this.securityGroup = this.addSecurityGroup(); - this.securityGroupId = this.securityGroup.securityGroupId; this.connections = new ec2.Connections({ - securityGroups: [this.securityGroup], + securityGroups: [securityGroup], + defaultPortRange: new ec2.TcpPort(443), // Control Plane has an HTTPS API }); - const clusterProps: cloudformation.ClusterResourceProps = { + // 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: role.roleArn, + roleArn: this.role.roleArn, version: props.version, resourcesVpcConfig: { - securityGroupIds: new Array(this.securityGroupId), - subnetIds, - }, - }; - this.cluster = this.createCluster(clusterProps); - this.clusterName = this.cluster.clusterName; - this.clusterArn = this.cluster.clusterArn; - this.clusterEndpoint = this.cluster.clusterEndpoint; - this.clusterCA = this.cluster.clusterCertificateAuthorityData; - } + securityGroupIds: [securityGroup.securityGroupId], + subnetIds + } + }); - private createCluster(props: cloudformation.ClusterResourceProps) { - const cluster = new cloudformation.ClusterResource(this, 'Cluster', props); + this.clusterName = resource.clusterName; + this.clusterArn = resource.clusterArn; + this.clusterEndpoint = resource.clusterEndpoint; + this.clusterCertificateAuthorityData = resource.clusterCertificateAuthorityData; - return cluster; + new cdk.Output(this, 'ClusterName', { value: this.clusterName, disableExport: true }); } /** - * This is private because for now the EKS API is limited - * Once the security groups are assigned, one can modify the groups - * but any additional groups or removal of groups destroys and - * creates a brand new cluster + * 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. */ - private addSecurityGroup() { - return new ec2.SecurityGroup(this, 'ClusterSecurityGroup', { + public addWorkerNodes(id: string, options: AddWorkerNodesOptions): autoscaling.AutoScalingGroup { + const asg = new autoscaling.AutoScalingGroup(this, id, { + ...options, vpc: this.vpc, - description: 'Cluster API Server Security Group.', - tags: { - Name: 'Cluster SecurityGroup', - Description: 'The security group assigned to the cluster', - }, + machineImage: new EksOptimizedAmi({ + nodeType: nodeTypeForInstanceType(options.instanceType), + kubernetesVersion: this.version, + }), + updateType: options.updateType || autoscaling.UpdateType.RollingUpdate, + instanceType: options.instanceType, }); - } - private addClusterRole() { - const role = new iam.Role(this, 'ClusterRole', { - assumedBy: new iam.ServicePrincipal('eks.amazonaws.com'), - managedPolicyArns: [ - 'arn:aws:iam::aws:policy/AmazonEKSClusterPolicy', - 'arn:aws:iam::aws:policy/AmazonEKSServicePolicy', - ], + this.addAutoScalingGroup(asg, { + maxPods: maxPodsForInstanceType(options.instanceType), }); - return role; + return asg; } -} -/** - * Properties for instantiating an Autoscaling Group of worker nodes - * The options are limited on purpose, though moe can be added. - * The requirements for Kubernetes scaling and updated configurations - * are a bit different. - * - * More properties will be added to match those in the future. - */ -export interface INodeProps { - vpc: ec2.VpcNetworkRef; - cluster: ClusterRef; /** - * The ec2 InstanceClass to use on the worker nodes - * Note, not all instance classes are supported - * ref: https://amazon-eks.s3-us-west-2.amazonaws.com/cloudformation/2018-08-30/amazon-eks-nodegroup.yaml + * Add compute capacity to this EKS cluster in the form of an AutoScalingGroup * - * example: ec2.InstanceClass.M5 + * The AutoScalingGroup must be running an EKS-optimized AMI containing the + * /etc/eks/bootstrap.sh script. * - * @default M5 + * Prefer to use `addWorkerNodes` if possible. */ - nodeClass?: ec2.InstanceClass; - /** - * The size of the chosen instance class. - * Note, not all instancer sizes are supported per class. - * ref: https://amazon-eks.s3-us-west-2.amazonaws.com/cloudformation/2018-08-30/amazon-eks-nodegroup.yaml - * - * example: ec2.InstanceSize.Large - * - * @default Large - */ - nodeSize?: ec2.InstanceSize; - /** - * The instance type for EKS to support - * Whether to support GPU optimized EKS or Normal instances - * - * @default Normal - */ - nodeType?: NodeType; - /** - * Minimum number of instances in the worker group - * - * @default 1 - */ - minNodes?: number; - /** - * Maximum number of instances in the worker group - * - * @default 1 - */ - maxNodes?: number; - /** - * The name of the SSH keypair to grant access to the worker nodes - * This must be created in the AWS Console first - * - * @default No SSH access granted - */ - sshKeyName?: string; - /** - * Additional tags to associate with the worker group - */ - tags?: cdk.Tags; -} -export class Nodes extends cdk.Construct { - /** - * A VPC reference to place the autoscaling group of nodes inside - * - * @type {ec2.VpcNetworkRef} - * @memberof Nodes - */ - public readonly vpc: ec2.VpcNetworkRef; - /** - * The autoscaling group used to setup the worker nodes - * - * @type {asg.AutoScalingGroup} - * @memberof Nodes - */ - public readonly nodeGroup: asg.AutoScalingGroup; - /** - * An array of worker nodes as multiple groups can be deployed - * within a Stack. This is mainly to track and can be read from - * - * @type {asg.AutoScalingGroup[]} - * @memberof Nodes - */ - public readonly nodeGroups: asg.AutoScalingGroup[] = []; + public addAutoScalingGroup(autoScalingGroup: autoscaling.AutoScalingGroup, options: AddAutoScalingGroupOptions) { + // self rules + autoScalingGroup.connections.allowInternally(new ec2.AllTraffic()); - private readonly vpcPlacement: ec2.VpcPlacementStrategy; - private readonly clusterName: string; - private readonly cluster: ClusterRef; + // Cluster to:nodes rules + autoScalingGroup.connections.allowFrom(this, new ec2.TcpPort(443)); + autoScalingGroup.connections.allowFrom(this, new ec2.TcpPortRange(1025, 65535)); - /** - * Creates an instance of Nodes. - * - * @param {cdk.Construct} parent - * @param {string} name - * @param {INodeProps} props - * @memberof Nodes - */ - constructor(parent: cdk.Construct, name: string, props: INodeProps) { - super(parent, name); + // Allow HTTPS from Nodes to Cluster + autoScalingGroup.connections.allowTo(this, new ec2.TcpPort(443)); - this.cluster = props.cluster; - this.clusterName = props.cluster.clusterName; - this.vpc = props.vpc; - this.vpcPlacement = props.cluster.vpcPlacement; + // Allow all node outbound traffic + autoScalingGroup.connections.allowToAnyIPv4(new ec2.TcpAllPorts()); + autoScalingGroup.connections.allowToAnyIPv4(new ec2.UdpAllPorts()); + autoScalingGroup.connections.allowToAnyIPv4(new ec2.IcmpAllTypesAndCodes()); - const nodeClass = props.nodeClass || ec2.InstanceClass.M5; - const nodeSize = props.nodeSize || ec2.InstanceSize.Large; - const nodeType = props.nodeType || NodeType.Normal; + 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 - const type = new ec2.InstanceTypePair(nodeClass, nodeSize); - const nodeProps: asg.AutoScalingGroupProps = { - vpc: this.vpc, - instanceType: type, - machineImage: new ec2.GenericLinuxImage(nodeAmi[nodeType]), - minSize: props.minNodes || 1, - maxSize: props.maxNodes || 1, - desiredCapacity: props.minNodes || 1, - keyName: props.sshKeyName, - vpcPlacement: this.vpcPlacement, - tags: props.tags, - }; - this.nodeGroup = this.addNodes(nodeProps, type); - } + 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); - private addNodes(props: asg.AutoScalingGroupProps, type: ec2.InstanceTypePair) { - const nodes = new asg.AutoScalingGroup(this, `NodeGroup-${type.toString()}`, props); // EKS Required Tags - nodes.tags.setTag(`kubernetes.io/cluster/${this.clusterName}`, 'owned', { - overwrite: false, + autoScalingGroup.tags.setTag(`kubernetes.io/cluster/${this.clusterName}`, 'owned', { + propagate: true, }); - this.addRole(nodes.role); - - // bootstrap nodes - this.addUserData({ nodes, type: type.toString() }); - this.addDefaultRules({ nodes }); - - this.nodeGroups.push(nodes); - - return nodes; + // 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 + }); } - private addDefaultRules(props: { nodes: asg.AutoScalingGroup }) { - // self rules - props.nodes.connections.allowInternally(new ec2.AllTraffic()); - - // Cluster to:nodes rules - props.nodes.connections.allowFrom(this.cluster, new ec2.TcpPort(443)); - props.nodes.connections.allowFrom(this.cluster, new ec2.TcpPortRange(1025, 65535)); + /** + * 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 }); - // Allow HTTPS from Nodes to Cluster - props.nodes.connections.allowTo(this.cluster, new ec2.TcpPort(443)); + for (const subnet of privates) { + if (!isTaggableSubnet(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; + } - // Allow all node outbound traffic - props.nodes.connections.allowToAnyIPv4(new ec2.TcpAllPorts()); - props.nodes.connections.allowToAnyIPv4(new ec2.UdpAllPorts()); - props.nodes.connections.allowToAnyIPv4(new ec2.IcmpAllTypesAndCodes()); + subnet.tags.setTag("kubernetes.io/role/internal-elb", "1"); + } } +} - private addUserData(props: { nodes: asg.AutoScalingGroup; type: string }) { - const max = maxPods.get(props.type); - props.nodes.addUserData( - 'set -o xtrace', - `/etc/eks/bootstrap.sh ${this.clusterName} --use-max-pods ${max}`, - ); - } +function isTaggableSubnet(subnet: ec2.IVpcSubnet): subnet is ec2.VpcSubnet { + return (subnet as any).tags !== undefined; +} - private addRole(role: iam.Role) { - role.attachManagedPolicy('arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy'); - role.attachManagedPolicy('arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy'); - role.attachManagedPolicy('arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly'); +/** + * Options for adding worker nodes + */ +export interface AddWorkerNodesOptions extends autoscaling.BasicAutoScalingGroupProps { + /** + * Instance type of the instances to start + */ + instanceType: ec2.InstanceType; +} - return role; - } +/** + * 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 - * This cluster was not created here - * - * @default NO - * - * Cross stack currently runs into an issue with references - * to security groups that are in stacks not yet deployed */ -class ImportedCluster extends ClusterRef { +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 vpcPlacement: ec2.VpcPlacementStrategy; - public readonly securityGroupId: string; - public readonly connections: ec2.Connections; + public readonly connections = new ec2.Connections(); - constructor(parent: cdk.Construct, name: string, props: IClusterRefProps) { + constructor(parent: cdk.Construct, name: string, props: ClusterImportProps) { super(parent, name); + this.vpc = ec2.VpcNetwork.import(this, "VPC", props.vpc); this.clusterName = props.clusterName; this.clusterEndpoint = props.clusterEndpoint; this.clusterArn = props.clusterArn; - this.vpcPlacement = props.vpcPlacement; - this.securityGroupId = props.securityGroupId; - this.connections = props.connections; + 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 540045137f19b..07f3f07aa8fff 100644 --- a/packages/@aws-cdk/aws-eks/lib/index.ts +++ b/packages/@aws-cdk/aws-eks/lib/index.ts @@ -1,5 +1,6 @@ +export * from "./cluster-base"; export * from "./cluster"; -export * from "./instance-data"; +export * from "./ami"; // AWS::EKS CloudFormation Resources: 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 index ea3d7958575a2..137bcbdd6cab0 100644 --- a/packages/@aws-cdk/aws-eks/lib/instance-data.ts +++ b/packages/@aws-cdk/aws-eks/lib/instance-data.ts @@ -1,9 +1,11 @@ +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 */ -export const maxPods = Object.freeze( +const MAX_PODS = Object.freeze( new Map([ ['c4.large', 29], ['c4.xlarge', 58], @@ -63,50 +65,10 @@ export const maxPods = Object.freeze( ]), ); -/** - * Whether the worker nodes should support GPU or just normal instances - */ -export const enum NodeType { - Normal = 'Normal', - GPU = 'GPUSupport', -} - -/** - * Select AMI to use based on the AWS Region being deployed - * - * TODO: Create dynamic mappign by searching SSM Store - */ -export const nodeAmi = Object.freeze({ - Normal: { - ['us-east-1']: 'ami-0440e4f6b9713faf6', - ['us-west-2']: 'ami-0a54c984b9f908c81', - ['eu-west-1']: 'ami-0c7a4976cb6fafd3a', - }, - GPUSupport: { - ['us-east-1']: 'ami-058bfb8c236caae89', - ['us-west-2']: 'ami-0731694d53ef9604b', - ['eu-west-1']: 'ami-0706dc8a5eed2eed9', - }, -}); - -/** - * The type of update to perform on instances in this AutoScalingGroup - */ -export enum UpdateType { - /** - * Don't do anything - */ - None = 'None', - - /** - * Replace the entire AutoScalingGroup - * - * Builds a new AutoScalingGroup first, then delete the old one. - */ - ReplacingUpdate = 'Replace', - - /** - * Replace the instances in the AutoScalingGroup. - */ - RollingUpdate = 'RollingUpdate', -} +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/test/MANUAL_TEST.md b/packages/@aws-cdk/aws-eks/test/MANUAL_TEST.md new file mode 100644 index 0000000000000..0673637b221fb --- /dev/null +++ b/packages/@aws-cdk/aws-eks/test/MANUAL_TEST.md @@ -0,0 +1,50 @@ +# 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/ +``` 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..d49626ab3ee3a --- /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.addWorkerNodes('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.ts b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.lit.ts new file mode 100644 index 0000000000000..125a45f3e79c8 --- /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.addWorkerNodes('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.eks.ts b/packages/@aws-cdk/aws-eks/test/test.eks.ts deleted file mode 100644 index 6f39992c61c69..0000000000000 --- a/packages/@aws-cdk/aws-eks/test/test.eks.ts +++ /dev/null @@ -1,616 +0,0 @@ -import { expect, haveResource } 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'); - -// tslint:disable:object-literal-key-quotes - -export = { - 'can create default cluster, no worker nodes'(test: Test) { - const stack = new cdk.Stack(undefined, '', { - env: { region: 'us-east-1', account: '123456' }, - }); - - new eks.Cluster(stack, 'MyCluster', { - vpc: exportedVpc(stack), - vpcPlacement: { - subnetsToUse: ec2.SubnetType.Public, - }, - }); - - expect(stack).toMatch({ - Resources: { - MyClusterClusterRole0A67D5C4: { - Type: 'AWS::IAM::Role', - Properties: { - AssumeRolePolicyDocument: { - Statement: [ - { - Action: 'sts:AssumeRole', - Effect: 'Allow', - Principal: { - Service: 'eks.amazonaws.com', - }, - }, - ], - Version: '2012-10-17', - }, - ManagedPolicyArns: [ - 'arn:aws:iam::aws:policy/AmazonEKSClusterPolicy', - 'arn:aws:iam::aws:policy/AmazonEKSServicePolicy', - ], - }, - }, - MyClusterClusterSecurityGroup17ACDE00: { - Type: 'AWS::EC2::SecurityGroup', - Properties: { - GroupDescription: 'Cluster API Server Security Group.', - SecurityGroupEgress: [ - { - CidrIp: '0.0.0.0/0', - Description: 'Allow all outbound traffic by default', - IpProtocol: '-1', - }, - ], - SecurityGroupIngress: [], - Tags: [ - { - Key: 'Name', - Value: 'Cluster SecurityGroup', - }, - { - Key: 'Description', - Value: 'The security group assigned to the cluster', - }, - ], - VpcId: 'test-vpc-1234', - }, - }, - MyCluster9CF8BB78: { - Type: 'AWS::EKS::Cluster', - Properties: { - ResourcesVpcConfig: { - SecurityGroupIds: [ - { - 'Fn::GetAtt': ['MyClusterClusterSecurityGroup17ACDE00', 'GroupId'], - }, - ], - SubnetIds: ['pub1'], - }, - RoleArn: { - 'Fn::GetAtt': ['MyClusterClusterRole0A67D5C4', 'Arn'], - }, - }, - }, - }, - }); - - test.done(); - }, - 'can create worker group, default settings'(test: Test) { - const stack = new cdk.Stack(undefined, '', { - env: { region: 'us-east-1', account: '123456' }, - }); - - const testVpc = exportedVpc(stack); - - const cl = new eks.Cluster(stack, 'MyCluster', { - vpc: testVpc, - vpcPlacement: { - subnetsToUse: ec2.SubnetType.Public, - }, - }); - - new eks.Nodes(stack, 'NodeGroup1', { - vpc: testVpc, - cluster: cl, - }); - - expect(stack).to( - haveResource('AWS::AutoScaling::AutoScalingGroup', { - MaxSize: '1', - MinSize: '1', - DesiredCapacity: '1', - LaunchConfigurationName: { - Ref: 'NodeGroup1NodeGroupm5largeLaunchConfig593791C7', - }, - Tags: [ - { - Key: 'Name', - PropagateAtLaunch: true, - Value: 'NodeGroup1/NodeGroup-m5.large', - }, - { - Key: { - 'Fn::Join': [ - '', - [ - 'kubernetes.io/cluster/', - { - Ref: 'MyCluster9CF8BB78', - }, - ], - ], - }, - PropagateAtLaunch: true, - Value: 'owned', - }, - ], - VPCZoneIdentifier: ['pub1'], - }), - ); - - expect(stack).to( - haveResource('AWS::AutoScaling::LaunchConfiguration', { - ImageId: 'ami-0440e4f6b9713faf6', - InstanceType: 'm5.large', - IamInstanceProfile: { - Ref: 'NodeGroup1NodeGroupm5largeInstanceProfileCED4339B', - }, - SecurityGroups: [ - { - 'Fn::GetAtt': ['NodeGroup1NodeGroupm5largeInstanceSecurityGroup68AD5F49', 'GroupId'], - }, - ], - UserData: { - 'Fn::Base64': { - 'Fn::Join': [ - '', - [ - '#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', - { - Ref: 'MyCluster9CF8BB78', - }, - ' --use-max-pods 29', - ], - ], - }, - }, - }), - ); - - expect(stack).to( - haveResource('AWS::IAM::Role', { - AssumeRolePolicyDocument: { - Statement: [ - { - Action: 'sts:AssumeRole', - Effect: 'Allow', - Principal: { - Service: 'ec2.amazonaws.com', - }, - }, - ], - Version: '2012-10-17', - }, - ManagedPolicyArns: [ - 'arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy', - 'arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy', - 'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly', - ], - }), - ); - - expect(stack).to( - haveResource('AWS::IAM::InstanceProfile', { - Roles: [ - { - Ref: 'NodeGroup1NodeGroupm5largeInstanceRoleA66DE8CA', - }, - ], - }), - ); - - expect(stack).to( - haveResource('AWS::EC2::SecurityGroup', { - GroupDescription: 'NodeGroup1/NodeGroup-m5.large/InstanceSecurityGroup', - SecurityGroupEgress: [ - { - CidrIp: '0.0.0.0/0', - Description: 'Allow all outbound traffic by default', - IpProtocol: '-1', - }, - ], - SecurityGroupIngress: [], - Tags: [ - { - Key: 'Name', - Value: 'NodeGroup1/NodeGroup-m5.large', - }, - { - Key: { - 'Fn::Join': [ - '', - [ - 'kubernetes.io/cluster/', - { - Ref: 'MyCluster9CF8BB78', - }, - ], - ], - }, - Value: 'owned', - }, - ], - VpcId: 'test-vpc-1234', - }), - ); - - test.done(); - }, - 'can create second worker group, custom settings'(test: Test) { - const stack = new cdk.Stack(undefined, '', { - env: { region: 'us-east-1', account: '123456' }, - }); - - const testVpc = exportedVpc(stack); - - const cl = new eks.Cluster(stack, 'MyCluster', { - vpc: testVpc, - vpcPlacement: { - subnetsToUse: ec2.SubnetType.Public, - }, - }); - - new eks.Nodes(stack, 'NodeGroup1', { - vpc: testVpc, - cluster: cl, - }); - - new eks.Nodes(stack, 'NodeGroup2', { - vpc: testVpc, - cluster: cl, - nodeClass: ec2.InstanceClass.T2, - nodeSize: ec2.InstanceSize.Medium, - nodeType: eks.NodeType.Normal, - minNodes: 2, - maxNodes: 4, - sshKeyName: 'aws-dev-key', - }); - - expect(stack).to( - haveResource('AWS::AutoScaling::AutoScalingGroup', { - MaxSize: '4', - MinSize: '2', - DesiredCapacity: '2', - LaunchConfigurationName: { - Ref: 'NodeGroup2NodeGroupt2mediumLaunchConfigA973C952', - }, - Tags: [ - { - Key: 'Name', - PropagateAtLaunch: true, - Value: 'NodeGroup2/NodeGroup-t2.medium', - }, - { - Key: { - 'Fn::Join': [ - '', - [ - 'kubernetes.io/cluster/', - { - Ref: 'MyCluster9CF8BB78', - }, - ], - ], - }, - PropagateAtLaunch: true, - Value: 'owned', - }, - ], - VPCZoneIdentifier: ['pub1'], - }), - ); - - expect(stack).to( - haveResource('AWS::AutoScaling::LaunchConfiguration', { - ImageId: 'ami-0440e4f6b9713faf6', - InstanceType: 't2.medium', - IamInstanceProfile: { - Ref: 'NodeGroup2NodeGroupt2mediumInstanceProfile32011E3A', - }, - KeyName: 'aws-dev-key', - SecurityGroups: [ - { - 'Fn::GetAtt': ['NodeGroup2NodeGroupt2mediumInstanceSecurityGroup5F0C1B74', 'GroupId'], - }, - ], - UserData: { - 'Fn::Base64': { - 'Fn::Join': [ - '', - [ - '#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', - { - Ref: 'MyCluster9CF8BB78', - }, - ' --use-max-pods 17', - ], - ], - }, - }, - }), - ); - - expect(stack).to( - haveResource('AWS::IAM::Role', { - AssumeRolePolicyDocument: { - Statement: [ - { - Action: 'sts:AssumeRole', - Effect: 'Allow', - Principal: { - Service: 'ec2.amazonaws.com', - }, - }, - ], - Version: '2012-10-17', - }, - ManagedPolicyArns: [ - 'arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy', - 'arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy', - 'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly', - ], - }), - ); - - expect(stack).to( - haveResource('AWS::IAM::InstanceProfile', { - Roles: [ - { - Ref: 'NodeGroup2NodeGroupt2mediumInstanceRoleFDAFEE3C', - }, - ], - }), - ); - - expect(stack).to( - haveResource('AWS::EC2::SecurityGroup', { - GroupDescription: 'NodeGroup1/NodeGroup-m5.large/InstanceSecurityGroup', - SecurityGroupEgress: [ - { - CidrIp: '0.0.0.0/0', - Description: 'Allow all outbound traffic by default', - IpProtocol: '-1', - }, - ], - SecurityGroupIngress: [], - Tags: [ - { - Key: 'Name', - Value: 'NodeGroup1/NodeGroup-m5.large', - }, - { - Key: { - 'Fn::Join': [ - '', - [ - 'kubernetes.io/cluster/', - { - Ref: 'MyCluster9CF8BB78', - }, - ], - ], - }, - Value: 'owned', - }, - ], - VpcId: 'test-vpc-1234', - }), - ); - - expect(stack).to( - haveResource('AWS::EC2::SecurityGroup', { - GroupDescription: 'NodeGroup2/NodeGroup-t2.medium/InstanceSecurityGroup', - SecurityGroupEgress: [ - { - CidrIp: '0.0.0.0/0', - Description: 'Allow all outbound traffic by default', - IpProtocol: '-1', - }, - ], - SecurityGroupIngress: [], - Tags: [ - { - Key: 'Name', - Value: 'NodeGroup2/NodeGroup-t2.medium', - }, - { - Key: { - 'Fn::Join': [ - '', - [ - 'kubernetes.io/cluster/', - { - Ref: 'MyCluster9CF8BB78', - }, - ], - ], - }, - Value: 'owned', - }, - ], - VpcId: 'test-vpc-1234', - }), - ); - - test.done(); - }, - 'can export cluster'(test: Test) { - const stack = new cdk.Stack(undefined, 'TestStack', { - env: { region: 'us-east-1', account: '123456' }, - }); - - const testVpc = exportedVpc(stack); - - const cl = new eks.Cluster(stack, 'MyCluster', { - vpc: testVpc, - vpcPlacement: { - subnetsToUse: ec2.SubnetType.Public, - }, - }); - - const exportedCluster = cl.export(); - - new eks.Nodes(stack, 'NodeGroup1', { - vpc: testVpc, - cluster: cl, - }); - - const stack2 = new cdk.Stack(undefined, 'TestStack2', { - env: { region: 'us-east-1', account: '123456' }, - }); - - const testVpc2 = exportedVpc(stack2); - const importedCluster = eks.ClusterRef.import(stack2, 'importedCluster', exportedCluster); - - new eks.Nodes(stack2, 'NodeGroup2', { - vpc: testVpc2, - cluster: importedCluster, - nodeClass: ec2.InstanceClass.P3, - nodeSize: ec2.InstanceSize.Large, - nodeType: eks.NodeType.Normal, - minNodes: 4, - maxNodes: 8, - sshKeyName: 'aws-dev-key', - tags: { ['Environment']: 'test' }, - }); - - expect(stack2).to( - haveResource('AWS::AutoScaling::AutoScalingGroup', { - MaxSize: '8', - MinSize: '4', - DesiredCapacity: '4', - LaunchConfigurationName: { - Ref: 'NodeGroup2NodeGroupp3largeLaunchConfigE29DC643', - }, - Tags: [ - { - Key: 'Environment', - Value: 'test', - }, - { - Key: 'Name', - PropagateAtLaunch: true, - Value: 'NodeGroup2/NodeGroup-p3.large', - }, - { - Key: { - 'Fn::Join': [ - '', - [ - 'kubernetes.io/cluster/', - { - 'Fn::ImportValue': 'TestStack:MyClusterClusterName37E9AAAB', - }, - ], - ], - }, - PropagateAtLaunch: true, - Value: 'owned', - }, - ], - VPCZoneIdentifier: ['pub1'], - }), - ); - - expect(stack2).to( - haveResource('AWS::AutoScaling::LaunchConfiguration', { - ImageId: 'ami-0440e4f6b9713faf6', - InstanceType: 'p3.large', - IamInstanceProfile: { - Ref: 'NodeGroup2NodeGroupp3largeInstanceProfileA6647AEB', - }, - KeyName: 'aws-dev-key', - SecurityGroups: [ - { - 'Fn::GetAtt': ['NodeGroup2NodeGroupp3largeInstanceSecurityGroupEDB7AF89', 'GroupId'], - }, - ], - UserData: { - 'Fn::Base64': { - 'Fn::Join': [ - '', - [ - '#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', - { - 'Fn::ImportValue': 'TestStack:MyClusterClusterName37E9AAAB', - }, - ' --use-max-pods undefined', - ], - ], - }, - }, - }), - ); - - expect(stack2).to( - haveResource('AWS::IAM::Role', { - AssumeRolePolicyDocument: { - Statement: [ - { - Action: 'sts:AssumeRole', - Effect: 'Allow', - Principal: { - Service: 'ec2.amazonaws.com', - }, - }, - ], - Version: '2012-10-17', - }, - ManagedPolicyArns: [ - 'arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy', - 'arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy', - 'arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly', - ], - }), - ); - - expect(stack2).to( - haveResource('AWS::IAM::InstanceProfile', { - Roles: [ - { - Ref: 'NodeGroup2NodeGroupp3largeInstanceRole480076E3', - }, - ], - }), - ); - - expect(stack).to( - haveResource('AWS::EC2::SecurityGroup', { - GroupDescription: 'Cluster API Server Security Group.', - SecurityGroupEgress: [ - { - CidrIp: '0.0.0.0/0', - Description: 'Allow all outbound traffic by default', - IpProtocol: '-1', - }, - ], - SecurityGroupIngress: [], - Tags: [ - { - Key: 'Name', - Value: 'Cluster SecurityGroup', - }, - { - Key: 'Description', - Value: 'The security group assigned to the cluster', - }, - ], - VpcId: 'test-vpc-1234', - }), - ); - - test.done(); - }, -}; - -function exportedVpc(stack: cdk.Stack) { - return ec2.VpcNetwork.import(stack, 'TestVpc', { - vpcId: 'test-vpc-1234', - availabilityZones: ['us-east-1d'], - publicSubnetIds: ['pub1'], - isolatedSubnetIds: [], - }); -} From 68528774e18b42ee7c0b3f513609738cb878edb7 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 1 Feb 2019 17:29:44 +0100 Subject: [PATCH 03/10] Get rid of EKS cluster example, the integ test does it all --- .../eks-cluster/index.ts | 99 ------------------- 1 file changed, 99 deletions(-) delete mode 100644 examples/cdk-examples-typescript/eks-cluster/index.ts diff --git a/examples/cdk-examples-typescript/eks-cluster/index.ts b/examples/cdk-examples-typescript/eks-cluster/index.ts deleted file mode 100644 index 4621cc684fa85..0000000000000 --- a/examples/cdk-examples-typescript/eks-cluster/index.ts +++ /dev/null @@ -1,99 +0,0 @@ -import ec2 = require("@aws-cdk/aws-ec2"); -import eks = require("@aws-cdk/aws-eks"); -import cdk = require("@aws-cdk/cdk"); - -const ENV = "dev"; -const app = new cdk.App(); - -/** - * Ths stack creates the VPC and network for the cluster - * - * @default single public subnet per availability zone (3) - * This creates three (3) total subnets with an Internet Gateway - * The subnets could be private with a Nat Gateway - * they must not be isolated, as instances later need to - * have outbound internet access to contact the API Server - */ -const networkStack = new cdk.Stack(app, "Network"); - -const vpc = new ec2.VpcNetwork(networkStack, "VPC", { - cidr: "10.244.0.0/16", - maxAZs: 3, - natGateways: 0, - subnetConfiguration: [ - { - name: "pub", - cidrMask: 24, - subnetType: ec2.SubnetType.Public - } - ], - tags: { - env: `${ENV}` - } -}); -const vpcExport = vpc.export(); - -/** - * This stack creates the EKS Cluster with the imported VPC - * above, and puts the cluster inside the chosen placement - * - * clusterName can be set (not recommended), let cfn generate - * version can be specified, only 1.10 supported now - * will become useful when more versions are supported - * - * It also creates a group of 3 worker nodes with default types - * and given min, max and sshKeys - */ -const clusterStack = new cdk.Stack(app, "Cluster"); - -const clusterVpc = ec2.VpcNetworkRef.import( - clusterStack, - "ClusterVpc", - vpcExport -); -const cluster = new eks.Cluster(clusterStack, "Cluster", { - vpc: clusterVpc, - vpcPlacement: { - subnetsToUse: ec2.SubnetType.Public - } -}); - -/** - * This is optional and should be more specific to given - * corparate CIDRS for access from the outside, maybe - * even a bastion host inside AWS. - */ -cluster.connections.allowFromAnyIPv4(new ec2.TcpPort(443)); - -const grp1 = new eks.Nodes(clusterStack, "NodeGroup1", { - vpc: clusterVpc, - cluster, - minNodes: 3, - maxNodes: 6, - sshKeyName: "aws-dev-key" -}); -grp1.nodeGroup.connections.allowFromAnyIPv4(new ec2.TcpPort(22)); - -/** - * This adds a second group of worker nodes of different - * InstanceClass and InstanceSize - * This gets pushed into an Array of Nodes - */ -const grp2 = new eks.Nodes(clusterStack, "NodeGroup2", { - vpc: clusterVpc, - cluster, - nodeClass: ec2.InstanceClass.T2, - nodeSize: ec2.InstanceSize.Medium, - nodeType: eks.NodeType.Normal, - minNodes: 2, - maxNodes: 4, - sshKeyName: "aws-dev-key" -}); -/** - * This is optional and should be more specific to given - * corparate CIDRS for access from the outside, maybe - * even a bastion host inside AWS. - */ -grp2.nodeGroup.connections.allowFromAnyIPv4(new ec2.TcpPort(22)); - -app.run(); From f37065c860d0ce2a96186114647c2dcafb1bd1db Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 8 Feb 2019 10:49:36 +0100 Subject: [PATCH 04/10] BasicAutoScalingGroupProps => CommonAutoScalingGroupProps --- packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts | 4 ++-- packages/@aws-cdk/aws-ecs/lib/cluster.ts | 2 +- packages/@aws-cdk/aws-eks/lib/cluster.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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 3a37987c49dc4..8397f1507e6ce 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -23,7 +23,7 @@ const NAME_TAG: string = 'Name'; * Constructs that want to create AutoScalingGroups can inherit * this interface and specialize the essential parts in various ways. */ -export interface BasicAutoScalingGroupProps { +export interface CommonAutoScalingGroupProps { /** * Minimum number of instances in the fleet * @@ -149,7 +149,7 @@ export interface BasicAutoScalingGroupProps { /** * Properties of a Fleet */ -export interface AutoScalingGroupProps extends BasicAutoScalingGroupProps { +export interface AutoScalingGroupProps extends CommonAutoScalingGroupProps { /** * VPC to launch these instances in. */ diff --git a/packages/@aws-cdk/aws-ecs/lib/cluster.ts b/packages/@aws-cdk/aws-ecs/lib/cluster.ts index abad1a6d132df..3db14586b1508 100644 --- a/packages/@aws-cdk/aws-ecs/lib/cluster.ts +++ b/packages/@aws-cdk/aws-ecs/lib/cluster.ts @@ -349,7 +349,7 @@ export interface AddAutoScalingGroupCapacityOptions { /** * Properties for adding autoScalingGroup */ -export interface AddDefaultAutoScalingGroupOptions extends AddAutoScalingGroupCapacityOptions, autoscaling.BasicAutoScalingGroupProps { +export interface AddDefaultAutoScalingGroupOptions extends AddAutoScalingGroupCapacityOptions, autoscaling.CommonAutoScalingGroupProps { /** * The type of EC2 instance to launch into your Autoscaling Group */ diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index 3f712d1d39c9b..84fbf1ddaf87b 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -275,7 +275,7 @@ function isTaggableSubnet(subnet: ec2.IVpcSubnet): subnet is ec2.VpcSubnet { /** * Options for adding worker nodes */ -export interface AddWorkerNodesOptions extends autoscaling.BasicAutoScalingGroupProps { +export interface AddWorkerNodesOptions extends autoscaling.CommonAutoScalingGroupProps { /** * Instance type of the instances to start */ From af00ae0a09c063b89070db4d5525ea4c00ab4744 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 8 Feb 2019 11:32:24 +0100 Subject: [PATCH 05/10] Review comments --- packages/@aws-cdk/aws-eks/README.md | 2 +- packages/@aws-cdk/aws-eks/lib/ami.ts | 6 ++-- packages/@aws-cdk/aws-eks/lib/cluster.ts | 36 +++++++++++++------ .../test/example.ssh-into-nodes.lit.ts | 2 +- .../aws-eks/test/integ.eks-cluster.lit.ts | 2 +- 5 files changed, 32 insertions(+), 16 deletions(-) diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index 9b61ba6054a74..281d5abe1015d 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -20,7 +20,7 @@ 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 exampled](test/example.ssh-into-nodes.lit.ts) +[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 diff --git a/packages/@aws-cdk/aws-eks/lib/ami.ts b/packages/@aws-cdk/aws-eks/lib/ami.ts index 182a6566b6569..52c7516f09083 100644 --- a/packages/@aws-cdk/aws-eks/lib/ami.ts +++ b/packages/@aws-cdk/aws-eks/lib/ami.ts @@ -13,6 +13,8 @@ export interface EksOptimizedAmiProps { /** * The Kubernetes version to use + * + * @default The latest version */ kubernetesVersion?: string; } @@ -44,7 +46,7 @@ export const enum NodeType { /** * GPU instances */ - GPU = 'GPUSupport', + GPU = 'GPU', } export function nodeTypeForInstanceType(instanceType: ec2.InstanceType) { @@ -58,7 +60,7 @@ export function nodeTypeForInstanceType(instanceType: ec2.InstanceType) { * * @see https://docs.aws.amazon.com/eks/latest/userguide/eks-optimized-ami.html */ -export const EKS_AMI: {[version: string]: {[type: string]: {[region: string]: string}}} = { +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 diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index 84fbf1ddaf87b..f81fe71320e6e 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -21,6 +21,14 @@ export interface ClusterProps { * * 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[]; @@ -145,7 +153,6 @@ export class Cluster extends ClusterBase { vpc: props.vpc, description: 'EKS Control Plane Security Group', }); - // FIXME: Tag Security Group as soon as we can w/: "kubernetes.io/cluster/${ClusterName}": "owned" this.connections = new ec2.Connections({ securityGroups: [securityGroup], @@ -166,6 +173,10 @@ export class Cluster extends ClusterBase { } }); + if (ec2.SecurityGroupBase.isSecurityGroup(securityGroup)) { + securityGroup.apply(new cdk.Tag(`kubernetes.io/cluster/${resource.clusterName}`, "owned")); + } + this.clusterName = resource.clusterName; this.clusterArn = resource.clusterArn; this.clusterEndpoint = resource.clusterEndpoint; @@ -180,7 +191,7 @@ export class Cluster extends ClusterBase { * The nodes will automatically be configured with the right VPC and AMI * for the instance type and Kubernetes version. */ - public addWorkerNodes(id: string, options: AddWorkerNodesOptions): autoscaling.AutoScalingGroup { + public addCapacity(id: string, options: AddWorkerNodesOptions): autoscaling.AutoScalingGroup { const asg = new autoscaling.AutoScalingGroup(this, id, { ...options, vpc: this.vpc, @@ -203,9 +214,14 @@ export class Cluster extends ClusterBase { * 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. + * /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. * - * Prefer to use `addWorkerNodes` if possible. + * @see https://docs.aws.amazon.com/eks/latest/userguide/launch-workers.html */ public addAutoScalingGroup(autoScalingGroup: autoscaling.AutoScalingGroup, options: AddAutoScalingGroupOptions) { // self rules @@ -235,9 +251,7 @@ export class Cluster extends ClusterBase { autoScalingGroup.role.attachManagedPolicy(new iam.AwsManagedPolicy('AmazonEC2ContainerRegistryReadOnly', this).policyArn); // EKS Required Tags - autoScalingGroup.tags.setTag(`kubernetes.io/cluster/${this.clusterName}`, 'owned', { - propagate: true, - }); + 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', { @@ -257,19 +271,19 @@ export class Cluster extends ClusterBase { const privates = this.vpc.subnets({ subnetsToUse: ec2.SubnetType.Private }); for (const subnet of privates) { - if (!isTaggableSubnet(subnet)) { + 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.tags.setTag("kubernetes.io/role/internal-elb", "1"); + subnet.apply(new cdk.Tag("kubernetes.io/role/internal-elb", "1")); } } } -function isTaggableSubnet(subnet: ec2.IVpcSubnet): subnet is ec2.VpcSubnet { - return (subnet as any).tags !== undefined; +function isRealSubnetConstruct(subnet: ec2.IVpcSubnet): subnet is ec2.VpcSubnet { + return (subnet as any).addDefaultRouteToIGW !== undefined; } /** 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 index d49626ab3ee3a..0784d1aca03f5 100644 --- 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 @@ -13,7 +13,7 @@ class EksClusterStack extends cdk.Stack { }); /// !show - const asg = cluster.addWorkerNodes('Nodes', { + const asg = cluster.addCapacity('Nodes', { instanceType: new ec2.InstanceType('t2.medium'), vpcPlacement: { subnetsToUse: ec2.SubnetType.Public }, keyName: 'my-key-name', 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 index 125a45f3e79c8..611bab4f7cce3 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.lit.ts +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.lit.ts @@ -13,7 +13,7 @@ class EksClusterStack extends cdk.Stack { vpc }); - cluster.addWorkerNodes('Nodes', { + cluster.addCapacity('Nodes', { instanceType: new ec2.InstanceType('t2.medium'), desiredCapacity: 1, // Raise this number to add more nodes }); From 7bab6d4402bb6a99ad02f29da1a7973c4a4381cc Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 8 Feb 2019 11:52:47 +0100 Subject: [PATCH 06/10] Fix static checks --- packages/@aws-cdk/aws-eks/lib/cluster-base.ts | 5 +++++ packages/@aws-cdk/aws-eks/lib/cluster.ts | 16 ++++++++-------- packages/@aws-cdk/aws-eks/package.json | 3 +++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-base.ts b/packages/@aws-cdk/aws-eks/lib/cluster-base.ts index 65ed106f20859..357fbf72a87f9 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-base.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-base.ts @@ -30,6 +30,11 @@ export interface ICluster extends cdk.IConstruct, ec2.IConnectable { * The certificate-authority-data for your cluster. */ readonly clusterCertificateAuthorityData: string; + + /** + * Export cluster references to use in other stacks + */ + export(): ClusterImportProps; } /** diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index f81fe71320e6e..98a5a2db6a3a6 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -72,12 +72,12 @@ export class Cluster extends ClusterBase { /** * Import an existing cluster * - * @param parent the construct parent, in most cases 'this' + * @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(parent: cdk.Construct, id: string, props: ClusterImportProps): ICluster { - return new ImportedCluster(parent, id, props); + public static import(scope: cdk.Construct, id: string, props: ClusterImportProps): ICluster { + return new ImportedCluster(scope, id, props); } /** @@ -129,12 +129,12 @@ export class Cluster extends ClusterBase { /** * Initiates an EKS Cluster with the supplied arguments * - * @param parent a Construct, most likely a cdk.Stack created + * @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(parent: cdk.Construct, name: string, props: ClusterProps) { - super(parent, name); + constructor(scope: cdk.Construct, id: string, props: ClusterProps) { + super(scope, id); this.vpc = props.vpc; this.version = props.version; @@ -320,8 +320,8 @@ class ImportedCluster extends ClusterBase { public readonly clusterEndpoint: string; public readonly connections = new ec2.Connections(); - constructor(parent: cdk.Construct, name: string, props: ClusterImportProps) { - super(parent, name); + constructor(scope: cdk.Construct, id: string, props: ClusterImportProps) { + super(scope, id); this.vpc = ec2.VpcNetwork.import(this, "VPC", props.vpc); this.clusterName = props.clusterName; diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index fbbe99697366a..2daa69fdbfcd0 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -68,6 +68,9 @@ }, "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": { From cfcffb95511e57f60c83bc6fe2c8d2052808334f Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 8 Feb 2019 12:54:34 +0100 Subject: [PATCH 07/10] Don't fail GenericLinuxImage in integ test mode --- packages/@aws-cdk/aws-ec2/lib/machine-image.ts | 2 +- packages/@aws-cdk/aws-eks/lib/cluster.ts | 4 ---- packages/@aws-cdk/aws-eks/test/MANUAL_TEST.md | 8 ++++++++ 3 files changed, 9 insertions(+), 5 deletions(-) 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-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index 98a5a2db6a3a6..406d30bd704aa 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -173,10 +173,6 @@ export class Cluster extends ClusterBase { } }); - if (ec2.SecurityGroupBase.isSecurityGroup(securityGroup)) { - securityGroup.apply(new cdk.Tag(`kubernetes.io/cluster/${resource.clusterName}`, "owned")); - } - this.clusterName = resource.clusterName; this.clusterArn = resource.clusterArn; this.clusterEndpoint = resource.clusterEndpoint; diff --git a/packages/@aws-cdk/aws-eks/test/MANUAL_TEST.md b/packages/@aws-cdk/aws-eks/test/MANUAL_TEST.md index 0673637b221fb..463e4c5f2a0b3 100644 --- a/packages/@aws-cdk/aws-eks/test/MANUAL_TEST.md +++ b/packages/@aws-cdk/aws-eks/test/MANUAL_TEST.md @@ -48,3 +48,11 @@ 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 + +``` From c4eeef2c97fc2db24035ee09692aae6ec5bc7561 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 8 Feb 2019 13:36:37 +0100 Subject: [PATCH 08/10] Add tests --- .../test/integ.eks-cluster.lit.expected.json | 919 ++++++++++++++++++ .../@aws-cdk/aws-eks/test/test.cluster.ts | 116 +++ 2 files changed, 1035 insertions(+) create mode 100644 packages/@aws-cdk/aws-eks/test/integ.eks-cluster.lit.expected.json create mode 100644 packages/@aws-cdk/aws-eks/test/test.cluster.ts 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/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts new file mode 100644 index 0000000000000..b3d908f41b55c --- /dev/null +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -0,0 +1,116 @@ +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(); + }, +}; + +function testFixture(): [cdk.Stack, ec2.VpcNetwork] { + const stack = new cdk.Stack(undefined, undefined, { env: { region: 'us-east-1' }}); + const vpc = new ec2.VpcNetwork(stack, 'VPC'); + + return [stack, vpc]; +} From 1416768bc2e3c6662602c5e7428d6f1017676e0a Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 8 Feb 2019 14:50:17 +0100 Subject: [PATCH 09/10] Update more ECS tests --- packages/@aws-cdk/aws-ecs/test/ec2/integ.event-task.lit.ts | 2 +- .../@aws-cdk/aws-ecs/test/ec2/test.ec2-event-rule-target.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/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 +}; From dc6d4df363153a8d847ed9cc47c01527a99cca5e Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Fri, 8 Feb 2019 16:04:22 +0100 Subject: [PATCH 10/10] Exercise export to make coverage checker happy --- packages/@aws-cdk/aws-eks/lib/cluster-base.ts | 2 +- .../@aws-cdk/aws-eks/test/test.cluster.ts | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-base.ts b/packages/@aws-cdk/aws-eks/lib/cluster-base.ts index 357fbf72a87f9..9de91498e56c4 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-base.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-base.ts @@ -54,7 +54,7 @@ export abstract class ClusterBase extends cdk.Construct implements ICluster { public export(): ClusterImportProps { return { vpc: this.vpc.export(), - clusterName: this.makeOutput('ClusterName', this.clusterName), + clusterName: this.makeOutput('ClusterNameExport', this.clusterName), clusterArn: this.makeOutput('ClusterArn', this.clusterArn), clusterEndpoint: this.makeOutput('ClusterEndpoint', this.clusterEndpoint), clusterCertificateAuthorityData: this.makeOutput('ClusterCAData', this.clusterCertificateAuthorityData), diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index b3d908f41b55c..38f0d8924f547 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -106,10 +106,27 @@ export = { 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, undefined, { env: { region: 'us-east-1' }}); + const stack = new cdk.Stack(undefined, 'Stack', { env: { region: 'us-east-1' }}); const vpc = new ec2.VpcNetwork(stack, 'VPC'); return [stack, vpc];