Programacao

Kubernetes Operators de uma maneira bem fácil de entender

Um operator Kubernetes (k8s) é uma extensão do Kubernetes que automatiza e gerencia recursos personalizados do cluster. Ele é composto pelos seguintes recursos:        

  1. Custom Resource Definition (CRD): O CRD define um novo tipo de recurso personalizado que será gerenciado pelo operador. Ele descreve a estrutura de dados, campos e propriedades do recurso.
  2. Custom Resource (CR): Este é um exemplo de um recurso personalizado que foi criado com base no CRD. Os usuários do Kubernetes interagem com CRs da mesma forma que interagem com recursos nativos do Kubernetes, como pods e serviços.
  3. Controlador (Controller): O controlador é a lógica principal do operador. Ele monitora os recursos personalizados e garante que o estado desejado seja mantido, tomando ações conforme necessário. Controladores são geralmente implementados como um processo em execução fora do cluster (um pod) que se comunica com a API do Kubernetes.
  4. RBAC (Role-Based Access Control): Para que o controlador possa interagir com a API do Kubernetes e gerenciar os recursos personalizados, ele precisa de permissões apropriadas. As permissões são concedidas através de recursos RBAC, como Role, ClusterRole, RoleBinding e ClusterRoleBinding.

   Então, um operador Kubernetes é composto principalmente por um CRD, CRs, um controlador e RBAC para permitir que o controlador gerencie os recursos personalizados no cluster.  

Criar e implantar um operador Kubernetes  

     

   Para criar e implantar um operador Kubernetes, siga estas etapas:  

  1. Planejamento e design: Antes de começar a desenvolver um operador, identifique os recursos e a funcionalidade que você deseja automatizar. Isso inclui a definição dos recursos personalizados e as operações que o controlador executará para garantir o estado desejado.
  2. Desenvolvimento do CRD: Desenvolva o Custom Resource Definition que define o tipo de recurso personalizado, incluindo sua estrutura de dados e campos. Esse arquivo YAML é usado para registrar o recurso personalizado no Kubernetes.
  3. Implementação do controlador: Desenvolva o controlador, que será responsável por monitorar os recursos personalizados e tomar as ações necessárias para manter o estado desejado. O controlador geralmente é escrito em uma linguagem de programação como Go, Python ou Java e se comunica com a API do Kubernetes para criar, atualizar ou excluir recursos.
  4. Configuração do RBAC: Defina as permissões necessárias para que o controlador possa interagir com a API do Kubernetes e gerenciar os recursos personalizados. Crie e configure os recursos RBAC, como Role, ClusterRole, RoleBinding e ClusterRoleBinding.
  5. Empacotamento e implantação: Empacote o controlador e seus arquivos de configuração em um container e crie um Deployment ou um StatefulSet para implantá-lo no cluster Kubernetes. Além disso, aplique o CRD e os recursos RBAC no cluster.
  6. Teste e monitoramento: Teste o operador para garantir que ele esteja funcionando corretamente e monitore o desempenho do controlador. Use ferramentas de monitoramento e logging para acompanhar os eventos e garantir que o operador esteja operando conforme o esperado.
  7. Manutenção e atualização: À medida que o operador é usado, você pode identificar melhorias e correções de bugs necessárias. Atualize o CRD, o controlador e os recursos RBAC conforme necessário e implante as novas versões no cluster.

   Os operadores Kubernetes são ferramentas poderosas para estender a funcionalidade do Kubernetes e automatizar o gerenciamento de recursos e aplicativos complexos. Ao seguir estas etapas, você pode desenvolver e implantar com sucesso um operador Kubernetes para atender às suas necessidades específicas.  

     

Criando um Operator de Exemplo em Go  

     

   Criar um operador Kubernetes completo exige bastante código e planejamento. No entanto, posso te mostrar um exemplo simples de um operador em Go usando a biblioteca "Operator SDK" da Red Hat. Para simplificar, vamos criar um operador que gerencia um recurso personalizado chamado "HelloWorld".  

     

   Primeiro, certifique-se de que você possui o Operator SDK instalado. Siga as instruções de instalação em: https://sdk.operatorframework.io/docs/installation/  

     

   Agora, crie um novo projeto de operador:  

   operator-sdk init --domain example.com --repo github.com/your-username/helloworld-operator      

   

Em seguida, crie um novo API e controlador para o recurso personalizado HelloWorld:  

   cd helloworld-operator
operator-sdk create api --group hello --version v1 --kind HelloWorld --resource --controller      

   Isso criará os arquivos básicos para nosso CRD e controlador.  

     

   Abra o arquivo api/v1/helloworld_types.go e defina a estrutura HelloWorld:  

   package v1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// HelloWorldSpec define o estado desejado de HelloWorld
type HelloWorldSpec struct {
Message string `json:"message,omitempty"`
}

