-
Notifications
You must be signed in to change notification settings - Fork 4k
/
cluster-resource.ts
227 lines (202 loc) · 8.86 KB
/
cluster-resource.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import * as ec2 from '@aws-cdk/aws-ec2';
import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import * as lambda from '@aws-cdk/aws-lambda';
import { ArnComponents, CustomResource, Token, Stack, Lazy } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { CLUSTER_RESOURCE_TYPE } from './cluster-resource-handler/consts';
import { ClusterResourceProvider } from './cluster-resource-provider';
import { CfnCluster } from './eks.generated';
// v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch.
// eslint-disable-next-line
import { Construct as CoreConstruct } from '@aws-cdk/core';
export interface ClusterResourceProps {
readonly resourcesVpcConfig: CfnCluster.ResourcesVpcConfigProperty;
readonly roleArn: string;
readonly encryptionConfig?: Array<CfnCluster.EncryptionConfigProperty>;
readonly kubernetesNetworkConfig?: CfnCluster.KubernetesNetworkConfigProperty;
readonly name: string;
readonly version?: string;
readonly endpointPrivateAccess: boolean;
readonly endpointPublicAccess: boolean;
readonly publicAccessCidrs?: string[];
readonly vpc: ec2.IVpc;
readonly environment?: { [key: string]: string };
readonly subnets?: ec2.ISubnet[];
readonly secretsEncryptionKey?: kms.IKey;
readonly onEventLayer?: lambda.ILayerVersion;
readonly clusterHandlerSecurityGroup?: ec2.ISecurityGroup;
readonly tags?: { [key: string]: string };
readonly logging?: { [key: string]: [ { [key: string]: any } ] };
}
/**
* A low-level CFN resource Amazon EKS cluster implemented through a custom
* resource.
*
* Implements EKS create/update/delete through a CloudFormation custom resource
* in order to allow us to control the IAM role which creates the cluster. This
* is required in order to be able to allow CloudFormation to interact with the
* cluster via `kubectl` to enable Kubernetes management capabilities like apply
* manifest and IAM role/user RBAC mapping.
*/
export class ClusterResource extends CoreConstruct {
public readonly attrEndpoint: string;
public readonly attrArn: string;
public readonly attrCertificateAuthorityData: string;
public readonly attrClusterSecurityGroupId: string;
public readonly attrEncryptionConfigKeyArn: string;
public readonly attrOpenIdConnectIssuerUrl: string;
public readonly attrOpenIdConnectIssuer: string;
public readonly ref: string;
public readonly adminRole: iam.Role;
constructor(scope: Construct, id: string, props: ClusterResourceProps) {
super(scope, id);
if (!props.roleArn) {
throw new Error('"roleArn" is required');
}
const provider = ClusterResourceProvider.getOrCreate(this, {
subnets: props.subnets,
vpc: props.vpc,
environment: props.environment,
onEventLayer: props.onEventLayer,
securityGroup: props.clusterHandlerSecurityGroup,
});
this.adminRole = this.createAdminRole(provider, props);
const resource = new CustomResource(this, 'Resource', {
resourceType: CLUSTER_RESOURCE_TYPE,
serviceToken: provider.serviceToken,
properties: {
// the structure of config needs to be that of 'aws.EKS.CreateClusterRequest' since its passed as is
// to the eks.createCluster sdk invocation.
Config: {
name: props.name,
version: props.version,
roleArn: props.roleArn,
encryptionConfig: props.encryptionConfig,
kubernetesNetworkConfig: props.kubernetesNetworkConfig,
resourcesVpcConfig: {
subnetIds: (props.resourcesVpcConfig as CfnCluster.ResourcesVpcConfigProperty).subnetIds,
securityGroupIds: (props.resourcesVpcConfig as CfnCluster.ResourcesVpcConfigProperty).securityGroupIds,
endpointPublicAccess: props.endpointPublicAccess,
endpointPrivateAccess: props.endpointPrivateAccess,
publicAccessCidrs: props.publicAccessCidrs,
},
tags: props.tags,
logging: props.logging,
},
AssumeRoleArn: this.adminRole.roleArn,
// IMPORTANT: increment this number when you add new attributes to the
// resource. Otherwise, CloudFormation will error with "Vendor response
// doesn't contain XXX key in object" (see #8276) by incrementing this
// number, you will effectively cause a "no-op update" to the cluster
// which will return the new set of attribute.
AttributesRevision: 2,
},
});
resource.node.addDependency(this.adminRole);
this.ref = resource.ref;
this.attrEndpoint = Token.asString(resource.getAtt('Endpoint'));
this.attrArn = Token.asString(resource.getAtt('Arn'));
this.attrCertificateAuthorityData = Token.asString(resource.getAtt('CertificateAuthorityData'));
this.attrClusterSecurityGroupId = Token.asString(resource.getAtt('ClusterSecurityGroupId'));
this.attrEncryptionConfigKeyArn = Token.asString(resource.getAtt('EncryptionConfigKeyArn'));
this.attrOpenIdConnectIssuerUrl = Token.asString(resource.getAtt('OpenIdConnectIssuerUrl'));
this.attrOpenIdConnectIssuer = Token.asString(resource.getAtt('OpenIdConnectIssuer'));
}
private createAdminRole(provider: ClusterResourceProvider, props: ClusterResourceProps) {
const stack = Stack.of(this);
// the role used to create the cluster. this becomes the administrator role
// of the cluster.
const creationRole = new iam.Role(this, 'CreationRole', {
// the role would be assumed by the provider handlers, as they are the ones making
// the requests.
assumedBy: new iam.CompositePrincipal(provider.provider.onEventHandler.role!, provider.provider.isCompleteHandler!.role!),
});
// the CreateCluster API will allow the cluster to assume this role, so we
// need to allow the lambda execution role to pass it.
creationRole.addToPolicy(new iam.PolicyStatement({
actions: ['iam:PassRole'],
resources: [props.roleArn],
}));
// if we know the cluster name, restrict the policy to only allow
// interacting with this specific cluster otherwise, we will have to grant
// this role to manage all clusters in the account. this must be lazy since
// `props.name` may contain a lazy value that conditionally resolves to a
// physical name.
const resourceArns = Lazy.list({
produce: () => {
const arn = stack.formatArn(clusterArnComponents(stack.resolve(props.name)));
return stack.resolve(props.name)
? [arn, `${arn}/*`] // see https://github.com/aws/aws-cdk/issues/6060
: ['*'];
},
});
const fargateProfileResourceArn = Lazy.string({
produce: () => stack.resolve(props.name)
? stack.formatArn({ service: 'eks', resource: 'fargateprofile', resourceName: stack.resolve(props.name) + '/*' })
: '*',
});
creationRole.addToPolicy(new iam.PolicyStatement({
actions: [
'eks:CreateCluster',
'eks:DescribeCluster',
'eks:DescribeUpdate',
'eks:DeleteCluster',
'eks:UpdateClusterVersion',
'eks:UpdateClusterConfig',
'eks:CreateFargateProfile',
'eks:TagResource',
'eks:UntagResource',
],
resources: resourceArns,
}));
creationRole.addToPolicy(new iam.PolicyStatement({
actions: ['eks:DescribeFargateProfile', 'eks:DeleteFargateProfile'],
resources: [fargateProfileResourceArn],
}));
creationRole.addToPolicy(new iam.PolicyStatement({
actions: ['iam:GetRole', 'iam:listAttachedRolePolicies'],
resources: ['*'],
}));
creationRole.addToPolicy(new iam.PolicyStatement({
actions: ['iam:CreateServiceLinkedRole'],
resources: ['*'],
}));
// see https://github.com/aws/aws-cdk/issues/9027
// these actions are the combined 'ec2:Describe*' actions taken from the EKS SLR policies.
// (AWSServiceRoleForAmazonEKS, AWSServiceRoleForAmazonEKSForFargate, AWSServiceRoleForAmazonEKSNodegroup)
creationRole.addToPolicy(new iam.PolicyStatement({
actions: [
'ec2:DescribeInstances',
'ec2:DescribeNetworkInterfaces',
'ec2:DescribeSecurityGroups',
'ec2:DescribeSubnets',
'ec2:DescribeRouteTables',
'ec2:DescribeDhcpOptions',
'ec2:DescribeVpcs',
],
resources: ['*'],
}));
// grant cluster creation role sufficient permission to access the specified key
// see https://docs.aws.amazon.com/eks/latest/userguide/create-cluster.html
if (props.secretsEncryptionKey) {
creationRole.addToPolicy(new iam.PolicyStatement({
actions: [
'kms:Encrypt',
'kms:Decrypt',
'kms:DescribeKey',
'kms:CreateGrant',
],
resources: [props.secretsEncryptionKey.keyArn],
}));
}
return creationRole;
}
}
export function clusterArnComponents(clusterName: string): ArnComponents {
return {
service: 'eks',
resource: 'cluster',
resourceName: clusterName,
};
}