diff --git a/.gitignore b/.gitignore index 8f77f768..8f11aef6 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ dist/ # JetBrains IDE .idea/ +# VSCode IDE +.vscode/ + # Unit test reports TEST*.xml @@ -53,6 +56,7 @@ Thumbs.db # CDK asset staging directory .cdk.staging cdk.out +*.tsbuildinfo *.d.ts *.js diff --git a/package-lock.json b/package-lock.json index bfc990fc..adc7dce3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,10 @@ "resolved": "packages/geoip-redirect", "link": true }, + "node_modules/@aligent/cdk-graphql-mesh-server": { + "resolved": "packages/graphql-mesh-server", + "link": true + }, "node_modules/@aligent/cdk-lambda-at-edge-handlers": { "resolved": "packages/lambda-at-edge-handlers", "link": true @@ -1876,6 +1880,16 @@ "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", "dev": true }, + "node_modules/@types/yaml": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@types/yaml/-/yaml-1.9.7.tgz", + "integrity": "sha512-8WMXRDD1D+wCohjfslHDgICd2JtMATZU8CkhH8LVJqcJs6dyYj5TGptzP8wApbmEullGBSsCEzzap73DQ1HJaA==", + "deprecated": "This is a stub types definition. yaml provides its own type definitions, so you do not need this installed.", + "dev": true, + "dependencies": { + "yaml": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.25", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.25.tgz", @@ -5900,6 +5914,14 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, + "node_modules/yaml": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", + "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -6028,6 +6050,28 @@ "typescript": "~5.2.2" } }, + "packages/graphql-mesh-server": { + "name": "@aligent/cdk-graphql-mesh-server", + "version": "0.0.1", + "license": "GPL-3.0-only", + "dependencies": { + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21", + "yaml": "^2.3.1" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.122", + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "@types/yaml": "^1.9.7", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + } + }, "packages/lambda-at-edge-handlers": { "name": "@aligent/cdk-lambda-at-edge-handlers", "version": "0.1.0", diff --git a/packages/graphql-mesh-server/.npmignore b/packages/graphql-mesh-server/.npmignore new file mode 100644 index 00000000..bfd115ba --- /dev/null +++ b/packages/graphql-mesh-server/.npmignore @@ -0,0 +1,11 @@ +*.ts +!lib/handlers/*.ts +!*.d.ts +!*.js + +# CDK asset staging directory +.cdk.staging +cdk.out + +# Samples +sample/ diff --git a/packages/graphql-mesh-server/README.md b/packages/graphql-mesh-server/README.md new file mode 100644 index 00000000..4263295b --- /dev/null +++ b/packages/graphql-mesh-server/README.md @@ -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 diff --git a/packages/graphql-mesh-server/assets/buildspec.yml b/packages/graphql-mesh-server/assets/buildspec.yml new file mode 100644 index 00000000..c2db9f73 --- /dev/null +++ b/packages/graphql-mesh-server/assets/buildspec.yml @@ -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) diff --git a/packages/graphql-mesh-server/index.ts b/packages/graphql-mesh-server/index.ts new file mode 100644 index 00000000..cd739cad --- /dev/null +++ b/packages/graphql-mesh-server/index.ts @@ -0,0 +1,3 @@ +import { MeshHosting } from "./lib/graphql-mesh-server"; + +export { MeshHosting }; diff --git a/packages/graphql-mesh-server/lib/fargate.ts b/packages/graphql-mesh-server/lib/fargate.ts new file mode 100644 index 00000000..b4a13e25 --- /dev/null +++ b/packages/graphql-mesh-server/lib/fargate.ts @@ -0,0 +1,221 @@ +import { Construct } from "constructs"; +import { Duration, Token } 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(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, + }); + } +} diff --git a/packages/graphql-mesh-server/lib/graphql-mesh-server.ts b/packages/graphql-mesh-server/lib/graphql-mesh-server.ts new file mode 100644 index 00000000..399c2457 --- /dev/null +++ b/packages/graphql-mesh-server/lib/graphql-mesh-server.ts @@ -0,0 +1,99 @@ +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, + }); + } +} diff --git a/packages/graphql-mesh-server/lib/pipeline.ts b/packages/graphql-mesh-server/lib/pipeline.ts new file mode 100644 index 00000000..a4e7c7c6 --- /dev/null +++ b/packages/graphql-mesh-server/lib/pipeline.ts @@ -0,0 +1,94 @@ +import { Duration } from "aws-cdk-lib"; +import { Artifact, Pipeline } from "aws-cdk-lib/aws-codepipeline"; +import { Repository } from "aws-cdk-lib/aws-ecr"; +import { FargateService } from "aws-cdk-lib/aws-ecs"; +import * as pipe_actions from "aws-cdk-lib/aws-codepipeline-actions"; +import * as codebuild from "aws-cdk-lib/aws-codebuild"; +import { Construct } from "constructs"; +import * as fs from "fs"; +import * as path from "path"; +import * as YAML from "yaml"; + +export interface CodePipelineServiceProps { + /** + * Repository the code container is pushed too + */ + repository: Repository; + + /** + * Services to deploy Code container updates to + */ + service: FargateService; + + /** + * Path to buildspec.yml (default: '../assets/buildspec.yml') + */ + buildspecPath?: string; +} + +export class CodePipelineService extends Construct { + public readonly pipeline: Pipeline; + + constructor(scope: Construct, id: string, props: CodePipelineServiceProps) { + super(scope, id); + + this.pipeline = new Pipeline(this, "deploy-pipeline"); + + const sourceOutput = new Artifact(); + const sourceAction = new pipe_actions.EcrSourceAction({ + actionName: "ECR", + repository: props.repository, + output: sourceOutput, + }); + + this.pipeline.addStage({ + stageName: "Source", + actions: [sourceAction], + }); + + const file = fs.readFileSync( + path.resolve(__dirname, props.buildspecPath || "../assets/buildspec.yml"), + "utf8" + ); + const project: codebuild.PipelineProject = new codebuild.PipelineProject( + this, + "project", + { + buildSpec: codebuild.BuildSpec.fromObject(YAML.parse(file)), + } + ); + + const buildOutput = new Artifact(); + this.pipeline.addStage({ + stageName: "Build", + actions: [ + new pipe_actions.CodeBuildAction({ + actionName: "CodeBuild", + project, + input: sourceOutput, + outputs: [buildOutput], + environmentVariables: { + IMAGE_URI: { + value: sourceAction.variables.imageUri, + }, + CONTAINER_NAME: { + value: + props.service.taskDefinition.defaultContainer?.containerName, + }, + }, + }), + ], + }); + this.pipeline.addStage({ + stageName: "Deploy", + actions: [ + new pipe_actions.EcsDeployAction({ + actionName: "DeployAction", + service: props.service, + input: buildOutput, + deploymentTimeout: Duration.minutes(10), + }), + ], + }); + } +} diff --git a/packages/graphql-mesh-server/lib/redis-construct.ts b/packages/graphql-mesh-server/lib/redis-construct.ts new file mode 100644 index 00000000..5a9ba5b0 --- /dev/null +++ b/packages/graphql-mesh-server/lib/redis-construct.ts @@ -0,0 +1,90 @@ +import { SecurityGroup, Vpc } from "aws-cdk-lib/aws-ec2"; +import { + CfnCacheCluster, + CfnSubnetGroup, + CfnParameterGroup, +} from "aws-cdk-lib/aws-elasticache"; +import { CfnOutput, Reference, Token } from "aws-cdk-lib"; +import { Construct } from "constructs"; + +export interface RedisServiceProps { + /** + * VPC to attach Redis instance to + */ + vpc: Vpc; + /** + * Cache node type (default: 'cache.t2.micro') + */ + cacheNodeType?: string; +} + +export class RedisService extends Construct { + public readonly cacheCluster: CfnCacheCluster; + public readonly vpc: Vpc; + public readonly securityGroup: SecurityGroup; + + constructor(scope: Construct, id: string, props: RedisServiceProps) { + super(scope, id); + + this.vpc = props.vpc; + + this.securityGroup = new SecurityGroup(this, "RedisSecurityGroup", { + vpc: this.vpc, + }); + + const privateSubnets: string[] = this.vpc.privateSubnets.map(subnet => { + return subnet.subnetId; + }); + + const cacheSubnetGroup = new CfnSubnetGroup(this, "CacheSubnetGroup", { + description: "Subnet Group for Mesh Cache", + subnetIds: privateSubnets, + }); + + const cacheParameterGroup = new CfnParameterGroup( + this, + "CacheParameterGroup", + { + cacheParameterGroupFamily: "redis7", + description: "Parameter Group for Mesh Cache", + properties: { + "maxmemory-policy": "allkeys-lru", + }, + } + ); + + this.cacheCluster = new CfnCacheCluster(this, "cache-cluster", { + cacheNodeType: props.cacheNodeType || "cache.t2.micro", + engine: "redis", + numCacheNodes: 1, + autoMinorVersionUpgrade: true, + vpcSecurityGroupIds: [this.securityGroup.securityGroupId], + cacheSubnetGroupName: cacheSubnetGroup.ref, + cacheParameterGroupName: cacheParameterGroup.ref, + }); + + this.cacheCluster.addDependency(cacheParameterGroup); + this.cacheCluster.addDependency(cacheSubnetGroup); + + new CfnOutput(this, "RedisConnectionString", { + description: "RedisConnectionString", + value: this.cacheConnectionString, + }); + } + + public get cacheConnectionString(): string { + return `redis://${this.cacheCluster + .getAtt("RedisEndpoint.Address") + .toString()}:${this.cacheCluster + .getAtt("RedisEndpoint.Port") + .toString()}`; + } + + public get connectionEndPoint(): string { + return Token.asString(this.cacheCluster.getAtt("RedisEndpoint.Address")); + } + + public get connectionPort(): number { + return Token.asNumber(this.cacheCluster.getAtt("RedisEndpoint.Port")); + } +} diff --git a/packages/graphql-mesh-server/lib/web-application-firewall.ts b/packages/graphql-mesh-server/lib/web-application-firewall.ts new file mode 100644 index 00000000..98837990 --- /dev/null +++ b/packages/graphql-mesh-server/lib/web-application-firewall.ts @@ -0,0 +1,168 @@ +import { CfnWebACL, CfnWebACLAssociation } from "aws-cdk-lib/aws-wafv2"; +import { Construct } from "constructs"; + +export enum Action { + BLOCK = "BLOCK", + ALLOW = "ALLOW", +} + +export enum Scope { + CLOUDFRONT = "CLOUDFRONT", + REGIONAL = "REGIONAL", +} + +export enum ManagedRule { + BOT_CONTROL_RULE_SET = "AWSManagedRulesBotControlRuleSet", + KNOWN_BAD_INPUTS_RULE_SET = "AWSManagedRulesKnownBadInputsRuleSet", + COMMON_RULE_SET = "AWSManagedRulesCommonRuleSet", + ANNONYMOUS_IP_LIST = "AWSManagedRulesAnonymousIpList", + AMAZON_IP_REPUTATION_LIST = "AWSManagedRulesAmazonIpReputationList", + ADMIN_PROTECTION_RULE_SET = "AWSManagedRulesAdminProtectionRuleSet", + SQLI_RULE_SET = "AWSManagedRulesSQLiRuleSet", + PHP_RULE_SET = "AWSManagedRulesPHPRuleSet", +} + +export interface VisibilityConfig { + /** + * Whether cloudwatch metrics are enabled or nor + */ + cloudWatchMetricsEnabled: boolean; + + /** + * Name of the metric in cloudwatch + */ + metricName: string; + + /** + * Whether to keep samples of blocked requests + */ + sampledRequestsEnabled: boolean; +} + +export interface AWSManagedRule { + /** + * Which AWS Rule to add + */ + name: ManagedRule; + + /** + * @default to the name property + */ + metricName?: string; + + /** + * @default false + */ + sampledRequestsEnabled?: boolean; + + /** + * Any rules from this ruleset you wish to disable/exclude + */ + excludedRules?: Array<{ + name: string; + }>; + + /** + * Whether to override the default action to COUNT + */ + count?: boolean; +} + +export interface WebApplicationFirewallProps { + /** + * Name of the WAF + */ + name?: string; + + /** + * The action to perform if none of the `Rules` contained in the `WebACL` match. + * @default Action.ALLOW + */ + defaultAction?: Action; + + /** + * Specifies whether this is for an Amazon CloudFront distribution or for a regional application. + * @default Scope.REGIONAL + */ + scope?: Scope; + + /** + * Default visibility configuration + */ + visibilityConfig: VisibilityConfig; + + /** + * List of AWS Managed rules to add to the WAF + */ + managedRules?: AWSManagedRule[]; + + /** + * List of custom rules + */ + rules?: CfnWebACL.RuleProperty[]; +} + +export class WebApplicationFirewall extends Construct { + readonly acl: CfnWebACL; + readonly associations: CfnWebACLAssociation[]; + + constructor( + scope: Construct, + id: string, + props: WebApplicationFirewallProps + ) { + super(scope, id); + + let defaultAction: CfnWebACL.DefaultActionProperty = { allow: {} }; + + if (props.defaultAction == Action.BLOCK) { + defaultAction = { block: {} }; + } + + this.associations = []; + + const rules: CfnWebACL.RuleProperty[] = props.rules || []; + + // Convert from our AWSManagedRule type to a CfnWebACL.RuleProperty + if (props.managedRules) { + props.managedRules.forEach((rule, index) => { + rules.push({ + name: rule.name, + priority: index, + visibilityConfig: { + // if no metric name is passed then don't enable metrics + cloudWatchMetricsEnabled: rule.metricName ? true : false, + // Default to the rule name if a metric name isn't passed + metricName: rule.metricName || rule.name, + sampledRequestsEnabled: rule.sampledRequestsEnabled || false, + }, + statement: { + managedRuleGroupStatement: { + name: rule.name, + vendorName: "AWS", + excludedRules: rule.excludedRules || [], + }, + }, + overrideAction: rule.count ? { count: {} } : { none: {} }, + }); + }); + } + + this.acl = new CfnWebACL(this, "WebAcl", { + name: props.name, + defaultAction, + scope: props.scope || Scope.REGIONAL, + visibilityConfig: props.visibilityConfig, + rules: rules, + }); + } + + public addAssociation(id: string, resourceArn: string) { + this.associations.push( + new CfnWebACLAssociation(this, id, { + webAclArn: this.acl.attrArn, + resourceArn, + }) + ); + } +} diff --git a/packages/graphql-mesh-server/package.json b/packages/graphql-mesh-server/package.json new file mode 100644 index 00000000..b3a40c28 --- /dev/null +++ b/packages/graphql-mesh-server/package.json @@ -0,0 +1,36 @@ +{ + "name": "@aligent/cdk-graphql-mesh-server", + "version": "0.0.1", + "description": "A construct to host Graphql Mesh in Fargate", + "main": "index.js", + "scripts": { + "build": "tsc", + "prepublish": "tsc" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/aligent/cdk-constructs.git" + }, + "license": "GPL-3.0-only", + "bugs": { + "url": "https://github.com/aligent/cdk-constructs/issues" + }, + "homepage": "https://github.com/aligent/cdk-constructs#readme", + "devDependencies": { + "@types/yaml": "^1.9.7", + "@types/jest": "^29.5.5", + "@types/node": "20.6.3", + "@types/aws-lambda": "^8.10.122", + "aws-cdk": "2.97.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "typescript": "~5.2.2" + }, + "dependencies": { + "yaml": "^2.3.1", + "aws-cdk-lib": "2.97.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + } +} diff --git a/packages/graphql-mesh-server/tsconfig.json b/packages/graphql-mesh-server/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/packages/graphql-mesh-server/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +}