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!