From f7b89e133f7d898ab6184c44ab56736a8aed944d Mon Sep 17 00:00:00 2001 From: Andre Kurait Date: Tue, 24 Sep 2024 12:38:32 -0500 Subject: [PATCH] Add VPCe for all aws services Signed-off-by: Andre Kurait --- .../lib/common-utilities.ts | 10 ++- .../lib/network-stack.ts | 47 ++++++++++- .../lib/stack-composer.ts | 2 +- .../test/network-stack.test.ts | 84 ++++++++++++++++++- 4 files changed, 138 insertions(+), 5 deletions(-) diff --git a/deployment/cdk/opensearch-service-migration/lib/common-utilities.ts b/deployment/cdk/opensearch-service-migration/lib/common-utilities.ts index 84557ba31..0da81c177 100644 --- a/deployment/cdk/opensearch-service-migration/lib/common-utilities.ts +++ b/deployment/cdk/opensearch-service-migration/lib/common-utilities.ts @@ -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'; @@ -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-'); +} diff --git a/deployment/cdk/opensearch-service-migration/lib/network-stack.ts b/deployment/cdk/opensearch-service-migration/lib/network-stack.ts index 579a284bd..61fef0b44 100644 --- a/deployment/cdk/opensearch-service-migration/lib/network-stack.ts +++ b/deployment/cdk/opensearch-service-migration/lib/network-stack.ts @@ -1,4 +1,6 @@ import { + GatewayVpcEndpointAwsService, + InterfaceVpcEndpointAwsService, IpAddresses, IVpc, Port, SecurityGroup, SubnetType, Vpc @@ -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; @@ -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); @@ -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) { @@ -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) { diff --git a/deployment/cdk/opensearch-service-migration/lib/stack-composer.ts b/deployment/cdk/opensearch-service-migration/lib/stack-composer.ts index 73111629e..1ac567091 100644 --- a/deployment/cdk/opensearch-service-migration/lib/stack-composer.ts +++ b/deployment/cdk/opensearch-service-migration/lib/stack-composer.ts @@ -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) } diff --git a/deployment/cdk/opensearch-service-migration/test/network-stack.test.ts b/deployment/cdk/opensearch-service-migration/test/network-stack.test.ts index 384336b24..c2a4f9eaa 100644 --- a/deployment/cdk/opensearch-service-migration/test/network-stack.test.ts +++ b/deployment/cdk/opensearch-service-migration/test/network-stack.test.ts @@ -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 @@ -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) @@ -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(); + + 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"