Nossa experiência com dados no cluster Kubernetes etcd diretamente (sem API K8s)

Cada vez mais, os clientes nos solicitam acesso ao cluster Kubernetes para interagir com os serviços internos: para se conectar diretamente a um banco de dados ou serviço, ou para permitir que um aplicativo local se comunique com aplicativos dentro do cluster...

Nossa experiência com dados no cluster Kubernetes etcd diretamente (sem API K8s)

Por exemplo, pode haver a necessidade de conectar-se de uma máquina local ao serviço memcached.staging.svc.cluster.local. Oferecemos essa possibilidade através de uma VPN dentro do cluster, à qual o cliente se conecta. Para isso, anunciamos as sub-redes dos pods, serviços e enviamos o DNS do cluster ao cliente. Assim, quando o cliente tenta se conectar ao serviço memcached.staging.svc.cluster.local, a solicitação vai para o DNS do cluster e ele recebe o endereço do serviço da rede de serviços do cluster ou o endereço do pod.

Configuramos clusters K8s usando kubeadm, onde por padrão a sub-rede de serviço é 192.168.0.0/16, e a rede de pods é 10.244.0.0/16. Normalmente, tudo funciona bem, mas existem algumas questões:

  • A sub-rede 192.168.*.* é frequentemente utilizada em redes de clientes, e ainda mais frequentemente em redes domésticas de desenvolvedores. E então enfrentamos conflitos: roteadores domésticos operam nessa sub-rede e a VPN envia essas sub-redes do cluster para o cliente.
  • Temos vários clusters (clusters de produção, estágio e/ou vários clusters de desenvolvimento). Assim, todos eles terão por padrão as mesmas sub-redes para pods e serviços, o que cria grandes dificuldades ao trabalhar simultaneamente com serviços em vários clusters.

Já adotamos há algum tempo a prática de usar diferentes sub-redes para serviços e pods dentro de um único projeto — em suma, para que todos os clusters tenham redes diferentes. No entanto, existem muitos clusters em operação que não gostaríamos de reconstruir do zero, pois muitos serviços, aplicativos stateful, etc., estão em execução neles.

E então nos perguntamos: como podemos alterar a sub-rede em um cluster existente?

Busca por soluções

A prática mais comum é recriar tudo serviços do tipo ClusterIP. Como alternativa, podem recomendar e isso:

O seguinte processo apresenta um problema: após tudo configurado, os pods surgem com o IP antigo como servidor DNS em /etc/resolv.conf.
Como ainda não encontrei a solução, tive que redefinir todo o cluster com kubeadm reset e iniciá-lo novamente.

Mas nem todos aceitam isso... Aqui estão informações mais detalhadas para o nosso caso:

  • Utiliza-se Flannel;
  • Existem clusters tanto em nuvens quanto em hardware;
  • Gostaríamos de evitar a reimplantação de todos os serviços no cluster;
  • Há uma necessidade de fazer tudo com o mínimo de problemas possível;
  • A versão do Kubernetes é 1.16.6 (no entanto, as etapas posteriores serão semelhantes para outras versões);
  • A principal tarefa resume-se a substituir, no cluster implantado com kubeadm, a sub-rede de serviços 192.168.0.0/16, por 172.24.0.0/16.

E coincidiu que há muito tempo estávamos interessados em ver o que e como o Kubernetes armazena no etcd, o que pode ser feito com isso... Então pensamos: "Por que simplesmente não atualizar os dados no etcd, substituindo os IPs antigos (sub-rede) pelos novos??»

Depois de procurar ferramentas prontas para trabalhar com dados no etcd, não encontramos nada que resolvesse completamente a tarefa proposta. (A propósito, se você souber de alguma utilidade para trabalhar com dados diretamente no etcd - ficaríamos gratos por referências.) No entanto, um bom ponto de partida foi etcdhelper do OpenShift (obrigado a seus autores!).

Esta ferramenta consegue conectar-se ao etcd usando certificados e ler dados a partir daí com os comandos ls, get, dump.

Adicionando etcdhelper

A próxima ideia é lógica: "O que impede de adicionar essa funcionalidade na utilidade, permitindo a gravação de dados no etcd?"

Ela se materializou em uma versão modificada do etcdhelper com duas novas funções changeServiceCIDR e changePodCIDR. Seu código pode ser visualizado aqui.

