Lesson 11 Ingress + cert-manager + DNS
Theme:

Solutions — Homework 11

Lesson 11 · Cloud Infrastructure & DevOps

Task 1 — Rate-limit annotations

values.yaml additions (already included in the shipped chart; shown here for reference):

ingress:
  ...
  rateLimitRps: 0       # 0 = disabled
  rateLimitBurst: 0     # 0 = nginx default of 5

Patch to templates/ingress.yaml — inside the annotations: block:

    {{- if gt (int .Values.ingress.rateLimitRps) 0 }}
    nginx.ingress.kubernetes.io/limit-rps: {{ .Values.ingress.rateLimitRps | quote }}
    nginx.ingress.kubernetes.io/limit-burst-multiplier: {{ .Values.ingress.rateLimitBurst | default 5 | quote }}
    {{- end }}

Common mistake — type mismatch in Go templates

Setting rateLimitRps as a string ("5") in a values overlay and then evaluating it with if .Values.ingress.rateLimitRps will always render the annotation because non-empty strings are truthy in Go templates — even "0" is truthy. Always cast to int and compare greater-than-zero: gt (int .Values.ingress.rateLimitRps) 0. This gives a clear disabled=0 / enabled=N contract that survives typos in values files and --set overrides.

Task 2 — Wildcard cert via dns-01

dns-01 ClusterIssuer (Cloudflare):

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod-dns
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: ops@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-dns-key
    solvers:
      - dns01:
          cloudflare:
            apiTokenSecretRef:
              name: cloudflare-api-token
              key: token

Wildcard Certificate resource:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com-wildcard
  namespace: shortener
spec:
  secretName: example-com-wildcard-tls
  issuerRef:
    name: letsencrypt-prod-dns
    kind: ClusterIssuer
  dnsNames:
    - "*.example.com"
    - "example.com"

Why dns-01 is required for wildcards: http-01 challenges require the ACME server to fetch a token from a specific URL at http://<host>/.well-known/acme-challenge/<token>. A wildcard like *.example.com covers infinite possible subdomains — there is no single host to serve the challenge from, and serving it from example.com itself does not validate the wildcard. dns-01 places a TXT record at _acme-challenge.example.com instead; one TXT record covers every name under the domain, so the wildcard validation works. The trade-off is that dns-01 requires API credentials for your DNS provider stored in the cluster as a Secret, and the credential scope must be narrow (ideally scoped to the single zone, not your full account).

Common mistake — pairing wildcards with http-01

cert-manager will accept a Certificate requesting *.example.com with an http-01 issuer without error at admission time. The Order will be created and will then fail permanently with reason: Failed because the ACME server rejects http-01 for wildcards per RFC 8555. Always pair wildcard dnsNames with a dns-01 solver.

Task 3 — Second app sharing the Ingress controller

Greeter namespace + service (quick scaffold):

kubectl create namespace greeter
kubectl -n greeter create deployment greeter \
  --image=hashicorp/http-echo:latest \
  -- -text="Hello from greeter"
kubectl -n greeter expose deployment greeter --port=80 --target-port=5678

Greeter Ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: greeter
  namespace: greeter
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts: [greeter.example.com]
      secretName: greeter-tls
  rules:
    - host: greeter.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: greeter
                port: {number: 80}

Cluster-wide vs per-namespace boundary: ingress-nginx is cluster-wide — one Deployment and one Service handle every Ingress in every namespace, routing by Host header. The ClusterIssuer is also cluster-wide — both apps reference letsencrypt-prod by name from any namespace without needing a local copy. However, Certificate, Order, and Challenge resources, and the resulting TLS Secret, are all namespaced: each app’s cert artefacts live alongside the app in its own namespace. This means shortener’s shortener-tls Secret in namespace shortener cannot be mounted directly by greeter’s Ingress in namespace greeter — Secrets do not cross namespace boundaries. The standard pattern for sharing a wildcard cert across namespaces is to use a Secret mirroring operator (such as Reflector or Kubernetes Replicator), or to use External Secrets Operator to inject the cert from a central vault into each namespace that needs it.

Common mistake — referencing another namespace’s Secret

Attempting to use secretName: shortener-tls in greeter’s Ingress fails silently: ingress-nginx will look for the Secret in the greeter namespace, not find it, and serve an empty certificate. The browser shows “NET::ERR_CERT_INVALID”. Each app needs its own Certificate (issued from the same ClusterIssuer) into its own namespace, or a mirroring mechanism for wildcards.

Module 5 — observability (Prometheus + Grafana + Loki for the four golden signals), HA + autoscaling (HPA + PDB + topology constraints), and GitOps with ArgoCD. The platform you built in M4 is now ready to host whatever production scrutiny it needs.