Operator Pattern em aplicações Kubernetes

Neste artigo você vai ver:

Se tem uma ferramenta que veio para otimizar processos é o Kubernetes (K8S). Neste artigo vamos te explicar o que é o Operator Pattern (ou padrão operador em português) do K8s, além de trazer um exemplo prático usando o framework Operator SDK, que facilita bastante a criação de um Operator.

Por que você precisa conhecer o Operator Pattern?

Utilizando o Operator Pattern podemos criar aplicações Kubernetes nativas ou operar aplicações complexas em um cluster K8S.

Inclusive, várias aplicações bem conhecidas no mercado utilizam o padrão operador, por exemplo:

Mais exemplos podem ser encontrados em OperatorHub.io. Tem muita coisa bacana!

Tá, mas o que é o Operator Pattern?

O Operator Pattern pode ser definido como:

Padrão controlador (Controller Pattern)

  • API de extensão de Kubernetes (CRDs)
  • foco em uma aplicação específica

Em outras palavras, um operador é uma aplicação que utiliza o padrão controlador, para controlar um CRD de uma aplicação específica.

Então para entender o que é um operador precisamos saber o que é o padrão controlador e os CRDs. Mas não se preocupe que é justamente o que vamos abordar a seguir.

Padrão Controlador (Controller Pattern)

O Padrão Controlador, ou Controller Pattern, é muito utilizado internamente no Kubernetes e consiste em um loop que:

  • Observa o estado atual.
  • Observa o estado desejado.
  • Se o estado atual for diferente do estado desejado, atua de forma a trazer o estado atual mais perto do estado desejado.

Custom Resources Definition (CRD)

Custom Resources Definition (CRD), ou em português “definição customizada de recurso”, é uma forma de estender a API do Kubernetes. Por padrão o K8S já possui diversos recursos: service, pod, deployment, etc.

Caso você queira criar o seu próprio recurso, é necessário criar uma definição customizada de recurso (CRD) que declare os campos que o seu recurso terá. Com isso, você pode utilizar a API do K8S para criar, monitorar e atualizar o seu recurso.

Como criar um Operador?

Você pode criar o operator em qualquer linguagem, pois ele nada mais é que uma aplicação que utiliza a API do Kubernetes. Porém, para facilitar a criação de operadores a Red Hat e a comunidade K8S criou o framework Operator SDK.

Utilizando o Operator SDK você pode criar um operator utilizando Golang, Ansible ou Helm. Se você escolher Ansible ou Golang pode criar um operator de nível 5, porém utilizando o Helm é possível apenas um operator nível 2.

Quer entender melhor sobre o que faz cada nível de operador? Então leia essa página da documentação do Operator SDK (em inglês). Na imagem a seguir temos um resumo também:

Exemplo prático de criação de um operador com o Operator SDK

Para exemplificar a utilização e a criação de um operador iremos utilizar o framework  Operator SDK para criar um operator que ao receber um CRD de pipeline irá criar uma POD, esperar a POD ficar pronta e salvar os logs no CRD.

1.Pré-requisitos:

  • git
  • go version 1.15
  • docker version 17.03+
  • make

Rodar o K8s Local

Escolher entre o kind ou k3d e usar o comando: Install operator-sdk

Para usuários Mac ou Linux, use este tutorial. Já para usuários windows, é preciso fazer o build do repositório

2.Criando a estrutura inicial do projeto

Para criar a estrutura inicial do projeto iremos utilizar o CLI do Operator SDK:

– criar uma pasta vazia

mkdir poc-operator-sdk
cd poc-operator-sdk

Para utilizar o operator-sdk init devemos fornecer:

–domain insira o domain dos groups

–repo insira o module do gomod

Exemplo:

operator-sdk init --domain example.com --repo github.com/viniciusCSreis/poc-operator-sdk

Após criar a estrutura inicial do projeto vamos adicionar um API, ou seja, um CRD:

operator-sdk create api --group pipeline --version v1alpha1 --kind Pipeline --resource --controller

