Skip to content

Commit

Permalink
feat: add initial construct for mesh hosting
Browse files Browse the repository at this point in the history
  • Loading branch information
TheOrangePuff committed Aug 29, 2023
1 parent c325d5c commit 33edbfe
Show file tree
Hide file tree
Showing 14 changed files with 2,101 additions and 841 deletions.
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ When making a release (including experimental releases), the release tag should
|---------------|---------------|
| Experimental | 1.1.0-beta |
| Final | 1.1.0 |

## Testing locally

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
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,
});
}
}
95 changes: 95 additions & 0 deletions packages/graphql-mesh-server/lib/graphql-mesh-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Construct } from 'constructs';
import { MeshService, MeshServiceProps } from './fargate';
import { RedisService, RedisServiceProps } from './redis-construct';
import { CodePipelineService } from './pipeline';
import { SecurityGroup, Vpc } from 'aws-cdk-lib/aws-ec2';
import { Repository } from 'aws-cdk-lib/aws-ecr';
import { FargateService } from 'aws-cdk-lib/aws-ecs';
import { CfnCacheCluster } from 'aws-cdk-lib/aws-elasticache';
import * as ssm from 'aws-cdk-lib/aws-ssm';

export type MeshHostingProps = {
/**
* VPC to attach Redis and Fargate instances to (default: create a vpc)
*/
vpc?: Vpc;
/**
* If no VPC is provided create one with this name (default: 'graphql-server-vpc')
*/
vpcName?: string;
/**
* Cache node type (default: 'cache.t2.micro')
*/
cacheNodeType?: string;
/**
* Repository to pull the container image from
*/
repository?: 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 Fargate instance (default: 512)
*/
cpu?: number;
/**
* Amount of memory per Fargate 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 MeshHosting extends Construct {
public readonly vpc: Vpc;
public readonly repository: Repository;
public readonly service: FargateService;
public readonly cacheCluster: CfnCacheCluster;
public readonly securityGroup: SecurityGroup;

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

this.vpc = props.vpc || new Vpc(this, 'graphql-server-vpc', {
vpcName: props.vpcName || 'graphql-server-vpc',
natGateways: 1
});

const redis = props.redis || new RedisService(this, 'redis', {
...props,
vpc: this.vpc
});

this.cacheCluster = redis.cacheCluster;
this.securityGroup = redis.securityGroup;

const mesh = new MeshService(this, 'mesh', {
...props,
vpc: this.vpc,
redis,
});

this.service = mesh.service;
this.repository = mesh.repository;

new CodePipelineService(this, 'pipeline', {
repository: this.repository,
service: this.service,
});
}
}
Loading

0 comments on commit 33edbfe

Please sign in to comment.