Skip to content

Commit

Permalink
Add support for ALB
Browse files Browse the repository at this point in the history
Signed-off-by: Andre Kurait <[email protected]>
  • Loading branch information
AndreKurait committed Jun 14, 2024
1 parent 48a2179 commit b2f77fa
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 58 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ In order to deploy both the stacks the user needs to provide a set of required a
| certificateArn | Optional | string | Add ACM certificate to the any listener (OpenSearch or OpenSearch-Dashboards) whose port is mapped to 443. e.g., `--context certificateArn=arn:1234`|
| mapOpensearchPortTo | Optional | integer | Load balancer port number to map to OpenSearch. e.g., `--context mapOpensearchPortTo=8440` Defaults to 80 when security is disabled and 443 when security is enabled |
| mapOpensearchDashboardsPortTo | Optional | integer | Load balancer port number to map to OpenSearch-Dashboards. e.g., `--context mapOpensearchDashboardsPortTo=443` Always defaults to 8443 |
| loadBalancerType | Optional | string | The type of load balancer to deploy. Valid values are nlb for Network Load Balancer or alb for Application Load Balancer. Defaults to nlb. e.g., `--context loadBalancerType=alb` |

* Before starting this step, ensure that your AWS CLI is correctly configured with access credentials.
* Also ensure that you're running these commands in the current directory
Expand Down Expand Up @@ -169,7 +170,7 @@ All the ec2 instances are hosted in private subnet and can only be accessed usin

## Port Mapping

The ports to access the cluster are dependent on the `security` parameter value
The ports to access the cluster are dependent on the `security` parameter value and are identical whether using an Application Load Balancer (ALB) or a Network Load Balancer (NLB):
* If `security` is `disable` (HTTP),
* OpenSearch 9200 is mapped to port 80 on the LB
* If `security` is `enable` (HTTPS),
Expand Down
214 changes: 158 additions & 56 deletions lib/infra/infra-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ import {
SubnetType,
} from 'aws-cdk-lib/aws-ec2';
import {
ApplicationListener,
ApplicationLoadBalancer,
ApplicationProtocol,
BaseApplicationListenerProps,
BaseListener,
BaseLoadBalancer,
BaseNetworkListenerProps,
ListenerCertificate,
NetworkListener, NetworkLoadBalancer, Protocol,
} from 'aws-cdk-lib/aws-elasticloadbalancingv2';
Expand All @@ -55,6 +62,11 @@ enum cpuArchEnum{
ARM64='arm64'
}

export enum LoadBalancerType {
NLB = 'nlb',
ALB = 'alb'
}

