cert-manager and external-dns#
These two controllers solve the two most tedious parts of exposing services on Kubernetes: getting TLS certificates and creating DNS records. Together, they make it so that creating an Ingress resource automatically provisions a DNS record pointing to your cluster and a valid TLS certificate for the hostname.
cert-manager#
cert-manager watches for Certificate resources and Ingress annotations, then obtains and renews TLS certificates automatically.
Installation#
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set crds.enabled=trueThe crds.enabled=true flag installs the CRDs as part of the Helm release. Verify with kubectl get pods -n cert-manager – you should see cert-manager, cert-manager-cainjector, and cert-manager-webhook all Running.
Issuer vs ClusterIssuer#
An Issuer is namespace-scoped. It can only issue certificates for resources in its own namespace. A ClusterIssuer is cluster-scoped and works across all namespaces. For most setups, use a ClusterIssuer.
ACME HTTP01 challenge (Let’s Encrypt):
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
ingressClassName: nginxHTTP01 works by cert-manager temporarily creating an Ingress that serves a challenge token at http://<domain>/.well-known/acme-challenge/<token>. This requires that your domain already points to the cluster’s Ingress controller and that port 80 is reachable from the internet.
ACME DNS01 challenge (for wildcards or private clusters):
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-dns
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef:
name: letsencrypt-dns-key
solvers:
- dns01:
cloudflare:
email: ops@example.com
apiTokenSecretRef:
name: cloudflare-api-token
key: api-tokenCreate the API token secret:
kubectl create secret generic cloudflare-api-token \
--from-literal=api-token=your-cloudflare-api-token \
-n cert-managerDNS01 is required for wildcard certificates (*.example.com). It proves domain ownership via TXT records. Supported providers include Cloudflare, Route53, Google Cloud DNS, and Azure DNS. For Route53, use IAM Roles for Service Accounts (IRSA) on EKS or explicit access keys.
Always start with staging. Let’s Encrypt production has strict rate limits. Use the staging server first:#
server: https://acme-staging-v02.api.letsencrypt.org/directorySwitch to production only after confirming certificates are issued correctly.
Certificate Resources and Ingress Integration#
You can create Certificate resources explicitly, but the common pattern is letting cert-manager create them automatically from Ingress annotations:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-app
port:
number: 80cert-manager sees the annotation, creates a Certificate resource, performs the ACME challenge, and populates the app-tls Secret. Renewal happens automatically 30 days before expiry.
Debug certificate issues by walking the chain: Certificate -> CertificateRequest -> Order -> Challenge. Use kubectl describe on each to find where it is stuck.
external-dns#
external-dns watches Services and Ingress resources, then creates DNS records in your DNS provider to match.
Installation#
helm repo add external-dns https://kubernetes-sigs.github.io/external-dns
helm install external-dns external-dns/external-dns \
--namespace external-dns \
--create-namespace \
--set provider.name=cloudflare \
--set env[0].name=CF_API_TOKEN \
--set env[0].valueFrom.secretKeyRef.name=cloudflare-api-token \
--set env[0].valueFrom.secretKeyRef.key=api-token \
--set policy=upsert-only \
--set domainFilters[0]=example.comKey values: provider.name supports cloudflare, aws (Route53), google (Cloud DNS), azure, and more. Set policy=upsert-only to avoid accidental deletions (vs sync which deletes too). Always set domainFilters to restrict which zones external-dns can touch. Set txtOwnerId to a unique identifier when running multiple clusters.
How It Works#
external-dns reads the external-dns.alpha.kubernetes.io/hostname annotation on Ingress or Service resources:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
external-dns.alpha.kubernetes.io/hostname: app.example.com
external-dns.alpha.kubernetes.io/ttl: "300"
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-app
port:
number: 80external-dns creates an A record (or CNAME for LoadBalancer hostnames) pointing app.example.com to the Ingress controller’s external IP. The same annotation works on Services of type LoadBalancer.
external-dns uses TXT ownership records to track which DNS records it manages, preventing conflicts with manually created records or other external-dns instances. If records are not being updated, check that TXT ownership records exist and match the current txtOwnerId.
Using Both Together#
The full automation flow: create an Ingress with both annotations, and the system handles the rest.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
external-dns.alpha.kubernetes.io/hostname: app.example.com
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
secretName: app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-app
port:
number: 80The sequence: (1) external-dns creates the DNS A record. (2) cert-manager starts the ACME HTTP01 challenge. (3) Let’s Encrypt resolves the domain to the cluster and validates. (4) cert-manager populates the TLS secret. (5) The Ingress controller serves HTTPS.
If you use HTTP01 challenges, the DNS record must exist before cert-manager can complete the challenge. external-dns typically creates records within 1-2 minutes, and cert-manager retries on failure, so they converge naturally. If issuance fails, check DNS propagation with dig app.example.com and verify port 80 is reachable.