Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eks): ability to query runtime information from the cluster #9535

Merged
merged 29 commits into from
Aug 14, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0d0d0eb
working version of integ test
iliapolo Aug 1, 2020
95163e3
mid work
iliapolo Aug 3, 2020
cc74d36
added docstring
iliapolo Aug 4, 2020
a7dd5ee
Merge branch 'master' into epolon/kubectl-describe
iliapolo Aug 8, 2020
0ca92b6
refactor integ test
iliapolo Aug 8, 2020
398aee3
integ
iliapolo Aug 8, 2020
b23c7da
added README
iliapolo Aug 8, 2020
61f8890
docstrings fixes
iliapolo Aug 8, 2020
3481106
remove debug logs
iliapolo Aug 8, 2020
81754fd
fix test assertion
iliapolo Aug 8, 2020
45f788f
integ
iliapolo Aug 8, 2020
d5fdf22
integ expectations
iliapolo Aug 8, 2020
4a57715
bring back inference in integ test
iliapolo Aug 9, 2020
f92f6b2
update expectation file
iliapolo Aug 9, 2020
e4cd6ec
delete service-description and add namespace option to kube get
iliapolo Aug 11, 2020
ea139a4
change k8s-resource to k8s-manifest and k8s-get to k8s-resource-attri…
iliapolo Aug 11, 2020
50bf0aa
whoops
iliapolo Aug 11, 2020
f61fc46
integ tests
iliapolo Aug 12, 2020
ea07951
README changes according to new method and construct names
iliapolo Aug 12, 2020
61f2ae7
Merge branch 'master' into epolon/kubectl-describe
iliapolo Aug 12, 2020
95f6788
rename 'namespace' to 'resourceNamespace' for consistency
iliapolo Aug 12, 2020
4f73741
more renaming changes
iliapolo Aug 12, 2020
6434229
rename ResourceAttribute to ObjectValue
iliapolo Aug 13, 2020
0581e54
compilation error
iliapolo Aug 13, 2020
24bce53
fix typo and expectations
iliapolo Aug 13, 2020
d00bded
Merge branch 'master' into epolon/kubectl-describe
iliapolo Aug 13, 2020
8b7229c
integ
iliapolo Aug 13, 2020
5e7c819
fix readme
iliapolo Aug 13, 2020
88d9f29
Merge branch 'master' into epolon/kubectl-describe
mergify[bot] Aug 14, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
46 changes: 43 additions & 3 deletions packages/@aws-cdk/aws-eks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,10 @@ ClusterConfigCommand43AAE40F = aws eks update-kubeconfig --name cluster-xxxxx --
```

> The IAM role specified in this command is called the "**masters role**". This is
> an IAM role that is associated with the `system:masters` [RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)
> an IAM role that is associated with the `system:masters` [RBAC](https://kubernetes.io/docs/reference/access-authn-authz/rbac/)
> group and has super-user access to the cluster.
>
> You can specify this role using the `mastersRole` option, or otherwise a role will be
> You can specify this role using the `mastersRole` option, or otherwise a role will be
> automatically created for you. This role can be assumed by anyone in the account with
> `sts:AssumeRole` permissions for this role.

Expand Down Expand Up @@ -384,7 +384,7 @@ and will be applied sequentially (the standard behavior in `kubectl`).

### Patching Kubernetes Resources

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

Expand All @@ -397,6 +397,46 @@ new KubernetesPatch(this, 'hello-kub-deployment-label', {
})
```

### Querying Kubernetes Resources

The `KubernetesGet` construct can be used to query for information about kubernetes resources,
and use that as part of your CDK application.

For example, you can fetch the address of a [`LoadBalancer`](https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer) type service:

```typescript
// query the load balancer address
const myServiceAddress = new KubernetesGet(this, 'LoadBalancerAttribute', {
iliapolo marked this conversation as resolved.
Show resolved Hide resolved
cluster: cluster,
iliapolo marked this conversation as resolved.
Show resolved Hide resolved
resourceType: 'service',
resourceName: 'my-service',
jsonPath: '.status.loadBalancer.ingress[0].hostname', // https://kubernetes.io/docs/reference/kubectl/jsonpath/
});

// pass the address to a lambda function
const proxyFunction = new lambda.Function(this, 'ProxyFunction', {
...
environment: {
myServiceAddress: myServiceAddress.value
},
})
```

#### Service Description

The `ServiceDescription` construct can be used to quickly access information about services. Internally, it uses the `KubernetesGet` resource for all its attributes.
iliapolo marked this conversation as resolved.
Show resolved Hide resolved

