From c233ff0202734c8bf449398ff5312aaec1eb1e20 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Wed, 26 Mar 2025 15:26:27 -0400 Subject: [PATCH 1/5] Add API Gateway to the stack --- backend/src/iac/backend-stack.ts | 294 +++++++++++++++++++++++-------- 1 file changed, 223 insertions(+), 71 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index ae522ffe..b0acd15c 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -2,8 +2,11 @@ import * as cdk from 'aws-cdk-lib'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as logs from 'aws-cdk-lib/aws-logs'; -import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import * as cognito from 'aws-cdk-lib/aws-cognito'; +import * as apigateway from 'aws-cdk-lib/aws-apigateway'; +import * as servicediscovery from 'aws-cdk-lib/aws-servicediscovery'; +import * as iam from 'aws-cdk-lib/aws-iam'; + import { Construct } from 'constructs'; import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; import { RemovalPolicy } from 'aws-cdk-lib'; @@ -12,8 +15,6 @@ interface BackendStackProps extends cdk.StackProps { environment: string; cognitoClientId: string; cognitoUserPoolId: string; - domainName?: string; // Optional domain name for certificate - hostedZoneId?: string; // Optional hosted zone ID for domain } export class BackendStack extends cdk.Stack { @@ -23,26 +24,34 @@ export class BackendStack extends cdk.Stack { const isProd = props.environment === 'production'; const appName = 'AIMedicalReport'; - // Look up existing VPC or create a new one - const vpc: ec2.IVpc = new ec2.Vpc(this, `${appName}VPC`, { + // VPC + const vpc = new ec2.Vpc(this, `${appName}VPC`, { vpcName: `${appName}VPC-${props.environment}`, maxAzs: 2, + natGateways: isProd ? 2 : 1, }); + // ECS Cluster const cluster = new ecs.Cluster(this, `${appName}Cluster`, { vpc, clusterName: `${appName}Cluster-${props.environment}`, containerInsights: true, + enableFargateCapacityProviders: true, + }); + + // CloudMap Namespace for service discovery + const namespace = cluster.addDefaultCloudMapNamespace({ + name: `${appName.toLowerCase()}.local`, }); - // Create Log Group for container + // Log Group const logGroup = new logs.LogGroup(this, `${appName}LogGroup`, { logGroupName: `/ecs/${appName}-${props.environment}`, retention: isProd ? logs.RetentionDays.ONE_MONTH : logs.RetentionDays.ONE_WEEK, removalPolicy: isProd ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY, }); - // Create DynamoDB table for reports + // DynamoDB table for reports const reportsTable = new Table(this, `${appName}ReportsTable-${props.environment}`, { tableName: `${appName}ReportsTable${props.environment}`, partitionKey: { @@ -57,7 +66,7 @@ export class BackendStack extends cdk.Stack { removalPolicy: isProd ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY, }); - // Add a GSI for querying by date (most recent first) + // Add GSI for querying by date reportsTable.addGlobalSecondaryIndex({ indexName: 'userIdDateIndex', partitionKey: { @@ -70,20 +79,50 @@ export class BackendStack extends cdk.Stack { }, }); - // Look up existing Cognito User Pool - const userPoolId = - props.cognitoUserPoolId || - cognito.UserPool.fromUserPoolId(this, `${appName}UserPool`, 'us-east-1_PszlvSmWc').userPoolId; + // Cognito User Pool + const userPool = cognito.UserPool.fromUserPoolId( + this, + `${appName}UserPool`, + props.cognitoUserPoolId || 'us-east-1_PszlvSmWc', + ); - // Create a Cognito domain if it doesn't exist + // Cognito domain const userPoolDomain = cognito.UserPoolDomain.fromDomainName( this, `${appName}ExistingDomain-${props.environment}`, - 'us-east-1pszlvsmwc', // The domain prefix without the .auth.region.amazoncognito.com part + 'us-east-1pszlvsmwc', + ); + + // User Pool Client + const userPoolClient = cognito.UserPoolClient.fromUserPoolClientId( + this, + `${appName}UserPoolClient-${props.environment}`, + props.cognitoClientId, + ); + + // Security Group for Fargate service + const serviceSecurityGroup = new ec2.SecurityGroup( + this, + `${appName}ServiceSG-${props.environment}`, + { + vpc, + allowAllOutbound: true, + description: 'Security group for Fargate service', + }, ); - // Replace the userPoolClient reference with a direct reference to the client ID - const userPoolClientId = props.cognitoClientId; + // Add inbound rules to allow traffic from API Gateway + serviceSecurityGroup.addIngressRule( + ec2.Peer.ipv4(vpc.vpcCidrBlock), + ec2.Port.tcp(3000), + 'Allow inbound HTTP traffic from within VPC', + ); + + serviceSecurityGroup.addIngressRule( + ec2.Peer.ipv4(vpc.vpcCidrBlock), + ec2.Port.tcp(3443), + 'Allow inbound HTTPS traffic from within VPC', + ); // Task Definition const taskDefinition = new ecs.FargateTaskDefinition( @@ -95,6 +134,23 @@ export class BackendStack extends cdk.Stack { }, ); + // Grant DynamoDB permissions to task + reportsTable.grantReadWriteData(taskDefinition.taskRole); + + // Create a secrets manager for the SSL certificate and key + const certificateSecret = new cdk.aws_secretsmanager.Secret(this, `${appName}CertSecret-${props.environment}`, { + secretName: `${appName}/ssl-cert-${props.environment}`, + description: 'SSL certificate and private key for HTTPS', + generateSecretString: { + secretStringTemplate: JSON.stringify({ + // You'll need to populate these values after deployment + certificate: '-----BEGIN CERTIFICATE-----\nYour certificate here\n-----END CERTIFICATE-----', + privateKey: '-----BEGIN PRIVATE KEY-----\nYour private key here\n-----END PRIVATE KEY-----' + }), + generateStringKey: 'dummy' // This key won't be used but is required + }, + }); + // Container const container = taskDefinition.addContainer(`${appName}Container-${props.environment}`, { image: ecs.ContainerImage.fromAsset('../backend/', { @@ -107,85 +163,68 @@ export class BackendStack extends cdk.Stack { // Basic environment variables NODE_ENV: props.environment, PORT: '3000', + HTTPS_PORT: '3443', // Add HTTPS port + ENABLE_HTTPS: 'true', // Enable HTTPS // AWS related AWS_REGION: this.region, - AWS_COGNITO_USER_POOL_ID: userPoolId, - AWS_COGNITO_CLIENT_ID: userPoolClientId, + AWS_COGNITO_USER_POOL_ID: userPool.userPoolId, + AWS_COGNITO_CLIENT_ID: userPoolClient.userPoolClientId, DYNAMODB_REPORTS_TABLE: reportsTable.tableName, // Perplexity related PERPLEXITY_API_KEY_SECRET_NAME: `medical-reports-explainer/${props.environment}/perplexity-api-key`, PERPLEXITY_MODEL: 'sonar', PERPLEXITY_MAX_TOKENS: '2048', + + // SSL Certificate secret + SSL_CERT_SECRET_NAME: certificateSecret.secretName, }, logging: ecs.LogDrivers.awsLogs({ streamPrefix: appName, logGroup, }), + healthCheck: { + command: ['CMD-SHELL', 'curl -f -k https://localhost:3443/api/health || exit 1'], + interval: cdk.Duration.seconds(30), + timeout: cdk.Duration.seconds(5), + retries: 3, + startPeriod: cdk.Duration.seconds(60), + }, }); + // Grant the task role access to read the SSL certificate secret + certificateSecret.grantRead(taskDefinition.taskRole); + container.addPortMappings({ containerPort: 3000, + name: 'http-api', protocol: ecs.Protocol.TCP, }); - // 1. Create ALB - const alb = new elbv2.ApplicationLoadBalancer(this, `${appName}ALB-${props.environment}`, { - vpc, - internetFacing: true, - loadBalancerName: `${appName}-${props.environment}`, - }); - - // 2. Create ALB Target Group - const targetGroup = new elbv2.ApplicationTargetGroup( - this, - `${appName}TargetGroup-${props.environment}`, - { - vpc, - port: 3000, - protocol: elbv2.ApplicationProtocol.HTTP, - targetType: elbv2.TargetType.IP, - healthCheck: { - path: '/api/health', - interval: cdk.Duration.seconds(30), - timeout: cdk.Duration.seconds(5), - }, - }, - ); - - // 3. HTTP 80 Listener - const httpListener = alb.addListener(`${appName}HttpListener-${props.environment}`, { - port: 80, - protocol: elbv2.ApplicationProtocol.HTTP, - defaultAction: elbv2.ListenerAction.forward([targetGroup]), + container.addPortMappings({ + containerPort: 3443, + name: 'https-api', + protocol: ecs.Protocol.TCP, }); - // 4. Create a security group for the Fargate service - const serviceSecurityGroup = new ec2.SecurityGroup( - this, - `${appName}ServiceSG-${props.environment}`, - { - vpc, - allowAllOutbound: true, - }, - ); - - // 5. Create the Fargate service WITHOUT registering it with the target group yet + // Create Fargate Service with CloudMap service discovery const fargateService = new ecs.FargateService(this, `${appName}Service-${props.environment}`, { cluster, taskDefinition, desiredCount: isProd ? 2 : 1, - assignPublicIp: false, securityGroups: [serviceSecurityGroup], + assignPublicIp: false, // Using private subnets with NAT gateway + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, + cloudMapOptions: { + name: `${appName.toLowerCase()}-service`, + dnsRecordType: servicediscovery.DnsRecordType.A, + dnsTtl: cdk.Duration.seconds(30), + container: container, + containerPort: 3000, + }, }); - // 6. Add explicit dependency to ensure the listener exists before the service - fargateService.node.addDependency(httpListener); - - // 7. Now register the service with the target group - targetGroup.addTarget(fargateService); - // Add autoscaling for production if (isProd) { const scaling = fargateService.autoScaleTaskCount({ @@ -200,22 +239,135 @@ export class BackendStack extends cdk.Stack { }); } - // Add output for the table name + // Create VPC Link for API Gateway (using HTTP API VPC Link) + const vpcLink = new apigateway.VpcLink(this, `${appName}VpcLink-${props.environment}`, { + vpc, + description: `VPC Link for ${appName} ${props.environment}`, + vpcLinkName: `${appName}VpcLink-${props.environment}`, + }); + + // Create API Gateway + const api = new apigateway.RestApi(this, `${appName}Api-${props.environment}`, { + restApiName: `${appName}-${props.environment}`, + description: `API for ${appName} ${props.environment}`, + deployOptions: { + stageName: props.environment, + loggingLevel: apigateway.MethodLoggingLevel.INFO, + dataTraceEnabled: true, + }, + defaultCorsPreflightOptions: { + allowOrigins: apigateway.Cors.ALL_ORIGINS, + allowMethods: apigateway.Cors.ALL_METHODS, + allowHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date', 'X-Api-Key'], + }, + }); + + // Create Cognito Authorizer + const authorizer = new apigateway.CognitoUserPoolsAuthorizer( + this, + `${appName}Authorizer-${props.environment}`, + { + cognitoUserPools: [userPool], + authorizerName: `${appName}Authorizer-${props.environment}`, + identitySource: 'method.request.header.Authorization', + }, + ); + + // Get the service URL from CloudMap (now using HTTPS) + const serviceUrl = `https://${appName.toLowerCase()}-service.${appName.toLowerCase()}.local:3443`; + + // Create proxy resource with Cognito authorization + const proxyResource = api.root.addResource('{proxy+}'); + + // Integration with Fargate service via VPC Link + const integration = new apigateway.Integration({ + type: apigateway.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'ANY', + options: { + connectionType: apigateway.ConnectionType.VPC_LINK, + vpcLink: vpcLink, + requestParameters: { + 'integration.request.path.proxy': 'method.request.path.proxy', + }, + // Skip TLS verification for self-signed certificates in internal traffic + tlsConfig: { + insecureSkipVerification: true, + }, + }, + uri: `${serviceUrl}/{proxy}`, + }); + + proxyResource.addMethod('ANY', integration, { + authorizer: authorizer, + authorizationType: apigateway.AuthorizationType.COGNITO, + requestParameters: { + 'method.request.path.proxy': true, + }, + }); + + // Add health check endpoint without authorization + const healthResource = api.root.addResource('health'); + healthResource.addMethod( + 'GET', + new apigateway.Integration({ + type: apigateway.IntegrationType.HTTP_PROXY, + integrationHttpMethod: 'GET', + options: { + connectionType: apigateway.ConnectionType.VPC_LINK, + vpcLink: vpcLink, + // Skip TLS verification for self-signed certificates in internal traffic + tlsConfig: { + insecureSkipVerification: true, + }, + }, + uri: `${serviceUrl}/api/health`, + }), + ); + + // Add execution role policy to allow API Gateway to access VPC resources + const executionRole = new iam.Role(this, `${appName}APIGatewayVPCRole-${props.environment}`, { + assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonAPIGatewayPushToCloudWatchLogs'), + iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonVPCCrossAccountNetworkInterfaceOperations'), + ], + }); + + // Attach the execution role to the API + const apiResource = api.node.findChild('Default') as apigateway.CfnRestApi; + apiResource.addPropertyOverride('Policy', { + Version: '2012-10-17', + Statement: [ + { + Effect: 'Allow', + Principal: { + Service: 'apigateway.amazonaws.com', + }, + Action: 'sts:AssumeRole', + Resource: executionRole.roleArn, + }, + ], + }); + + // Outputs new cdk.CfnOutput(this, 'ReportsTableName', { value: reportsTable.tableName, description: 'DynamoDB Reports Table Name', }); - // Add output for Cognito domain new cdk.CfnOutput(this, 'CognitoDomain', { value: `https://${userPoolDomain.domainName}.auth.${this.region}.amazoncognito.com`, description: 'Cognito Domain URL', }); - // Outputs - new cdk.CfnOutput(this, 'LoadBalancerDNS', { - value: alb.loadBalancerDnsName, - description: 'Load Balancer DNS Name', + new cdk.CfnOutput(this, 'ApiGatewayUrl', { + value: api.url, + description: 'API Gateway URL', + }); + + new cdk.CfnOutput(this, 'ServiceDiscoveryUrl', { + value: serviceUrl, + description: 'Service Discovery URL', }); } } From 0bb9dfc1040a69cb873659fdbaff94fadf219962 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Wed, 26 Mar 2025 15:26:40 -0400 Subject: [PATCH 2/5] Add API Gateway to the stack --- backend/src/iac/backend-stack.ts | 38 ++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index b0acd15c..433785d6 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -40,7 +40,7 @@ export class BackendStack extends cdk.Stack { }); // CloudMap Namespace for service discovery - const namespace = cluster.addDefaultCloudMapNamespace({ + cluster.addDefaultCloudMapNamespace({ name: `${appName.toLowerCase()}.local`, }); @@ -138,18 +138,24 @@ export class BackendStack extends cdk.Stack { reportsTable.grantReadWriteData(taskDefinition.taskRole); // Create a secrets manager for the SSL certificate and key - const certificateSecret = new cdk.aws_secretsmanager.Secret(this, `${appName}CertSecret-${props.environment}`, { - secretName: `${appName}/ssl-cert-${props.environment}`, - description: 'SSL certificate and private key for HTTPS', - generateSecretString: { - secretStringTemplate: JSON.stringify({ - // You'll need to populate these values after deployment - certificate: '-----BEGIN CERTIFICATE-----\nYour certificate here\n-----END CERTIFICATE-----', - privateKey: '-----BEGIN PRIVATE KEY-----\nYour private key here\n-----END PRIVATE KEY-----' - }), - generateStringKey: 'dummy' // This key won't be used but is required + const certificateSecret = new cdk.aws_secretsmanager.Secret( + this, + `${appName}CertSecret-${props.environment}`, + { + secretName: `${appName}/ssl-cert-${props.environment}`, + description: 'SSL certificate and private key for HTTPS', + generateSecretString: { + secretStringTemplate: JSON.stringify({ + // You'll need to populate these values after deployment + certificate: + '-----BEGIN CERTIFICATE-----\nYour certificate here\n-----END CERTIFICATE-----', + privateKey: + '-----BEGIN PRIVATE KEY-----\nYour private key here\n-----END PRIVATE KEY-----', + }), + generateStringKey: 'dummy', // This key won't be used but is required + }, }, - }); + ); // Container const container = taskDefinition.addContainer(`${appName}Container-${props.environment}`, { @@ -328,8 +334,12 @@ export class BackendStack extends cdk.Stack { const executionRole = new iam.Role(this, `${appName}APIGatewayVPCRole-${props.environment}`, { assumedBy: new iam.ServicePrincipal('apigateway.amazonaws.com'), managedPolicies: [ - iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonAPIGatewayPushToCloudWatchLogs'), - iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonVPCCrossAccountNetworkInterfaceOperations'), + iam.ManagedPolicy.fromAwsManagedPolicyName( + 'service-role/AmazonAPIGatewayPushToCloudWatchLogs', + ), + iam.ManagedPolicy.fromAwsManagedPolicyName( + 'AmazonVPCCrossAccountNetworkInterfaceOperations', + ), ], }); From 5c1d6095b5f759e61e1828e0cc14e2387494f376 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Wed, 26 Mar 2025 15:48:13 -0400 Subject: [PATCH 3/5] Add API Gateway with ALB --- backend/package-lock.json | 10 +++--- backend/package.json | 18 +++++------ backend/src/iac/backend-stack.ts | 53 ++++++++++++++++++++++---------- 3 files changed, 51 insertions(+), 30 deletions(-) diff --git a/backend/package-lock.json b/backend/package-lock.json index 431b674c..853ab10c 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -54,7 +54,7 @@ "@typescript-eslint/parser": "^7.9.0", "@vitest/coverage-c8": "^0.33.0", "aws-cdk": "2.139.0", - "aws-cdk-lib": "^2.184.1", + "aws-cdk-lib": "^2.185.0", "dotenv-cli": "^8.0.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.0.0", @@ -72,7 +72,7 @@ "vitest": "^0.33.0" }, "peerDependencies": { - "aws-cdk-lib": "2.184.1" + "aws-cdk-lib": "^2.185.0" } }, "node_modules/@ampproject/remapping": { @@ -4677,9 +4677,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.184.1", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.184.1.tgz", - "integrity": "sha512-No9g0SGadiDz0IEUIeJg4wSV/jFCGcouW2zUOTjV8OU4gTMoGiqC8BYSv7E6ucUtW6rmSFVK+pbc8XOFZOo1cg==", + "version": "2.185.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.185.0.tgz", + "integrity": "sha512-RNcQeNnInumDF1hq3gAf+/A6jhvYDof5a7418gEs/y6359gTYZpTCQkgItC50iV3MmkgerrBAdOE7CDEtQNDWw==", "bundleDependencies": [ "@balena/dockerignore", "case", diff --git a/backend/package.json b/backend/package.json index 7d20ce1f..03c865e8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -28,14 +28,15 @@ }, "dependencies": { "@aws-sdk/client-dynamodb": "^3.758.0", - "@aws-sdk/util-dynamodb": "^3.758.0", "@aws-sdk/client-secrets-manager": "^3.758.0", + "@aws-sdk/util-dynamodb": "^3.758.0", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", "@nestjs/jwt": "^10.2.0", - "@nestjs/platform-express": "^10.0.0", "@nestjs/passport": "^10.0.3", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^7.1.13", "@types/jest": "^29.5.12", "axios": "^1.8.1", "class-transformer": "^0.5.1", @@ -47,15 +48,14 @@ "helmet": "^7.0.0", "jsonwebtoken": "^9.0.2", "jwk-to-pem": "^2.0.5", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "source-map-support": "^0.5.21", - "web-vitals": "^2.1.4", - "aws-cdk-lib": "2.184.1", - "@nestjs/swagger": "^7.1.13", "swagger-ui-express": "^5.0.0", - "passport": "^0.7.0", - "passport-jwt": "^4.0.1" + "web-vitals": "^2.1.4", + "aws-cdk-lib": "^2.185.0" }, "devDependencies": { "@aws-cdk/assert": "^2.68.0", @@ -73,7 +73,7 @@ "@typescript-eslint/parser": "^7.9.0", "@vitest/coverage-c8": "^0.33.0", "aws-cdk": "2.139.0", - "aws-cdk-lib": "^2.184.1", + "aws-cdk-lib": "^2.185.0", "dotenv-cli": "^8.0.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.0.0", @@ -91,6 +91,6 @@ "vitest": "^0.33.0" }, "peerDependencies": { - "aws-cdk-lib": "2.184.1" + "aws-cdk-lib": "^2.185.0" } } diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index 433785d6..3be31ecc 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -6,6 +6,7 @@ import * as cognito from 'aws-cdk-lib/aws-cognito'; import * as apigateway from 'aws-cdk-lib/aws-apigateway'; import * as servicediscovery from 'aws-cdk-lib/aws-servicediscovery'; import * as iam from 'aws-cdk-lib/aws-iam'; +import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import { Construct } from 'constructs'; import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb'; @@ -245,11 +246,39 @@ export class BackendStack extends cdk.Stack { }); } - // Create VPC Link for API Gateway (using HTTP API VPC Link) - const vpcLink = new apigateway.VpcLink(this, `${appName}VpcLink-${props.environment}`, { + // Create a Network Load Balancer for the Fargate service + const nlb = new elbv2.NetworkLoadBalancer(this, `${appName}NLB-${props.environment}`, { vpc, + internetFacing: false, + vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, + }); + + // Add a listener to the NLB + const listener = nlb.addListener(`${appName}Listener-${props.environment}`, { + port: 80, + protocol: elbv2.Protocol.TCP, + }); + + // Add the Fargate service as a target to the listener + listener.addTargets(`${appName}TargetGroup-${props.environment}`, { + targets: [fargateService], + port: 3000, + protocol: elbv2.Protocol.TCP, + healthCheck: { + enabled: true, + protocol: elbv2.Protocol.HTTP, + path: '/api/health', + interval: cdk.Duration.seconds(30), + healthyThresholdCount: 2, + unhealthyThresholdCount: 2, + timeout: cdk.Duration.seconds(5), + }, + }); + + // Create VPC Link for API Gateway using the NLB + const vpcLink = new apigateway.VpcLink(this, `${appName}VpcLink-${props.environment}`, { + targets: [nlb], description: `VPC Link for ${appName} ${props.environment}`, - vpcLinkName: `${appName}VpcLink-${props.environment}`, }); // Create API Gateway @@ -279,8 +308,8 @@ export class BackendStack extends cdk.Stack { }, ); - // Get the service URL from CloudMap (now using HTTPS) - const serviceUrl = `https://${appName.toLowerCase()}-service.${appName.toLowerCase()}.local:3443`; + // Use the NLB DNS name for the service URL + const serviceUrl = `http://${nlb.loadBalancerDnsName}`; // Create proxy resource with Cognito authorization const proxyResource = api.root.addResource('{proxy+}'); @@ -295,10 +324,6 @@ export class BackendStack extends cdk.Stack { requestParameters: { 'integration.request.path.proxy': 'method.request.path.proxy', }, - // Skip TLS verification for self-signed certificates in internal traffic - tlsConfig: { - insecureSkipVerification: true, - }, }, uri: `${serviceUrl}/{proxy}`, }); @@ -321,10 +346,6 @@ export class BackendStack extends cdk.Stack { options: { connectionType: apigateway.ConnectionType.VPC_LINK, vpcLink: vpcLink, - // Skip TLS verification for self-signed certificates in internal traffic - tlsConfig: { - insecureSkipVerification: true, - }, }, uri: `${serviceUrl}/api/health`, }), @@ -375,9 +396,9 @@ export class BackendStack extends cdk.Stack { description: 'API Gateway URL', }); - new cdk.CfnOutput(this, 'ServiceDiscoveryUrl', { - value: serviceUrl, - description: 'Service Discovery URL', + new cdk.CfnOutput(this, 'NetworkLoadBalancerDns', { + value: nlb.loadBalancerDnsName, + description: 'Network Load Balancer DNS Name', }); } } From df333f673d8f49e77d3e885377cc7412dd01e26b Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Wed, 26 Mar 2025 15:50:57 -0400 Subject: [PATCH 4/5] Fixing CDK errors --- backend/src/iac/backend-stack.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index 3be31ecc..d7b99987 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -364,22 +364,22 @@ export class BackendStack extends cdk.Stack { ], }); - // Attach the execution role to the API - const apiResource = api.node.findChild('Default') as apigateway.CfnRestApi; - apiResource.addPropertyOverride('Policy', { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Principal: { - Service: 'apigateway.amazonaws.com', - }, - Action: 'sts:AssumeRole', - Resource: executionRole.roleArn, - }, + // Replace the problematic code with a proper way to set the API Gateway policy + const apiPolicy = new iam.PolicyDocument({ + statements: [ + new iam.PolicyStatement({ + effect: iam.Effect.ALLOW, + principals: [new iam.ServicePrincipal('apigateway.amazonaws.com')], + actions: ['sts:AssumeRole'], + resources: [executionRole.roleArn], + }), ], }); + // Apply the policy to the API Gateway using CfnRestApi + const cfnApi = api.node.defaultChild as apigateway.CfnRestApi; + cfnApi.policy = apiPolicy.toJSON(); + // Outputs new cdk.CfnOutput(this, 'ReportsTableName', { value: reportsTable.tableName, From cdf57b65df8d2ebf226aa364dd315de226c4e280 Mon Sep 17 00:00:00 2001 From: Guido Percu Date: Wed, 26 Mar 2025 20:52:52 -0400 Subject: [PATCH 5/5] Remove healthcheck --- backend/src/iac/backend-stack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/iac/backend-stack.ts b/backend/src/iac/backend-stack.ts index d7b99987..5f03de43 100644 --- a/backend/src/iac/backend-stack.ts +++ b/backend/src/iac/backend-stack.ts @@ -191,13 +191,13 @@ export class BackendStack extends cdk.Stack { streamPrefix: appName, logGroup, }), - healthCheck: { + /*healthCheck: { command: ['CMD-SHELL', 'curl -f -k https://localhost:3443/api/health || exit 1'], interval: cdk.Duration.seconds(30), timeout: cdk.Duration.seconds(5), retries: 3, startPeriod: cdk.Duration.seconds(60), - }, + },*/ }); // Grant the task role access to read the SSL certificate secret