O que fazem as novas funções? O algoritmo changeServiceCIDR:

  • cria um deserializador;
  • compila uma expressão regular para substituir o CIDR;
  • percorre todos os serviços do tipo ClusterIP no cluster:
    • decodifica o valor do etcd em um objeto Go;
    • com a expressão regular substitui os dois primeiros bytes do endereço;
    • atribui ao serviço um IP da nova sub-rede;
    • cria um serializador, transforma o objeto Go em protobuf, grava os novos dados no etcd.

Função changePodCIDR é essencialmente análoga changeServiceCIDR — apenas, em vez de editar a especificação dos serviços, fazemos isso para o nó e mudamos .spec.PodCIDR para a nova sub-rede.

Prática

Mudança do serviceCIDR

O plano para a implementação da tarefa proposta é muito simples, mas implica em downtime durante a recriação de todos os pods no cluster. Após descrever os passos principais, também compartilharemos ideias sobre como teoricamente minimizar esse tempo de inatividade.

Ações preparatórias:

  • instalação do software necessário e compilação do etcdhelper modificado;
  • backup do etcd e /etc/kubernetes.

Um plano resumido para a mudança do serviceCIDR:

  • modificação dos manifestos do apiserver e do controller-manager;
  • renovação de certificados;
  • mudança dos serviços ClusterIP no etcd;
  • reiniciar todos os pods no cluster.

A seguir, apresentamos a sequência completa de ações em detalhes.

1. Instalamos o etcd-client para fazer o dump dos dados:

apt install etcd-client

2. Compilamos o etcdhelper:

  • Instalamos o golang:
    GOPATH=\/root\/golang
    mkdir -p $GOPATH\/local
    curl -sSL https:\/\/dl.google.com\/go\/go1.14.1.linux-amd64.tar.gz | tar -xzvC $GOPATH\/local
    echo "export GOPATH="$GOPATH"" >> ~\/.bashrc
    echo 'export GOROOT="$GOPATH\/local\/go"' >> ~\/.bashrc
    echo 'export PATH="$PATH:$GOPATH\/local\/go\/bin"' >> ~\/.bashrc
  • Salvamos a nós mesmos etcdhelper.go, carregamos as dependências, compilamos:
    wget https:\/\/raw.githubusercontent.com\/flant\/examples\/master\/2020\/04-etcdhelper\/etcdhelper.go
    go get go.etcd.io\/etcd\/clientv3 k8s.io\/kubectl\/pkg\/scheme k8s.io\/apimachinery\/pkg\/runtime
    go build -o etcdhelper etcdhelper.go

3. Fazemos backup do etcd:

backup_dir=\/root\/backup
mkdir ${backup_dir}
cp -rL \/etc\/kubernetes ${backup_dir}
ETCDCTL_API=3 etcdctl --cacert=\/etc\/kubernetes\/pki\/etcd\/ca.crt --key=\/etc\/kubernetes\/pki\/etcd\/server.key --cert=\/etc\/kubernetes\/pki\/etcd\/server.crt --endpoints https:\/\/192.168.199.100:2379 snapshot save ${backup_dir}\/etcd.snapshot

4. Mudamos a sub-rede do serviço nos manifestos do Kubernetes control plane. Nos arquivos /etc/kubernetes/manifests/kube-apiserver.yaml e /etc/kubernetes/manifests/kube-controller-manager.yaml mudamos o parâmetro --service-cluster-ip-range para a nova sub-rede: 172.24.0.0/16 em vez de 192.168.0.0/16.

