diff --git a/packages/graphql-mesh-server/assets/nginx/Dockerfile b/packages/graphql-mesh-server/assets/nginx/Dockerfile new file mode 100644 index 00000000..e97b348a --- /dev/null +++ b/packages/graphql-mesh-server/assets/nginx/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx:stable-alpine + +COPY nginx.conf /etc/nginx/nginx.conf \ No newline at end of file diff --git a/packages/graphql-mesh-server/assets/nginx/nginx.conf b/packages/graphql-mesh-server/assets/nginx/nginx.conf new file mode 100644 index 00000000..aaf09b07 --- /dev/null +++ b/packages/graphql-mesh-server/assets/nginx/nginx.conf @@ -0,0 +1,53 @@ +events { + worker_connections 1024; +} + +http { + log_format json_combined escape=json + '{' + '"time_local":"$time_local",' + '"remote_addr":"$remote_addr",' + '"remote_user":"$remote_user",' + '"request":"$request",' + '"status": "$status",' + '"body_bytes_sent":"$body_bytes_sent",' + '"request_time":"$request_time",' + '"http_referer":"$http_referer",' + '"http_user_agent":"$http_user_agent"' + '}'; + + gzip on; + gzip_proxied any; + gzip_types text/plain application/json; + gzip_min_length 1000; + + server { + listen 80; + access_log /var/log/nginx/access.log json_combined; + + location /graphql { + # Reject requests with unsupported HTTP method + if ($request_method !~ ^(GET|POST|HEAD|OPTIONS|PUT|DELETE)$) { + return 405; + } + + # Only requests matching the expectations will + # get sent to the application server + proxy_pass http://localhost:4000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_cache_bypass $http_upgrade; + } + + location /healthcheck { + return 200; + } + + location / { + return 403; + } + } +} \ No newline at end of file diff --git a/packages/graphql-mesh-server/lib/fargate.ts b/packages/graphql-mesh-server/lib/fargate.ts index 2eaa025d..9e0fcb36 100644 --- a/packages/graphql-mesh-server/lib/fargate.ts +++ b/packages/graphql-mesh-server/lib/fargate.ts @@ -20,6 +20,7 @@ import { CfnIPSet, CfnWebACL } from "aws-cdk-lib/aws-wafv2"; import { ScalingInterval, AdjustmentType } from "aws-cdk-lib/aws-autoscaling"; import { ApplicationLoadBalancer } from "aws-cdk-lib/aws-elasticloadbalancingv2"; import { LogGroup } from "aws-cdk-lib/aws-logs"; +import path = require("path"); export interface MeshServiceProps { /** @@ -182,6 +183,25 @@ export interface MeshServiceProps { * @default - AWS generated task definition family name */ taskDefinitionFamilyName?: string; + + /** + * Nginx image to use + * + * @default ecs.ContainerImage.fromRegistry("nginx:stable-alpine") + */ + nginxImage?: ecs.ContainerImage; + + /** + * Disable the nginx sidecar container + * + * @default - false + */ + disableNginx?: boolean; + + /** + * Optional manual overrides for nginx sidecar container + */ + nginxConfigOverride?: Partial; } export class MeshService extends Construct { @@ -203,6 +223,8 @@ export class MeshService extends Construct { ) : undefined; + if (!certificate) throw Error("Must pass certificate"); + this.vpc = props.vpc || new Vpc(this, "vpc", { @@ -294,6 +316,65 @@ export class MeshService extends Construct { logGroup: this.logGroup, }); + const taskDefinition = new ecs.FargateTaskDefinition(this, "TaskDef", { + memoryLimitMiB: props.memory || 1024, + cpu: props.cpu || 512, + }); + + // Configure nginx + if (!props.disableNginx) { + taskDefinition.addContainer("nginx", { + image: + props.nginxImage || + ecs.ContainerImage.fromAsset( + path.resolve(__dirname, "../assets/nginx") + ), + containerName: "nginx", + essential: true, + healthCheck: { + command: [ + "CMD-SHELL", + "curl -f http://localhost || echo 'Health check failed'", + ], + startPeriod: Duration.seconds(5), + }, + logging: logDriver, + portMappings: [{ containerPort: 80 }], + ...props.nginxConfigOverride, + }); + } + + // Add the main mesh container + taskDefinition.addContainer("mesh", { + image: ecs.ContainerImage.fromEcrRepository(this.repository), + containerName: "mesh", + environment: environment, + secrets: props.secrets ? props.secrets : ssmSecrets, + healthCheck: { + command: [ + "CMD-SHELL", + "curl -f http://localhost || echo 'Health check failed'", + ], + startPeriod: Duration.seconds(5), + }, + logging: logDriver, + portMappings: [{ containerPort: 4000 }], // Main application listens on port 4000 + }); + + // Configure x-ray + taskDefinition.addContainer("xray", { + image: ecs.ContainerImage.fromRegistry("amazon/aws-xray-daemon"), + cpu: 32, + containerName: "xray", + memoryReservationMiB: 256, + essential: false, + healthCheck: { + command: ["CMD-SHELL", "pgrep xray || echo 'Health check failed'"], + startPeriod: Duration.seconds(5), + }, + portMappings: [{ containerPort: 4000, protocol: ecs.Protocol.UDP }], + }); + // Create a load-balanced Fargate service and make it public const fargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(this, `fargate`, { @@ -304,22 +385,8 @@ export class MeshService extends Construct { enableExecuteCommand: true, cpu: props.cpu || 512, // 0.5 vCPU memoryLimitMiB: props.memory || 1024, // 1 GB - taskImageOptions: { - image: ecs.ContainerImage.fromEcrRepository(this.repository), - enableLogging: true, // default - containerPort: 4000, // graphql mesh gateway port - secrets: props.secrets ? props.secrets : ssmSecrets, // Prefer v2 secrets using secrets manager - environment: environment, - logDriver: logDriver, - taskRole: new iam.Role(this, "MeshTaskRole", { - assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"), - }), - family: - props.taskDefinitionFamilyName !== undefined - ? props.taskDefinitionFamilyName - : undefined, - }, - publicLoadBalancer: true, // default, + taskDefinition: taskDefinition, + publicLoadBalancer: true, // defult, taskSubnets: { subnets: [...this.vpc.privateSubnets], }, @@ -329,23 +396,11 @@ export class MeshService extends Construct { this.service = fargateService.service; this.loadBalancer = fargateService.loadBalancer; - // Configure x-ray - const xray = this.service.taskDefinition.addContainer("xray", { - image: ecs.ContainerImage.fromRegistry("amazon/aws-xray-daemon"), - cpu: 32, - memoryReservationMiB: 256, - essential: false, - }); - xray.addPortMappings({ - containerPort: 2000, - protocol: ecs.Protocol.UDP, - }); - - this.service.taskDefinition.taskRole.addManagedPolicy({ + taskDefinition.taskRole.addManagedPolicy({ managedPolicyArn: "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy", }); - this.service.taskDefinition.taskRole.addManagedPolicy({ + taskDefinition.taskRole.addManagedPolicy({ managedPolicyArn: "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess", }); @@ -539,7 +594,7 @@ export class MeshService extends Construct { this.firewall.addAssociation( "loadbalancer-association", - fargateService.loadBalancer.loadBalancerArn + this.loadBalancer.loadBalancerArn ); fargateService.targetGroup.configureHealthCheck({ @@ -547,7 +602,7 @@ export class MeshService extends Construct { }); // Setup auto scaling policy - const scaling = fargateService.service.autoScaleTaskCount({ + const scaling = this.service.autoScaleTaskCount({ minCapacity: props.minCapacity || 1, maxCapacity: props.maxCapacity || 5, }); @@ -558,7 +613,7 @@ export class MeshService extends Construct { { lower: 85, change: +3 }, ]; - const cpuUtilization = fargateService.service.metricCpuUtilization(); + const cpuUtilization = this.service.metricCpuUtilization(); scaling.scaleOnMetric("auto-scale-cpu", { metric: cpuUtilization, scalingSteps: cpuScalingSteps, diff --git a/packages/graphql-mesh-server/lib/maintenance.ts b/packages/graphql-mesh-server/lib/maintenance.ts index faa38466..9f710ecd 100644 --- a/packages/graphql-mesh-server/lib/maintenance.ts +++ b/packages/graphql-mesh-server/lib/maintenance.ts @@ -98,13 +98,15 @@ export class Maintenance extends Construct { readOnly: true, sourceVolume: "maintenanceVolume", }; - props.fargateService.taskDefinition.defaultContainer?.addMountPoints( - mountPoint - ); - props.fargateService.taskDefinition.defaultContainer?.addEnvironment( - "MAINTENANCE_FILE_PATH", - `${efsVolumeMountPath}/maintenance.enabled` - ); + props.fargateService.taskDefinition + .findContainer("mesh") + ?.addMountPoints(mountPoint); + props.fargateService.taskDefinition + .findContainer("mesh") + ?.addEnvironment( + "MAINTENANCE_FILE_PATH", + `${efsVolumeMountPath}/maintenance.enabled` + ); const api = new apigateway.RestApi(this, "maintenance-apigw"); const apiKey = api.addApiKey("maintenance-api-key", {