Ansible for the Homelab — Part 6: Cloudflare Integration
In Part 5 we deployed services using Docker and Ansible. Those services are running inside the homelab, accessible on the local network. But to reach them from outside - without opening firewall ports or exposing your home IP - you need a tunnel.
This is the final piece: using Ansible to automate Cloudflare Tunnel configuration and DNS, so adding a new service is a matter of editing a vars file and running two playbooks.
Why Cloudflare Tunnel
Cloudflare Tunnel runs a lightweight agent (cloudflared) on one of your hosts. It connects outbound to Cloudflare's network and receives traffic for your domains - no inbound firewall rules, no port forwarding, no exposed IP address. Cloudflare handles TLS termination and proxying.
The two things you need to manage when adding a service: 1. Tunnel ingress - tell cloudflared which hostname maps to which internal service 2. DNS - create a CNAME record pointing the hostname at the tunnel
Both can be fully automated with Ansible.
The Two Roles
The homelab uses two roles for Cloudflare automation:
roles/
├── cloudflare-dns/ ← creates/updates DNS records
│ ├── tasks/main.yml
│ └── vars/
│ ├── main.yml ← the DNS records to manage
│ └── vault.yml ← encrypted API token
└── cloudflare-tunnel/ ← configures ingress routes
├── tasks/main.yml
├── handlers/main.yml ← restarts cloudflared on config change
└── vars/
├── main.yml ← ingress route definitions
└── vault.yml ← encrypted tunnel token
Defining Ingress Routes
All tunnel routes live in roles/cloudflare-tunnel/vars/main.yml. Each entry maps a public hostname to an internal service:
cloudflare_ingress:
- hostname: app.example.com
service: http://app-prod.lan:8000
- hostname: tools.example.com
service: http://tools.lan:3000
# Catch-all must be last
- service: http_status:404
When you deploy a new service and want it publicly accessible, you add an entry here, then run the tunnel playbook. That's it - no logging into the Cloudflare dashboard.
Defining DNS Records
DNS records live in roles/cloudflare-dns/vars/main.yml. Each new hostname needs a CNAME pointing at the tunnel:
dns_records:
- zone: example.com
record: app
type: CNAME
value: "{{ cloudflare_tunnel_id }}.cfargotunnel.com"
proxied: true
- zone: example.com
record: tools
type: CNAME
value: "{{ cloudflare_tunnel_id }}.cfargotunnel.com"
proxied: true
{{ cloudflare_tunnel_id }} comes from group_vars/all.yml - define it once, reference it everywhere.
The Playbooks
Two playbooks, one for each role.
Update DNS:
ansible-playbook playbooks/setup-cloudflare-dns.yml
This runs against localhost - it's all API calls to Cloudflare, so no remote host is needed. It uses community.general.cloudflare_dns to create or update each record defined in the vars file.
Update the tunnel:
ansible-playbook playbooks/setup-cloudflare-tunnel.yml
This deploys the new ingress config to tools.lan (where cloudflared runs), writes the config file, and calls the Cloudflare API to push the update remotely. The handler restarts cloudflared if the config changed.
Adding a New Service End-to-End
When you deploy a new service and want it on the internet:
- Deploy the container (Part 5)
- Add the ingress route to
roles/cloudflare-tunnel/vars/main.yml - Add the DNS record to
roles/cloudflare-dns/vars/main.yml - Run both playbooks
ansible-playbook playbooks/setup-cloudflare-dns.yml
ansible-playbook playbooks/setup-cloudflare-tunnel.yml
New service, live on the internet, no dashboard clicking, no firewall changes.
Where This Leaves You
That wraps the series. Starting from a bare machine and ending with services running behind Cloudflare Tunnel - fully automated, secrets encrypted, structure maintainable. The homelab is now a platform you manage with code rather than one you manage with SSH sessions and muscle memory.
The playbooks, roles, and inventory files are all in version control. Rebuilding from scratch is a series of ansible-playbook commands.