// HelloWorldStatus define o estado observado de HelloWorld
type HelloWorldStatus struct {
// INSERIR CAMPO DE STATUS ADICIONAL - definir o estado observado do cluster
// Importante: execute "make" para regenerar o código após modificar este arquivo
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// HelloWorld é o esquema para a API helloworlds
type HelloWorld struct {
metav1.TypeMeta   `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec   HelloWorldSpec   `json:"spec,omitempty"`
Status HelloWorldStatus `json:"status,omitempty"`
}

// +kubebuilder:object:root=true

// HelloWorldList contém uma lista de HelloWorld
type HelloWorldList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items           []HelloWorld `json:"items"`
}    
 

Execute make generate e make manifests para gerar os arquivos CRD e DeepCopy.  

     

   Agora, vamos adicionar a lógica do controlador. Abra o arquivo controllers/helloworld_controller.go e adicione o seguinte código no método Reconcile:  

   package controllers

import (
"context"
"fmt"

"github.com/go-logr/logr"
hellov1 "github.com/your-username/helloworld-operator/api/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)

// HelloWorldReconciler reconcilia um objeto HelloWorld
type HelloWorldReconciler struct {
client.Client
Log logr.Logger
}

//+kubebuilder:rbac:groups=hello.example.com,resources=helloworlds,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=hello.example.com,resources=helloworlds/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=hello.example.com,resources=helloworlds/finalizers,verbs=update

func (r *HelloWorldReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("helloworld", req.NamespacedName)

//Obtenha o recurso HelloWorld
var hw hellov1.HelloWorld
if err := r.Get(ctx, req.NamespacedName, &hw); err != nil {
 log.Error(err, "unable to fetch HelloWorld")
 return ctrl.Result{}, client.IgnoreNotFound(err) }

// Imprimir mensagem HelloWorld
fmt.Printf("HelloWorld message: %s\n", hw.Spec.Message)

// Atualize o status HelloWorld
hw.Status.Message = fmt.Sprintf("HelloWorld message: %s", hw.Spec.Message)
if err := r.Status().Update(ctx, &hw); err != nil {
 log.Error(err, "unable to update HelloWorld status")
 return ctrl.Result{}, err}return ctrl.Result{}, nil

// SetupWithManager configura o controlador com o Manager.
func (r *HelloWorldReconciler) SetupWithManager(mgr ctrl.Manager) error {
 return ctrl.NewControllerManagedBy(mgr)
 For(&hellov1.HelloWorld{})
  Complete(r) }}      

   Aqui, nós modificamos o método `Reconcile` para buscar o recurso HelloWorld, imprimir a mensagem especificada e atualizar o status do recurso HelloWorld com a mensagem.  

   Agora, compile o operador e gere o arquivo de imagem do Docker:  

   make docker-build docker-push IMG=<your-docker-image>      

   Substitua <your-docker-image> pelo caminho do seu repositório de imagens Docker.  

     

   Implante o operador no seu cluster Kubernetes:  

   make deploy IMG=<your-docker-image>      

   Crie um exemplo de recurso HelloWorld:  

   apiVersion: hello.example.com/v1
kind: HelloWorld
metadata:
 name: helloworld-sample
spec:
 message: "Olá, mundo!"      

   Salve-o como helloworld-sample.yaml e aplique no cluster:  

   kubectl apply -f helloworld-sample.yaml      

 

Agora você deve ver o operador imprimir a mensagem "Hello, Word!" no log do pod do controlador. Você também pode verificar o status do recurso HelloWorld:  

   kubectl get helloworlds.hello.example.com helloworld-sample -o jsonpath='{.status.message}'      

   

Este exemplo é bastante simples e apenas ilustra a estrutura básica de um operador em Go. Um operador real pode envolver a criação e gerenciamento de outros recursos do Kubernetes, como Deployments e Services, para automatizar o gerenciamento de aplicativos complexos.  

     

Aprofundando e gerenciando Deployments e Services  

     

   Para expandir o exemplo anterior, você pode começar a explorar o gerenciamento de recursos adicionais do Kubernetes, como Deployments e Services, dentro do seu operador HelloWorld. Vamos adicionar a criação de um Deployment e um Service sempre que um novo recurso HelloWorld for criado ou atualizado.  

     

   Primeiro, adicione as permissões necessárias no arquivo config/rbac/role.yaml. Adicione o seguinte:  

   - apiGroups:
 - apps
 resources:
 - deployments
 verbs:
 - get
 - list
 - watch
 - create
 - update
 - patch
 - delete
- apiGroups:
 - ""
 resources:
 - services
 verbs:
 - get
 - list
 - watch
 - create
 - update
 - patch
 - delete      

   Agora, vamos adicionar a lógica de criação e atualização de Deployment e Service no controlador. Modifique o arquivo controllers/helloworld_controller.go:  

   import (
   // ...
   appsv1 "k8s.io/api/apps/v1"
   corev1 "k8s.io/api/core/v1"
   "k8s.io/apimachinery/pkg/api/errors"
   metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
   "k8s.io/apimachinery/pkg/types"
)

// ...

func (r *HelloWorldReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
   // ...

   // Verifique se o Deployment já existe, caso contrário crie um novo
   foundDeployment := &appsv1.Deployment{}
   err := r.Get(ctx, types.NamespacedName{Name: hw.Name, Namespace: hw.Namespace}, foundDeployment)
   if err != nil && errors.IsNotFound(err) {
       // Definir uma nova implantação
       dep := r.deploymentForHelloWorld(&hw)
       log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
       err = r.Create(ctx, dep)
       if err != nil {
           log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
           return ctrl.Result{}, err
       }
   } else if err != nil {
       log.Error(err, "Failed to get Deployment")
       return ctrl.Result{}, err
   }

   // Verifique se o serviço já existe, caso contrário crie um novo
   foundService := &corev1.Service{}
   err = r.Get(ctx, types.NamespacedName{Name: hw.Name, Namespace: hw.Namespace}, foundService)
   if err != nil && errors.IsNotFound(err) {
       // Definir um novo serviço
       svc := r.serviceForHelloWorld(&hw)
       log.Info("Creating a new Service", "Service.Namespace", svc.Namespace, "Service.Name", svc.Name)
       err = r.Create(ctx, svc)
       if err != nil {
           log.Error(err, "Failed to create new Service", "Service.Namespace", svc.Namespace, "Service.Name", svc.Name)
           return ctrl.Result{}, err
       }
   } else if err != nil {
       log.Error(err, "Failed to get Service")
       return ctrl.Result{}, err
   }

   // ...

   return ctrl.Result{}, nil
}

func (r *HelloWorldReconciler) deploymentForHelloWorld(hw *hellov1.HelloWorld) *appsv1.Deployment {
   // Defina o Deployment desejado para HelloWorld
}

func (r *HelloWorldReconciler) serviceForHelloWorld(hw *hellov1.HelloWorld) *corev1.Service {
// Defina o Serviço desejado para HelloWorld }      
   Agora, implemente as funções `deploymentForHelloWorld` e `serviceForHelloWorld` para criar os recursos necessários:  
   func (r *HelloWorldReconciler) deploymentForHelloWorld(hw *hellov1.HelloWorld) *appsv1.Deployment {
   labels := map[string]string{
       "app": hw.Name,
   }
   replicas := int32(1)

   dep := &appsv1.Deployment{
       ObjectMeta: metav1.ObjectMeta{
           Name:      hw.Name,
           Namespace: hw.Namespace,
       },
       Spec: appsv1.DeploymentSpec{
           Replicas: &replicas,
           Selector: &metav1.LabelSelector{
               MatchLabels: labels,
           },
           Template: corev1.PodTemplateSpec{
               ObjectMeta: metav1.ObjectMeta{
                   Labels: labels,
               },
               Spec: corev1.PodSpec{
                   Containers: []corev1.Container{{
                       Name:  "hello-world",
                       Image: "busybox",
                       Args: []string{
                           "/bin/sh",
                           "-c",
                           fmt.Sprintf("echo '%s' && sleep 3600", hw.Spec.Message),
                       },
                   }},
               },
           },
       },
   }

   ctrl.SetControllerReference(hw, dep, r.Scheme)
   return dep
}

func (r *HelloWorldReconciler) serviceForHelloWorld(hw *hellov1.HelloWorld) *corev1.Service {
   labels := map[string]string{
       "app": hw.Name,
   }

   svc := &corev1.Service{
       ObjectMeta: metav1.ObjectMeta{
           Name:      hw.Name,
           Namespace: hw.Namespace,
       },
       Spec: corev1.ServiceSpec{
           Selector: labels,
           Ports: []corev1.ServicePort{{
               Port: 80,
               TargetPort: intstr.FromInt(8080),
           }},
           Type: corev1.ServiceTypeClusterIP,
       },
   }

   ctrl.SetControllerReference(hw, svc, r.Scheme)
   return svc
}      

   A função deploymentForHelloWorld cria um Deployment que executa um contêiner BusyBox e imprime a mensagem especificada no recurso HelloWorld. A função serviceForHelloWorld cria um Service do tipo ClusterIP para expor o contêiner no cluster.  

     

   Recompile o operador, gere a imagem do Docker e implante o operador novamente, conforme descrito anteriormente.  

     

   Agora, quando você criar ou atualizar um recurso HelloWorld, o operador criará ou atualizará automaticamente um Deployment e um Service associados.  

     

   Lembre-se de que este é apenas um exemplo simples para ilustrar como um operador Kubernetes pode gerenciar recursos adicionais. Operators reais podem gerenciar muitos recursos diferentes e aplicar lógica complexa para gerenciar o estado desejado dos aplicativos.