External DNS records with Crossplane and Cloudflare
This document explains how I deploy Crossplane with the Cloudflare provider to declaratively manage external CNAME DNS records in my GitOps workflow.
About external DNS records with Crossplane and Cloudflare
Crossplane transforms cloud services into Kubernetes APIs, enabling GitOps-friendly management of DNS entries. By installing the Crossplane control plane, configuring the Cloudflare provider, and declaring Record
resources, we automate the lifecycle of CNAME records.
Maintain manifest definitions in k8s/infrastructure/controllers/crossplane/
and each application’s directory to prevent documentation drift.
More details about the Cloudflare provider setup
-
Crossplane chart Deploys Crossplane into the
crossplane-system
namespace to enable its CRDs and controllers. -
ExternalSecret Retrieves the Cloudflare API token from a Bitwarden-backed
ClusterSecretStore
and templates a Kubernetes Secret containing acreds
field with JSON credentials. -
Provider Installs the Crossplane Cloudflare provider package.
-
ProviderConfig References the synced Secret's
creds
key to supply credentials to the provider.
Prerequisites
- A Kubernetes cluster with
kubectl
access. - A
ClusterSecretStore
containing a valid Cloudflare API token under the keyinfra-cloudflare-api-token
. - GitOps tooling (Argo CD, Flux, etc.) configured to apply this repository.
Overview of steps
- Deploy Crossplane: Install the Crossplane Helm chart into
crossplane-system
. - Fetch Cloudflare token: Create an
ExternalSecret
to sync the API token into a Secret. - Install Cloudflare provider: Apply the
Provider
andProviderConfig
manifests. - Declare DNS records: Add
dns-record.yaml
and updatekustomization.yaml
in each service repo. - Apply and verify: Deploy manifests and confirm records in Crossplane and Cloudflare.
Deploy Crossplane and configure the Cloudflare provider
-
Install Crossplane chart Apply the official Crossplane Helm chart into
crossplane-system
via your GitOps tool. -
Configure external secrets Define an
ExternalSecret
pointing to the Bitwarden-backedClusterSecretStore
to syncinfra-cloudflare-api-token
andinfra-cloudflare-account-id
into a Kubernetes Secret namedcloudflare-api-token
. The secret includes acreds
key containing{ "api_token": "...", "account_id": "..." }
. -
Install provider and credentials Apply the Crossplane
Provider
for Cloudflare and aProviderConfig
that references thecreds
key incloudflare-api-token
.
Apply DNS records for each service
Each application repository should include:
-
A
dns-record.yaml
manifest declaring aRecord
(typeCNAME
) with:metadata.name
andmetadata.namespace
matching the service.spec.forProvider.name
as the hostname (e.g.,frigate
).spec.forProvider.value
as the Cloudflare Tunnel target.
-
An update to
kustomization.yaml
addingdns-record.yaml
to the resources list.
Repeat for Immich, Jellyfin, Jellyseerr, Baby Buddy, IT Tools, Authentik, etc., adjusting names and namespaces per service.
Verify the configuration
-
Crossplane resources Run
kubectl get records.dns.cloudflare.upbound.io -A
and confirm each record’s status isREADY
. -
Cloudflare dashboard In Cloudflare UI, go to DNS → your.domain.tld and verify that CNAME entries point to the correct Tunnel hostnames.