3.Alterar o CRD gerado

Agora que o seu projeto foi gerado e já temos uma API, vamos alterar os valores do arquivo `api/v1alpha1/pipeline_types.go` onde iremos definir nosso CRD.

Adicione a struct PipelineEnvs antes da struct PipelineSpec:

type PipelineEnvs struct {
    //Name env name
    Name string `json:"name"`
    //Value env value
    Value string `json:"value"`
}

Altere PipelineSpec para:

// PipelineSpec defines the desired state of Pipeline
type PipelineSpec struct {
    //Envs to run pipeline
    Envs []PipelineEnvs `json:"envs"`
    //Timeout pipeline timeout in seconds
    Timeout int `json:"timeout"`
}

Altere PipelineStatus para:

// PipelineStatus defines the observed state of Pipeline
type PipelineStatus struct {
    // Phase pipeline phase: [pending, running, completed]
    Phase string `json:"phase"`
    // Logs logs of a finished pipeline
    Logs string `json:"logs"`
}

Gere os arquivos de config:

make generate manifests

Para verificar se realmente gerou os arquivos acesse: `config/crd/bases/pipeline.example.com_pipelines.yaml`.

Em seguida, verifique se spec.versions[0].schema.openAPIV3Schema.properties.spec possui as properties Envs eTimeout.

Depois verifique se spec.versions[0].schema.openAPIV3Schema.properties.status possui as properties Phase e Logs. 

Também é possível observar que o comentário acima da variável nas structs gera o campo description das properties no CRD.

4.Mudar o comportamento do operator

Agora que você já definiu o CRD precisamos mudar o comportamento do operator. Lembra que neste exemplo, depois da criação do CRD ele precisa criar uma POD, esperar a POD ficar pronta e salvar os logs no CRD.

Para mudar o comportamento do operator, basta mudar o arquivo: `controllers/pipeline_controller.go` para: 

Além de adicionar a implementação do controller, alteramos a função SetupWithManager onde definimos os eventos que chamaram a função Reconcile. Por padrão está configurado para “qualquer alteração no objeto pipeline chamar a função Reconcile”, mas como também iremos criar uma POD, alteramos essa função para ouvir também aos eventos das POD criadas pela pipeline:

Na implementação utilizamos objeto `v1.Pod`, logo precisamos adicionar a dependência:

 go get k8s.io/api@v0.22.1
 go mod tidy 

5.Gerar imagem Docker da POD

A ideia é criar uma POD, esperar a POD ficar pronta e salvar os logs no CRD. Porém, para criar uma POD precisamos definir qual a imagem Docker a POD vai rodar. Utilizando o kind ou k3d você pode fazer o build da imagem docker localmente e importar no cluster 

Criar Arquivo echo.Dockerfile:

 FROM alpine
 RUN apk add --no-cache bash
 ENTRYPOINT echo "Hello: $MSG_TO_PRINT" && sleep 3 

Importar no cluster:

k3d:

k3d image import generic-dockerimage:local

kind:

kind load docker-image generic-dockerimage:local

6.API Kubernetes nativa

Lembra que um dos nossos objetivos é salvar os logs no CRD? Porém, utilizando apenas o client do controller-runtime (client fornecido pelo 4operator-sdk) não é possível chamar alguns recursos da API do K8s. 

Portanto, para pegar os logs da POD chamaremos a API do K8S diretamente, no arquivo main.go na função main:

