import * as cdk from "aws-cdk-lib/core"; import { Construct } from "constructs"; import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; import * as ec2 from "aws-cdk-lib/aws-ec2"; import * as ecs from "aws-cdk-lib/aws-ecs"; import * as ssm from "aws-cdk-lib/aws-ssm" import * as acm from "aws-cdk-lib/aws-certificatemanager" import * as route53 from "aws-cdk-lib/aws-route53" import * as route53Targets from "aws-cdk-lib/aws-route53-targets" import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2" import * as path from "path"; interface StackProps extends cdk.StackProps { environment: string } export class GhostV2Stack extends cdk.Stack { constructor(scope: Construct, id: string, props: StackProps) { super(scope, id, props); const table = new dynamodb.TableV2(this, "ghostv2-table", { partitionKey: { name: "pk", type: dynamodb.AttributeType.STRING }, sortKey: { name: "sk", type: dynamodb.AttributeType.STRING }, billing: dynamodb.Billing.onDemand(), }); table.addGlobalSecondaryIndex({ indexName: "gsi1", partitionKey: { name: "gsi1pk", type: dynamodb.AttributeType.STRING }, sortKey: { name: "gsi1sk", type: dynamodb.AttributeType.STRING }, }); if (props.environment !== "prod") { return; } const vpc = new ec2.Vpc(this, "ghostv2 vpc", { maxAzs: 2, natGateways: 0, subnetConfiguration: [ { name: "Public", subnetType: ec2.SubnetType.PUBLIC, cidrMask: 24, }, ], }); const ecsSecurityGroup = new ec2.SecurityGroup(this, "ghostv2 security group", { vpc, description: "Security group for ECS Fargate tasks", allowAllOutbound: true, }); ecsSecurityGroup.addIngressRule( ec2.Peer.anyIpv4(), ec2.Port.tcp(80), "Allow HTTP" ); ecsSecurityGroup.addIngressRule( ec2.Peer.anyIpv4(), ec2.Port.tcp(443), "Allow HTTPS" ); ecsSecurityGroup.addIngressRule( ec2.Peer.anyIpv4(), ec2.Port.tcp(8080), "Allow API" ); const cluster = new ecs.Cluster(this, "ghostv2 cluster", { vpc, }); const taskDefinition = new ecs.FargateTaskDefinition( this, "ghostv2 api", { memoryLimitMiB: 512, cpu: 256, runtimePlatform: { cpuArchitecture: ecs.CpuArchitecture.X86_64, operatingSystemFamily: ecs.OperatingSystemFamily.LINUX, }, } ); const secretImports = ["DATABASE_URL", "GH_CLIENT_ID", "GH_CLIENT_SECRET", "REPOS_TABLE_NAME", "SENTRY_DSN", "FRONTEND_BASE_URL"]; const secrets = secretImports.reduce>((acc, name) => { acc[name] = ecs.Secret.fromSsmParameter(ssm.StringParameter.fromSecureStringParameterAttributes(this, `param${name}`, { parameterName: `/ghostv2/${props.environment}/${name}` })) return acc }, {}) taskDefinition.addContainer("api", { image: ecs.ContainerImage.fromAsset(path.join(__dirname, "../../api")), environment: { RUST_LOG: "info", }, portMappings: [ { containerPort: 8080, protocol: ecs.Protocol.TCP, }, ], secrets }); table.grantReadWriteData(taskDefinition.taskRole); const service = new ecs.FargateService(this, "ghostv2 service", { cluster, taskDefinition, desiredCount: 1, assignPublicIp: true, vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC, }, securityGroups: [ecsSecurityGroup], capacityProviderStrategies: [ { capacityProvider: "FARGATE_SPOT", weight: 1, }, ], }); const hostedZone = route53.HostedZone.fromLookup(this, "hosted zone", { domainName: "lucalise.ca" }) const certificate = new acm.Certificate(this, "ghostv2 api cert", { domainName: "api.ghostv2.lucalise.ca", validation: acm.CertificateValidation.fromDns(hostedZone) }) const alb = new elbv2.ApplicationLoadBalancer(this, "ghostv2 alb", { vpc, internetFacing: true, securityGroup: ecsSecurityGroup, }); alb.setAttribute("idle_timeout.timeout_seconds", "120") alb.addRedirect({ sourcePort: 80, sourceProtocol: elbv2.ApplicationProtocol.HTTP, targetPort: 443, targetProtocol: elbv2.ApplicationProtocol.HTTPS }) const listener = alb.addListener("https listener", { port: 443, open: true, certificates: [certificate] }) listener.addTargets("ECSTargets", { port: 8080, protocol: elbv2.ApplicationProtocol.HTTP, targets: [service], healthCheck: { path: "/", port: "8080" } }) new route53.ARecord(this, "alb record", { zone: hostedZone, recordName: "api.ghostv2.lucalise.ca", target: route53.RecordTarget.fromAlias(new route53Targets.LoadBalancerTarget(alb)) }) } }