5. Como estamos mudando a sub-rede do serviço, para a qual o kubeadm emite certificados para o apiserver (entre outros), é necessário renová-los:

  1. Vamos ver para quais domínios e endereços IP o certificado atual foi emitido:
    openssl x509 -noout -ext subjectAltName <\/etc\/kubernetes\/pki\/apiserver.crt
    X509v3 Subject Alternative Name:
        DNS:dev-1-master, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:apiserver, IP Address:192.168.0.1, IP Address:10.0.0.163, IP Address:192.168.199.100
  2. Prepararemos uma configuração mínima para o kubeadm:
    cat kubeadm-config.yaml
    apiVersion: kubeadm.k8s.io\/v1beta1
    kind: ClusterConfiguration
    networking:
      podSubnet: "10.244.0.0\/16"
      serviceSubnet: "172.24.0.0\/16"
    apiServer:
      certSANs:
      - "192.168.199.100" # Endereço IP do nó mestre
  3. Removeremos os antigos crt e key, pois sem isso o novo certificado não será emitido:
    rm \/etc\/kubernetes\/pki\/apiserver.{key,crt}
  4. Renovaremos os certificados para o API-server:
    kubeadm init phase certs apiserver --config=kubeadm-config.yaml
  5. Verificaremos se o certificado foi emitido para a nova sub-rede:
    openssl x509 -noout -ext subjectAltName <\/etc\/kubernetes\/pki\/apiserver.crt
    X509v3 Subject Alternative Name:
        DNS:kube-2-master, DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, IP Address:172.24.0.1, IP Address:10.0.0.163, IP Address:192.168.199.100
  6. Após a renovação do certificado do API-server, reiniciaremos seu contêiner:
    docker ps | grep k8s_kube-apiserver | awk '{print $1}' | xargs docker restart
  7. Regeneraremos a configuração para admin.conf:
    kubeadm alpha certs renew admin.conf
  8. Editaremos os dados no etcd:
    .\/etcdhelper -cacert \/etc\/kubernetes\/pki\/etcd\/ca.crt -cert \/etc\/kubernetes\/pki\/etcd\/server.crt -key \/etc\/kubernetes\/pki\/etcd\/server.key -endpoint https:\/\/127.0.0.1:2379 change-service-cidr 172.24.0.0\/16 

    Atenção! Neste momento, o cluster para de resolver domínios, pois os pods existentes estão com /etc/resolv.conf o antigo endereço do CoreDNS (kube-dns) configurado, e o kube-proxy alterou as regras do iptables da antiga sub-rede para a nova. O restante do artigo discute possíveis maneiras de minimizar o tempo de inatividade.

  9. Vamos ajustar os ConfigMaps no namespace kube-system:
    kubectl -n kube-system edit cm kubelet-config-1.16

    — aqui vamos substituir clusterDNS pelo novo endereço IP do serviço kube-dns: kubectl -n kube-system get svc kube-dns.

    kubectl -n kube-system edit cm kubeadm-config

    — vamos corrigir data.ClusterConfiguration.networking.serviceSubnet para a nova sub-rede.

  10. Como o endereço do kube-dns mudou, é necessário atualizar a configuração do kubelet em todos os nós:
    kubeadm upgrade node phase kubelet-config && systemctl restart kubelet
  11. Agora resta reiniciar todos os pods no cluster:
    kubectl get pods --no-headers=true --all-namespaces | sed -r 's\/(S+)s+(S+).*\/kubectl --namespace 1 delete pod 2\/e'

Minimizando o tempo de inatividade

Ideias sobre como minimizar o downtime:

  1. Após as alterações nos manifestos do control plane, criar um novo serviço kube-dns, por exemplo, com o nome kube-dns-tmp e um novo endereço 172.24.0.10.
  2. Fazer if no etcdhelper, que não irá modificar o serviço kube-dns.
  3. Substituir em todos os kubelets o endereço ClusterDNS pelo novo, enquanto o serviço antigo continuará funcionando ao mesmo tempo que o novo.
  4. Aguardar até que os pods com aplicações sejam reinicializados, seja naturalmente ou em um horário acordado.
  5. Excluir o serviço kube-dns-tmp e alterar serviceSubnetCIDR para o serviço kube-dns.

Este plano permitirá minimizar o downtime para cerca de um minuto — durante a exclusão do serviço kube-dns-tmp e a substituição da sub-rede do serviço kube-dns.

Modificação do podNetwork

Além disso, decidimos ver como modificar o podNetwork utilizando o etcdhelper que criamos. A sequência de ações é a seguinte:

  • corrigimos as configurações em kube-system;
  • corrigimos o manifesto do kube-controller-manager;
  • alteramos podCIDR diretamente no etcd;
  • reinicializamos todos os nós do cluster.

Agora, mais detalhes sobre essas ações:

1. Modificamos os ConfigMaps no namespace kube-system:

kubectl -n kube-system edit cm kubeadm-config

— corrigimos data.ClusterConfiguration.networking.podSubnet para a nova sub-rede 10.55.0.0/16.

kubectl -n kube-system edit cm kube-proxy

