Como dividir o DNS entre Kubernetes e um sidecar OpenVPN
Um pod sidecar precisa de dois mundos de nomes. Aponte um resolver loopback para o cluster e o túnel, e pare de depender de um resolv.conf editado.
Pods que executam um sidecar OpenVPN falham a resolução de nomes num padrão reconhecível: nomes do cluster penduram, nomes remotos respondem até o pod reiniciar, e o arquivo de resolver que você editou volta ao anterior. Nada nessa sequência é uma falha do túnel. Um pod tem um arquivo de resolver e dois mundos de nomes, e o arquivo pode listar servidores em ordem mas não pode expressar zonas. A correção duradoura é um resolver que roda dentro do pod e possui a divisão, então o arquivo continua trivial e nenhuma política tem motivo para reescrevê-lo.
Por que a edição que você fez continua desaparecendo
O arquivo pertence ao kubelet, não a você. Sob dnsPolicy: ClusterFirst o kubelet gera o arquivo para apontar ao DNS do cluster e o regenera sempre que um contêiner inicia. Tudo o que você muda via kubectl exec vive na camada gravável do contêiner, então reinício, evicção ou rollout descartam. A especificação do pod ainda carrega a mesma política, e é por isso que o mesmo arquivo volta.
Definir dnsPolicy: None interrompe a regeneração e também remove a resolução do cluster. Você ganha um arquivo que controla e perde os nomes de que precisava. Essa troca é o problema inteiro: resolv.conf não tem noção de zonas, apenas servidores tentados em ordem, então não pode enviar cluster.local a um endereço e uma zona privada a outro.
Execute um resolver no pod, em loopback
Aponte o pod para um único servidor de nomes em loopback, depois deixe um contêiner CoreDNS que compartilha o namespace de rede do pod responder na porta 53. Defina a política como None, dê ao pod esse único endereço e adicione o contêiner DNS ao lado da VPN e da aplicação:
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-corefileOs três contêineres compartilham um namespace de rede, então 127.0.0.1:53 do contêiner da aplicação alcança o CoreDNS sem serviço, sem port-forward e sem desvio pela rede do cluster. Montar o Corefile de um ConfigMap o mantém como entrada versionada em vez de arquivo editado no local. O resolv.conf gerado então contém um servidor de nomes e nada mais, um estado sobre o qual dá para raciocinar.
Dois detalhes mordem aqui. A imagem oficial liga a porta 53 como root, e algumas políticas de cluster exigem que você escreva isso explicitamente. Todo contêiner do pod agora depende daquele único contêiner estar ativo, então adicione uma readiness probe contra um nome inofensivo e deixe a aplicação tolerar falhas de inicialização.
Dê a cada zona privada seu próprio upstream
Um Corefile é uma lista de zonas, cada uma nomeando o servidor que pode responder. Um bloco cobre a zona do túnel, e o bloco catch-all vem por último e leva tudo o que nenhum bloco explícito reivindicou. O catch-all deve apontar para um upstream real, nunca de volta ao pod: .:53 { forward . 10.100.0.10 } reutiliza o resolver do cluster porque ele já encaminha nomes públicos aos resolvers do próprio nó. Nada mais no pod precisa saber a qual rede um nome pertence.
.:53 {
errors
cache 30
forward . 10.100.0.10
}
svc.acme.internal:53 {
errors
cache 30
forward . 192.168.240.1
}Leia o endereço do cluster do serviço kube-dns em kube-system em vez de copiar de um artigo, porque os CIDRs de serviço variam. O endereço do túnel vem do valor dhcp-option DNS que o servidor OpenVPN envia, ou do servidor autoritativo daquela zona. Se o resolver do cluster não responde a nomes públicos, coloque o endereço do resolver do nó no catch-all, lido no nó e não dentro do pod.
Não aponte o catch-all para /etc/resolv.conf, que é a linha única tentadora e uma armadilha nesta configuração exata. Sob dnsPolicy: None o arquivo do próprio pod contém o único servidor de nomes 127.0.0.1, então esse alvo é o próprio CoreDNS e toda consulta sem correspondência repete até expirar. Um bloco chamado svc.acme.internal casa com esse nome e todo nome abaixo dele, o que cobre db.svc.acme.internal e exclui svc.acme.internal.example.net. O ponto final é cosmético, porque nomes de zona são normalizados para a forma totalmente qualificada; a fronteira do sufixo decide onde a consulta cai.
Decida se o encaminhamento pertence ao resolver do cluster
Se toda carga de trabalho no namespace precisa da zona do túnel, um bloco de encaminhamento no ConfigMap do CoreDNS sob kube-system custa menos que um sidecar por pod. Essa mudança é em nível de cluster: exige direitos de administrador e reinício de um deployment do qual todos dependem. Limite-a àquela única zona, nunca à raiz. O endereço de túnel de uma carga de trabalho vira então o resolver de todo pod para a zona, e no dia em que o túnel cai o cluster inteiro perde o nome.
O sidecar mantém o raio de explosão dentro de um pod, que é o padrão melhor quando só alguns serviços precisam da rede remota. Seja o que for que escolher, anote qual resolver possui a zona, porque a próxima pessoa a depurar um timeout vai perguntar.
Nomes que resolvem enquanto as conexões ainda falham
A resolução é uma das duas coisas que o túnel precisa carregar; a outra é o caminho de volta, e a rede do pod o esconde bem. Se o próprio servidor OpenVPN roda como pod, as regras que você adiciona naquele contêiner não governam o tráfego que sai pelo nó. Essa falha é estreita: só os pods no nó que também rodava o servidor recebiam respostas, e todo o resto expirou.
Rodar o servidor com hostNetwork: true o coloca de volta no namespace do nó, onde uma regra FORWARD e a tradução de origem funcionam. Traduza o pool de clientes na saída, senão nenhum nó remoto tem rota de volta a um endereço de túnel como 10.8.0.6. Verifique os dois sentidos antes de tocar no Corefile outra vez. Um túnel que carrega a consulta mas não a resposta produz exatamente o mesmo timeout de um resolver que nunca recebeu a consulta.
Verifique a partir de um pod novo, não da sessão que você corrigiu
Pergunte às duas zonas explicitamente, depois repita a consulta após o pod ser substituído. Uma configuração que existe só num contêiner em execução não é configuração:
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 +shortSe o primeiro par responde e o par após a substituição não, o Corefile está certo e a especificação do pod não. Se ambos respondem e a aplicação ainda não conecta, pare de depurar DNS e teste o caminho TCP com nc -vz db.svc.acme.internal 5432 de dentro do contêiner. Guarde a saída do dig no ticket, porque é o artefato que separa um problema de nome de um problema de roteamento.