Skip to content

Architecture

The tunnel is a Layer 4 proxy over Cloudflare’s network. cloudflared runs on EC2 inside each environment’s VPC and opens an outbound session to the Cloudflare edge. Cloudflare terminates Worker traffic at the edge, and Hyperdrive’s pooled connections travel through the tunnel to the private Aurora endpoint. Nothing about this path requires an inbound port on AWS, a public database endpoint, or a VPN.

The whole stack is Terraform in adventive-platform-infra, split across three root modules: imagebuilder builds the AMI, cflared-asg runs the EC2 fleet per environment, and cloudflare-tunnels creates the Cloudflare tunnels and writes their credentials to Secrets Manager.

Connection path, Worker to the private Aurora endpoint Worker Public API, internal services Hyperdrive pooled connections Cloudflare edge terminates traffic Tunnel adv-cflared-<env> config_src = local ingress from config.yml cloudflared on EC2 Auto Scaling Group inside the VPC Aurora MySQL (private) outbound tunnel · 7844 Every hop is outbound; nothing requires an inbound port on AWS, a public database endpoint, or a VPN.

Each environment’s tunnel is a named Cloudflare tunnel with config_src = local, meaning the host reads its ingress rules from /etc/cloudflared/config.yml. That config, plus the tunnel credentials, is rendered by the cloudflare-tunnels Terraform module and stored in AWS Secrets Manager at /adventive/cloudflared/<env>. The public tunnel hostnames are proxied CNAMEs to <tunnel-uuid>.cfargotunnel.com; cloudflared routes each to the service its ingress rule names (for the databases, tcp://<aurora-endpoint>:3306), with an implicit catch-all returning http_status:404.

Dev and staging run a single cloudflared host in one Availability Zone; production runs two hosts across two Availability Zones. Each is kept alive by an Auto Scaling Group sized min = desired, max = desired + 1, so an instance refresh launches a replacement before terminating the old instance. Because cloudflared supports multiple connectors on one tunnel, that overlap keeps the tunnel up through a roll rather than dropping it, and in production the second host keeps serving even while the first is replaced.

As-built topology, a per-environment Auto Scaling Group Worker + Hyperdrive edge compute + pooling Cloudflare edge load-balances connectors VPC SUBNET Auto Scaling Group adv-cflared-<env> dev/stg 1 host · prd 2 (2 AZs) launch-before-terminate refresh one or two connectors per env Aurora (private) no public endpoint outbound tunnel TCP / 3306 Hosts sit in a public subnet today with security-group-only isolation (no inbound rule, Session Manager access). Moving to private subnets with NAT egress is the tracked pre-production hardening step and does not change the tunnel design.

Hosts sit in a public subnet today with security-group-only isolation (no inbound rule, no SSH; access is AWS Systems Manager Session Manager). Moving to private subnets with NAT egress is the documented pre-production hardening step, tracked separately, and does not change the tunnel design.

The tunnel heals at two layers, and neither is a CloudWatch alarm loop. First, the cloudflared systemd unit is Restart=on-failure with a 5 second backoff, so a crashed daemon restarts in place. Second, the ASG’s EC2 health check replaces the whole instance if EC2 status checks fail; the replacement boots from the golden AMI, its bootstrap re-fetches credentials from Secrets Manager, and cloudflared reconnects. cloudflared’s own edge reconnect handles transient drops. There is no custom tunnel-aware health check or self-heal Lambda today; adding one is a future enhancement that the New Relic signal would feed.

cloudflared natively supports multiple replicas sharing one tunnel, with Cloudflare load-balancing across the healthy connectors. Production runs two replicas across two AZs; dev and staging run a single host. Raising an environment’s replica count is a change to that environment’s entry in the cflared-asg environments map (its subnet_ids and desired_capacity, with min_healthy_percentage at 50 so a rolling refresh keeps one host serving), not a redesign: the AMI, the Secrets Manager credentials, and the ingress rules are unchanged. The trigger criteria for raising staging to two replicas are in the runbook.

Every resource derives its name from the environment. The full table is in Reference.

ResourcePatternExample
Cloudflare Tunneladv-cflared-{env}adv-cflared-prd
EC2 launch template / ASGadv-cflared-{env}adv-cflared-prd
Runtime IAM role / instance profileadv-cflared-runtime-{env}adv-cflared-runtime-prd
AMI recipe / pipelineadv-cflared-recipe / adv-cflared-pipelineshared, env-agnostic
SSM AMI pointer/adventive/cloudflared/ami-id-latestshared
Secrets Manager tunnel secret/adventive/cloudflared/{env}one per environment
Secrets Manager New Relic key/adventive/newrelic/license-keyshared
Instance tagsadv:env, adv:project=cloudflare-tunnel, adv:role=tunnel-runtimeon every instance

The tunnel is a generic TCP proxy: the same tunnel serves the Public API, the auth helper, and any future Worker needing Aurora reachback. Each environment’s ingress rules map its tunnel hostnames to the private Aurora endpoints; Hyperdrive resolves those hostnames. See the Hyperdrive standard for the Worker-side config and credential naming.