Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

Este ano, a principal conferência europeia de Kubernetes - KubeCon + CloudNativeCon Europe 2020 - foi virtual. No entanto, tal mudança de formato não nos impediu de entregar o nosso relatório há muito planeado “Go? Bash! Conheça o operador Shell” dedicado ao nosso projeto Open Source operador shell.

Este artigo, inspirado na palestra, apresenta uma abordagem para simplificar o processo de criação de operadores para Kubernetes e mostra como você pode criar o seu próprio com o mínimo de esforço usando um operador shell.

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

Apresentando vídeo da reportagem (~23 minutos em inglês, visivelmente mais informativo que o artigo) e o trecho principal dele em forma de texto. Ir!

Na Flant otimizamos e automatizamos tudo constantemente. Hoje falaremos sobre outro conceito interessante. Encontrar: script de shell nativo da nuvem!

Porém, comecemos pelo contexto em que tudo isso acontece: Kubernetes.

API e controladores do Kubernetes

A API no Kubernetes pode ser representada como uma espécie de servidor de arquivos com diretórios para cada tipo de objeto. Os objetos (recursos) neste servidor são representados por arquivos YAML. Além disso, o servidor possui uma API básica que permite fazer três coisas:

  • receber recurso por tipo e nome;
  • mudar recurso (neste caso, o servidor armazena apenas objetos “corretos” - todos os formados incorretamente ou destinados a outros diretórios são descartados);
  • seguir para o recurso (neste caso, o usuário recebe imediatamente sua versão atual/atualizada).

Assim, o Kubernetes atua como uma espécie de servidor de arquivos (para manifestos YAML) com três métodos básicos (sim, na verdade existem outros, mas vamos omiti-los por enquanto).

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

O problema é que o servidor só pode armazenar informações. Para fazer funcionar você precisa controlador - o segundo conceito mais importante e fundamental no mundo do Kubernetes.

Existem dois tipos principais de controladores. O primeiro pega informações do Kubernetes, processa-as de acordo com a lógica aninhada e as retorna ao K8s. O segundo obtém informações do Kubernetes, mas, diferentemente do primeiro tipo, altera o estado de alguns recursos externos.

Vamos dar uma olhada mais de perto no processo de criação de uma implantação no Kubernetes:

  • Controlador de implantação (incluído em kube-controller-manager) recebe informações sobre a implantação e cria um ReplicaSet.
  • O ReplicaSet cria duas réplicas (dois pods) com base nessas informações, mas esses pods ainda não estão agendados.
  • O agendador agenda pods e adiciona informações do nó aos seus YAMLs.
  • Kubelets fazem alterações em um recurso externo (digamos Docker).

Então toda essa sequência é repetida na ordem inversa: o kubelet verifica os contêineres, calcula o status do pod e o envia de volta. O controlador ReplicaSet recebe o status e atualiza o estado do conjunto de réplicas. A mesma coisa acontece com o Deployment Controller e o usuário finalmente obtém o status atualizado (atual).

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

Operador Shell

Acontece que o Kubernetes é baseado no trabalho conjunto de vários controladores (os operadores do Kubernetes também são controladores). Surge a pergunta: como criar sua própria operadora com o mínimo esforço? E aqui aquele que desenvolvemos vem em socorro operador shell. Ele permite que administradores de sistema criem suas próprias declarações usando métodos familiares.

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

Exemplo simples: copiar segredos

Vejamos um exemplo simples.

Digamos que temos um cluster Kubernetes. Tem um espaço para nome default com algum segredo mysecret. Além disso, existem outros namespaces no cluster. Alguns deles possuem uma etiqueta específica anexada a eles. Nosso objetivo é copiar o Secret em namespaces com um rótulo.

A tarefa é complicada pelo fato de que novos namespaces podem aparecer no cluster, e alguns deles podem ter esse rótulo. Por outro lado, quando o rótulo é excluído, o Secret também deve ser excluído. Além disso, o próprio Secret também pode mudar: neste caso, o novo Secret deve ser copiado para todos os namespaces com rótulos. Se o segredo for excluído acidentalmente de qualquer namespace, nosso operador deverá restaurá-lo imediatamente.

