Skip to content

Commit

Permalink
Add VPCe for all aws services
Browse files Browse the repository at this point in the history
Signed-off-by: Andre Kurait <[email protected]>
  • Loading branch information
AndreKurait committed Sep 24, 2024
1 parent 2107ccb commit f7b89e1
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Effect, PolicyStatement, Role, ServicePrincipal} from "aws-cdk-lib/aws-iam";
import {Construct} from "constructs";
import {CpuArchitecture} from "aws-cdk-lib/aws-ecs";
import {RemovalPolicy} from "aws-cdk-lib";
import {RemovalPolicy, Stack} from "aws-cdk-lib";
import { IStringParameter, StringParameter } from "aws-cdk-lib/aws-ssm";
import * as forge from 'node-forge';
import * as yargs from 'yargs';
Expand Down Expand Up @@ -421,3 +421,11 @@ export function parseClusterDefinition(json: any): ClusterYaml {
}
return new ClusterYaml({endpoint, version, auth})
}

export function isStackInGovCloud(stack: Stack): boolean {
return isRegionGovCloud(stack.region);
}

export function isRegionGovCloud(region: string): boolean {
return region.startsWith('us-gov-');
}
47 changes: 45 additions & 2 deletions deployment/cdk/opensearch-service-migration/lib/network-stack.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
GatewayVpcEndpointAwsService,
InterfaceVpcEndpointAwsService,
IpAddresses, IVpc, Port, SecurityGroup,
SubnetType,
Vpc
Expand All @@ -11,8 +13,9 @@ import { ARecord, HostedZone, RecordTarget } from "aws-cdk-lib/aws-route53";
import { LoadBalancerTarget } from "aws-cdk-lib/aws-route53-targets";
import { AcmCertificateImporter } from "./service-stacks/acm-cert-importer";
import { Stack } from "aws-cdk-lib";
import { createMigrationStringParameter, getMigrationStringParameterName, MigrationSSMParameter } from "./common-utilities";
import { createMigrationStringParameter, getMigrationStringParameterName, isStackInGovCloud, MigrationSSMParameter } from "./common-utilities";
import { StringParameter } from "aws-cdk-lib/aws-ssm";
import { GatewayVpcEndpoint, InterfaceVpcEndpoint } from "aws-cdk-lib/aws-ec2";

export interface NetworkStackProps extends StackPropsExt {
readonly vpcId?: string;
Expand Down Expand Up @@ -78,6 +81,43 @@ export class NetworkStack extends Stack {
}
}

private createVpcEndpoints(vpc: IVpc) {
// Gateway endpoints
new GatewayVpcEndpoint(this, 'S3VpcEndpoint', {
service: GatewayVpcEndpointAwsService.S3,
vpc: vpc,
});

// Interface endpoints
const createInterfaceVpcEndpoint = (service: InterfaceVpcEndpointAwsService) => {
new InterfaceVpcEndpoint(this, `${service.shortName}VpcEndpoint`, {
service: service,
vpc: vpc,
});
};

// General interface endpoints
const interfaceEndpoints = [
InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS, // Push Logs from tasks
InterfaceVpcEndpointAwsService.CLOUDWATCH_MONITORING, // Pull Metrics from Migration Console
InterfaceVpcEndpointAwsService.ECR_DOCKER, // Pull Images on Startup
InterfaceVpcEndpointAwsService.ECR, // List Images on Startup
InterfaceVpcEndpointAwsService.ECS_AGENT, // Task Container Metrics
InterfaceVpcEndpointAwsService.ECS_TELEMETRY, // Task Container Metrics
InterfaceVpcEndpointAwsService.ECS, // ECS Task Control
InterfaceVpcEndpointAwsService.ELASTIC_LOAD_BALANCING, // Control ALB
InterfaceVpcEndpointAwsService.SECRETS_MANAGER, // Cluster Password Secret
InterfaceVpcEndpointAwsService.SSM_MESSAGES, // Session Manager
InterfaceVpcEndpointAwsService.SSM, // Parameter Store
InterfaceVpcEndpointAwsService.XRAY, // X-Ray Traces
isStackInGovCloud(this) ?
InterfaceVpcEndpointAwsService.ELASTIC_FILESYSTEM_FIPS : // EFS Control Plane GovCloud
InterfaceVpcEndpointAwsService.ELASTIC_FILESYSTEM, // EFS Control Plane

];
interfaceEndpoints.forEach(service => createInterfaceVpcEndpoint(service));
}

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

Expand Down Expand Up @@ -126,7 +166,10 @@ export class NetworkStack extends Stack {
cidrMask: 24,
},
],
natGateways: 0,
});
// Only create interface endpoints if VPC not imported
this.createVpcEndpoints(this.vpc);
}
this.validateVPC(this.vpc)
if(!props.addOnMigrationDeployId) {
Expand Down Expand Up @@ -266,7 +309,7 @@ export class NetworkStack extends Stack {
}

getSecureListenerSslPolicy() {
return (this.partition === "aws-us-gov") ? SslPolicy.FIPS_TLS13_12_EXT2 : SslPolicy.RECOMMENDED_TLS
return isStackInGovCloud(this) ? SslPolicy.FIPS_TLS13_12_EXT2 : SslPolicy.RECOMMENDED_TLS
}

createSecureListener(serviceName: string, listeningPort: number, alb: IApplicationLoadBalancer, cert: ICertificate, albTargetGroup?: IApplicationTargetGroup) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ export class StackComposer {
sourceClusterEndpoint,
targetClusterUsername: targetCluster ? targetClusterAuth?.basicAuth?.username : fineGrainedManagerUserName,
targetClusterPasswordSecretArn: targetCluster ? targetClusterAuth?.basicAuth?.password_from_secret_arn : fineGrainedManagerUserSecretManagerKeyARN,
env: props.env
env: props.env,
})
this.stacks.push(networkStack)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,33 @@ import { createStackComposer } from "./test-utils";
import { ContainerImage } from "aws-cdk-lib/aws-ecs";
import { StringParameter } from "aws-cdk-lib/aws-ssm";
import { describe, beforeEach, afterEach, test, expect, jest } from '@jest/globals';
import { GatewayVpcEndpointAwsService, InterfaceVpcEndpointAwsService } from "aws-cdk-lib/aws-ec2";
import { Stack } from "aws-cdk-lib";

