Skip to content

Commit

Permalink
fix(aws-eks): support http proxy in EKS onEvent lambda (#16657)
Browse files Browse the repository at this point in the history
## Summary

Currently when a user wants to route all of the EKS lambda's `aws-sdk-js` requests through a proxy then they are [instructed to configure an env var named `HTTP_PROXY` or `http_proxy`](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-eks-readme.html#cluster-handler).

e.g.
```ts
const cluster = new eks.Cluster(this, 'hello-eks', {
  version: eks.KubernetesVersion.V1_21,
  clusterHandlerEnvironment: {
    'http_proxy': 'http://proxy.myproxy.com'
  }
});
```

However the JS SDK [requires further configuration to enable proxy support](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/node-configuring-proxies.html).

This PR:

**The below changes have been refactored to avoid use of `NodeJsFunction`. See the PR comments below for [reasoning](#16657 (comment)) and [updated changes](#16657 (comment)
- ~~Adds a `package.json` with the dependency ['http-proxy-agent'](https://github.com/TooTallNate/node-http-proxy-agent) to the `cluster-resource-handler/` lambda bundle~~
- ~~Uses `NodeJSFunction` to install lambda dependencies and bundle.~~
- Adds a condition that checks the environment for `HTTP_PROXY` or `http_proxy` values. If present then configures the aws-sdk to use that proxy (using `http-proxy-agent`).

~~Note: I placed the `http-proxy-agent` in the `devDependencies` of `package.json`. If the dependency is placed in the `dependencies` section then the CDK builder [throws an error: `NPM Package cluster-resources-handler inside jsii package '@aws-cdk/aws-eks', can only have devDependencies`](https://github.com/aws/aws-cdk/blob/7dae114b7aac46321b8d8572e6837428b4c633b2/tools/pkglint/lib/rules.ts#L1332)~~

Fixes: SIM D29159517, #12469

Tested this using squid proxy on an ec2 instance within the same VPC as the EKS cluster.

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
ryparker authored Sep 28, 2021
1 parent be6aa2e commit 87c9570
Show file tree
Hide file tree
Showing 35 changed files with 1,196 additions and 1,110 deletions.
5 changes: 4 additions & 1 deletion packages/@aws-cdk/aws-eks/.npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ tsconfig.json
junit.xml
test/
!*.lit.ts
jest.config.js
jest.config.js

# Don't include lambda node_modules. These are installed at build time.
lib/cluster-resource-handler/node_modules
19 changes: 10 additions & 9 deletions packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable no-console */

// eslint-disable-next-line import/no-extraneous-dependencies
import { IsCompleteResponse, OnEventResponse } from '@aws-cdk/custom-resources/lib/provider-framework/types';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as aws from 'aws-sdk';
Expand All @@ -23,7 +24,7 @@ export class ClusterResourceHandler extends ResourceHandler {
super(eks, event);

this.newProps = parseProps(this.event.ResourceProperties);
this.oldProps = event.RequestType === 'Update' ? parseProps(event.OldResourceProperties) : { };
this.oldProps = event.RequestType === 'Update' ? parseProps(event.OldResourceProperties) : {};
}

// ------
Expand Down Expand Up @@ -271,16 +272,16 @@ export class ClusterResourceHandler extends ResourceHandler {

function parseProps(props: any): aws.EKS.CreateClusterRequest {

const parsed = props?.Config ?? { };
const parsed = props?.Config ?? {};

// this is weird but these boolean properties are passed by CFN as a string, and we need them to be booleanic for the SDK.
// Otherwise it fails with 'Unexpected Parameter: params.resourcesVpcConfig.endpointPrivateAccess is expected to be a boolean'

if (typeof(parsed.resourcesVpcConfig?.endpointPrivateAccess) === 'string') {
if (typeof (parsed.resourcesVpcConfig?.endpointPrivateAccess) === 'string') {
parsed.resourcesVpcConfig.endpointPrivateAccess = parsed.resourcesVpcConfig.endpointPrivateAccess === 'true';
}

if (typeof(parsed.resourcesVpcConfig?.endpointPublicAccess) === 'string') {
if (typeof (parsed.resourcesVpcConfig?.endpointPublicAccess) === 'string') {
parsed.resourcesVpcConfig.endpointPublicAccess = parsed.resourcesVpcConfig.endpointPublicAccess === 'true';
}

Expand All @@ -303,13 +304,13 @@ function analyzeUpdate(oldProps: Partial<aws.EKS.CreateClusterRequest>, newProps
console.log('old props: ', JSON.stringify(oldProps));
console.log('new props: ', JSON.stringify(newProps));

const newVpcProps = newProps.resourcesVpcConfig || { };
const oldVpcProps = oldProps.resourcesVpcConfig || { };
const newVpcProps = newProps.resourcesVpcConfig || {};
const oldVpcProps = oldProps.resourcesVpcConfig || {};

const oldPublicAccessCidrs = new Set(oldVpcProps.publicAccessCidrs ?? []);
const newPublicAccessCidrs = new Set(newVpcProps.publicAccessCidrs ?? []);
const newEnc = newProps.encryptionConfig || { };
const oldEnc = oldProps.encryptionConfig || { };
const newEnc = newProps.encryptionConfig || {};
const oldEnc = oldProps.encryptionConfig || {};

return {
replaceName: newProps.name !== oldProps.name,
Expand All @@ -329,4 +330,4 @@ function analyzeUpdate(oldProps: Partial<aws.EKS.CreateClusterRequest>, newProps

function setsEqual(first: Set<string>, second: Set<string>) {
return first.size === second.size || [...first].every((e: string) => second.has(e));
}
}
21 changes: 21 additions & 0 deletions packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/common.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { IsCompleteResponse, OnEventResponse } from '@aws-cdk/custom-resources/lib/provider-framework/types';

// eslint-disable-next-line import/no-extraneous-dependencies
Expand Down Expand Up @@ -37,6 +38,16 @@ export abstract class ResourceHandler {
RoleArn: roleToAssume,
RoleSessionName: `AWSCDK.EKSCluster.${this.requestType}.${this.requestId}`,
});

const proxyAddress = this.httpProxyFromEnvironment();
if (proxyAddress) {
this.log(`Using proxy server: ${proxyAddress}`);
// eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-extraneous-dependencies
const ProxyAgent: any = require('proxy-agent');
aws.config.update({
httpOptions: { agent: new ProxyAgent(proxyAddress) },
});
}
}

public onEvent() {
Expand Down Expand Up @@ -64,6 +75,16 @@ export abstract class ResourceHandler {
console.log(JSON.stringify(x, undefined, 2));
}

private httpProxyFromEnvironment(): string | undefined {
if (process.env.http_proxy) {
return process.env.http_proxy;
}
if (process.env.HTTP_PROXY) {
return process.env.HTTP_PROXY;
}
return undefined;
}

protected abstract async onCreate(): Promise<OnEventResponse>;
protected abstract async onDelete(): Promise<OnEventResponse | void>;
protected abstract async onUpdate(): Promise<(OnEventResponse & EksUpdateId) | void>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable no-console */

// eslint-disable-next-line import/no-extraneous-dependencies
import { IsCompleteResponse } from '@aws-cdk/custom-resources/lib/provider-framework/types';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as aws from 'aws-sdk';
Expand Down Expand Up @@ -57,4 +57,4 @@ function createResourceHandler(event: AWSLambda.CloudFormationCustomResourceEven
default:
throw new Error(`Unsupported resource type "${event.ResourceType}`);
}
}
}
18 changes: 17 additions & 1 deletion packages/@aws-cdk/aws-eks/lib/cluster-resource-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as iam from '@aws-cdk/aws-iam';
import * as lambda from '@aws-cdk/aws-lambda';
import { Duration, NestedStack, Stack } from '@aws-cdk/core';
import * as cr from '@aws-cdk/custom-resources';
import { NodeProxyAgentLayer } from '@aws-cdk/lambda-layer-node-proxy-agent';
import { Construct } from 'constructs';

// v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch.
Expand Down Expand Up @@ -33,6 +34,13 @@ export interface ClusterResourceProviderProps {
* Environment to add to the handler.
*/
readonly environment?: { [key: string]: string };

/**
* An AWS Lambda layer that includes the NPM dependency `proxy-agent`.
*
* If not defined, a default layer will be used.
*/
readonly onEventLayer?: lambda.ILayerVersion;
}

/**
Expand Down Expand Up @@ -69,6 +77,14 @@ export class ClusterResourceProvider extends NestedStack {
vpcSubnets: props.subnets ? { subnets: props.subnets } : undefined,
});

// Allow user to customize the layer
if (!props.onEventLayer) {
// `NodeProxyAgentLayer` provides `proxy-agent` which is needed to configure `aws-sdk-js` with a user provided proxy.
onEvent.addLayers(new NodeProxyAgentLayer(this, 'NodeProxyAgentLayer'));
} else {
onEvent.addLayers(props.onEventLayer);
}

const isComplete = new lambda.Function(this, 'IsCompleteHandler', {
code: lambda.Code.fromAsset(HANDLER_DIR),
description: 'isComplete handler for EKS cluster resource provider',
Expand Down Expand Up @@ -96,4 +112,4 @@ export class ClusterResourceProvider extends NestedStack {
* The custom resource service token for this provider.
*/
public get serviceToken() { return this.provider.serviceToken; }
}
}
3 changes: 3 additions & 0 deletions packages/@aws-cdk/aws-eks/lib/cluster-resource.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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';
Expand All @@ -24,6 +25,7 @@ export interface ClusterResourceProps {
readonly environment?: { [key: string]: string };
readonly subnets?: ec2.ISubnet[];
readonly secretsEncryptionKey?: kms.IKey;
readonly onEventLayer?: lambda.ILayerVersion;
}

/**
Expand Down Expand Up @@ -62,6 +64,7 @@ export class ClusterResource extends CoreConstruct {
subnets: props.subnets,
vpc: props.vpc,
environment: props.environment,
onEventLayer: props.onEventLayer,
});

const resource = new CustomResource(this, 'Resource', {
Expand Down
53 changes: 53 additions & 0 deletions packages/@aws-cdk/aws-eks/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,13 @@ export interface ICluster extends IResource, ec2.IConnectable {
*/
readonly kubectlMemory?: Size;

/**
* An AWS Lambda layer that includes the NPM dependency `proxy-agent`.
*
* If not defined, a default layer will be used.
*/
readonly onEventLayer?: lambda.ILayerVersion;

/**
* Indicates whether Kubernetes resources can be automatically pruned. When
* this is enabled (default), prune labels will be allocated and injected to
Expand Down Expand Up @@ -281,6 +288,18 @@ export interface ClusterAttributes {
*/
readonly kubectlMemory?: Size;

/**
* An AWS Lambda Layer which includes the NPM dependency `proxy-agent`. This layer
* is used by the onEvent handler to route AWS SDK requests through a proxy.
*
* The handler expects the layer to include the following node_modules:
*
* proxy-agent
*
* @default - a layer bundled with this module.
*/
readonly onEventLayer?: lambda.ILayerVersion;

/**
* Indicates whether Kubernetes resources added through `addManifest()` can be
* automatically pruned. When this is enabled (default), prune labels will be
Expand Down Expand Up @@ -450,6 +469,30 @@ export interface ClusterOptions extends CommonClusterOptions {
*/
readonly kubectlMemory?: Size;

/**
* An AWS Lambda Layer which includes the NPM dependency `proxy-agent`.
*
* By default, the provider will use the layer included in the
* "aws-lambda-layer-node-proxy-agent" SAR application which is available in all
* commercial regions.
*
* To deploy the layer locally, visit
* https://github.com/aws-samples/aws-lambda-layer-node-proxy-agent/blob/master/cdk/README.md
* for instructions on how to prepare the .zip file and then define it in your
* app as follows:
*
* ```ts
* const layer = new lambda.LayerVersion(this, 'node-proxy-agent-layer', {
* code: lambda.Code.fromAsset(`${__dirname}/layer.zip`)),
* compatibleRuntimes: [lambda.Runtime.NODEJS_14_X]
* })
* ```
*
* @default - the layer provided by the `aws-lambda-layer-node-proxy-agent` SAR app.
* @see https://github.com/aws-samples/aws-lambda-layer-node-proxy-agent
*/
readonly onEventLayer?: lambda.ILayerVersion;

/**
* Indicates whether Kubernetes resources added through `addManifest()` can be
* automatically pruned. When this is enabled (default), prune labels will be
Expand Down Expand Up @@ -898,6 +941,12 @@ export class Cluster extends ClusterBase {
*/
public readonly kubectlMemory?: Size;

/**
* The AWS Lambda layer that contains the NPM dependency `proxy-agent`. If
* undefined, a SAR app that contains this layer will be used.
*/
public readonly onEventLayer?: lambda.ILayerVersion;

/**
* Determines if Kubernetes resources can be pruned automatically.
*/
Expand Down Expand Up @@ -988,6 +1037,7 @@ export class Cluster extends ClusterBase {
this.endpointAccess = props.endpointAccess ?? EndpointAccess.PUBLIC_AND_PRIVATE;
this.kubectlEnvironment = props.kubectlEnvironment;
this.kubectlLayer = props.kubectlLayer;
this.onEventLayer = props.onEventLayer;
this.kubectlMemory = props.kubectlMemory;

const privateSubents = this.selectPrivateSubnets().slice(0, 16);
Expand Down Expand Up @@ -1037,6 +1087,7 @@ export class Cluster extends ClusterBase {
secretsEncryptionKey: props.secretsEncryptionKey,
vpc: this.vpc,
subnets: placeClusterHandlerInVpc ? privateSubents : undefined,
onEventLayer: this.onEventLayer,
});

if (this.endpointAccess._config.privateAccess && privateSubents.length !== 0) {
Expand Down Expand Up @@ -1735,6 +1786,7 @@ class ImportedCluster extends ClusterBase {
public readonly kubectlSecurityGroup?: ec2.ISecurityGroup | undefined;
public readonly kubectlPrivateSubnets?: ec2.ISubnet[] | undefined;
public readonly kubectlLayer?: lambda.ILayerVersion;
public readonly onEventLayer?: lambda.ILayerVersion;
public readonly kubectlMemory?: Size;
public readonly prune: boolean;

Expand All @@ -1752,6 +1804,7 @@ class ImportedCluster extends ClusterBase {
this.kubectlEnvironment = props.kubectlEnvironment;
this.kubectlPrivateSubnets = props.kubectlPrivateSubnetIds ? props.kubectlPrivateSubnetIds.map((subnetid, index) => ec2.Subnet.fromSubnetId(this, `KubectlSubnet${index}`, subnetid)) : undefined;
this.kubectlLayer = props.kubectlLayer;
this.onEventLayer = props.onEventLayer;
this.kubectlMemory = props.kubectlMemory;
this.prune = props.prune ?? true;

Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-eks/lib/fargate-profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export class FargateProfile extends CoreConstruct implements ITaggable {

const provider = ClusterResourceProvider.getOrCreate(this, {
adminRole: props.cluster.adminRole,
onEventLayer: props.cluster.onEventLayer,
});

this.podExecutionRole = props.podExecutionRole ?? new iam.Role(this, 'PodExecutionRole', {
Expand Down
6 changes: 5 additions & 1 deletion packages/@aws-cdk/aws-eks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,13 @@
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/aws-kms": "0.0.0",
"@aws-cdk/aws-lambda": "0.0.0",
"@aws-cdk/aws-lambda-nodejs": "0.0.0",
"@aws-cdk/aws-ssm": "0.0.0",
"@aws-cdk/core": "0.0.0",
"@aws-cdk/custom-resources": "0.0.0",
"@aws-cdk/lambda-layer-awscli": "0.0.0",
"@aws-cdk/lambda-layer-kubectl": "0.0.0",
"@aws-cdk/lambda-layer-node-proxy-agent": "0.0.0",
"constructs": "^3.3.69",
"yaml": "1.10.2"
},
Expand All @@ -111,12 +113,14 @@
"@aws-cdk/aws-iam": "0.0.0",
"@aws-cdk/aws-kms": "0.0.0",
"@aws-cdk/aws-lambda": "0.0.0",
"@aws-cdk/aws-lambda-nodejs": "0.0.0",
"@aws-cdk/aws-ssm": "0.0.0",
"@aws-cdk/core": "0.0.0",
"@aws-cdk/custom-resources": "0.0.0",
"constructs": "^3.3.69",
"@aws-cdk/lambda-layer-awscli": "0.0.0",
"@aws-cdk/lambda-layer-kubectl": "0.0.0"
"@aws-cdk/lambda-layer-kubectl": "0.0.0",
"@aws-cdk/lambda-layer-node-proxy-agent": "0.0.0"
},
"engines": {
"node": ">= 10.13.0 <13 || >=13.7.0"
Expand Down
14 changes: 6 additions & 8 deletions packages/@aws-cdk/aws-eks/test/cluster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ describe('cluster', () => {

const template = SynthUtils.toCloudFormation(nested);
expect(template.Resources.OnEventHandler42BEBAE0.Properties.Environment).toEqual({ Variables: { foo: 'bar' } });


});

test('throws when trying to place cluster handlers in a vpc with no private subnets', () => {
Expand Down Expand Up @@ -651,7 +649,7 @@ describe('cluster', () => {
const { stack } = testFixtureNoVpc();

// WHEN
new eks.Cluster(stack, 'cluster', { version: CLUSTER_VERSION, prune: false }) ;
new eks.Cluster(stack, 'cluster', { version: CLUSTER_VERSION, prune: false });

// THEN
expect(stack).toHaveResource('AWS::EC2::VPC');
Expand Down Expand Up @@ -2469,7 +2467,7 @@ describe('cluster', () => {
version: CLUSTER_VERSION,
prune: false,
endpointAccess:
eks.EndpointAccess.PRIVATE,
eks.EndpointAccess.PRIVATE,
vpcSubnets: [{
subnets: [ec2.PrivateSubnet.fromSubnetAttributes(stack, 'Private1', {
subnetId: 'subnet1',
Expand Down Expand Up @@ -2568,14 +2566,14 @@ describe('cluster', () => {
const subnetConfiguration: ec2.SubnetConfiguration[] = [];

for (let i = 0; i < 20; i++) {
subnetConfiguration.push( {
subnetConfiguration.push({
subnetType: ec2.SubnetType.PRIVATE,
name: `Private${i}`,
},
);
}

subnetConfiguration.push( {
subnetConfiguration.push({
subnetType: ec2.SubnetType.PUBLIC,
name: 'Public1',
});
Expand Down Expand Up @@ -2619,14 +2617,14 @@ describe('cluster', () => {
const subnetConfiguration: ec2.SubnetConfiguration[] = [];

for (let i = 0; i < 20; i++) {
subnetConfiguration.push( {
subnetConfiguration.push({
subnetType: ec2.SubnetType.PRIVATE,
name: `Private${i}`,
},
);
}

subnetConfiguration.push( {
subnetConfiguration.push({
subnetType: ec2.SubnetType.PUBLIC,
name: 'Public1',
});
Expand Down
Loading

0 comments on commit 87c9570

Please sign in to comment.