```typescript
const service = new eks.ServiceDescription(stack, 'MyServiceDescription', {
cluster: cluster,
serviceName: 'my-service',
});

const loadBalancerAddress = service.loadBalancerAddress
```

> You can also fetch the service description from a specific cluster with the `cluster.describeService` method

### AWS IAM Mapping

As described in the [Amazon EKS User Guide](https://docs.aws.amazon.com/en_us/eks/latest/userguide/add-user-role.html),
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-eks/lib/cluster-resource-provider.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as path from 'path';
import * as iam from '@aws-cdk/aws-iam';
import * as lambda from '@aws-cdk/aws-lambda';
import { Construct, Duration, NestedStack, Stack } from '@aws-cdk/core';
import * as cr from '@aws-cdk/custom-resources';
import * as path from 'path';

const HANDLER_DIR = path.join(__dirname, 'cluster-resource-handler');
const HANDLER_RUNTIME = lambda.Runtime.NODEJS_12_X;
Expand Down
13 changes: 13 additions & 0 deletions packages/@aws-cdk/aws-eks/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { KubernetesResource } from './k8s-resource';
import { KubectlProvider, KubectlProviderProps } from './kubectl-provider';
import { Nodegroup, NodegroupOptions } from './managed-nodegroup';
import { ServiceAccount, ServiceAccountOptions } from './service-account';
import { ServiceDescription, DescribeServiceOptions } from './service-description';
import { LifecycleLabel, renderAmazonLinuxUserData, renderBottlerocketUserData } from './user-data';

// defaults are based on https://eksctl.io
Expand Down Expand Up @@ -714,6 +715,18 @@ export class Cluster extends Resource implements ICluster {
this.defineCoreDnsComputeType(props.coreDnsComputeType ?? CoreDnsComputeType.EC2);
}

/**
* Describe the service to retrieve runtime information from the cluster.
*
* @param options The operation options.
*/
public describeService(options: DescribeServiceOptions): ServiceDescription {
return new ServiceDescription(this, `Service${options.serviceName}Description`, {
cluster: this,
...options,
});
}

/**
* Add nodes to this EKS cluster
*
Expand Down
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-eks/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export * from './k8s-patch';
export * from './k8s-resource';
export * from './fargate-cluster';
export * from './service-account';
export * from './managed-nodegroup';
export * from './managed-nodegroup';
export * from './service-description';
77 changes: 77 additions & 0 deletions packages/@aws-cdk/aws-eks/lib/k8s-get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Construct, CustomResource, Token, Duration } from '@aws-cdk/core';
import { Cluster } from './cluster';

/**
* Properties for KubernetesGet.
*/
export interface KubernetesGetProps {
/**
* The EKS cluster to fetch attributes from.
*
* [disable-awslint:ref-via-interface]
*/
readonly cluster: Cluster;

/**
* The resource type to query. (e.g 'service', 'pod'...)
*/
readonly resourceType: string;

/**
* The name of the resource to query.
*/
readonly resourceName: string;

/**
* JSONPath to use in the query.
*
* @see https://kubernetes.io/docs/reference/kubectl/jsonpath/
*/
readonly jsonPath: string;

/**
* Timeout for waiting on a value.
*/
readonly timeout: Duration;

}

/**
* Represents an attribute of a resource deployed in the cluster.
* Use this to fetch runtime information about resources.
*/
export class KubernetesGet extends Construct {
/**
* The CloudFormation reosurce type.
*/
public static readonly RESOURCE_TYPE = 'Custom::AWSCDK-EKS-KubernetesGet';

private _resource: CustomResource;

constructor(scope: Construct, id: string, props: KubernetesGetProps) {
super(scope, id);

const provider = props.cluster._attachKubectlResourceScope(this);

this._resource = new CustomResource(this, 'Resource', {
resourceType: KubernetesGet.RESOURCE_TYPE,
serviceToken: provider.serviceToken,
properties: {
ClusterName: props.cluster.clusterName,
RoleArn: props.cluster._kubectlCreationRole.roleArn,
ResourceType: props.resourceType,
ResourceName: props.resourceName,
JsonPath: props.jsonPath,
TimeoutSeconds: props.timeout.toSeconds(),
},
});

}

/**
* The value as a string token.
*/
public get value(): string {
return Token.asString(this._resource.getAtt('Value'));
}
}
76 changes: 76 additions & 0 deletions packages/@aws-cdk/aws-eks/lib/kubectl-handler/get/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import json
import logging
import os
import subprocess
import time

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# these are coming from the kubectl layer
os.environ['PATH'] = '/opt/kubectl:/opt/awscli:' + os.environ['PATH']

outdir = os.environ.get('TEST_OUTDIR', '/tmp')
kubeconfig = os.path.join(outdir, 'kubeconfig')

def get_handler(event, context):
logger.info(json.dumps(event))

request_type = event['RequestType']
props = event['ResourceProperties']

# resource properties (all required)
cluster_name = props['ClusterName']
role_arn = props['RoleArn']

# "log in" to the cluster
subprocess.check_call([ 'aws', 'eks', 'update-kubeconfig',
'--role-arn', role_arn,
'--name', cluster_name,
'--kubeconfig', kubeconfig
])

resource_type = props['ResourceType']
resource_name = props['ResourceName']
json_path = props['JsonPath']
timeout_seconds = props['TimeoutSeconds']

# json path should be surrouded with '{}'
path = '{{{0}}}'.format(json_path)
if request_type == 'Create' or request_type == 'Update':
output = wait_for_output(['get', resource_type, resource_name, "-o=jsonpath='{{{0}}}'".format(json_path)], int(timeout_seconds))
return {'Data': {'Value': output}}
elif request_type == 'Delete':
pass
else:
raise Exception("invalid request type %s" % request_type)

def wait_for_output(args, timeout_seconds):

end_time = time.time() + timeout_seconds

while time.time() < end_time:
# the output is surrounded with '', so we unquote
output = kubectl(args).decode('utf-8')[1:-1]
if output:
return output
time.sleep(10)

raise RuntimeError(f'Timeout waiting for output from kubectl command: {args}')

def kubectl(args):
retry = 3
while retry > 0:
try:
cmd = [ 'kubectl', '--kubeconfig', kubeconfig ] + args
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exc:
output = exc.output
if b'i/o timeout' in output and retry > 0:
logger.info("kubectl timed out, retries left: %s" % retry)
retry = retry - 1
else:
raise Exception(output)
else:
logger.info(output)
return output
4 changes: 4 additions & 0 deletions packages/@aws-cdk/aws-eks/lib/kubectl-handler/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from apply import apply_handler
from helm import helm_handler
from patch import patch_handler
from get import get_handler

def handler(event, context):
print(json.dumps(event))
Expand All @@ -18,4 +19,7 @@ def handler(event, context):
if resource_type == 'Custom::AWSCDK-EKS-KubernetesPatch':
return patch_handler(event, context)

if resource_type == 'Custom::AWSCDK-EKS-KubernetesGet':
return get_handler(event, context)

raise Exception("unknown resource type %s" % resource_type)
65 changes: 65 additions & 0 deletions packages/@aws-cdk/aws-eks/lib/service-description.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Construct, Duration } from '@aws-cdk/core';
import { Cluster } from './cluster';
import { KubernetesGet as KubernetesGet } from './k8s-get';