jest.mock('aws-cdk-lib/aws-ecr-assets');

function getExpectedEndpoints(networkStack: NetworkStack): (InterfaceVpcEndpointAwsService | GatewayVpcEndpointAwsService)[] {
return [
InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS,
InterfaceVpcEndpointAwsService.CLOUDWATCH_MONITORING,
InterfaceVpcEndpointAwsService.ECR,
InterfaceVpcEndpointAwsService.ECR_DOCKER,
InterfaceVpcEndpointAwsService.ECS_AGENT,
InterfaceVpcEndpointAwsService.ECS_TELEMETRY,
InterfaceVpcEndpointAwsService.ECS,
InterfaceVpcEndpointAwsService.ELASTIC_LOAD_BALANCING,
InterfaceVpcEndpointAwsService.ELASTIC_FILESYSTEM,
InterfaceVpcEndpointAwsService.SECRETS_MANAGER,
InterfaceVpcEndpointAwsService.SSM,
InterfaceVpcEndpointAwsService.SSM_MESSAGES,
InterfaceVpcEndpointAwsService.XRAY,
GatewayVpcEndpointAwsService.S3,
Stack.of(networkStack).region.startsWith('us-gov-')
? InterfaceVpcEndpointAwsService.ELASTIC_FILESYSTEM_FIPS
: InterfaceVpcEndpointAwsService.ELASTIC_FILESYSTEM,
];
}

describe('NetworkStack Tests', () => {
beforeEach(() => {
// Mock value returned from SSM call
Expand Down Expand Up @@ -49,7 +74,7 @@ describe('NetworkStack Tests', () => {
const networkTemplate = Template.fromStack(networkStack)

networkTemplate.resourceCountIs("AWS::EC2::VPC", 1)
networkTemplate.resourceCountIs("AWS::EC2::SecurityGroup", 1)
networkTemplate.resourceCountIs("AWS::EC2::SecurityGroup", getExpectedEndpoints(networkStack).length)
// For each AZ, a private and public subnet is created
networkTemplate.resourceCountIs("AWS::EC2::Subnet", 4)

Expand Down Expand Up @@ -78,6 +103,63 @@ describe('NetworkStack Tests', () => {
networkTemplate.resourceCountIs("AWS::EC2::SecurityGroup", 0)
});

test('Test VPC Endpoints are created correctly', () => {
const contextOptions = {
vpcEnabled: true,
vpcAZCount: 2,
sourceCluster: {
"endpoint": "https://test-cluster",
"auth": {"type": "none"}
}
}

const openSearchStacks = createStackComposer(contextOptions)
const networkStack: NetworkStack = (openSearchStacks.stacks.filter((s) => s instanceof NetworkStack)[0]) as NetworkStack
const networkTemplate = Template.fromStack(networkStack)

// Define the expected VPC endpoints
const expectedEndpoints = getExpectedEndpoints(networkStack);

// Check for S3 Gateway Endpoint
networkTemplate.hasResourceProperties('AWS::EC2::VPCEndpoint', {
VpcEndpointType: 'Gateway',
});

// Loop through the VPC endpoints in the network stack and check that the service name is unique and in the list of expectedEndpoints
const vpcEndpoints = networkTemplate.findResources('AWS::EC2::VPCEndpoint');
const uniqueServiceNames = new Set<string>();

const expectedServiceNames = expectedEndpoints.map(endpoint => {
return endpoint instanceof GatewayVpcEndpointAwsService ?
endpoint.name.toLowerCase().split('.').pop() as string : endpoint.shortName.toLowerCase();
});

for (const endpointKey in vpcEndpoints) {
const endpoint = vpcEndpoints[endpointKey];
let serviceName: string;
if (endpoint.Properties.ServiceName['Fn::Join']) {
const joinParts = endpoint.Properties.ServiceName['Fn::Join'][1];
serviceName = (joinParts[joinParts.length - 1] as string).split('.').slice(1).join('.') as string;
} else {
serviceName = endpoint.Properties.ServiceName.split('.').slice(3).join('.');
}
expect(uniqueServiceNames.has(serviceName)).toBe(false);
uniqueServiceNames.add(serviceName);

const matchingEndpoint = expectedServiceNames.find(e => serviceName.includes(e));
if (!matchingEndpoint) {
console.error(`Failed assertion for service: ${serviceName}`);
console.error(`Expected: ${serviceName} to be in ${expectedServiceNames.join(', ')}`);
console.error(`Received: ${matchingEndpoint}`);
}
expect(matchingEndpoint).toBeDefined();
}

expect(uniqueServiceNames.size).toBe(expectedEndpoints.length);
// Verify the total number of VPC Endpoints
networkTemplate.resourceCountIs('AWS::EC2::VPCEndpoint', expectedEndpoints.length);
});

test('Test valid https imported target cluster endpoint with port is formatted correctly', () => {
const inputTargetEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443"
const expectedFormattedEndpoint = "https://vpc-domain-abcdef.us-east-1.es.amazonaws.com:443"
Expand Down

0 comments on commit f7b89e1

Please sign in to comment.