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

MICRO-150: Graphql mesh server #1065

Merged
merged 4 commits into from
Aug 31, 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 All @@ -51,6 +54,7 @@ Thumbs.db
# CDK asset staging directory
.cdk.staging
cdk.out
*.tsbuildinfo

*.d.ts
*.js
Expand Down
2,212 changes: 1,372 additions & 840 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"esbuild": "^0.12.15"
},
"dependencies": {
"aws-cdk-lib": "^2.26.0",
"aws-cdk-lib": "^2.90.0",
"constructs": "10.1.56"
},
"repository": {
Expand Down
6 changes: 3 additions & 3 deletions packages/basic-auth/lib/basic-auth-construct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ export class BasicAuthFunction extends Construct {
this,
'BasicAuthFunction',
{
runtime: Runtime.NODEJS_12_X,
runtime: Runtime.NODEJS_16_X,
handler: 'index.handler',
code: Bundling.bundle({
code: Bundling.bundle(this, {
entry: `${__dirname}/handlers/basic-auth.ts`,
runtime: Runtime.NODEJS_12_X,
runtime: Runtime.NODEJS_16_X,
sourceMap: true,
projectRoot: `${__dirname}/handlers/`,
depsLockFilePath: `${__dirname}/handlers/package-lock.json`,
Expand Down
6 changes: 3 additions & 3 deletions packages/cloudfront-security-headers/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ export class SecurityHeaderFunction extends Construct {
this,
'SecurityHeaderFunction',
{
code: Bundling.bundle({
code: Bundling.bundle(this, {
entry: `${__dirname}/handlers/security-header.ts`,
runtime: Runtime.NODEJS_14_X,
runtime: Runtime.NODEJS_16_X,
sourceMap: true,
projectRoot: `${__dirname}/handlers/`,
depsLockFilePath: `${__dirname}/handlers/package-lock.json`,
define: defineOptions
} as any), // TODO fix typing
runtime: Runtime.NODEJS_14_X,
runtime: Runtime.NODEJS_16_X,
handler: 'index.handler',
}
);
Expand Down
6 changes: 3 additions & 3 deletions packages/geoip-redirect/lib/redirect-construct.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ export class RedirectFunction extends Construct {
this,
'RedirectFunction',
{
code: Bundling.bundle({
code: Bundling.bundle(this, {
entry: `${__dirname}/handlers/redirect.ts`,
runtime: Runtime.NODEJS_12_X,
runtime: Runtime.NODEJS_16_X,
sourceMap: true,
projectRoot: `${__dirname}/handlers/`,
depsLockFilePath: `${__dirname}/handlers/package-lock.json`,
Expand All @@ -37,7 +37,7 @@ export class RedirectFunction extends Construct {
'process.env.DEFAULT_REGION': JSON.stringify(options.defaultRegion),
}
} as any),
runtime: Runtime.NODEJS_12_X,
runtime: Runtime.NODEJS_16_X,
handler: 'index.handler',
}
);
Expand Down
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
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 };
220 changes: 220 additions & 0 deletions packages/graphql-mesh-server/lib/fargate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { Construct } from 'constructs';
import { Duration } from 'aws-cdk-lib';
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(Number(props.redis.connectionPort))
);

environment['REDIS_ENDPOINT'] = props.redis.connectionEndPoint;
environment['REDIS_PORT'] = props.redis.connectionPort;
}

// 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