Vamos alterar as linhas do NewManager para:

 k8sRestConfig := ctrl.GetConfigOrDie()
 k8sClient := kubernetes.NewForConfigOrDie(k8sRestConfig)
 mgr, err := ctrl.NewManager(k8sRestConfig, ctrl.Options{ 

E passar esse k8sClient para o controller:

 if err = (&controllers.PipelineReconciler{
 Client:    mgr.GetClient(),
 Scheme:    mgr.GetScheme(),
 K8sClient: k8sClient,
 }) 

Com isso agora o controller pode pegar os logs da POD. No final o arquivo deve ficar assim: 

7.Rodar aplicação

Para rodar a aplicação utilize os comandos gerados pelo operator-sdk no makefile:

 make install
 make run 

7.1.Executar pipeline

Para executar a pipeline é necessário apenas criar um CRD. Logo podemos alterar o arquivo gerado pelo operator-sdk localizado em config/samples/pipeline_v1alpha1_pipeline.yaml para:

apiVersion: pipeline.example.com/v1alpha1
kind: Pipeline
metadata:
  name: pipeline-sample-3
spec:
  envs:
    - name: "MSG_TO_PRINT"
      value: "BLA_BLOW"
  timeout: 60

No final o arquivo deve ficar: 

Depois de alterar esse arquivo você pode utilizar o kubectl para fazer o apply. 

Em outro terminal execute:

kubectl apply -f config/samples/pipeline_v1alpha1_pipeline.yaml

Para verificar se executou com sucesso verifique os logs do operator ou verifique se a POD executou corretamente:

kubectl get pods
kubectl logs pipeline-sample-3

8.Criando testes para os controllers

Por padrão, o Operator SDK cria o arquivo controllers/suite_test.go. Esse arquivo tem a responsabilidade de testar os nossos controllers.

Para realizar o teste do controller o time do Operator SDK incentiva a utilização do envtest, que é um binário que simula um ambiente Kubernetes, em vez de criar mocks da API do k8s.

Para instalar o envtest é só utilizar o comando:

make envtest

Além disso, o time do Operator SDK incentiva a utilização do framework ginkgo para a criação de testes utilizando o BDD. Porém, para não deixar essa POC muito complexa, nos testes dos controller iremos utilizar a biblioteca padrão de test do Golang e o padrão de table test gerado automaticamente pela IDE Goland.

A implementação completa dos testes pode ser encontrada:

Uma sugestão é alterar o make test gerado automaticamente e adicionar a flag -v ao go test. Assim é possível visualizar os logs do operator ao rodar os testes. 

Para rodar todos os testes é só executar:

make test

9.Métricas

Por padrão, o Operator SDK gera uma main que exporta as urls: /healthz e /readyz que podemos utilizar como health check. Além disso, também é configurado o endpoint /metrics que pode ser utilizado para exportar metrics para o Prometheus.

A porta e as outras configurações de health check, além das metrics (caso seja necessário alterar alguma coisa) estão definidas na função main ao instanciar o manager por meio das variáveis metricsAddr e probeAddr:

mgr, err := ctrl.NewManager(k8sRestConfig, ctrl.Options{
 Scheme:                 scheme,
 MetricsBindAddress:     metricsAddr,
 Port:                   9443,
 HealthProbeBindAddress: probeAddr,
 LeaderElection:         enableLeaderElection,
 LeaderElectionID:       "7f0c45a6.example.com",
 })

Conclusão

Utilizando o framework do Operator SDK conseguimos rapidamente criar um operator utilizando padrões recomendados pela comunidade de Kubernetes, configurar um ambiente local de teste e experimentação e ainda gerar metrics automáticas por meio do /metrics.

Portanto, apesar do Operator Pattern ter a possibilidade de ser criado em qualquer linguagem, é uma boa ideia para o primeiro operator utilizar o framework operator-sdk.

E aí, o que achou do Operator Pattern e da nossa POC com o Operator SDK? Conta pra gente nos comentários!

Capa do artigo "Operator Pattern em aplicações Kubernetes" em que vemos as mãos de uma pessoa negra enquanto usa o mouse do computador.
Foto de Vinicius Sousa
Senior Software Developer
Desenvolvedor back-end com experiência com Java, Kotlin e Golang. Bacharel em sistema de informação pela Universidade Federal de Uberlândia. Github: https://github.com/viniciuscsreis

Este site utiliza cookies para proporcionar uma experiência de navegação melhor. Consulte nossa Política de Privacidade.