Cloudflare¶
Cloudflare's role in tomoda is narrow: it is the authoritative DNS for tomoda.life and nothing else. It is not an edge cache, not a TLS terminator for application traffic, and not a WAF. Every record managed by Terraform sets proxied = false, which means requests resolve straight through to the origin — CloudFront for assets, the GCP load balancer (Traefik) for the API and app.
flowchart LR
Client[Browser / mobile client] --> CF[Cloudflare DNS<br/>tomoda.life zone<br/>proxied = false]
CF -- assets.tomoda.life --> CFt[CloudFront<br/>edge cache]
CFt --> S3[(S3<br/>tomoda-assets-*)]
CF -- api.tomoda.life<br/>app.tomoda.life --> GLB[GCP Load Balancer]
GLB --> Traefik[Traefik<br/>on GKE]
Traefik --> Backend[tomoda backend pods]
Records managed by Terraform¶
infrastructure/aws/cloudflare.tf defines exactly two record types in the Cloudflare zone, both per-environment via Terraform workspaces.
1. ACM validation CNAMEs¶
When aws_acm_certificate.cert is created (see ACM), ACM emits a randomised CNAME that the domain owner must publish to prove control. Terraform reads domain_validation_options off the ACM resource and writes the corresponding record into Cloudflare:
resource "cloudflare_record" "acm_validation" {
for_each = {
for dvo in aws_acm_certificate.cert.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
record = dvo.resource_record_value
type = dvo.resource_record_type
}
}
zone_id = data.cloudflare_zone.this.id
name = each.value.name
content = each.value.record
type = each.value.type
proxied = false
ttl = 60
}
proxied = false is required — ACM does not resolve names through Cloudflare's edge, so a proxied (orange-cloud) record would silently fail validation.
2. Asset CNAME¶
The user-facing asset domain CNAMEs straight to the CloudFront distribution:
resource "cloudflare_record" "assets_cname" {
zone_id = data.cloudflare_zone.this.id
name = local.assets_subdomain # "assets" (prod) or "assets-dev" (dev)
content = aws_cloudfront_distribution.s3_distribution.domain_name
type = "CNAME"
proxied = false
ttl = 300
}
This is the only DNS edge between a browser and CloudFront. Because proxied = false, the client opens the TCP connection directly to a CloudFront IP — Cloudflare's role ends as soon as the A/AAAA lookup completes.
Records managed by external-dns (not Terraform)¶
The application-surface records — api.tomoda.life, app.tomoda.life, www.tomoda.life, and the matching -dev subdomains — are dynamically managed by external-dns running on the cluster, not by this Terraform root. See External-DNS for the configuration: the controller watches Kubernetes Ingress and Service resources, filters on domainFilters: [tomoda.life], and writes/updates Cloudflare records to match what's deployed. TXT-record ownership is tagged with owner: k8s-dev so multiple controllers can coexist safely.
The Cloudflare API token used by Terraform here is the same token external-dns uses (external-dns-cloudflare-secret in the cluster). Token scope: Zone:DNS:Edit on tomoda.life.
Do not edit application records by hand
Editing api.*, app.*, or www.* records in the Cloudflare console will be silently reverted on the next external-dns sync loop (default: every minute). To change a hostname, update the Ingress in k8s/apps/tomoda/overlays/<env>/ and let external-dns reconcile.
Why DNS-only and not Cloudflare CDN¶
There were two viable shapes:
- Proxy everything through Cloudflare — orange-cloud
app.tomoda.life,api.tomoda.life, andassets.tomoda.life. Cloudflare becomes the edge for all three. - DNS-only — Cloudflare resolves names, CloudFront handles asset caching, Traefik handles app/API. (Current choice.)
Tomoda picked (2) deliberately:
- No double-CDN for assets. Putting Cloudflare in front of CloudFront means objects are cached twice with different invalidation models — purging Cloudflare does not purge CloudFront and vice versa. Asset cache responsibility lives in exactly one place: CloudFront.
- No edge proxy for API traffic. The API is mostly authenticated, mostly POST, and mostly uncacheable. Cloudflare's edge would be doing nothing useful but would add another hop, another set of TLS certs to manage, and another vendor that can drop requests in incident postmortems.
- Origin IPs are not particularly sensitive. CloudFront's IPs are already public and Traefik sits behind a GCP load balancer with its own DDoS protections. The "hide the origin" argument for orange-clouding does not apply strongly here.
The result is a flat, easy-to-grep zone file where every record's content tells you exactly which backend serves it. If a record's content is *.cloudfront.net, the request goes to AWS. If it's a GCP load-balancer IP or hostname, it goes to GKE. There is no third option.
Operational notes¶
- The Terraform-managed token is read at apply time from a Kubernetes secret (
external-dns-cloudflare-secret). It must have at minimum Zone:DNS:Edit scope on thetomoda.lifezone. - DNS changes propagate in well under a minute given the 60–300 second TTLs in use.
- Re-running
terraform applyafter a workspace switch is the supported way to update records; do not edit Terraform-managed records by hand in the Cloudflare console, as the next apply will revert them.