const getInstanceType = (instanceType: string, arch: string) => {
if (arch === 'x64') {
if (instanceType !== 'undefined') {
Expand Down Expand Up @@ -133,10 +145,13 @@ export interface InfraProps extends StackProps {
readonly mapOpensearchPortTo ?: number
/** Map opensearch-dashboards port on load balancer to */
readonly mapOpensearchDashboardsPortTo ?: number
/** Type of load balancer to use (e.g., 'nlb' or 'alb') */
readonly loadBalancerType?: LoadBalancerType
}

export class InfraStack extends Stack {
public readonly nlb: NetworkLoadBalancer;
public readonly elb: NetworkLoadBalancer | ApplicationLoadBalancer;
public readonly elbType: LoadBalancerType;

private instanceRole: Role;

Expand Down Expand Up @@ -200,8 +215,6 @@ export class InfraStack extends Stack {

constructor(scope: Stack, id: string, props: InfraProps) {
super(scope, id, props);
let opensearchListener: NetworkListener;
let dashboardsListener: NetworkListener;
let managerAsgCapacity: number;
let dataAsgCapacity: number;
let clientNodeAsg: AutoScalingGroup;
Expand Down Expand Up @@ -398,11 +411,28 @@ export class InfraStack extends Stack {

const certificateArn = `${props?.certificateArn ?? scope.node.tryGetContext('certificateArn')}`;

this.nlb = new NetworkLoadBalancer(this, 'clusterNlb', {
vpc: props.vpc,
internetFacing: (!this.isInternal),
crossZoneEnabled: true,
});
// Set the load balancer type, defaulting to NLB if not specified
const loadBalancerTypeStr = scope.node.tryGetContext('loadBalancerType') ?? 'nlb'
this.elbType = props?.loadBalancerType ?? LoadBalancerType[(loadBalancerTypeStr).toUpperCase() as keyof typeof LoadBalancerType];
switch (this.elbType) {
case LoadBalancerType.NLB:
this.elb = new NetworkLoadBalancer(this, 'clusterNlb', {
vpc: props.vpc,
internetFacing: (!this.isInternal),
crossZoneEnabled: true,
});
break;
case LoadBalancerType.ALB:
this.elb = new ApplicationLoadBalancer(this, 'clusterAlb', {
vpc: props.vpc,
internetFacing: (!this.isInternal),
crossZoneEnabled: true,
securityGroup: props.securityGroup,
});
break;
default:
throw new Error('Invalid load balancer type provided. Valid values are ' + Object.values(LoadBalancerType).join(', '));
}

const opensearchPortMap = `${props?.mapOpensearchPortTo ?? scope.node.tryGetContext('mapOpensearchPortTo')}`;
const opensearchDashboardsPortMap = `${props?.mapOpensearchDashboardsPortTo ?? scope.node.tryGetContext('mapOpensearchDashboardsPortTo')}`;
Expand All @@ -428,34 +458,27 @@ export class InfraStack extends Stack {
+ ` Current mapping is OpenSearch:${this.opensearchPortMapping} OpenSearch-Dashboards:${this.opensearchDashboardsPortMapping}`);
}

if (!this.securityDisabled && !this.minDistribution && this.opensearchPortMapping === 443 && certificateArn !== 'undefined') {
opensearchListener = this.nlb.addListener('opensearch', {
port: this.opensearchPortMapping,
protocol: Protocol.TLS,
certificates: [ListenerCertificate.fromArn(certificateArn)],
});
} else {
opensearchListener = this.nlb.addListener('opensearch', {
port: this.opensearchPortMapping,
protocol: Protocol.TCP,
});
}
const useSSLOpensearchListener = !this.securityDisabled && !this.minDistribution && this.opensearchPortMapping === 443 && certificateArn !== 'undefined';
const opensearchListener = InfraStack.createListener(
this.elb,
this.elbType,
'opensearch',
this.opensearchPortMapping,
(useSSLOpensearchListener) ? certificateArn : undefined
);

let dashboardsListener: NetworkListener | ApplicationListener;
if (this.dashboardsUrl !== 'undefined') {
if (!this.securityDisabled && !this.minDistribution && this.opensearchDashboardsPortMapping === 443 && certificateArn !== 'undefined') {
dashboardsListener = this.nlb.addListener('dashboards', {
port: this.opensearchDashboardsPortMapping,
protocol: Protocol.TLS,
certificates: [ListenerCertificate.fromArn(certificateArn)],
});
} else {
dashboardsListener = this.nlb.addListener('dashboards', {
port: this.opensearchDashboardsPortMapping,
protocol: Protocol.TCP,
});
}
const useSSLDashboardsListener = !this.securityDisabled && !this.minDistribution
&& this.opensearchDashboardsPortMapping === 443 && certificateArn !== 'undefined';
dashboardsListener = InfraStack.createListener(
this.elb,
this.elbType,
'dashboards',
this.opensearchDashboardsPortMapping,
(useSSLDashboardsListener) ? certificateArn : undefined
);
}

if (this.singleNodeCluster) {
console.log('Single node value is true, creating single node configurations');
singleNodeInstance = new Instance(this, 'single-node-instance', {
Expand Down Expand Up @@ -483,19 +506,23 @@ export class InfraStack extends Stack {
});
Tags.of(singleNodeInstance).add('role', 'client');

opensearchListener.addTargets('single-node-target', {
port: 9200,
protocol: Protocol.TCP,
targets: [new InstanceTarget(singleNodeInstance)],
});
// Disable target security for now, can be provided as an option in the future
InfraStack.addTargets(
opensearchListener,
this.elbType,
'single-node-target',
9200,
new InstanceTarget(singleNodeInstance),
false);

if (this.dashboardsUrl !== 'undefined') {
// @ts-ignore
dashboardsListener.addTargets('single-node-osd-target', {
port: 5601,
protocol: Protocol.TCP,
targets: [new InstanceTarget(singleNodeInstance)],
});
InfraStack.addTargets(
dashboardsListener!,
this.elbType,
'single-node-osd-target',
5601,
new InstanceTarget(singleNodeInstance),
false);
}
new CfnOutput(this, 'private-ip', {
value: singleNodeInstance.instancePrivateIp,
Expand Down Expand Up @@ -660,23 +687,27 @@ export class InfraStack extends Stack {
Tags.of(mlNodeAsg).add('role', 'ml-node');
}

opensearchListener.addTargets('opensearchTarget', {
port: 9200,
protocol: Protocol.TCP,
targets: [clientNodeAsg],
});
// Disable target security for now, can be provided as an option in the future
InfraStack.addTargets(
opensearchListener,
this.elbType,
'opensearchTarget',
9200,
clientNodeAsg,
false);

if (this.dashboardsUrl !== 'undefined') {
// @ts-ignore
dashboardsListener.addTargets('dashboardsTarget', {
port: 5601,
protocol: Protocol.TCP,
targets: [clientNodeAsg],
});
InfraStack.addTargets(
dashboardsListener!,
this.elbType,
'dashboardsTarget',
5601,
clientNodeAsg,
false);
}
}
new CfnOutput(this, 'loadbalancer-url', {
value: this.nlb.loadBalancerDnsName,
value: this.elb.loadBalancerDnsName,
});

if (this.enableMonitoring) {
Expand Down Expand Up @@ -1013,4 +1044,75 @@ export class InfraStack extends Stack {

return cfnInitConfig;
}

/**
* Creates a listener for the given load balancer.
* If a certificate is provided, the protocol will be set to TLS/HTTPS.
* Otherwise, the protocol will be set to TCP/HTTP.
*/
private static createListener(elb: BaseLoadBalancer, elbType: LoadBalancerType, id: string, port: number,
certificateArn?: string): ApplicationListener | NetworkListener {
const useSSL = !!certificateArn;

let protocol: ApplicationProtocol | Protocol;
switch(elbType) {
case LoadBalancerType.ALB:
protocol = useSSL ? ApplicationProtocol.HTTPS : ApplicationProtocol.HTTP;
break;
case LoadBalancerType.NLB:
protocol = useSSL ? Protocol.TLS : Protocol.TCP;
break;
default:
throw new Error('Unsupported load balancer type.');
}

const listenerProps: BaseApplicationListenerProps | BaseNetworkListenerProps = {
port: port,
protocol: protocol,
certificates: useSSL ? [ListenerCertificate.fromArn(certificateArn)] : undefined,
};

switch(elbType) {
case LoadBalancerType.ALB: {
const alb = elb as ApplicationLoadBalancer;
return alb.addListener(id, listenerProps as BaseApplicationListenerProps);
}
case LoadBalancerType.NLB: {
const nlb = elb as NetworkLoadBalancer;
return nlb.addListener(id, listenerProps as BaseNetworkListenerProps);
}
default:
throw new Error('Unsupported load balancer type.');
}
}

/**
* Adds targets to the given listener.
* Works for both Application Load Balancers and Network Load Balancers.
*/
private static addTargets(listener: BaseListener, elbType: LoadBalancerType, id: string, port: number, target: AutoScalingGroup | InstanceTarget,
securityEnabled: boolean) {
switch(elbType) {
case LoadBalancerType.ALB: {
const albListener = listener as ApplicationListener;
albListener.addTargets(id, {
port: port,
protocol: securityEnabled ? ApplicationProtocol.HTTPS : ApplicationProtocol.HTTP,
targets: [target],
});
break;
}
case LoadBalancerType.NLB: {
const nlbListener = listener as NetworkListener;
nlbListener.addTargets(id, {
port: port,
protocol: securityEnabled ? Protocol.TLS : Protocol.TCP,
targets: [target],
});
break;
}
default:
throw new Error('Unsupported load balancer type.');
}
}
}
49 changes: 48 additions & 1 deletion test/opensearch-cluster-cdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ The OpenSearch Contributors require contributions made to
this file be licensed under the Apache-2.0 license or a
compatible open source license. */

import { App } from 'aws-cdk-lib';
import { App, Stack } from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { InfraStack } from '../lib/infra/infra-stack';
import { NetworkStack } from '../lib/networking/vpc-stack';
Expand Down Expand Up @@ -1070,3 +1070,50 @@ test('Ensure target group protocol is always TCP', () => {
TargetType: 'instance',
});
});


describe.each([
{ loadBalancerType: 'alb', securityDisabled: false, expectedType: 'application', expectedProtocol: 'HTTPS' },
{ loadBalancerType: 'alb', securityDisabled: true, expectedType: 'application', expectedProtocol: 'HTTP' },
{ loadBalancerType: 'nlb', securityDisabled: false, expectedType: 'network', expectedProtocol: 'TLS' },
{ loadBalancerType: 'nlb', securityDisabled: true, expectedType: 'network', expectedProtocol: 'TCP' },
])('Test $loadBalancerType creation with securityDisabled=$securityDisabled', ({ loadBalancerType, securityDisabled, expectedType, expectedProtocol }) => {
test(`should create ${loadBalancerType} with securityDisabled=${securityDisabled}`, () => {
const app = new App({
context: {
securityDisabled,
certificateArn: (securityDisabled) ? undefined : 'arn:1234',
minDistribution: false,
distributionUrl: 'www.example.com',
cpuArch: 'x64',
singleNodeCluster: false,
dashboardsUrl: 'www.example.com',
distVersion: '1.0.0',
serverAccessType: 'ipv4',
restrictServerAccessTo: 'all',
loadBalancerType,
},
});

// WHEN
const networkStack = new NetworkStack(app, 'opensearch-network-stack', {
env: { account: 'test-account', region: 'us-east-1' },
});

const infraStack = new InfraStack(app as unknown as Stack, 'opensearch-infra-stack', {
vpc: networkStack.vpc,
securityGroup: networkStack.osSecurityGroup,
env: { account: 'test-account', region: 'us-east-1' },
});

// THEN
const infraTemplate = Template.fromStack(infraStack);
infraTemplate.hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', {
Type: expectedType,
});

infraTemplate.hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', {
Protocol: expectedProtocol,
});
});
});

0 comments on commit b2f77fa

Please sign in to comment.