feat: deploy to vercel & use ALB on ecs

This commit is contained in:
2026-01-21 23:03:22 -08:00
parent cfd2e0d2c2
commit d9622ea451
11 changed files with 89 additions and 62 deletions

View File

@@ -3,5 +3,9 @@
"ca-central-1a",
"ca-central-1b",
"ca-central-1d"
]
],
"hosted-zone:account=585061171043:domainName=lucalise.ca:region=ca-central-1": {
"Id": "/hostedzone/Z0948300LINP3SX1WI4O",
"Name": "lucalise.ca."
}
}

View File

@@ -4,8 +4,10 @@ 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 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 {
@@ -26,6 +28,9 @@ export class GhostV2Stack extends cdk.Stack {
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,
@@ -45,22 +50,24 @@ export class GhostV2Stack extends cdk.Stack {
allowAllOutbound: true,
});
ecsSecurityGroup.addIngressRule(
ec2.Peer.ipv4(vpc.vpcCidrBlock),
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 traffic from VPC"
"Allow API"
);
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",
@@ -73,9 +80,7 @@ export class GhostV2Stack extends cdk.Stack {
},
}
);
table.grantReadWriteData(taskDefinition.taskRole);
const secretImports = ["DATABASE_URL", "GH_CLIENT_ID", "GH_CLIENT_SECRET", "REPOS_TABLE_NAME", "SENTRY_DSN"];
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}`
@@ -95,7 +100,7 @@ export class GhostV2Stack extends cdk.Stack {
],
secrets
});
table.grantReadWriteData(taskDefinition.taskRole);
const service = new ecs.FargateService(this, "ghostv2 service", {
cluster,
taskDefinition,
@@ -111,46 +116,21 @@ export class GhostV2Stack extends cdk.Stack {
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,
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)) })
}
}