Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port Graphql Mesh changes from DO branch #1108

Merged
merged 8 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ dist/
# JetBrains IDE
.idea/

# VSCode IDE
.vscode/

# Unit test reports
TEST*.xml

Expand Down Expand Up @@ -53,6 +56,7 @@ Thumbs.db
# CDK asset staging directory
.cdk.staging
cdk.out
*.tsbuildinfo

*.d.ts
*.js
44 changes: 44 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions packages/graphql-mesh-server/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
*.ts
!lib/handlers/*.ts
!*.d.ts
!*.js

# CDK asset staging directory
.cdk.staging
cdk.out

# Samples
sample/
15 changes: 15 additions & 0 deletions packages/graphql-mesh-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Prerender in Fargate
A construct host [GraphQL Mesh](https://the-guild.dev/graphql/mesh) server in Fargate.

## Props
- `vpc?`: VPC to attach Redis and Fargate instances to (default: create a vpc)
- `vpcName?`: If no VPC is provided create one with this name (default: 'graphql-server-vpc')
- `cacheNodeType?`: Cache node type (default: 'cache.t2.micro')
- `repository?`: Repository to pull the container image from
- `certificateArn:` ARN of the certificate to add to the load balancer
- `minCapacity?`: Minimum number of Fargate instances
- `maxCapacity?`: Maximum number of Fargate instances
- `cpu?`: Amount of vCPU per Fargate instance (default: 512)
- `memory?`: Amount of memory per Fargate instance (default: 1024)
- `redis?`: Redis instance to use for mesh caching
- `secrets?`: SSM values to pass through to the container as secrets
10 changes: 10 additions & 0 deletions packages/graphql-mesh-server/assets/buildspec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
version: 0.2

phases:
build:
commands:
- jq -n --arg image_uri $IMAGE_URI --arg container_name $CONTAINER_NAME '[{"name":"\($container_name)","imageUri":"\($image_uri)"}]' > imagedefinitions.json
artifacts:
files:
- 'imagedefinitions.json'
name: images-$(date +%Y-%m-%d)
3 changes: 3 additions & 0 deletions packages/graphql-mesh-server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { MeshHosting } from "./lib/graphql-mesh-server";

export { MeshHosting };
221 changes: 221 additions & 0 deletions packages/graphql-mesh-server/lib/fargate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import { Construct } from "constructs";
import { Duration, Token } from "aws-cdk-lib";

Check warning on line 2 in packages/graphql-mesh-server/lib/fargate.ts

View workflow job for this annotation

GitHub Actions / build

'Token' is defined but never used
import { RemovalPolicy } from "aws-cdk-lib";
import * as acm from "aws-cdk-lib/aws-certificatemanager";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as ecr from "aws-cdk-lib/aws-ecr";
import * as ecsPatterns from "aws-cdk-lib/aws-ecs-patterns";
import * as iam from "aws-cdk-lib/aws-iam";
import * as ssm from "aws-cdk-lib/aws-ssm";
import * as auto_scaling from "aws-cdk-lib/aws-autoscaling";
import { Port, SecurityGroup, Vpc } from "aws-cdk-lib/aws-ec2";
import { RedisService } from "./redis-construct";
import {
ManagedRule,
Scope,
WebApplicationFirewall,
} from "./web-application-firewall";

export interface MeshServiceProps {
/**
* VPC to attach Redis instance to
*/
vpc?: Vpc;
/**
* Repository to pull the container image from
*/
repository?: ecr.Repository;
/**
* ARN of the certificate to add to the load balancer
*/
certificateArn: string;
/**
* Minimum number of Fargate instances
*/
minCapacity?: number;
/**
* Maximum number of Fargate instances
*/
maxCapacity?: number;
/**
* Amount of vCPU per instance (default: 512)
*/
cpu?: number;
/**
* Amount of memory per instance (default: 1024)
*/
memory?: number;
/**
* Redis instance to use for mesh caching
*/
redis: RedisService;
/**
* SSM values to pass through to the container as secrets
*/
secrets?: { [key: string]: ssm.IStringParameter | ssm.IStringListParameter };
}

