Cloudflare Hyperdrive with AWS Postgres
Connect a Cloudflare Worker to an AWS RDS Postgres database through Cloudflare Hyperdrive. Since RDS lives in a private VPC, a Cloudflare tunnel runs on Fargate to expose the database to Hyperdrive. Cloudflare Access locks the tunnel down to Hyperdrive using a service token.
The worker uses the sst.cloudflare.Hyperdrive binding. The lambda connects
directly through the VPC for comparison.
const domain = "sst-dev.org";
const vpc = new sst.aws.Vpc("Vpc", { nat: "managed" });const cluster = new sst.aws.Cluster("Cluster", { vpc });const postgres = new sst.aws.Postgres("Postgres", { vpc });
const zone = cloudflare.getZoneOutput({ filter: { name: domain } });
const tunnelSecret = new random.RandomString("TunnelSecret", { length: 32,});
const tunnel = new cloudflare.ZeroTrustTunnelCloudflared("Tunnel", { accountId: sst.cloudflare.DEFAULT_ACCOUNT_ID, name: `${$app.name}-${$app.stage}-tunnel`, tunnelSecret: tunnelSecret.result.apply((v) => Buffer.from(v).toString("base64"), ),});
const record = new cloudflare.DnsRecord("TunnelRecord", { name: `hyperdrive-${$app.stage}.${domain}`, ttl: 1, type: "CNAME", zoneId: zone.zoneId, content: $interpolate`${tunnel.id}.cfargotunnel.com`, proxied: true,});
const tunnelConfig = new cloudflare.ZeroTrustTunnelCloudflaredConfig( "TunnelConfig", { accountId: sst.cloudflare.DEFAULT_ACCOUNT_ID, tunnelId: tunnel.id, config: { ingresses: [ { hostname: record.name, service: $interpolate`tcp://${postgres.host}:${postgres.port}`, }, { service: "http_status:404" }, ], }, },);
const tunnelToken = cloudflare.getZeroTrustTunnelCloudflaredTokenOutput({ accountId: sst.cloudflare.DEFAULT_ACCOUNT_ID, tunnelId: tunnel.id,}).token;
const serviceToken = new cloudflare.ZeroTrustAccessServiceToken( "HyperdriveServiceToken", { name: `${$app.name}-${$app.stage}-hyperdrive-token`, accountId: sst.cloudflare.DEFAULT_ACCOUNT_ID, },);
new cloudflare.ZeroTrustAccessApplication("HyperdriveAccess", { accountId: sst.cloudflare.DEFAULT_ACCOUNT_ID, type: "self_hosted", name: `${$app.name}-${$app.stage}-hyperdrive`, domain: record.name, destinations: [{ uri: record.name, type: "public" }], appLauncherVisible: false, policies: [ { decision: "non_identity", includes: [ { serviceToken: { tokenId: serviceToken.id } }, ], name: `${$app.name}-${$app.stage}-hyperdrive-policy`, }, ],});
const cloudflaredService = new sst.aws.Service( "Cloudflared", { wait: true, capacity: "spot", cluster, containers: [ { name: "cloudflared", image: "cloudflare/cloudflared:latest", command: ["tunnel", "run"], environment: { TUNNEL_TOKEN: tunnelToken, TUNNEL_METRICS: "0.0.0.0:20241", }, health: { command: [ "CMD", "cloudflared", "tunnel", "--metrics", "localhost:20241", "ready", ], startPeriod: "60 seconds", timeout: "5 seconds", interval: "30 seconds", retries: 3, }, dev: { autostart: true, command: $interpolate`docker run \ --rm \ -e TUNNEL_LOGLEVEL=info \ --network ${$app.name} \ --name ${$app.name}-${$app.stage}-cloudflared \ cloudflare/cloudflared:latest \ tunnel run --token ${tunnelToken}`, }, }, ], }, // Make sure the tunnel's ingress rules exist before the container starts, // otherwise cloudflared serves 503s until it re-polls the config. { dependsOn: [tunnelConfig] },);
const hyperdrive = new sst.cloudflare.Hyperdrive( "Database", { origin: { host: record.name, user: postgres.username, password: postgres.password, database: postgres.database, accessClientId: serviceToken.clientId, accessClientSecret: serviceToken.clientSecret, scheme: "postgres", }, }, { dependsOn: [ postgres, tunnel, record, tunnelConfig, cloudflaredService, cluster, ], },);
const worker = new sst.cloudflare.Worker("Worker", { handler: "./worker.ts", link: [hyperdrive], url: true,});
const lambda = new sst.aws.Function("Lambda", { handler: "lambda.handler", link: [postgres], vpc, url: true,});import postgres from "postgres";import { Resource } from "sst";
export default { async fetch(_request: Request, _env: unknown, ctx: ExecutionContext) { const sql = postgres(Resource.Database.connectionString);
try { const now = Date.now(); const result = await sql`select * from pg_tables limit 10`; const delay = Date.now() - now;
ctx.waitUntil(sql.end());
return Response.json({ delay, result }); } catch (e: any) { console.log(e); return Response.json({ error: e.message }, { status: 500 }); } },};import postgres from "postgres";import { Resource } from "sst";
const sql = postgres( `postgres://${Resource.Postgres.username}:${Resource.Postgres.password}@${Resource.Postgres.host}:${Resource.Postgres.port}/${Resource.Postgres.database}`,);
export async function handler() { try { const now = Date.now(); const result = await sql`select * from pg_tables limit 10`; const delay = Date.now() - now; return { statusCode: 200, body: JSON.stringify({ result, delay }), }; } catch (e: any) { return { statusCode: 200, body: JSON.stringify({ error: e.message }), }; }}View the full example.