— corrigimos data.config.conf.clusterCIDR: 10.55.0.0/16.

2. Modificamos o manifesto do controller-manager:

vim /etc/kubernetes/manifests/kube-controller-manager.yaml

— corrigimos --cluster-cidr=10.55.0.0/16.

3. Verificamos os valores atuais .spec.podCIDR, .spec.podCIDRs, .InternalIP, .status.addresses para todos os nós do cluster:

kubectl get no -o json | jq '[.items[] | {"name": .metadata.name, "podCIDR": .spec.podCIDR, "podCIDRs": .spec.podCIDRs, "InternalIP": (.status.addresses[] | select(.type == "InternalIP") | .address)}]'

[
  {
    "name": "kube-2-master",
    "podCIDR": "10.244.0.0/24",
    "podCIDRs": [
      "10.244.0.0/24"
    ],
    "InternalIP": "192.168.199.2"
  },
  {
    "name": "kube-2-master",
    "podCIDR": "10.244.0.0/24",
    "podCIDRs": [
      "10.244.0.0/24"
    ],
    "InternalIP": "10.0.1.239"
  },
  {
    "name": "kube-2-worker-01f438cf-579f9fd987-5l657",
    "podCIDR": "10.244.1.0/24",
    "podCIDRs": [
      "10.244.1.0/24"
    ],
    "InternalIP": "192.168.199.222"
  },
  {
    "name": "kube-2-worker-01f438cf-579f9fd987-5l657",
    "podCIDR": "10.244.1.0/24",
    "podCIDRs": [
      "10.244.1.0/24"
    ],
    "InternalIP": "10.0.4.73"
  }
]

4. Vamos substituir o podCIDR, fazendo alterações diretamente no etcd:

./etcdhelper -cacert /etc/kubernetes/pki/etcd/ca.crt -cert /etc/kubernetes/pki/etcd/server.crt -key /etc/kubernetes/pki/etcd/server.key -endpoint https://127.0.0.1:2379 change-pod-cidr 10.55.0.0/16

5. Vamos verificar se o podCIDR realmente mudou:

kubectl get no -o json | jq '[.items[] | {"name": .metadata.name, "podCIDR": .spec.podCIDR, "podCIDRs": .spec.podCIDRs, "InternalIP": (.status.addresses[] | select(.type == "InternalIP") | .address)}]'

[
  {
    "name": "kube-2-master",
    "podCIDR": "10.55.0.0/24",
    "podCIDRs": [
      "10.55.0.0/24"
    ],
    "InternalIP": "192.168.199.2"
  },
  {
    "name": "kube-2-master",
    "podCIDR": "10.55.0.0/24",
    "podCIDRs": [
      "10.55.0.0/24"
    ],
    "InternalIP": "10.0.1.239"
  },
  {
    "name": "kube-2-worker-01f438cf-579f9fd987-5l657",
    "podCIDR": "10.55.1.0/24",
    "podCIDRs": [
      "10.55.1.0/24"
    ],
    "InternalIP": "192.168.199.222"
  },
  {
    "name": "kube-2-worker-01f438cf-579f9fd987-5l657",
    "podCIDR": "10.55.1.0/24",
    "podCIDRs": [
      "10.55.1.0/24"
    ],
    "InternalIP": "10.0.4.73"
  }
]

6. Reinicialize todos os nós do cluster, um por um.

7. Se ao menos um nó mantiver o antigo podCIDR, o kube-controller-manager não conseguirá iniciar, e os pods no cluster não serão agendados.

Na verdade, a alteração do podCIDR pode ser feita de maneira mais simples (por exemplo, assim). Mas queríamos aprender a trabalhar com etcd diretamente, porque há situações em que editar objetos do Kubernetes no etcd é a única opção viável. (Por exemplo, não é possível simplesmente mudar o campo spec.clusterIP do Service sem downtime..)

Resultado

O artigo aborda a possibilidade de trabalhar com dados diretamente no etcd, ou seja, bypassando a API do Kubernetes. Às vezes, essa abordagem permite realizar "manobras inteligentes". As operações descritas no texto foram testadas em clusters K8s reais. No entanto, a sua prontidão para uso geral é PoC (prova de conceito). Portanto, se você quiser usar uma versão modificada da ferramenta etcdhelper em seus clusters, faça isso por sua conta e risco.

P.S.

Confira também em nosso blog:

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster