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 servicediscovery from "aws-cdk-lib/aws-servicediscovery"; import * as apigatewayv2 from "aws-cdk-lib/aws-apigatewayv2"; 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 }, }); 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.ipv4(vpc.vpcCidrBlock), ec2.Port.tcp(8080), "Allow traffic from VPC" ); const cluster = new ecs.Cluster(this, "ghostv2 cluster", { vpc, }); const namespace = new servicediscovery.PrivateDnsNamespace( this, "ghostv2 namespace", { name: "ghostv2.local", vpc, } ); const taskDefinition = new ecs.FargateTaskDefinition( this, "ghostv2 api", { memoryLimitMiB: 512, cpu: 256, runtimePlatform: { cpuArchitecture: ecs.CpuArchitecture.X86_64, operatingSystemFamily: ecs.OperatingSystemFamily.LINUX, }, } ); table.grantReadWriteData(taskDefinition.taskRole); const secretImports = ["DATABASE_URL", "GH_CLIENT_ID", "GH_CLIENT_SECRET", "REPOS_TABLE_NAME", "SENTRY_DSN"]; 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 }); 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, }, ], cloudMapOptions: { name: "api", cloudMapNamespace: namespace, dnsRecordType: servicediscovery.DnsRecordType.SRV, containerPort: 8080, dnsTtl: cdk.Duration.seconds(10), }, }); const vpcLink = new apigatewayv2.CfnVpcLink(this, "ghostv2 vpc link", { name: "ghostv2-vpc-link", subnetIds: vpc.publicSubnets.map((s) => s.subnetId), securityGroupIds: [ecsSecurityGroup.securityGroupId], }); const httpApi = new apigatewayv2.CfnApi(this, "ghostv2 http-api", { name: "ghostv2-api", protocolType: "HTTP", }); const integration = new apigatewayv2.CfnIntegration(this, "Integration", { apiId: httpApi.ref, integrationType: "HTTP_PROXY", integrationMethod: "ANY", integrationUri: service.cloudMapService!.serviceArn, connectionType: "VPC_LINK", connectionId: vpcLink.ref, payloadFormatVersion: "1.0", }); new apigatewayv2.CfnRoute(this, "DefaultRoute", { apiId: httpApi.ref, routeKey: "$default", target: `integrations/${integration.ref}`, }); new apigatewayv2.CfnStage(this, "Stage", { apiId: httpApi.ref, stageName: "$default", autoDeploy: true, }); } }