I pod che eseguono un sidecar OpenVPN falliscono la risoluzione dei nomi con uno schema riconoscibile: i nomi del cluster si bloccano, i nomi remoti rispondono finché il pod non viene riavviato, e il file resolver modificato torna indietro. Nulla in quella sequenza è un guasto del tunnel. Un pod ha un solo file resolver e due mondi di nomi, e il file può elencare i server in ordine ma non può esprimere zone. La soluzione duratura è un resolver che gira dentro il pod e possiede la divisione, così il file resta banale e nessuna policy ha motivo di riscriverlo.

Perché la modifica che avete fatto continua a sparire

Il file appartiene al kubelet, non a voi. Con dnsPolicy: ClusterFirst il kubelet lo genera per puntare al DNS del cluster e lo rigenera ogni volta che parte un container. Tutto ciò che cambiate tramite kubectl exec vive nello strato scrivibile del container, quindi un riavvio, un'eviction o un rollout lo scarta. La specifica del pod porta ancora la stessa policy, ed è per questo che torna lo stesso file.

Impostare dnsPolicy: None ferma la rigenerazione e rimuove anche la risoluzione del cluster. Guadagnate un file che controllate e perdete i nomi che vi servivano. Questo scambio è tutto il problema: resolv.conf non ha nozione di zone, solo di server da provare in ordine, quindi non può inviare cluster.local a un indirizzo e una zona privata a un altro.

Eseguite un resolver nel pod, su loopback

Puntate il pod a un unico nameserver su loopback, poi lasciate che un container CoreDNS che condivide lo spazio dei nomi di rete del pod risponda sulla porta 53. Impostate la policy su None, date al pod quell'unico indirizzo e aggiungete il container DNS accanto alla VPN e all'applicazione:

spec:
  dnsPolicy: None
  dnsConfig:
    nameservers:
      - 127.0.0.1
    options:
      - name: ndots
        value: "5"
  containers:
    - name: dns
      image: coredns/coredns
      args: ["-conf", "/etc/coredns/Corefile"]
      volumeMounts:
        - {name: corefile, mountPath: /etc/coredns, readOnly: true}
    - name: vpn
      image: your-openvpn-client-image
    - name: app
      image: your-app-image
  volumes:
    - name: corefile
      configMap:
        name: split-dns-corefile

Tutti e tre i container condividono un unico spazio dei nomi di rete, quindi 127.0.0.1:53 dal container dell'applicazione raggiunge CoreDNS senza servizio, senza port-forward e senza deviazioni attraverso la rete del cluster. Montare il Corefile da una ConfigMap lo mantiene come input versionato invece di un file modificato sul posto. Il resolv.conf generato contiene poi un solo nameserver e nient'altro, uno stato su cui potete ragionare.

Due dettagli mordono qui. L'immagine ufficiale lega la porta 53 come root, e alcune policy del cluster vi obbligano a scriverlo esplicitamente. Ogni container del pod ora dipende dal fatto che quell'unico container sia attivo, quindi aggiungete una readiness probe su un nome innocuo e lasciate che l'applicazione tolleri i fallimenti di avvio.

Date a ogni zona privata il proprio upstream

Un Corefile è un elenco di zone, ognuna nomina il server che può rispondere. Un blocco copre la zona del tunnel, e il blocco catch-all arriva per ultimo e prende tutto ciò che nessun blocco esplicito ha rivendicato. Il catch-all deve puntare a un upstream reale, mai di nuovo al pod: .:53 { forward . 10.100.0.10 } riusa il resolver del cluster perché quel resolver inoltra già i nomi pubblici ai resolver del nodo stesso. Nient'altro nel pod deve sapere a quale rete appartiene un nome.

.:53 {
  errors
  cache 30
  forward . 10.100.0.10
}

svc.acme.internal:53 {
  errors
  cache 30
  forward . 192.168.240.1
}

