cdk/lib/concierge-graphql.ts (130 lines of code) (raw):

import type {GuStackProps} from "@guardian/cdk/lib/constructs/core"; import {GuParameter, GuStack} from "@guardian/cdk/lib/constructs/core"; import type {App} from "aws-cdk-lib"; import {aws_ssm} from "aws-cdk-lib"; import {GuPlayApp} from "@guardian/cdk"; import {InstanceClass, InstanceSize, InstanceType, Peer, Port, Subnet, Vpc} from "aws-cdk-lib/aws-ec2"; import {AccessScope} from "@guardian/cdk/lib/constants"; import {getHostName} from "./hostname"; import {GuSecurityGroup, GuVpc} from "@guardian/cdk/lib/constructs/ec2"; import {HttpGateway, ValidStages} from "./gateway"; import {AttributeType, BillingMode, Table} from "aws-cdk-lib/aws-dynamodb"; import {GuPolicy} from "@guardian/cdk/lib/constructs/iam"; import {Effect, PolicyStatement} from "aws-cdk-lib/aws-iam"; import {StringParameter} from "aws-cdk-lib/aws-ssm"; import {GraphiqlExplorer} from "./graphiql-explorer"; export class ConciergeGraphql extends GuStack { constructor(scope: App, id: string, props: GuStackProps) { super(scope, id, props); const previewMode = this.stack.endsWith("-preview"); //Preview needs to live in a VPC so we can route to capi-preview const vpcId = new GuParameter(this, "vpcId", { description: "VPC to deploy into", default: this.getVpcIdPath(this, previewMode), fromSSM: true, type: "String" }); const subnetsList = new GuParameter(this, "subnets", { description: "Subnets to deploy into", default: this.getDeploymentSubnetsPath(this, previewMode), fromSSM: true, type: "List<String>" }); const vpc = Vpc.fromVpcAttributes(this, "vpc", { vpcId: vpcId.valueAsString, //len(publicSubnetIds) must be a multiple of len(availabilityZones) availabilityZones: ["eu-west-1a","eu-west-1b" ,"eu-west-1c"].slice(0, subnetsList.valueAsList.length), publicSubnetIds: subnetsList.valueAsList, }); const allSubnets = subnetsList.valueAsList.map((id, ctr)=>Subnet.fromSubnetId(this, `Subnet${ctr}`, id)); const hostedZoneId = aws_ssm.StringParameter.valueForStringParameter(this, `/account/services/capi.gutools/${this.stage}/hostedzoneid`); const lbDomainName = getHostName(this, ".internal"); const authTable = new Table(this, "AuthTable", { billingMode: BillingMode.PAY_PER_REQUEST, partitionKey: { name: "ApiKey", type: AttributeType.STRING } }); new StringParameter(this, "AuthTableParam", { parameterName: `/${this.stage}/${this.stack}/concierge-graphql/aws/auth_table`, stringValue: authTable.tableName }); const {loadBalancer, listener, autoScalingGroup} = new GuPlayApp(this, { access: { //You should put a gateway in front of this scope: AccessScope.INTERNAL, cidrRanges: [Peer.ipv4("10.0.0.0/8")], }, app: "concierge-graphql", certificateProps: { hostedZoneId, domainName: lbDomainName, }, instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.LARGE), monitoringConfiguration: { noMonitoring: true, }, privateSubnets: allSubnets, publicSubnets: allSubnets, scaling: { minimumInstances: 2, maximumInstances: 4, }, applicationLogging: { enabled: true, }, userData: { distributable: { fileName: "concierge-graphql_0.1.0_all.deb", executionStatement: "dpkg -i concierge-graphql/concierge-graphql_0.1.0_all.deb" } }, roleConfiguration: { additionalPolicies: [ new GuPolicy(this, "AuthTablePolicy", { statements: [ new PolicyStatement({ effect: Effect.ALLOW, actions: ["dynamodb:GetItem"], resources: [authTable.tableArn] })] }) ] }, vpc, }); const linkageSG = new GuSecurityGroup(this, "LinkageSG", { app: props.app ?? "concierge-graphql", vpc, }); loadBalancer.addSecurityGroup(linkageSG); const subnets = GuVpc.subnets(this, subnetsList.valueAsList); new HttpGateway(this, "GW", { stage: props.stage as ValidStages, backendLoadbalancer: loadBalancer, lbDomainName, previewMode, backendListener: listener, backendLbIncomingSg: linkageSG, subnets: { subnets, }, vpc }); autoScalingGroup.connections.allowTo(Peer.ipv4("10.0.0.0/8"), Port.tcp(9200), "Allow outgoing connections to Elasticsearch"); new GraphiqlExplorer(this, "Explorer", { appName: "graphiql-explorer" //needs to match the value in riff-raff.yaml }) //OK - so this is a good idea and should really be in here. But it's damn fiddly so leaving it out for now. //The idea is we need a connection to the relevant Elasticsearch instance. So, we define a "connection" (which basically //to an egress rule) on our SG which allows egress to the remote ES SG. You still manually need to add a rule on the relevant //remove Elasticsearch SG to allow ingress from us. //Because there is still a manual stage, I'm going to leave it as manual for now, and leave the code here for reference. // const elasticsearchSGID = new GuParameter(this, "ESConnectionID", { // description: "Security group ID for the elasticsearch cluster to connect to", // default: `/${this.stage}/${this.stack}/elasticsearch/securityGroupId`, // fromSSM: true, // type: "String" // }); // const elasticsearchSG = GuSecurityGroup.fromSecurityGroupId(this, "ESConnectionSG", elasticsearchSGID.valueAsString); // // app.autoScalingGroup.connections.allowTo(elasticsearchSG, Port.tcp(9200), "Allow connection to Elasticsearch") // //Note - this SG will need to be manually added to the incoming rules of the appropriate ES instance to allow contact // app.autoScalingGroup.connections.addSecurityGroup(new GuSecurityGroup(this, "ESAccess", { // app: "concierge-graphql", // description: "Allow access to Elasticsearch", // vpc, // allowAllOutbound: false, // egresses: [ // { // range: Peer.ipv4("10.0.0.0/24"), // port: Port.tcp(9200), // description: "Allow outgoing to Elasticsearch data port" // } // ] // })) } getAccountPath(scope:GuStack, isPreview:boolean, elementName: string) { const basePath = "/account/vpc"; if(isPreview) { return scope.stage.startsWith("CODE") ? `${basePath}/CODE-preview/${elementName}` : `${basePath}/PROD-preview/${elementName}`; } else { return scope.stage.startsWith("CODE") ? `${basePath}/CODE-live/${elementName}` : `${basePath}/PROD-live/${elementName}`; } } getVpcIdPath(scope:GuStack, isPreview:boolean) { return this.getAccountPath(scope, isPreview,"id"); } getDeploymentSubnetsPath(scope:GuStack, isPreview:boolean) { return this.getAccountPath(scope, isPreview,"subnets") } }