Agora que a tarefa foi formulada, é hora de começar a implementá-la usando o operador shell. Mas primeiro vale a pena dizer algumas palavras sobre o próprio operador shell.

Como funciona o operador shell

Como outras cargas de trabalho no Kubernetes, o operador shell é executado em seu próprio pod. Neste pod no diretório /hooks arquivos executáveis ​​são armazenados. Podem ser scripts em Bash, Python, Ruby, etc. Chamamos esses arquivos executáveis ​​de ganchos (ganchos).

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

O operador Shell assina eventos do Kubernetes e executa esses ganchos em resposta aos eventos de que precisamos.

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

Como o operador shell sabe qual gancho executar e quando? A questão é que todo gancho tem dois estágios. Durante a inicialização, o operador shell executa todos os ganchos com um argumento --config Este é o estágio de configuração. E depois disso, os ganchos são lançados da maneira normal - em resposta aos eventos aos quais estão anexados. Neste último caso, o gancho recebe o contexto de ligação (contexto vinculativo) - dados no formato JSON, dos quais falaremos com mais detalhes a seguir.

Fazendo um operador no Bash

Agora estamos prontos para implementação. Para fazer isso, precisamos escrever duas funções (a propósito, recomendamos a biblioteca shell_lib, o que simplifica bastante a escrita de ganchos no Bash):

  • o primeiro é necessário para o estágio de configuração - exibe o contexto de ligação;
  • o segundo contém a lógica principal do gancho.

#!/bin/bash

source /shell_lib.sh

function __config__() {
  cat << EOF
    configVersion: v1
    # BINDING CONFIGURATION
EOF
}

function __main__() {
  # THE LOGIC
}

hook::run "$@"

O próximo passo é decidir quais objetos precisamos. No nosso caso, precisamos rastrear:

  • segredo de origem para alterações;
  • todos os namespaces no cluster, para que você saiba quais deles possuem um rótulo anexado;
  • segredos de destino para garantir que todos estejam sincronizados com o segredo de origem.

Assine a fonte secreta

A configuração de ligação é bastante simples. Indicamos que estamos interessados ​​em Secret com o nome mysecret no espaço para nome default:

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

