1
+ import autoscaling = require( '@aws-cdk/aws-autoscaling' ) ;
2
+ import ec2 = require( '@aws-cdk/aws-ec2' ) ;
3
+ import iam = require( '@aws-cdk/aws-iam' ) ;
4
+ import cdk = require( '@aws-cdk/cdk' ) ;
5
+ import { EksOptimizedAmi , nodeTypeForInstanceType } from './ami' ;
6
+ import { ClusterBase , ClusterImportProps , ICluster } from './cluster-base' ;
7
+ import { CfnCluster } from './eks.generated' ;
8
+ import { maxPodsForInstanceType } from './instance-data' ;
9
+
10
+ /**
11
+ * Properties to instantiate the Cluster
12
+ */
13
+ export interface ClusterProps {
14
+ /**
15
+ * The VPC in which to create the Cluster
16
+ */
17
+ vpc : ec2 . IVpcNetwork ;
18
+
19
+ /**
20
+ * Where to place EKS Control Plane ENIs
21
+ *
22
+ * If you want to create public load balancers, this must include public subnets.
23
+ *
24
+ * For example, to only select private subnets, supply the following:
25
+ *
26
+ * ```
27
+ * vpcPlacements: [
28
+ * { subnetsToUse: ec2.SubnetType.Private }
29
+ * ]
30
+ * ```
31
+ *
32
+ * @default All public and private subnets
33
+ */
34
+ vpcPlacements ?: ec2 . VpcPlacementStrategy [ ] ;
35
+
36
+ /**
37
+ * Role that provides permissions for the Kubernetes control plane to make calls to AWS API operations on your behalf.
38
+ *
39
+ * @default A role is automatically created for you
40
+ */
41
+ role ?: iam . IRole ;
42
+
43
+ /**
44
+ * Name for the cluster.
45
+ *
46
+ * @default Automatically generated name
47
+ */
48
+ clusterName ?: string ;
49
+
50
+ /**
51
+ * Security Group to use for Control Plane ENIs
52
+ *
53
+ * @default A security group is automatically created
54
+ */
55
+ securityGroup ?: ec2 . ISecurityGroup ;
56
+
57
+ /**
58
+ * The Kubernetes version to run in the cluster
59
+ *
60
+ * @default If not supplied, will use Amazon default version
61
+ */
62
+ version ?: string ;
63
+ }
64
+
65
+ /**
66
+ * A Cluster represents a managed Kubernetes Service (EKS)
67
+ *
68
+ * This is a fully managed cluster of API Servers (control-plane)
69
+ * The user is still required to create the worker nodes.
70
+ */
71
+ export class Cluster extends ClusterBase {
72
+ /**
73
+ * Import an existing cluster
74
+ *
75
+ * @param scope the construct scope, in most cases 'this'
76
+ * @param id the id or name to import as
77
+ * @param props the cluster properties to use for importing information
78
+ */
79
+ public static import ( scope : cdk . Construct , id : string , props : ClusterImportProps ) : ICluster {
80
+ return new ImportedCluster ( scope , id , props ) ;
81
+ }
82
+
83
+ /**
84
+ * The VPC in which this Cluster was created
85
+ */
86
+ public readonly vpc : ec2 . IVpcNetwork ;
87
+
88
+ /**
89
+ * The Name of the created EKS Cluster
90
+ */
91
+ public readonly clusterName : string ;
92
+
93
+ /**
94
+ * The AWS generated ARN for the Cluster resource
95
+ *
96
+ * @example arn:aws:eks:us-west-2:666666666666:cluster/prod
97
+ */
98
+ public readonly clusterArn : string ;
99
+
100
+ /**
101
+ * The endpoint URL for the Cluster
102
+ *
103
+ * This is the URL inside the kubeconfig file to use with kubectl
104
+ *
105
+ * @example https://5E1D0CEXAMPLEA591B746AFC5AB30262.yl4.us-west-2.eks.amazonaws.com
106
+ */
107
+ public readonly clusterEndpoint : string ;
108
+
109
+ /**
110
+ * The certificate-authority-data for your cluster.
111
+ */
112
+ public readonly clusterCertificateAuthorityData : string ;
113
+
114
+ /**
115
+ * Manages connection rules (Security Group Rules) for the cluster
116
+ *
117
+ * @type {ec2.Connections }
118
+ * @memberof Cluster
119
+ */
120
+ public readonly connections : ec2 . Connections ;
121
+
122
+ /**
123
+ * IAM role assumed by the EKS Control Plane
124
+ */
125
+ public readonly role : iam . IRole ;
126
+
127
+ private readonly version : string | undefined ;
128
+
129
+ /**
130
+ * Initiates an EKS Cluster with the supplied arguments
131
+ *
132
+ * @param scope a Construct, most likely a cdk.Stack created
133
+ * @param name the name of the Construct to create
134
+ * @param props properties in the IClusterProps interface
135
+ */
136
+ constructor ( scope : cdk . Construct , id : string , props : ClusterProps ) {
137
+ super ( scope , id ) ;
138
+
139
+ this . vpc = props . vpc ;
140
+ this . version = props . version ;
141
+
142
+ this . tagSubnets ( ) ;
143
+
144
+ this . role = props . role || new iam . Role ( this , 'ClusterRole' , {
145
+ assumedBy : new iam . ServicePrincipal ( 'eks.amazonaws.com' ) ,
146
+ managedPolicyArns : [
147
+ new iam . AwsManagedPolicy ( 'AmazonEKSClusterPolicy' , this ) . policyArn ,
148
+ new iam . AwsManagedPolicy ( 'AmazonEKSServicePolicy' , this ) . policyArn ,
149
+ ] ,
150
+ } ) ;
151
+
152
+ const securityGroup = props . securityGroup || new ec2 . SecurityGroup ( this , 'ControlPlaneSecurityGroup' , {
153
+ vpc : props . vpc ,
154
+ description : 'EKS Control Plane Security Group' ,
155
+ } ) ;
156
+
157
+ this . connections = new ec2 . Connections ( {
158
+ securityGroups : [ securityGroup ] ,
159
+ defaultPortRange : new ec2 . TcpPort ( 443 ) , // Control Plane has an HTTPS API
160
+ } ) ;
161
+
162
+ // Get subnetIds for all selected subnets
163
+ const placements = props . vpcPlacements || [ { subnetsToUse : ec2 . SubnetType . Public } , { subnetsToUse : ec2 . SubnetType . Private } ] ;
164
+ const subnetIds = flatMap ( placements , p => this . vpc . subnets ( p ) ) . map ( s => s . subnetId ) ;
165
+
166
+ const resource = new CfnCluster ( this , 'Resource' , {
167
+ name : props . clusterName ,
168
+ roleArn : this . role . roleArn ,
169
+ version : props . version ,
170
+ resourcesVpcConfig : {
171
+ securityGroupIds : [ securityGroup . securityGroupId ] ,
172
+ subnetIds
173
+ }
174
+ } ) ;
175
+
176
+ this . clusterName = resource . clusterName ;
177
+ this . clusterArn = resource . clusterArn ;
178
+ this . clusterEndpoint = resource . clusterEndpoint ;
179
+ this . clusterCertificateAuthorityData = resource . clusterCertificateAuthorityData ;
180
+
181
+ new cdk . Output ( this , 'ClusterName' , { value : this . clusterName , disableExport : true } ) ;
182
+ }
183
+
184
+ /**
185
+ * Add nodes to this EKS cluster
186
+ *
187
+ * The nodes will automatically be configured with the right VPC and AMI
188
+ * for the instance type and Kubernetes version.
189
+ */
190
+ public addCapacity ( id : string , options : AddWorkerNodesOptions ) : autoscaling . AutoScalingGroup {
191
+ const asg = new autoscaling . AutoScalingGroup ( this , id , {
192
+ ...options ,
193
+ vpc : this . vpc ,
194
+ machineImage : new EksOptimizedAmi ( {
195
+ nodeType : nodeTypeForInstanceType ( options . instanceType ) ,
196
+ kubernetesVersion : this . version ,
197
+ } ) ,
198
+ updateType : options . updateType || autoscaling . UpdateType . RollingUpdate ,
199
+ instanceType : options . instanceType ,
200
+ } ) ;
201
+
202
+ this . addAutoScalingGroup ( asg , {
203
+ maxPods : maxPodsForInstanceType ( options . instanceType ) ,
204
+ } ) ;
205
+
206
+ return asg ;
207
+ }
208
+
209
+ /**
210
+ * Add compute capacity to this EKS cluster in the form of an AutoScalingGroup
211
+ *
212
+ * The AutoScalingGroup must be running an EKS-optimized AMI containing the
213
+ * /etc/eks/bootstrap.sh script. This method will configure Security Groups,
214
+ * add the right policies to the instance role, apply the right tags, and add
215
+ * the required user data to the instance's launch configuration.
216
+ *
217
+ * Prefer to use `addCapacity` if possible, it will automatically configure
218
+ * the right AMI and the `maxPods` number based on your instance type.
219
+ *
220
+ * @see https://docs.aws.amazon.com/eks/latest/userguide/launch-workers.html
221
+ */
222
+ public addAutoScalingGroup ( autoScalingGroup : autoscaling . AutoScalingGroup , options : AddAutoScalingGroupOptions ) {
223
+ // self rules
224
+ autoScalingGroup . connections . allowInternally ( new ec2 . AllTraffic ( ) ) ;
225
+
226
+ // Cluster to:nodes rules
227
+ autoScalingGroup . connections . allowFrom ( this , new ec2 . TcpPort ( 443 ) ) ;
228
+ autoScalingGroup . connections . allowFrom ( this , new ec2 . TcpPortRange ( 1025 , 65535 ) ) ;
229
+
230
+ // Allow HTTPS from Nodes to Cluster
231
+ autoScalingGroup . connections . allowTo ( this , new ec2 . TcpPort ( 443 ) ) ;
232
+
233
+ // Allow all node outbound traffic
234
+ autoScalingGroup . connections . allowToAnyIPv4 ( new ec2 . TcpAllPorts ( ) ) ;
235
+ autoScalingGroup . connections . allowToAnyIPv4 ( new ec2 . UdpAllPorts ( ) ) ;
236
+ autoScalingGroup . connections . allowToAnyIPv4 ( new ec2 . IcmpAllTypesAndCodes ( ) ) ;
237
+
238
+ autoScalingGroup . addUserData (
239
+ 'set -o xtrace' ,
240
+ `/etc/eks/bootstrap.sh ${ this . clusterName } --use-max-pods ${ options . maxPods } ` ,
241
+ ) ;
242
+ // FIXME: Add a cfn-signal call once we've sorted out UserData and can write reliable
243
+ // signaling scripts: https://github.com/awslabs/aws-cdk/issues/623
244
+
245
+ autoScalingGroup . role . attachManagedPolicy ( new iam . AwsManagedPolicy ( 'AmazonEKSWorkerNodePolicy' , this ) . policyArn ) ;
246
+ autoScalingGroup . role . attachManagedPolicy ( new iam . AwsManagedPolicy ( 'AmazonEKS_CNI_Policy' , this ) . policyArn ) ;
247
+ autoScalingGroup . role . attachManagedPolicy ( new iam . AwsManagedPolicy ( 'AmazonEC2ContainerRegistryReadOnly' , this ) . policyArn ) ;
248
+
249
+ // EKS Required Tags
250
+ autoScalingGroup . apply ( new cdk . Tag ( `kubernetes.io/cluster/${ this . clusterName } ` , 'owned' , { applyToLaunchedInstances : true } ) ) ;
251
+
252
+ // Create an Output for the Instance Role ARN (need to paste it into aws-auth-cm.yaml)
253
+ new cdk . Output ( autoScalingGroup , 'InstanceRoleARN' , {
254
+ disableExport : true ,
255
+ value : autoScalingGroup . role . roleArn
256
+ } ) ;
257
+ }
258
+
259
+ /**
260
+ * Opportunistically tag subnets with the required tags.
261
+ *
262
+ * If no subnets could be found (because this is an imported VPC), add a warning.
263
+ *
264
+ * @see https://docs.aws.amazon.com/eks/latest/userguide/network_reqs.html
265
+ */
266
+ private tagSubnets ( ) {
267
+ const privates = this . vpc . subnets ( { subnetsToUse : ec2 . SubnetType . Private } ) ;
268
+
269
+ for ( const subnet of privates ) {
270
+ if ( ! isRealSubnetConstruct ( subnet ) ) {
271
+ // Just give up, all of them will be the same.
272
+ this . node . addWarning ( 'Could not auto-tag private subnets with "kubernetes.io/role/internal-elb=1", please remember to do this manually' ) ;
273
+ return ;
274
+ }
275
+
276
+ subnet . apply ( new cdk . Tag ( "kubernetes.io/role/internal-elb" , "1" ) ) ;
277
+ }
278
+ }
279
+ }
280
+
281
+ function isRealSubnetConstruct ( subnet : ec2 . IVpcSubnet ) : subnet is ec2 . VpcSubnet {
282
+ return ( subnet as any ) . addDefaultRouteToIGW !== undefined ;
283
+ }
284
+
285
+ /**
286
+ * Options for adding worker nodes
287
+ */
288
+ export interface AddWorkerNodesOptions extends autoscaling . CommonAutoScalingGroupProps {
289
+ /**
290
+ * Instance type of the instances to start
291
+ */
292
+ instanceType : ec2 . InstanceType ;
293
+ }
294
+
295
+ /**
296
+ * Options for adding an AutoScalingGroup as capacity
297
+ */
298
+ export interface AddAutoScalingGroupOptions {
299
+ /**
300
+ * How many pods to allow on this instance.
301
+ *
302
+ * Should be at most equal to the maximum number of IP addresses available to
303
+ * the instance type less one.
304
+ */
305
+ maxPods : number ;
306
+ }
307
+
308
+ /**
309
+ * Import a cluster to use in another stack
310
+ */
311
+ class ImportedCluster extends ClusterBase {
312
+ public readonly vpc : ec2 . IVpcNetwork ;
313
+ public readonly clusterCertificateAuthorityData : string ;
314
+ public readonly clusterName : string ;
315
+ public readonly clusterArn : string ;
316
+ public readonly clusterEndpoint : string ;
317
+ public readonly connections = new ec2 . Connections ( ) ;
318
+
319
+ constructor ( scope : cdk . Construct , id : string , props : ClusterImportProps ) {
320
+ super ( scope , id ) ;
321
+
322
+ this . vpc = ec2 . VpcNetwork . import ( this , "VPC" , props . vpc ) ;
323
+ this . clusterName = props . clusterName ;
324
+ this . clusterEndpoint = props . clusterEndpoint ;
325
+ this . clusterArn = props . clusterArn ;
326
+ this . clusterCertificateAuthorityData = props . clusterCertificateAuthorityData ;
327
+
328
+ let i = 1 ;
329
+ for ( const sgProps of props . securityGroups ) {
330
+ this . connections . addSecurityGroup ( ec2 . SecurityGroup . import ( this , `SecurityGroup${ i } ` , sgProps ) ) ;
331
+ i ++ ;
332
+ }
333
+ }
334
+ }
335
+
336
+ function flatMap < T , U > ( xs : T [ ] , f : ( x : T ) => U [ ] ) : U [ ] {
337
+ const ret = new Array < U > ( ) ;
338
+ for ( const x of xs ) {
339
+ ret . push ( ...f ( x ) ) ;
340
+ }
341
+ return ret ;
342
+ }
0 commit comments