This repository is designed for a homelab environment and uses Terraform, Ansible, and GitHub Actions to deploy a containerized Cloudflare Dynamic DNS (DDNS) solution.
Important
This repository reflects my personal approach and preferences. While it can serve as a reference, it is not intended to be cloned and used without significant modifications.
- Homelab - Cloudflare DDNS
At a high level, this repository uses the following technologies:
- Terraform provisions a Proxmox VM
- Ansible automates AWX resource creation:
- Adds the host to an inventory and specified group
- Creates a project for the repository
- Generates deploy/destroy application job templates
- AWX launches an Ansible playbook to deploy the application using roles:
- Initial VM configuration (hostname setup, FreeIPA integration, etc.)
- Docker installation (and required dependencies)
- Docker Compose file generation and deployment
- GitHub Actions provides CI/CD and a GitOps experience:
- Deploys to the test environment on PR open/changes and auto destroys it
- Creates semantic versioned GitHub releases (and tags) on PR close
- Deploys to production on successful GitHub releases
- Offers a manual workflow to deploy/destroy to either environment
GitHub Actions serves as the glue that integrates all the above technologies. It provisions Proxmox VMs using Terraform, runs an Ansible playbook to create required AWX resources, and then launches AWX job templates to deploy the Cloudflare DDNS container.
The favonia/cloudflare-ddns container image is used to handle DDNS. It is a lightweight, Alpine-based container that uses the Cloudflare API to update DNS records. The container is deployed using Docker Compose,
The two primary tools used in this project are Terraform and Ansible. GitHub Actions and AWX can be easily replaced with your preferred CI/CD and automation solutions.
This project was built using Terraform (1.9.8) and Ansible (core 2.17.5). You can install these tools manually or use the devcontainer provided in this repository. The devcontainer includes all required dependencies, preconfigured development settings, and seamless SSH credential handling. If you have never used a devcontainer before, I highly recommend checking out Dev Containers!
For convenience, the devcontainer is configured to mount your home SSH directory (~/.ssh) to /home/vscode/.ssh inside the container. This setup simplifies SSH credential management when using WSL on Windows. Check this Stack Overflow post for a better long-term solution for those using WSL.
This project uses the terraform-homelab-pve-vm module, which handles:
- Creating the VM on Proxmox
- Creating the A-Record on PowerDNS
Below are the commands to launch the infrastructure deployment using Terraform. A partial Terraform backend is used, so the remaining backend configuration must be provided.
terraform init -backend-config=backend-example.hcl
terraform apply -var-file=envs/example.tfvarsThis project uses a partial backend configuration for the remote S3 backend. This allows a partially complete backend setup (see infra/terraform.tf for example), while the remaining configuration is provided separately.
Alternatively, you can configure the S3 backend using environment variables:
export AWS_ENDPOINT_URL_S3="https://minio.local:9000"
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_S3_STATE_BUCKET="terraform-state"An example backend configuration file to complete the supplied partial setup:
bucket = "terraform-state"
access_key="terraform-service-account"
secret_key="PASSWORD"
endpoints = {
s3 = "https://minio.local:9000"
}Below is an example of the tfvars file required to apply the Terraform configuration.
/* Proxmox/VM Vars */
pve_url="https://proxmox.local/api2/json"
pve_node="alpha"
vm_name="test"
vm_id=350
vm_ip_address="192.168.90.5"
vm_cidr_prefix_length=24
vm_gateway_address="192.168.90.1"
vm_dns_servers="192.168.90.100 192.168.90.101 192.168.90.1"
vm_vlan=20
vm_ci_store_pool="local-lvm"
vm_disk_store_pool="local-lvm"
/* PDNS Vars */
pdns_url="http://dns.local:8080/"
dns_record_name="test"
dns_zone="example.com"The Terraform providers for Proxmox and PowerDNS require the following secrets to be supplied via environment variables:
export PM_API_TOKEN_ID="your-proxmox-api-token-id"
export PM_API_TOKEN_SECRET="your-proxmox-api-token-secret"
export PDNS_API_KEY="your-powerdns-api-key"Note
This project uses API keys for authentication with Proxmox and PowerDNS. Refer to the respective provider documentation for alternative authentication methods.
Additionally, the Terraform module requires SSH key pairs used with the Debian Cloud-Init image. These should not be stored in tfvars files. Instead, they should be injected using environment variables:
export TF_VAR_pve_ci_ssh_private_key="your-private-key"
export TF_VAR_pve_ci_ssh_public_key="your-public-key"The deployment of the Cloudflare DDNS solution is handled with AWX and Ansible. There are three major Ansible components:
- AWX resource creation/destruction playbooks
- Adds the host to an inventory and specified group
- Creates a project for the repository
- Generates deploy/destroy application job templates
- The
cloudflare-ddnsrole - A deployment/destroy playbook
The only playbook designed to be launched manually (or via CI/CD) is the AWX resource creation playbook. AWX should be used to launch deployment and destroy playbooks. The deployment playbook applies multiple roles, including cloudflare-ddns and other dependency roles (Docker, FreeIPA client, etc.). The cloudflare-ddns role creates a service account and a Docker Compose file that runs a container to handle Cloudflare DDNS.
Before running this playbook, you need to install the required Ansible dependencies. You have two options:
# Install just the awx.awx collection required for the playbook
ansible-galaxy collection install awx.awx
# Or install all of the project's Ansible dependencies
ansible-galaxy collection install -r ./collections/requirements.ymlHere is an example command for running the playbook that creates all AWX resources:
# From within the ansible directory
ansible-playbook -i localhost create-awx-cloudflare-ddns-deployment-resources.yml \
-e awx_target_org="Homelab" \
-e awx_target_inv="Homelab" \
-e awx_git_credential="Github - AWX SSH Key" \
-e project_branch="main" \
-e target_host_fqdn="cloudflare-ddns.knighten.io" \
-e host_groups='["proxmox-hosts", "ipa-managed-clients"]' \
-e cloudflare_ddns_records='[{"zone":"knighten.io","subdomain":"game","proxied":false}]'If preferred, you can create a variable file like this:
awx_target_org: "Homelab"
awx_target_inv: "Homelab"
awx_git_credential: "Github - AWX SSH Key"
project_branch: "main"
target_host_fqdn: "cloudflare-ddns.knighten.io"
host_groups:
- "proxmox-hosts"
- "ipa-managed-clients"
cloudflare_ddns_records:
- zone: "knighten.io"
subdomain: "game"
proxied: falseThen, run the playbook like this:
# From inside the ansible directory
ansible-playbook -i localhost create-awx-cloudflare-ddns-deployment-resources.yml -e @vars.ymlThe deployment playbook is designed to be launched via AWX and is not meant to be run manually. Refer to the deploy and destroy playbooks used in the AWX job templates for more information.
The following variables and secrets must be configured in the AWX job templates that launch the deployment playbook.
The AWX resource creation playbook automatically links job templates to existing secrets and sets required variables.
During execution, I use variable files to pass required values—see them here.
Here are an overview of the variables used:
| Variable | Description |
|---|---|
awx_target_org |
Target AWX organization |
awx_target_inv |
Target AWX inventory |
awx_git_credential |
GitHub credential for AWX repository access |
awx_project_branch |
Git branch to deploy |
host_groups |
List of host groups |
job_template_credentials |
Credentials used in job templates |
| Variable | Description |
|---|---|
host |
Target host for deployment |
hostname |
Hostname for the deployed service |
ipaclient_domain |
FreeIPA domain name |
ipaclient_realm |
FreeIPA realm name |
ipaclient_server |
FreeIPA server address |
cloudflare_ddns_proxied_domains |
List of proxied domains |
cloudflare_ddns_non_proxied_domains |
List of non-proxied domains |
| Variable | Description |
|---|---|
cloudflare_ddns_app_path |
Path where the DDNS application is installed |
| Secret | Description |
|---|---|
ipaadmin_principal |
FreeIPA admin principal |
ipaadmin_password |
FreeIPA admin password |
cloudflare_api_key |
API key for Cloudflare |
| Environment Variable | Expected Value |
|---|---|
CONTROLLER_HOST |
AWX Hostname |
CONTROLLER_USERNAME |
AWX Username |
CONTROLLER_PASSWORD |
AWX User Password |
This project uses GitHub Actions to provide a complete CI/CD pipeline and a GitOps workflow. The workflows automate linting, deployment, and release management, reducing manual intervention.
| Workflow | Trigger | Description |
|---|---|---|
| Deploy to Test | PR Open / PR Change | Deploys infrastructure and application to the test environment on pull request creation or updates. |
| Ansible Lint | PR Open / PR Change | Runs Ansible linting to ensure playbooks and roles follow best practices. |
| Terraform Lint & Scan | PR Open / PR Change | Runs Terraform validation, formatting, and security scanning. |
| Destroy Test Deployment | PR Close | Destroys test environment resources when a pull request is closed or merged. |
| Create Release | Push to main |
Handles PR closures and creates a semantic versioned GitHub release. Also allows quick ad-hoc fixes. |
| Deploy to Production | GitHub Release Created | Automatically deploys the application to the production environment when a release is published. |
| Manual Deploy/Destroy | Manual Trigger | Provides an option to manually deploy or destroy in either the test or production environment. |
Here are a few additional details about my Homelab setup that enable the use of this repository and add more context to my code.
- A single Proxmox VE (PVE) host runs both production and test workloads.
- Minio serves as the Terraform S3 backend.
- Two virtualized Kubernetes clusters (Talos Linux) are used for test and production.
- HashiCorp Vault is deployed on Kubernetes for secret management.
- AWX is deployed on Kubernetes for Ansible-based automation.
- GitHub Actions Runner Controller (ARC) is deployed on Kubernetes, allowing GitHub Actions to run in my Homelab infrastructure.
| Component | Version |
|---|---|
| Proxmox VE | 8.2-2 |
| Minio | 1.2.2 RELEASE.2024-11-07T00-52-20Z |
| Kubernetes (Talos Linux) | v1.8.3 (k8s v1.31.1) |
| HashiCorp Vault | v1.18.1 |
| AWX | 24.6.1 |
| GitHub Actions Runner Controller (ARC) | gha-runner-scale-set 0.10.1 / controller 0.10.1 |