/**
* Options for `cluster.describeService` operation.
*/
export interface DescribeServiceOptions {
/**
* The service name.
*/
readonly serviceName: string;

/**
* Timeout for waiting on service attribute.
* For example, the external ip of a load balancer will not immediately available and needs to be waited for.
*
* @default Duration.minutes(5)
*/
readonly timeout?: Duration;

}

/**
* Properties for ServiceDescription.
*/
export interface ServiceDescriptionProps extends DescribeServiceOptions {

/**
* The Cluster that the service is deployed in.
*
* [disable-awslint:ref-via-interface]
*/
readonly cluster: Cluster;

}

/**
* Represents the description of an existing kubernetes service.
*/
export class ServiceDescription extends Construct {

/**
* Address of the Load Balancer created bu kubernetes in case the service
* type is 'LoadBalancer'
*
* Using this with a non load balancer service will result in an error.
* // TODO - Make this undefined in this case instead of throwing.
*/
public readonly loadBalancerAddress: string;

constructor(scope: Construct, id: string, props: ServiceDescriptionProps) {
super(scope, id);

const loadBalancerAddress = new KubernetesGet(this, 'LoadBalancerAttribute', {
cluster: props.cluster,
resourceType: 'service',
resourceName: props.serviceName,
jsonPath: '.status.loadBalancer.ingress[0].hostname',
timeout: props.timeout ?? Duration.minutes(5),
});

this.loadBalancerAddress = loadBalancerAddress.value;
}
}