cert-manager is a Kubernetes-native tool for requesting and managing SSL certificates, similar to what certbot does on the command line. Integrating it with Let’s Encrypt is straightforward — let’s walk through the setup on Google Cloud.
Prerequisites
Before configuring Kubernetes, we need to set up a Google Cloud service account.
Create the service account:
gcloud iam service-accounts create cert-manager \
--display-name="DNS01 Solver for cert-manager"
Grant it DNS admin permissions so it can update DNS challenges:
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:cert-manager@$PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/dns.admin"
Enable Workload Identity for the service account:
gcloud iam service-accounts add-iam-policy-binding \
"cert-manager@$PROJECT_ID.iam.gserviceaccount.com" \
--role roles/iam.workloadIdentityUser \
--member "serviceAccount:$PROJECT_ID.svc.id.goog[cert-manager/cert-manager]"
Install cert-manager
Deploy cert-manager using the official Helm chart:
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm upgrade cert-manager jetstack/cert-manager \
--install \
--create-namespace \
--namespace cert-manager \
--set installCRDs=true \
--set serviceAccount.annotations."iam\.gke\.io/gcp-service-account"=cert-manager@$PROJECT_ID.iam.gserviceaccount.com \
--wait
Create the Cluster Issuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: <EMAIL_ADDRESS>
profile: tlsserver
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- dns01:
cloudDNS:
project: $PROJECT_ID
envsubst < clusterissuer-yaml | kubectl apply -f -
There are several ways to request a certificate. The recommended approach is to create a Certificate resource and verify it has been issued — this allows the certificate to be reused across multiple ingresses.
Create the following resource in your target namespace:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: my-certificate
namespace: example
spec:
secretName: my-certificate-tls
privateKey:
rotationPolicy: Always
dnsNames:
- example.mydomain.com
usages:
- digital signature
- key encipherment
- server auth
issuerRef:
kind: ClusterIssuer
name: letsencrypt-prod
If you have cmctl installed, check the certificate status:
cmctl status certificate my-certificate -n example
You should see output similar to:
Name: my-certificate
Namespace: example
Created at: 2025-10-17T16:05:27-04:00
Conditions:
Ready: False, Reason: RequestChanged, Message: Fields on existing CertificateRequest resource not up to date: [spec.dnsNames]
Issuing: True, Reason: RequestChanged, Message: Fields on existing CertificateRequest resource not up to date: [spec.dnsNames]
DNS Names:
- example.mydomain.com
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Issuing 16m cert-manager-certificates-trigger Issuing certificate as Secret does not exist
...
Challenges:
- Name: my-certificate-1234-5678, Type: DNS-01, Token: 1234567890, Key: 1234567890, State: pending, Reason: , Processing: true, Presented: false
Once issued, the certificate is stored in the my-certificate-tls secret. Inspect it with:
$ cmctl inspect secret my-certificate-tls -n example
Valid for:
DNS Names:
- example.mydomain.com
URIs: <none>
IP Addresses: <none>
Email Addresses: <none>
Usages:
- digital signature
- key encipherment
- server auth
...
Continue to Part II — ExternalDNS.