export class MeshService extends Construct {
public readonly vpc: Vpc;
public readonly repository: ecr.Repository;
public readonly service: ecs.FargateService;
public readonly firewall: WebApplicationFirewall;

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

const certificate = acm.Certificate.fromCertificateArn(
this,
`certificate`,
props.certificateArn
);

this.vpc =
props.vpc ||
new Vpc(this, "vpc", {
natGateways: 1,
});

this.repository =
props.repository ||
new ecr.Repository(this, "repo", {
removalPolicy: RemovalPolicy.DESTROY,
autoDeleteImages: true,
});

if (!props.repository) {
// Delete all images older than 90 days BUT keep 10 from the latest tag
this.repository.addLifecycleRule({
tagPrefixList: ["latest"],
maxImageCount: 10,
});
this.repository.addLifecycleRule({
maxImageAge: Duration.days(90),
});
}

// Create a deploy user to push images to ECR
const deployUser = new iam.User(this, "deploy-user");

const deployPolicy = new iam.Policy(this, "deploy-policy");
deployPolicy.addStatements(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"ecr:CompleteLayerUpload",
"ecr:UploadLayerPart",
"ecr:InitiateLayerUpload",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage",
],
resources: [this.repository.repositoryArn],
}),
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["ecr:GetAuthorizationToken"],
resources: ["*"],
})
);

deployUser.attachInlinePolicy(deployPolicy);

const securityGroup = new SecurityGroup(this, "security-group", {
vpc: this.vpc,
});

const cluster = new ecs.Cluster(this, `cluster`, {
vpc: this.vpc,
});

const environment: { [key: string]: string } = {};

// If using Redis configure security group and pass connection string to container
if (props.redis) {
props.redis.securityGroup.addIngressRule(
securityGroup,
Port.tcp(props.redis.connectionPort)
);

environment["REDIS_ENDPOINT"] = props.redis.connectionEndPoint;
environment["REDIS_PORT"] = props.redis.connectionPort.toString();
}

// Construct secrets from provided ssm values
const secrets: { [key: string]: ecs.Secret } = {};
props.secrets = props.secrets || {};
for (const [key, ssm] of Object.entries(props.secrets)) {
secrets[key] = ecs.Secret.fromSsmParameter(ssm);
}
// Create a load-balanced Fargate service and make it public
const fargateService =
new ecsPatterns.ApplicationLoadBalancedFargateService(this, `fargate`, {
cluster,
certificate,
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: secrets,
environment: environment,
},
publicLoadBalancer: true, // default,
taskSubnets: {
subnets: [...this.vpc.privateSubnets],
},
securityGroups: [securityGroup],
});

this.service = fargateService.service;

this.firewall = new WebApplicationFirewall(this, "waf", {
scope: Scope.REGIONAL,
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: "firewall-request",
sampledRequestsEnabled: true,
},
managedRules: [
{
name: ManagedRule.COMMON_RULE_SET,
excludedRules: [
{
name: "SizeRestrictions_QUERYSTRING",
},
],
},
{
name: ManagedRule.KNOWN_BAD_INPUTS_RULE_SET,
},
],
});

this.firewall.addAssociation(
"loadbalancer-association",
fargateService.loadBalancer.loadBalancerArn
);

fargateService.targetGroup.configureHealthCheck({
path: "/healthcheck",
});

// Setup auto scaling policy
const scaling = fargateService.service.autoScaleTaskCount({
minCapacity: props.minCapacity || 1,
maxCapacity: props.maxCapacity || 5,
});

const cpuUtilization = fargateService.service.metricCpuUtilization();
scaling.scaleOnMetric("auto-scale-cpu", {
metric: cpuUtilization,
scalingSteps: [
{ upper: 30, change: -1 },
{ lower: 50, change: +1 },
{ lower: 85, change: +3 },
],
adjustmentType: auto_scaling.AdjustmentType.CHANGE_IN_CAPACITY,
});
}
}
Loading
Loading