feat: deploy to vercel & use ALB on ecs
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -23,3 +23,4 @@ Thumbs.db
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
.env*.local
|
||||
|
||||
6
.vercelignore
Normal file
6
.vercelignore
Normal file
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
.svelte-kit
|
||||
.git
|
||||
.vercel
|
||||
api
|
||||
cdk
|
||||
28
api/Cargo.lock
generated
28
api/Cargo.lock
generated
@@ -1396,6 +1396,17 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
@@ -1416,6 +1427,7 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
@@ -2932,6 +2944,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.4.13",
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
@@ -2954,12 +2967,14 @@ dependencies = [
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.4",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-http",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
@@ -4395,6 +4410,19 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-streams"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.83"
|
||||
|
||||
@@ -11,7 +11,7 @@ aws-sdk-dynamodb = "1.102.0"
|
||||
chrono = { version = "0.4.43", features = ["serde"] }
|
||||
jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] }
|
||||
mime_guess = "2.0.5"
|
||||
reqwest = { version = "0.13.1", features = ["json", "query"] }
|
||||
reqwest = { version = "0.13.1", features = ["json", "query", "stream"] }
|
||||
sentry = { version = "0.46.1", features = ["actix", "tracing"] }
|
||||
serde = "1.0.228"
|
||||
serde_dynamo = { version = "4.3.0", features = ["aws-sdk-dynamodb+1"] }
|
||||
|
||||
@@ -37,7 +37,7 @@ pub async fn proxy_file(
|
||||
.error_for_status_ref()
|
||||
.map_err(|_| crate::error::Error::NotFound)?;
|
||||
|
||||
let bytes = response.bytes().await?;
|
||||
let stream = response.bytes_stream();
|
||||
let mime = mime_guess::from_path(&path.file)
|
||||
.first_or_octet_stream()
|
||||
.to_string();
|
||||
@@ -45,5 +45,5 @@ pub async fn proxy_file(
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(mime)
|
||||
.insert_header(("Cache-Control", "public, max-age=3600"))
|
||||
.body(bytes))
|
||||
.streaming(stream))
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ async fn run() -> std::io::Result<()> {
|
||||
),
|
||||
)
|
||||
})
|
||||
.bind(("0.0.0.0", 8080))?
|
||||
.bind((env::var("ADDRESS").unwrap_or("0.0.0.0".to_string()), 8080))?
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ impl UserRepository {
|
||||
.item("gsi1pk", AttributeValue::S("REPOS".into()))
|
||||
.item("gsi1sk", AttributeValue::S(now.clone()))
|
||||
.item("imported_at", AttributeValue::S(now))
|
||||
.item("approved", AttributeValue::Bool(false))
|
||||
.item("approved", AttributeValue::Bool(true))
|
||||
.item("owner_id", AttributeValue::S(user_id.into()))
|
||||
.item("description", AttributeValue::S(repo.description.clone()))
|
||||
.send()
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,6 @@ import { jwtClient, adminClient } from 'better-auth/client/plugins';
|
||||
import { createAuthClient } from 'better-auth/svelte';
|
||||
|
||||
export const authClient = createAuthClient({
|
||||
baseURL: 'http://localhost:5173',
|
||||
baseURL: process.env.BETTER_AUTH_URL!,
|
||||
plugins: [jwtClient(), adminClient()]
|
||||
});
|
||||
|
||||
8
vercel.json
Normal file
8
vercel.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/api/v0/:path*",
|
||||
"destination": "https://s8do6ipb07.execute-api.ca-central-1.amazonaws.com/api/v0/:path*"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user