Leggete l'indirizzo del cluster dal servizio kube-dns in kube-system invece di copiarne uno da un articolo, perché i CIDR dei servizi differiscono. L'indirizzo del tunnel viene dal valore dhcp-option DNS che il server OpenVPN invia, o dal server autoritativo di quella zona. Se il vostro resolver del cluster non risponde ai nomi pubblici, mettete l'indirizzo del resolver del nodo nel catch-all, letto sul nodo e non dentro il pod.

Non puntate il catch-all a /etc/resolv.conf, che è la riga singola tentatrice e una trappola in questa configurazione esatta. Con dnsPolicy: None il file del pod stesso contiene l'unico nameserver 127.0.0.1, quindi quel bersaglio è CoreDNS stesso e ogni query non corrispondente ricorre finché non scade. Un blocco chiamato svc.acme.internal corrisponde a quel nome e a ogni nome sotto di esso, il che copre db.svc.acme.internal ed esclude svc.acme.internal.example.net. Il punto finale è cosmetico, perché i nomi di zona vengono normalizzati in forma pienamente qualificata; il confine del suffisso decide dove atterra una query.

Decidete se l'inoltro appartiene al resolver del cluster

Se ogni workload nello spazio dei nomi ha bisogno della zona del tunnel, un blocco di inoltro nella ConfigMap di CoreDNS sotto kube-system costa meno di un sidecar per pod. Quel cambiamento è a livello di cluster: richiede diritti di amministratore e un riavvio di un deployment da cui tutti dipendono. Limitatelo a quella sola zona, mai alla radice. L'indirizzo del tunnel di un singolo workload diventa allora il resolver di ogni pod per la zona, e il giorno in cui il tunnel cade l'intero cluster perde il nome.

Il sidecar tiene il raggio d'esplosione dentro un solo pod, che è il default migliore quando solo pochi servizi hanno bisogno della rete remota. Qualunque cosa scegliate, annotate quale resolver possiede la zona, perché la prossima persona che eseguirà il debug di un timeout lo chiederà.

Nomi che si risolvono mentre le connessioni ancora falliscono

La risoluzione è una delle due cose che il tunnel deve trasportare; l'altra è il percorso di ritorno, e la rete del pod lo nasconde bene. Se il server OpenVPN stesso gira come pod, le regole che aggiungete in quel container non governano il traffico che esce dal nodo. Quel guasto è ristretto: solo i pod sul nodo che eseguiva anche il server ottenevano risposte, e tutto il resto è andato in timeout.

Eseguire il server con hostNetwork: true lo riporta nello spazio dei nomi del nodo, dove una regola FORWARD e la traduzione della sorgente hanno effetto. Traducete il pool di client all'uscita, altrimenti nessun nodo remoto ha una rotta di ritorno verso un indirizzo di tunnel come 10.8.0.6. Controllate entrambe le direzioni prima di toccare di nuovo il Corefile. Un tunnel che trasporta la query e non la risposta produce esattamente lo stesso timeout di un resolver che non ha mai ricevuto la query.

Verificate da un pod nuovo, non dalla sessione che avete corretto

Chiedete esplicitamente a entrambe le zone, poi ripetete la query dopo la sostituzione del pod. Una configurazione che esiste solo in un container in esecuzione non è una configurazione:

kubectl exec -it deploy/app -- dig @127.0.0.1 kubernetes.default.svc.cluster.local +short
kubectl exec -it deploy/app -- dig @127.0.0.1 db.svc.acme.internal +short
kubectl delete pod -l app=your-app
kubectl wait --for=condition=Ready pod -l app=your-app --timeout=90s
kubectl exec -it deploy/app -- dig @127.0.0.1 db.svc.acme.internal +short

Se la prima coppia risponde e la coppia dopo la sostituzione no, il Corefile è a posto e la specifica del pod non lo è. Se entrambe rispondono e l'applicazione non riesce ancora a connettersi, smettete di fare debug del DNS e provate il percorso TCP con nc -vz db.svc.acme.internal 5432 dall'interno del container. Conservate l'output di dig nel ticket, perché è l'artefatto che separa un problema di nomi da un problema di routing.

Articoli correlati