Skip to content

Commit 4bc8188

Browse files
authored
feat(eks): ability to query runtime information from the cluster (#9535)
Introduce a `KubernetesResourceAttribute` construct that executes `kubectl get` commands to fetch runtime information on kubernetes resources. Resolves #8394 BREAKING CHANGE: `cluster.addResource` was renamed to `cluster.addManifest` and `KubernetesResource` was renamed to `KubernetesManifest` ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 43214b4 commit 4bc8188

24 files changed

+1194
-212
lines changed

packages/@aws-cdk/aws-eks/README.md

+47-16
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const cluster = new eks.Cluster(this, 'hello-eks', {
3131
});
3232

3333
// apply a kubernetes manifest to the cluster
34-
cluster.addResource('mypod', {
34+
cluster.addManifest('mypod', {
3535
apiVersion: 'v1',
3636
kind: 'Pod',
3737
metadata: { name: 'mypod' },
@@ -60,10 +60,10 @@ ClusterConfigCommand43AAE40F = aws eks update-kubeconfig --name cluster-xxxxx --
6060
```
6161

6262
> The IAM role specified in this command is called the "**masters role**". This is
63-
> an IAM role that is associated with the `system:masters` [RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)
63+
> an IAM role that is associated with the `system:masters` [RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)
6464
> group and has super-user access to the cluster.
6565
>
66-
> You can specify this role using the `mastersRole` option, or otherwise a role will be
66+
> You can specify this role using the `mastersRole` option, or otherwise a role will be
6767
> automatically created for you. This role can be assumed by anyone in the account with
6868
> `sts:AssumeRole` permissions for this role.
6969
@@ -214,7 +214,7 @@ const cluster = new eks.FargateCluster(this, 'MyCluster', {
214214
});
215215

216216
// apply k8s resources on this cluster
217-
cluster.addResource(...);
217+
cluster.addManifest(...);
218218
```
219219

220220
**NOTE**: Classic Load Balancers and Network Load Balancers are not supported on
@@ -266,7 +266,7 @@ the capacity.
266266

267267
### Kubernetes Resources
268268

269-
The `KubernetesResource` construct or `cluster.addResource` method can be used
269+
The `KubernetesManifest` construct or `cluster.addManifest` method can be used
270270
to apply Kubernetes resource manifests to this cluster.
271271

272272
The following examples will deploy the [paulbouwer/hello-kubernetes](https://github.com/paulbouwer/hello-kubernetes)
@@ -309,13 +309,13 @@ const service = {
309309
};
310310

311311
// option 1: use a construct
312-
new KubernetesResource(this, 'hello-kub', {
312+
new KubernetesManifest(this, 'hello-kub', {
313313
cluster,
314314
manifest: [ deployment, service ]
315315
});
316316

317-
// or, option2: use `addResource`
318-
cluster.addResource('hello-kub', service, deployment);
317+
// or, option2: use `addManifest`
318+
cluster.addManifest('hello-kub', service, deployment);
319319
```
320320

321321
##### Kubectl Environment
@@ -342,7 +342,7 @@ import * as request from 'sync-request';
342342

343343
const manifestUrl = 'https://url/of/manifest.yaml';
344344
const manifest = yaml.safeLoadAll(request('GET', manifestUrl).getBody());
345-
cluster.addResource('my-resource', ...manifest);
345+
cluster.addManifest('my-resource', ...manifest);
346346
```
347347

348348
Since Kubernetes resources are implemented as CloudFormation resources in the
@@ -356,17 +356,17 @@ There are cases where Kubernetes resources must be deployed in a specific order.
356356
For example, you cannot define a resource in a Kubernetes namespace before the
357357
namespace was created.
358358

359-
You can represent dependencies between `KubernetesResource`s using
359+
You can represent dependencies between `KubernetesManifest`s using
360360
`resource.node.addDependency()`:
361361

362362
```ts
363-
const namespace = cluster.addResource('my-namespace', {
363+
const namespace = cluster.addManifest('my-namespace', {
364364
apiVersion: 'v1',
365365
kind: 'Namespace',
366366
metadata: { name: 'my-app' }
367367
});
368368

369-
const service = cluster.addResource('my-service', {
369+
const service = cluster.addManifest('my-service', {
370370
metadata: {
371371
name: 'myservice',
372372
namespace: 'my-app'
@@ -377,14 +377,14 @@ const service = cluster.addResource('my-service', {
377377
service.node.addDependency(namespace); // will apply `my-namespace` before `my-service`.
378378
```
379379

380-
NOTE: when a `KubernetesResource` includes multiple resources (either directly
381-
or through `cluster.addResource()`) (e.g. `cluster.addResource('foo', r1, r2,
380+
NOTE: when a `KubernetesManifest` includes multiple resources (either directly
381+
or through `cluster.addManifest()`) (e.g. `cluster.addManifest('foo', r1, r2,
382382
r3,...))`), these resources will be applied as a single manifest via `kubectl`
383383
and will be applied sequentially (the standard behavior in `kubectl`).
384384

385385
### Patching Kubernetes Resources
386386

387-
The KubernetesPatch construct can be used to update existing kubernetes
387+
The `KubernetesPatch` construct can be used to update existing kubernetes
388388
resources. The following example can be used to patch the `hello-kubernetes`
389389
deployment from the example above with 5 replicas.
390390

@@ -397,6 +397,37 @@ new KubernetesPatch(this, 'hello-kub-deployment-label', {
397397
})
398398
```
399399

400+
### Querying Kubernetes Object Values
401+
402+
The `KubernetesObjectValue` construct can be used to query for information about kubernetes objects,
403+
and use that as part of your CDK application.
404+
405+
For example, you can fetch the address of a [`LoadBalancer`](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer) type service:
406+
407+
```typescript
408+
// query the load balancer address
409+
const myServiceAddress = new KubernetesObjectValue(this, 'LoadBalancerAttribute', {
410+
cluster: cluster,
411+
resourceType: 'service',
412+
resourceName: 'my-service',
413+
jsonPath: '.status.loadBalancer.ingress[0].hostname', // https://kubernetes.io/docs/reference/kubectl/jsonpath/
414+
});
415+
416+
// pass the address to a lambda function
417+
const proxyFunction = new lambda.Function(this, 'ProxyFunction', {
418+
...
419+
environment: {
420+
myServiceAddress: myServiceAddress.value
421+
},
422+
})
423+
```
424+
425+
Specifically, since the above use-case is quite common, there is an easier way to access that information:
426+
427+
```typescript
428+
const loadBalancerAddress = cluster.getServiceLoadBalancerAddress('my-service');
429+
```
430+
400431
### AWS IAM Mapping
401432

402433
As described in the [Amazon EKS User Guide](https://docs.aws.amazon.com/en_us/eks/latest/userguide/add-user-role.html),
@@ -548,7 +579,7 @@ const sa = cluster.addServiceAccount('MyServiceAccount');
548579
const bucket = new Bucket(this, 'Bucket');
549580
bucket.grantReadWrite(serviceAccount);
550581

551-
const mypod = cluster.addResource('mypod', {
582+
const mypod = cluster.addManifest('mypod', {
552583
apiVersion: 'v1',
553584
kind: 'Pod',
554585
metadata: { name: 'mypod' },

packages/@aws-cdk/aws-eks/lib/aws-auth.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as iam from '@aws-cdk/aws-iam';
22
import { Construct, Lazy, Stack } from '@aws-cdk/core';
33
import { AwsAuthMapping } from './aws-auth-mapping';
44
import { Cluster } from './cluster';
5-
import { KubernetesResource } from './k8s-resource';
5+
import { KubernetesManifest } from './k8s-manifest';
66

77
/**
88
* Configuration props for the AwsAuth construct.
@@ -32,7 +32,7 @@ export class AwsAuth extends Construct {
3232

3333
this.stack = Stack.of(this);
3434

35-
new KubernetesResource(this, 'manifest', {
35+
new KubernetesManifest(this, 'manifest', {
3636
cluster: props.cluster,
3737
manifest: [
3838
{

packages/@aws-cdk/aws-eks/lib/cluster-resource-provider.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import * as path from 'path';
12
import * as iam from '@aws-cdk/aws-iam';
23
import * as lambda from '@aws-cdk/aws-lambda';
34
import { Construct, Duration, NestedStack, Stack } from '@aws-cdk/core';
45
import * as cr from '@aws-cdk/custom-resources';
5-
import * as path from 'path';
66

77
const HANDLER_DIR = path.join(__dirname, 'cluster-resource-handler');
88
const HANDLER_RUNTIME = lambda.Runtime.NODEJS_12_X;

packages/@aws-cdk/aws-eks/lib/cluster.ts

+51-8
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@ import * as autoscaling from '@aws-cdk/aws-autoscaling';
44
import * as ec2 from '@aws-cdk/aws-ec2';
55
import * as iam from '@aws-cdk/aws-iam';
66
import * as ssm from '@aws-cdk/aws-ssm';
7-
import { CfnOutput, CfnResource, Construct, IResource, Resource, Stack, Tag, Token } from '@aws-cdk/core';
7+
import { CfnOutput, CfnResource, Construct, IResource, Resource, Stack, Tag, Token, Duration } from '@aws-cdk/core';
88
import * as YAML from 'yaml';
99
import { AwsAuth } from './aws-auth';
1010
import { clusterArnComponents, ClusterResource } from './cluster-resource';
1111
import { CfnClusterProps } from './eks.generated';
1212
import { FargateProfile, FargateProfileOptions } from './fargate-profile';
1313
import { HelmChart, HelmChartOptions } from './helm-chart';
14+
import { KubernetesManifest } from './k8s-manifest';
15+
import { KubernetesObjectValue } from './k8s-object-value';
1416
import { KubernetesPatch } from './k8s-patch';
15-
import { KubernetesResource } from './k8s-resource';
1617
import { KubectlProvider, KubectlProviderProps } from './kubectl-provider';
1718
import { Nodegroup, NodegroupOptions } from './managed-nodegroup';
1819
import { ServiceAccount, ServiceAccountOptions } from './service-account';
@@ -416,6 +417,27 @@ export class KubernetesVersion {
416417
private constructor(public readonly version: string) { }
417418
}
418419

420+
/**
421+
* Options for fetching a ServiceLoadBalancerAddress.
422+
*/
423+
export interface ServiceLoadBalancerAddressOptions {
424+
425+
/**
426+
* Timeout for waiting on the load balancer address.
427+
*
428+
* @default Duration.minutes(5)
429+
*/
430+
readonly timeout?: Duration;
431+
432+
/**
433+
* The namespace the service belongs to.
434+
*
435+
* @default 'default'
436+
*/
437+
readonly namespace?: string;
438+
439+
}
440+
419441
/**
420442
* A Cluster represents a managed Kubernetes Service (EKS)
421443
*
@@ -524,7 +546,7 @@ export class Cluster extends Resource implements ICluster {
524546

525547
private _spotInterruptHandler?: HelmChart;
526548

527-
private _neuronDevicePlugin?: KubernetesResource;
549+
private _neuronDevicePlugin?: KubernetesManifest;
528550

529551
private readonly endpointAccess: EndpointAccess;
530552

@@ -714,6 +736,27 @@ export class Cluster extends Resource implements ICluster {
714736
this.defineCoreDnsComputeType(props.coreDnsComputeType ?? CoreDnsComputeType.EC2);
715737
}
716738

739+
/**
740+
* Fetch the load balancer address of a service of type 'LoadBalancer'.
741+
*
742+
* @param serviceName The name of the service.
743+
* @param options Additional operation options.
744+
*/
745+
public getServiceLoadBalancerAddress(serviceName: string, options: ServiceLoadBalancerAddressOptions = {}): string {
746+
747+
const loadBalancerAddress = new KubernetesObjectValue(this, `${serviceName}LoadBalancerAddress`, {
748+
cluster: this,
749+
objectType: 'service',
750+
objectName: serviceName,
751+
objectNamespace: options.namespace,
752+
jsonPath: '.status.loadBalancer.ingress[0].hostname',
753+
timeout: options.timeout,
754+
});
755+
756+
return loadBalancerAddress.value;
757+
758+
}
759+
717760
/**
718761
* Add nodes to this EKS cluster
719762
*
@@ -913,16 +956,16 @@ export class Cluster extends Resource implements ICluster {
913956
}
914957

915958
/**
916-
* Defines a Kubernetes resource in this cluster.
959+
* Defines a Kubernetes manifest in this cluster.
917960
*
918961
* The manifest will be applied/deleted using kubectl as needed.
919962
*
920963
* @param id logical id of this manifest
921964
* @param manifest a list of Kubernetes resource specifications
922-
* @returns a `KubernetesResource` object.
965+
* @returns a `KubernetesManifest` object.
923966
*/
924-
public addResource(id: string, ...manifest: any[]) {
925-
return new KubernetesResource(this, `manifest-${id}`, { cluster: this, manifest });
967+
public addManifest(id: string, ...manifest: any[]) {
968+
return new KubernetesManifest(this, `manifest-${id}`, { cluster: this, manifest });
926969
}
927970

928971
/**
@@ -1109,7 +1152,7 @@ export class Cluster extends Resource implements ICluster {
11091152
if (!this._neuronDevicePlugin) {
11101153
const fileContents = fs.readFileSync(path.join(__dirname, 'addons/neuron-device-plugin.yaml'), 'utf8');
11111154
const sanitized = YAML.parse(fileContents);
1112-
this._neuronDevicePlugin = this.addResource('NeuronDevicePlugin', sanitized);
1155+
this._neuronDevicePlugin = this.addManifest('NeuronDevicePlugin', sanitized);
11131156
}
11141157

11151158
return this._neuronDevicePlugin;

packages/@aws-cdk/aws-eks/lib/index.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ export * from './eks.generated';
66
export * from './fargate-profile';
77
export * from './helm-chart';
88
export * from './k8s-patch';
9-
export * from './k8s-resource';
9+
export * from './k8s-manifest';
10+
export * from './k8s-object-value';
1011
export * from './fargate-cluster';
1112
export * from './service-account';
1213
export * from './managed-nodegroup';

packages/@aws-cdk/aws-eks/lib/k8s-resource.ts packages/@aws-cdk/aws-eks/lib/k8s-manifest.ts

+15-15
Original file line numberDiff line numberDiff line change
@@ -2,63 +2,63 @@ import { Construct, CustomResource, Stack } from '@aws-cdk/core';
22
import { Cluster } from './cluster';
33

44
/**
5-
* Properties for KubernetesResources
5+
* Properties for KubernetesManifest
66
*/
7-
export interface KubernetesResourceProps {
7+
export interface KubernetesManifestProps {
88
/**
9-
* The EKS cluster to apply this configuration to.
9+
* The EKS cluster to apply this manifest to.
1010
*
1111
* [disable-awslint:ref-via-interface]
1212
*/
1313
readonly cluster: Cluster;
1414

1515
/**
16-
* The resource manifest.
16+
* The manifest to apply.
1717
*
1818
* Consists of any number of child resources.
1919
*
20-
* When the resource is created/updated, this manifest will be applied to the
21-
* cluster through `kubectl apply` and when the resource or the stack is
22-
* deleted, the manifest will be deleted through `kubectl delete`.
20+
* When the resources are created/updated, this manifest will be applied to the
21+
* cluster through `kubectl apply` and when the resources or the stack is
22+
* deleted, the resources in the manifest will be deleted through `kubectl delete`.
2323
*
2424
* @example
2525
*
26-
* {
26+
* [{
2727
* apiVersion: 'v1',
2828
* kind: 'Pod',
2929
* metadata: { name: 'mypod' },
3030
* spec: {
3131
* containers: [ { name: 'hello', image: 'paulbouwer/hello-kubernetes:1.5', ports: [ { containerPort: 8080 } ] } ]
3232
* }
33-
* }
33+
* }]
3434
*
3535
*/
3636
readonly manifest: any[];
3737
}
3838

3939
/**
40-
* Represents a resource within the Kubernetes system.
40+
* Represents a manifest within the Kubernetes system.
4141
*
42-
* Alternatively, you can use `cluster.addResource(resource[, resource, ...])`
42+
* Alternatively, you can use `cluster.addManifest(resource[, resource, ...])`
4343
* to define resources on this cluster.
4444
*
45-
* Applies/deletes the resources using `kubectl` in sync with the resource.
45+
* Applies/deletes the manifest using `kubectl`.
4646
*/
47-
export class KubernetesResource extends Construct {
47+
export class KubernetesManifest extends Construct {
4848
/**
4949
* The CloudFormation reosurce type.
5050
*/
5151
public static readonly RESOURCE_TYPE = 'Custom::AWSCDK-EKS-KubernetesResource';
5252

53-
constructor(scope: Construct, id: string, props: KubernetesResourceProps) {
53+
constructor(scope: Construct, id: string, props: KubernetesManifestProps) {
5454
super(scope, id);
5555

5656
const stack = Stack.of(this);
5757
const provider = props.cluster._attachKubectlResourceScope(this);
5858

5959
new CustomResource(this, 'Resource', {
6060
serviceToken: provider.serviceToken,
61-
resourceType: KubernetesResource.RESOURCE_TYPE,
61+
resourceType: KubernetesManifest.RESOURCE_TYPE,
6262
properties: {
6363
// `toJsonString` enables embedding CDK tokens in the manifest and will
6464
// render a CloudFormation-compatible JSON string (similar to

0 commit comments

Comments
 (0)