Files
ghostv2/cdk/lib/cdk-stack.ts

137 lines
4.7 KiB
TypeScript

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<Record<string, ecs.Secret>>((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)) })
}
}