diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index 6330f7f837aae..38707268ca79f 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -69,8 +69,8 @@ new eks.Cluster(this, 'cluster', { defaultCapacityInstance: new ec2.InstanceType('m2.xlarge') }); ``` -To disable the default capacity, simply set `defaultCapacity` to `0`: +To disable the default capacity, simply set `defaultCapacity` to `0`: ```ts new eks.Cluster(this, 'cluster-with-no-capacity', { defaultCapacity: 0 }); @@ -121,13 +121,56 @@ new eks.Cluster(this, 'Cluster', { }); ``` -Now, given AWS credentials for a user that is trusted by the masters role, you -will be able to interact with your cluster like this: +When you `cdk deploy` this CDK app, you will notice that an output will be printed +with the `update-kubeconfig` command. + +Something like this: + +``` +Outputs: +eks-integ-defaults.ClusterConfigCommand43AAE40F = aws eks update-kubeconfig --name cluster-ba7c166b-c4f3-421c-bf8a-6812e4036a33 --role-arn arn:aws:iam::112233445566:role/eks-integ-defaults-Role1ABCC5F0-1EFK2W5ZJD98Y +``` + +Copy & paste the "`aws eks update-kubeconfig ...`" command to your shell in +order to connect to your EKS cluster with the "masters" role. + +Now, given [AWS CLI](https://aws.amazon.com/cli/) is configured to use AWS +credentials for a user that is trusted by the masters role, you should be able +to interact with your cluster through `kubectl` (the above example will trust +all users in the account). + +For example: ```console -$ aws eks update-kubeconfig --name CLUSTER-NAME +$ aws eks update-kubeconfig --name cluster-ba7c166b-c4f3-421c-bf8a-6812e4036a33 --role-arn arn:aws:iam::112233445566:role/eks-integ-defaults-Role1ABCC5F0-1EFK2W5ZJD98Y +Added new context arn:aws:eks:eu-west-2:112233445566:cluster/cluster-ba7c166b-c4f3-421c-bf8a-6812e4036a33 to /Users/boom/.kube/config + +$ kubectl get nodes # list all nodes +NAME STATUS ROLES AGE VERSION +ip-10-0-147-66.eu-west-2.compute.internal Ready 21m v1.13.7-eks-c57ff8 +ip-10-0-169-151.eu-west-2.compute.internal Ready 21m v1.13.7-eks-c57ff8 + $ kubectl get all -n kube-system -... +NAME READY STATUS RESTARTS AGE +pod/aws-node-fpmwv 1/1 Running 0 21m +pod/aws-node-m9htf 1/1 Running 0 21m +pod/coredns-5cb4fb54c7-q222j 1/1 Running 0 23m +pod/coredns-5cb4fb54c7-v9nxx 1/1 Running 0 23m +pod/kube-proxy-d4jrh 1/1 Running 0 21m +pod/kube-proxy-q7hh7 1/1 Running 0 21m + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +service/kube-dns ClusterIP 172.20.0.10 53/UDP,53/TCP 23m + +NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE +daemonset.apps/aws-node 2 2 2 2 2 23m +daemonset.apps/kube-proxy 2 2 2 2 2 23m + +NAME READY UP-TO-DATE AVAILABLE AGE +deployment.apps/coredns 2/2 2 2 23m + +NAME DESIRED CURRENT READY AGE +replicaset.apps/coredns-5cb4fb54c7 2 2 2 23m ``` For your convenience, an AWS CloudFormation output will automatically be @@ -274,7 +317,7 @@ not have administrative privileges on the EKS cluster. if you wish to be able to manually interact with your cluster, you will need to map an IAM role or user to the `system:masters` group. This can be either done by specifying a `mastersRole` when the cluster is defined, calling - `cluster.addMastersRole` or explicitly mapping an IAM role or IAM user to the + `cluster.awsAuth.addMastersRole` or explicitly mapping an IAM role or IAM user to the relevant Kubernetes RBAC groups using `cluster.addRoleMapping` and/or `cluster.addUserMapping`. @@ -293,12 +336,13 @@ and a new cluster to be created. When kubectl is disabled, you should be aware of the following: -1. When you log-in to your cluster, you don't need to specify `--role-arn` as long as you are using the same user that created - the cluster. +1. When you log-in to your cluster, you don't need to specify `--role-arn` as + long as you are using the same user that created the cluster. 2. As described in the Amazon EKS User Guide, you will need to manually - edit the [aws-auth ConfigMap](https://docs.aws.amazon.com/eks/latest/userguide/add-user-role.html) when you add capacity in order to map - the IAM instance role to RBAC to allow nodes to join the cluster. -3. Any `eks.Cluster` APIs that depend on programmatic kubectl support will fail with an error: `cluster.addResource`, `cluster.awsAuth`, `props.mastersRole`. + edit the [aws-auth ConfigMap](https://docs.aws.amazon.com/eks/latest/userguide/add-user-role.html) + when you add capacity in order to map the IAM instance role to RBAC to allow nodes to join the cluster. +3. Any `eks.Cluster` APIs that depend on programmatic kubectl support will fail + with an error: `cluster.addResource`, `cluster.awsAuth`, `props.mastersRole`. ### Roadmap diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index 55231016fbce9..753746d3b66d6 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -196,6 +196,31 @@ export interface ClusterProps { * @default m5.large */ readonly defaultCapacityInstance?: ec2.InstanceType; + + /** + * Determines whether a CloudFormation output with the name of the cluster + * will be synthesized. + * + * @default false + */ + readonly outputClusterName?: boolean; + + /** + * Determines whether a CloudFormation output with the ARN of the "masters" + * IAM role will be synthesized (if `mastersRole` is specified). + * + * @default false + */ + readonly outputMastersRoleArn?: boolean; + + /** + * Determines whether a CloudFormation output with the `aws eks + * update-kubeconfig` command will be synthesized. This command will include + * the cluster name and, if applicable, the ARN of the masters IAM role. + * + * @default true + */ + readonly outputConfigCommand?: boolean; } /** @@ -362,7 +387,11 @@ export class Cluster extends Resource implements ICluster { this.clusterEndpoint = resource.attrEndpoint; this.clusterCertificateAuthorityData = resource.attrCertificateAuthorityData; - new CfnOutput(this, 'ClusterName', { value: this.clusterName }); + let configCommand = `aws eks update-kubeconfig --name ${this.clusterName}`; + + if (props.outputClusterName) { + new CfnOutput(this, 'ClusterName', { value: this.clusterName }); + } // we maintain a single manifest custom resource handler per cluster since // permissions and role are scoped. This will return `undefined` if kubectl @@ -376,6 +405,12 @@ export class Cluster extends Resource implements ICluster { } this.awsAuth.addMastersRole(props.mastersRole); + + if (props.outputMastersRoleArn) { + new CfnOutput(this, 'MastersRoleArn', { value: props.mastersRole.roleArn }); + } + + configCommand += ` --role-arn ${props.mastersRole.roleArn}`; } // allocate default capacity if non-zero (or default). @@ -384,6 +419,11 @@ export class Cluster extends Resource implements ICluster { const instanceType = props.defaultCapacityInstance || DEFAULT_CAPACITY_TYPE; this.defaultCapacity = this.addCapacity('DefaultCapacity', { instanceType, desiredCapacity }); } + + const outputConfigCommand = props.outputConfigCommand === undefined ? true : props.outputConfigCommand; + if (outputConfigCommand) { + new CfnOutput(this, 'ConfigCommand', { value: configCommand }); + } } /** @@ -456,11 +496,6 @@ export class Cluster extends Resource implements ICluster { // EKS Required Tags autoScalingGroup.node.applyAspect(new Tag(`kubernetes.io/cluster/${this.clusterName}`, 'owned', { applyToLaunchedInstances: true })); - // Create an CfnOutput for the Instance Role ARN (need to paste it into aws-auth-cm.yaml) - new CfnOutput(autoScalingGroup, 'InstanceRoleARN', { - value: autoScalingGroup.role.roleArn - }); - if (options.mapRole === true && !this.kubectlEnabled) { throw new Error(`Cannot map instance IAM role to RBAC if kubectl is disabled for the cluster`); } @@ -477,6 +512,12 @@ export class Cluster extends Resource implements ICluster { 'system:nodes' ] }); + } else { + // since we are not mapping the instance role to RBAC, synthesize an + // output so it can be pasted into `aws-auth-cm.yaml` + new CfnOutput(autoScalingGroup, 'InstanceRoleARN', { + value: autoScalingGroup.role.roleArn + }); } } diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index 913550edb6eaf..0c22a0e61db57 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -88,5 +88,10 @@ "engines": { "node": ">= 8.10.0" }, + "awslint": { + "exclude": [ + "props-no-arn-refs:@aws-cdk/aws-eks.ClusterProps.outputMastersRoleArn" + ] + }, "stability": "experimental" -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.defaults.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.defaults.expected.json index 1a35f00a8133e..4b814f1f5ab6b 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.defaults.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.defaults.expected.json @@ -1192,16 +1192,16 @@ } }, "Outputs": { - "ClusterClusterNameEB26049E": { + "ClusterConfigCommand43AAE40F": { "Value": { - "Ref": "Cluster9EE0221C" - } - }, - "ClusterDefaultCapacityInstanceRoleARN7DADF219": { - "Value": { - "Fn::GetAtt": [ - "ClusterDefaultCapacityInstanceRole3E209969", - "Arn" + "Fn::Join": [ + "", + [ + "aws eks update-kubeconfig --name ", + { + "Ref": "Cluster9EE0221C" + } + ] ] } } diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.expected.json index 26e721325c34c..176d61e568687 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.kubectl-disabled.expected.json @@ -912,9 +912,17 @@ } }, "Outputs": { - "EKSClusterClusterName2B056109": { + "EKSClusterConfigCommand3809C9C9": { "Value": { - "Ref": "EKSClusterBA6ECF8F" + "Fn::Join": [ + "", + [ + "aws eks update-kubeconfig --name ", + { + "Ref": "EKSClusterBA6ECF8F" + } + ] + ] } }, "EKSClusterNodesInstanceRoleARN10992C84": { 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 index fa34edfc46a59..2f972e36ad11d 100644 --- 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 @@ -1192,16 +1192,16 @@ } }, "Outputs": { - "EKSClusterClusterName2B056109": { + "EKSClusterConfigCommand3809C9C9": { "Value": { - "Ref": "EKSClusterE11008B6" - } - }, - "EKSClusterNodesInstanceRoleARN10992C84": { - "Value": { - "Fn::GetAtt": [ - "EKSClusterNodesInstanceRoleEE5595D6", - "Arn" + "Fn::Join": [ + "", + [ + "aws eks update-kubeconfig --name ", + { + "Ref": "EKSClusterE11008B6" + } + ] ] } } diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-kubectl.lit.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-kubectl.lit.expected.json index 7c43f8ba75cce..3ce1f6525ccd3 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-kubectl.lit.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-kubectl.lit.expected.json @@ -1118,16 +1118,16 @@ } }, "Outputs": { - "cluster22ClusterNameC4896469": { + "cluster22ConfigCommand96B20279": { "Value": { - "Ref": "cluster227BD1CB20" - } - }, - "cluster22NodesInstanceRoleARNEE60B01D": { - "Value": { - "Fn::GetAtt": [ - "cluster22NodesInstanceRole51CD052F", - "Arn" + "Fn::Join": [ + "", + [ + "aws eks update-kubeconfig --name ", + { + "Ref": "cluster227BD1CB20" + } + ] ] } } diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index 391f77d9b49dd..5cf3dd7655120 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -327,6 +327,129 @@ export = { // THEN expect(stack).to(not(haveResource(KubernetesResource.RESOURCE_TYPE))); test.done(); + }, + + 'outputs': { + 'aws eks update-kubeconfig is the only output synthesized by default'(test: Test) { + // GIVEN + const { app, stack } = testFixtureNoVpc(); + + // WHEN + new eks.Cluster(stack, 'Cluster'); + + // THEN + const assembly = app.synth(); + const template = assembly.getStack(stack.stackName).template; + test.deepEqual(template.Outputs, { + ClusterConfigCommand43AAE40F: { + Value: { 'Fn::Join': [ '', [ 'aws eks update-kubeconfig --name ', { Ref: 'Cluster9EE0221C' } ] ] } + } + }); + test.done(); + }, + + 'if masters role is defined, it should be included in the config command'(test: Test) { + // GIVEN + const { app, stack } = testFixtureNoVpc(); + + // WHEN + const mastersRole = new iam.Role(stack, 'masters', { assumedBy: new iam.AccountRootPrincipal() }); + new eks.Cluster(stack, 'Cluster', { mastersRole }); + + // THEN + const assembly = app.synth(); + const template = assembly.getStack(stack.stackName).template; + test.deepEqual(template.Outputs, { + ClusterConfigCommand43AAE40F: { + Value: { 'Fn::Join': [ '', [ + 'aws eks update-kubeconfig --name ', + { Ref: 'Cluster9EE0221C' }, + ' --role-arn ', + { 'Fn::GetAtt': [ 'masters0D04F23D', 'Arn' ] } + ] ] } + } + }); + test.done(); + }, + + 'if `outputConfigCommand=false` will disabled the output'(test: Test) { + // GIVEN + const { app, stack } = testFixtureNoVpc(); + + // WHEN + const mastersRole = new iam.Role(stack, 'masters', { assumedBy: new iam.AccountRootPrincipal() }); + new eks.Cluster(stack, 'Cluster', { + mastersRole, + outputConfigCommand: false, + }); + + // THEN + const assembly = app.synth(); + const template = assembly.getStack(stack.stackName).template; + test.ok(!template.Outputs); // no outputs + test.done(); + }, + + '`outputClusterName` can be used to synthesize an output with the cluster name'(test: Test) { + // GIVEN + const { app, stack } = testFixtureNoVpc(); + + // WHEN + new eks.Cluster(stack, 'Cluster', { + outputConfigCommand: false, + outputClusterName: true + }); + + // THEN + const assembly = app.synth(); + const template = assembly.getStack(stack.stackName).template; + test.deepEqual(template.Outputs, { + ClusterClusterNameEB26049E: { Value: { Ref: 'Cluster9EE0221C' } } + }); + test.done(); + }, + + '`outputMastersRoleArn` can be used to synthesize an output with the arn of the masters role if defined'(test: Test) { + // GIVEN + const { app, stack } = testFixtureNoVpc(); + + // WHEN + new eks.Cluster(stack, 'Cluster', { + outputConfigCommand: false, + outputMastersRoleArn: true, + mastersRole: new iam.Role(stack, 'masters', { assumedBy: new iam.AccountRootPrincipal() }) + }); + + // THEN + const assembly = app.synth(); + const template = assembly.getStack(stack.stackName).template; + test.deepEqual(template.Outputs, { + ClusterMastersRoleArnB15964B1: { Value: { 'Fn::GetAtt': [ 'masters0D04F23D', 'Arn' ] } } + }); + test.done(); + }, + + 'when adding capacity, instance role ARN will not be outputed only if we do not auto-map aws-auth'(test: Test) { + // GIVEN + const { app, stack } = testFixtureNoVpc(); + + // WHEN + new eks.Cluster(stack, 'Cluster', { + outputConfigCommand: false, + kubectlEnabled: false + }); + + // THEN + const assembly = app.synth(); + const template = assembly.getStack(stack.stackName).template; + test.deepEqual(template.Outputs, { + ClusterDefaultCapacityInstanceRoleARN7DADF219: { + Value: { 'Fn::GetAtt': [ 'ClusterDefaultCapacityInstanceRole3E209969', 'Arn' ] } + } + }); + test.done(); + } + } };