function __config__() {
  cat << EOF
    configVersion: v1
    kubernetes:
    - name: src_secret
      apiVersion: v1
      kind: Secret
      nameSelector:
        matchNames:
        - mysecret
      namespace:
        nameSelector:
          matchNames: ["default"]
      group: main
EOF

Como resultado, o gancho será acionado quando o segredo de origem for alterado (src_secret) e receba o seguinte contexto de ligação:

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

Como você pode ver, ele contém o nome e o objeto inteiro.

Acompanhando namespaces

Agora você precisa se inscrever em namespaces. Para fazer isso, especificamos a seguinte configuração de ligação:

- name: namespaces
  group: main
  apiVersion: v1
  kind: Namespace
  jqFilter: |
    {
      namespace: .metadata.name,
      hasLabel: (
       .metadata.labels // {} |  
         contains({"secret": "yes"})
      )
    }
  group: main
  keepFullObjectsInMemory: false

Como você pode ver, um novo campo apareceu na configuração com o nome jqFiltro. Como o próprio nome sugere, jqFilter filtra todas as informações desnecessárias e cria um novo objeto JSON com os campos que nos interessam. Um gancho com configuração semelhante receberá o seguinte contexto de ligação:

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

Ele contém uma matriz filterResults para cada namespace no cluster. Variável booleana hasLabel indica se um rótulo está anexado a um determinado namespace. Seletor keepFullObjectsInMemory: false indica que não há necessidade de manter objetos completos na memória.

Rastreando segredos de destino

Assinamos todos os segredos que possuem uma anotação especificada managed-secret: "yes" (estes são o nosso alvo dst_secrets):

- name: dst_secrets
  apiVersion: v1
  kind: Secret
  labelSelector:
    matchLabels:
      managed-secret: "yes"
  jqFilter: |
    {
      "namespace":
        .metadata.namespace,
      "resourceVersion":
        .metadata.annotations.resourceVersion
    }
  group: main
  keepFullObjectsInMemory: false

Neste caso jqFilter filtra todas as informações, exceto o namespace e o parâmetro resourceVersion. O último parâmetro foi passado para a anotação na criação do segredo: permite comparar versões de segredos e mantê-los atualizados.

Um gancho configurado desta forma irá, quando executado, receber os três contextos de ligação descritos acima. Eles podem ser pensados ​​como uma espécie de instantâneo (instantâneo) conjunto.

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

Com base em todas essas informações, um algoritmo básico pode ser desenvolvido. Ele itera em todos os namespaces e:

  • se hasLabel assuntos true para o namespace atual:
    • compara o segredo global com o local:
      • se forem iguais, não faz nada;
      • se eles diferirem - executa kubectl replace ou create;
  • se hasLabel assuntos false para o namespace atual:
    • garante que Secret não esteja no namespace fornecido:
      • se o segredo local estiver presente, exclua-o usando kubectl delete;
      • se o segredo local não for detectado, não fará nada.

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

Implementação do algoritmo em Bash você pode baixar em nosso repositórios com exemplos.

Foi assim que conseguimos criar um controlador Kubernetes simples usando 35 linhas de configuração YAML e aproximadamente a mesma quantidade de código Bash! O trabalho do operador shell é conectá-los.

No entanto, copiar segredos não é a única área de aplicação do utilitário. Aqui estão mais alguns exemplos para mostrar do que ele é capaz.

Exemplo 1: Fazendo alterações no ConfigMap

Vejamos uma implantação que consiste em três pods. Os pods usam o ConfigMap para armazenar algumas configurações. Quando os pods foram iniciados, o ConfigMap estava em um determinado estado (vamos chamá-lo de v.1). Conseqüentemente, todos os pods usam esta versão específica do ConfigMap.

Agora vamos supor que o ConfigMap mudou (v.2). No entanto, os pods usarão a versão anterior do ConfigMap (v.1):

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

Como posso fazer com que eles mudem para o novo ConfigMap (v.2)? A resposta é simples: use um modelo. Vamos adicionar uma anotação de soma de verificação à seção template Configurações de implantação:

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

Como resultado, esse checksum será registrado em todos os pods e será igual ao do Deployment. Agora você só precisa atualizar a anotação quando o ConfigMap for alterado. E o operador shell é útil neste caso. Tudo que você precisa fazer é programar um gancho que assinará o ConfigMap e atualizará a soma de verificação.

Se o usuário fizer alterações no ConfigMap, o operador shell irá notá-las e recalcular a soma de verificação. Depois disso, a magia do Kubernetes entrará em ação: o orquestrador matará o pod, criará um novo, esperará que ele se torne Readye passa para o próximo. Como resultado, o Deployment sincronizará e mudará para a nova versão do ConfigMap.

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

Exemplo 2: Trabalhando com Definições de Recursos Personalizados

Como você sabe, o Kubernetes permite criar tipos personalizados de objetos. Por exemplo, você pode criar um tipo MysqlDatabase. Digamos que esse tipo tenha dois parâmetros de metadados: name и namespace.

apiVersion: example.com/v1alpha1
kind: MysqlDatabase
metadata:
  name: foo
  namespace: bar

Temos um cluster Kubernetes com diferentes namespaces nos quais podemos criar bancos de dados MySQL. Neste caso, o operador shell pode ser usado para rastrear recursos MysqlDatabase, conectando-os ao servidor MySQL e sincronizando os estados desejados e observados do cluster.

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

Exemplo 3: Monitoramento de Rede de Cluster

Como você sabe, usar ping é a maneira mais simples de monitorar uma rede. Neste exemplo mostraremos como implementar esse monitoramento usando o operador shell.

Primeiro de tudo, você precisará assinar os nós. O operador shell precisa do nome e endereço IP de cada nó. Com a ajuda deles, ele fará ping nesses nós.

configVersion: v1
kubernetes:
- name: nodes
  apiVersion: v1
  kind: Node
  jqFilter: |
    {
      name: .metadata.name,
      ip: (
       .status.addresses[] |  
        select(.type == "InternalIP") |
        .address
      )
    }
  group: main
  keepFullObjectsInMemory: false
  executeHookOnEvent: []
schedule:
- name: every_minute
  group: main
  crontab: "* * * * *"

Parâmetro executeHookOnEvent: [] impede que o gancho seja executado em resposta a qualquer evento (ou seja, em resposta à alteração, adição ou exclusão de nós). No entanto, ele correrá (e atualize a lista de nós) agendado - a cada minuto, conforme prescrito pelo campo schedule.

Agora surge a questão: como exatamente sabemos sobre problemas como perda de pacotes? Vamos dar uma olhada no código:

function __main__() {
  for i in $(seq 0 "$(context::jq -r '(.snapshots.nodes | length) - 1')"); do
    node_name="$(context::jq -r '.snapshots.nodes['"$i"'].filterResult.name')"
    node_ip="$(context::jq -r '.snapshots.nodes['"$i"'].filterResult.ip')"
    packets_lost=0
    if ! ping -c 1 "$node_ip" -t 1 ; then
      packets_lost=1
    fi
    cat >> "$METRICS_PATH" <<END
      {
        "name": "node_packets_lost",
        "add": $packets_lost,
        "labels": {
          "node": "$node_name"
        }
      }
END
  done
}

Iteramos pela lista de nós, obtemos seus nomes e endereços IP, executamos ping neles e enviamos os resultados para o Prometheus. Operador Shell pode exportar métricas para Prometheus, salvando-os em um arquivo localizado de acordo com o caminho especificado na variável de ambiente $METRICS_PATH.

Aqui tão você pode criar um operador para monitoramento de rede simples em um cluster.

Mecanismo de fila

Este artigo estaria incompleto sem descrever outro mecanismo importante incorporado ao operador shell. Imagine que ele executa algum tipo de gancho em resposta a um evento no cluster.

  • O que acontece se, ao mesmo tempo, algo acontecer no cluster? mais uma coisa evento?
  • O operador shell executará outra instância do gancho?
  • E se, digamos, cinco eventos acontecerem no cluster ao mesmo tempo?
  • O operador shell os processará em paralelo?
  • E quanto aos recursos consumidos, como memória e CPU?

Felizmente, o operador shell possui um mecanismo de enfileiramento integrado. Todos os eventos são enfileirados e processados ​​sequencialmente.

Vamos ilustrar isso com exemplos. Digamos que temos dois ganchos. O primeiro evento vai para o primeiro gancho. Assim que o processamento for concluído, a fila avança. Os próximos três eventos são redirecionados para o segundo gancho - eles são removidos da fila e inseridos em um “pacote”. Aquilo é hook recebe uma série de eventos — ou, mais precisamente, uma série de contextos vinculativos.

Também estes eventos podem ser combinados em um grande. O parâmetro é responsável por isso group na configuração de ligação.

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

Você pode criar qualquer número de filas/ganchos e suas diversas combinações. Por exemplo, uma fila pode funcionar com dois ganchos ou vice-versa.

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

Tudo que você precisa fazer é configurar o campo de acordo queue na configuração de ligação. Se um nome de fila não for especificado, o gancho será executado na fila padrão (default). Este mecanismo de enfileiramento permite resolver completamente todos os problemas de gerenciamento de recursos ao trabalhar com ganchos.

Conclusão

Explicamos o que é um operador shell, mostramos como ele pode ser usado para criar operadores Kubernetes de maneira rápida e fácil e demos vários exemplos de seu uso.

Informações detalhadas sobre o operador shell, bem como um rápido tutorial sobre como usá-lo, estão disponíveis no arquivo correspondente. repositórios no GitHub. Não hesite em nos contatar com dúvidas: você pode discuti-las em um especial Grupo de telegramas (em russo) ou em este fórum (em inglês)

E se você gostou, ficamos sempre felizes em ver novos assuntos/PR/estrelas no GitHub, onde, aliás, você pode encontrar outros projetos interessantes. Dentre eles vale destacar operador de complemento, que é o irmão mais velho do operador shell. Este utilitário usa gráficos Helm para instalar complementos, pode fornecer atualizações e monitorar vários parâmetros/valores de gráficos, controla o processo de instalação de gráficos e também pode modificá-los em resposta a eventos no cluster.

Ir? Bash! Conheça o operador shell (revisão e reportagem em vídeo da KubeCon EU'2020)

Vídeos e slides

Vídeo da performance (~23 minutos):


Apresentação do relatório:

PS

Leia também em nosso blog:

Fonte: habr.com

Adicionar um comentário