Skip to content

Instantly share code, notes, and snippets.

@benrowe
Created May 25, 2025 22:47
Show Gist options
  • Save benrowe/f61149b91e062d86f370f84ec1dc0067 to your computer and use it in GitHub Desktop.
Save benrowe/f61149b91e062d86f370f84ec1dc0067 to your computer and use it in GitHub Desktop.
Cloudflare tunnel & traefik setup via terraform
# main.tf
# Configure the Cloudflare provider
# Make sure to set CLOUDFLARE_API_TOKEN environment variable or configure it directly here.
# For security, using environment variables is recommended.
# export CLOUDFLARE_API_TOKEN="YOUR_CLOUDFLARE_API_TOKEN"
# export CLOUDFLARE_ACCOUNT_ID="YOUR_CLOUDFLARE_ACCOUNT_ID"
terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 4.0"
}
local = {
source = "hashicorp/local"
version = "~> 2.0"
}
}
}
# --- Variables ---
variable "cloudflare_account_id" {
description = "Your Cloudflare Account ID."
type = string
}
variable "cloudflare_zone_id" {
description = "The Zone ID of your domain in Cloudflare."
type = string
}
variable "domain_name" {
description = "Your primary domain name (e.g., example.com)."
type = string
}
variable "traefik_subdomain" {
description = "The subdomain for your Traefik instance (e.g., traefik)."
type = string
default = "traefik"
}
variable "tunnel_name" {
description = "A unique name for your Cloudflare Zero Trust tunnel."
type = string
default = "traefik-tunnel"
}
# --- Cloudflare Tunnel Setup ---
resource "cloudflare_tunnel" "main_tunnel" {
account_id = var.cloudflare_account_id
name = var.tunnel_name
# The tunnel secret is automatically generated by Cloudflare.
# It will be used by the cloudflared Docker container.
}
# Create a CNAME DNS record for the Traefik subdomain, pointing to the tunnel.
# This makes your Traefik instance accessible via the tunnel.
resource "cloudflare_record" "traefik_cname" {
zone_id = var.cloudflare_zone_id
name = var.traefik_subdomain
value = "${cloudflare_tunnel.main_tunnel.id}.cfargotunnel.com"
type = "CNAME"
proxied = true # Ensure it's proxied through Cloudflare for Zero Trust features
}
# Define a tunnel route for the Traefik service.
# This tells the tunnel where to send traffic for the specified hostname.
resource "cloudflare_tunnel_route" "traefik_route" {
account_id = var.cloudflare_account_id
tunnel_id = cloudflare_tunnel.main_tunnel.id
hostname = "${var.traefik_subdomain}.${var.domain_name}"
# The service URL points to the internal Traefik instance within the Docker network.
# Traefik will be listening on port 80 (HTTP) or 443 (HTTPS) internally.
# We'll use HTTP for simplicity here, Traefik will handle HTTPS termination.
service = "http://traefik:80"
}
# --- Docker Compose File Generation ---
# Generate the docker-compose.yaml file content.
# This file will define the cloudflared and traefik services.
resource "local_file" "docker_compose_file" {
content = <<-EOT
version: '3.8'
services:
# Cloudflare Tunnel service
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
environment:
# The TUNNEL_TOKEN is securely retrieved from the Cloudflare tunnel resource.
TUNNEL_TOKEN: "${cloudflare_tunnel.main_tunnel.tunnel_token}"
command: tunnel run --token \${TUNNEL_TOKEN}
# Mount a volume for cloudflared's configuration and logs (optional but good practice)
volumes:
- ./cloudflared:/etc/cloudflared
networks:
- traefik_proxy
# Traefik Reverse Proxy service
traefik:
image: traefik:latest
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
command:
# Enable Docker provider for Traefik
- --providers.docker=true
- --providers.docker.exposedbydefault=false
# Entrypoints for HTTP and HTTPS
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
# Enable Traefik Dashboard (optional, accessible via a separate route or locally)
- --api.dashboard=true
# Enable access logs (optional)
- --accesslog=true
# Enable Traefik logs (optional)
- --log.level=INFO
# Configure certificates (optional, for internal Traefik <-> service communication)
# If you want Traefik to handle SSL for your services, you'd add cert resolvers here.
# For Cloudflare tunnel, Cloudflare handles the public SSL, Traefik handles internal.
ports:
# The dashboard port (optional, for local access to Traefik dashboard)
- "8080:8080"
# Expose ports for Traefik to listen on for incoming HTTP/HTTPS traffic from the tunnel
- "80:80"
- "443:443"
volumes:
# Mount the Docker socket to allow Traefik to discover services
- /var/run/docker.sock:/var/run/docker.sock:ro
# Mount a volume for Traefik's dynamic configuration and logs
- ./traefik-data:/etc/traefik
networks:
- traefik_proxy
labels:
# Traefik labels to expose the dashboard internally (optional)
- "traefik.enable=true"
- "traefik.http.routers.traefik-dashboard.rule=Host(\`${var.traefik_subdomain}.${var.domain_name}\`) && PathPrefix(\`/dashboard\`)"
- "traefik.http.routers.traefik-dashboard.service=api@internal"
- "traefik.http.routers.traefik-dashboard.entrypoints=websecure"
# Redirect HTTP to HTTPS for the dashboard (optional)
- "traefik.http.routers.traefik-dashboard.middlewares=traefik-redirect-web-to-websecure"
# Middleware for HTTP to HTTPS redirection (if you want Traefik to handle it)
- "traefik.http.middlewares.traefik-redirect-web-to-websecure.redirectscheme.scheme=https"
- "traefik.http.middlewares.traefik-redirect-web-to-websecure.redirectscheme.permanent=true"
networks:
traefik_proxy:
external: true # Or create it if it doesn't exist.
# If you want Terraform to create the network, change to:
# name: traefik_proxy
# And remove the `external: true` line.
EOT
filename = "docker-compose.yaml"
}
# --- Outputs ---
output "tunnel_id" {
description = "The ID of the created Cloudflare Zero Trust tunnel."
value = cloudflare_tunnel.main_tunnel.id
}
output "tunnel_name" {
description = "The name of the created Cloudflare Zero Trust tunnel."
value = cloudflare_tunnel.main_tunnel.name
}
output "traefik_url" {
description = "The URL where your Traefik instance will be accessible via the tunnel."
value = "https://${var.traefik_subdomain}.${var.domain_name}"
}
output "docker_compose_file_path" {
description = "The path to the generated docker-compose.yaml file."
value = local_file.docker_compose_file.filename
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment