15 de novembro de 2022

Automatizando deploys no Kubernetes com Operators em Quarkus!

Usando Operators você pode automatizar a criação de recursos e o deploy de aplicações no Kubernetes, e o Quarkus Operator SDK te permite criar Operators de forma bem simplificada, ajudando inclusive na criação dos Custom Resources!

O que são Operators?

Bom, se você está aqui provavelmente já conhece Kubernetes e muitos de seus recursos, mas talvez ainda não conheça o conceito de Operators.

Operators podem ser considerados extensões para o Kubernetes. Sua função é basicamente realizar tarefas que você poderia fazer manualmente com o kubectl, mas que você deseja fazer de forma automática. Eles fazem isso através de um ou mais Controllers que monitoram Custom Resources e se comunicam com a API do Kubernetes para realizar as mudanças necessárias.

Bom, mas para você entender essa explicação, você precisa saber o que são os Controllers e o que são os Custom Resources.

Controllers

Os Controllers são aplicações que aplicam padrão de control loop para garantir que o estado atual do Kubernetes é o estado desejado. Neste mundo de controllers é bem comum ouvir os termos em inglês: desired state (estado desejado) e current state (estado atual).

O Kubernetes tem vários Controllers embutidos, como o Deployment Controller, que é o responsável por monitorar o resource chamado Deployment. Esse controller é o que garante que, por exemplo, quando você faz o apply de um recurso do tipo Deployment, um ReplicaSet será criado, para finalmente se materializar em um ou mais Pods.

O Deployment Controller internamente contém um control loop, que é basicamente uma forma de dizer que há um loop infinito que lê as configurações do deployment (estado desejado) e aplica no ReplicaSet (estado atual). Há também o ReplicaSet Controller, que irá fazer o mesmo: ler as configurações do ReplicaSet (estado desejado) e criar ou remover Pods conforme necessário.

Esses mecanismos geralmente são chamados de reconcile loop ou loop de reconciliação, e é por conta deles que algumas alterações que você faz diretamente em recursos do Kubernetes podem ser sobrescritas. Por exemplo, se você remover um Pod manualmente, o ReplicaSet Controller irá perceber que o estado desejado está diferente do estado atual, e irá criar um novo Pod.

Custom Resources

No Kubernetes os objetos que podem ser criados são chamados de recursos ou resources. Um deles é o Deployment, que já foi citado anteriormente. Se você já usa Kubernetes há um tempo, irá conhecer vários outros: Services, Volume, ConfigMap, Secret, etc. Vários recursos do Kubernetes funcionam da mesma forma: há um controller que os monita e ajusta o estado do cluster conforme necessário.

Um Custom Resource (CR) é simplesmente um recurso que você mesmo irá criar. Veja, se você quer escrever um Operator para ler um estado desejado e realizar alterações no estado atual, esse Operator precisa ler o estado desejado de algum lugar. Assim como o Deployment Controller lê o estado desejado através do recurso Deployment, o seu Operator, que irá conter um ou mais Controllers, irá ler o estado desejado através de um recurso que você mesmo irá definir.

CRs são definidos usando um CustomResourceDefinition (CRD). Um CRD está para um CR assim como um XSD está para um XML, ou um JSON Schema está para um JSON. Ele define os dados que deverão estar contidos no CR que seu Operator vai monitorar.

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: crontabs.stable.example.com
spec:
  group: stable.example.com
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                cronSpec:
                  type: string
                image:
                  type: string
                replicas:
                  type: integer
  scope: Namespaced
  names:
    plural: crontabs
    singular: crontab
    kind: CronTab
    shortNames:
    - ct

O exemplo acima foi retirado da documentação oficial do Kubernetes: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/

Quarkus Operator SDK

Bom, então agora você já sabe que para criar um Operator precisa definir pelo menos um CRD e codificar um Controller que irá monitorá-lo, e aí começa a mágica do Quarkus Operator SDK, que facilita sua vida para criar tudo isso. Vamos aos exemplos.

Irei apresentar o exemplo de um Operator simples, que apenas faz o deploy de uma imagem docker qualquer e expõe essa aplicação para o mundo. Para fazer isso o Operator vai criar 3 recursos do Kubernetes: um Deployment, um Service e um Ingress.

Definindo o CustomResourceDefinition

Vamos começar pela definição do CRD, que é aquilo que vamos solicitar de parâmetro de entrada do usuário que for usar nosso Operator. Aqui já começa a mágica. Como estamos usando o Quarkus Operator SDK, podemos declarar nosso CRD como uma classe Java! No final, quando fizermos a build, a SDK irá gerar os CRDs para serem aplicados ao Kubernetes.

Iremos chamar nosso recurso de Aplicacao. Todo recurso do Kubernetes deve ter um spec e um status. Vamos começar definindo o spec. Nesse exemplo eu quero pedir apenas que o usuário informe a imagem a ser usada, e algumas variáveis de ambiente que ele queira usar, então o que precisamos é apenas de um POJO com dois atributos.

public class AplicacaoSpec {

    private String imagem;
    private Map<String, String> variaveisDeAmbiente;

    // getters e setters omitidos

}

No status eu quero apenas armazenar a URL onde a aplicação foi disponibilizada, uma mensagem de status e uma flag.

public class AplicacaoStatus {

    private String url;
    private String mensagem;
    private boolean pronta = false;

    public AplicacaoStatus() {
        mensagem = "processando";
    }

    public AplicacaoStatus(String url) {
        this.mensagem = "pronta";
        this.url = url;
        pronta = true;
    }

    // getters e setters omitidos

}

Depois precisamos apenas juntar o spec e o status em uma classe que represente o recurso completo.

@Version("v1alpha1")
@Group("rinaldo.dev")
public class Aplicacao extends CustomResource<AplicacaoSpec, AplicacaoStatus> implements Namespaced {

    @Override
    protected AplicacaoSpec initSpec() {
        return new AplicacaoSpec();
    }

    @Override
    protected AplicacaoStatus initStatus() {
        return new AplicacaoStatus();
    }
}

Após a definição dos nossos recursos, ao executar a build do maven com ./mvnw clean install, no diretório target/kubernetes estarão presentes os CRDs prontos em formato yaml.

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: aplicacaoes.rinaldo.dev
spec:
  group: rinaldo.dev
  names:
    kind: Aplicacao
    plural: aplicacaoes
    singular: aplicacao
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        properties:
          spec:
            properties:
              variaveisDeAmbiente:
                additionalProperties:
                  type: string
                type: object
              imagem:
                type: string
            type: object
          status:
            properties:
              url:
                type: string
              mensagem:
                type: string
              pronta:
                type: boolean
            type: object
        type: object
    served: true
    storage: true

Imagine precisar definir esse CRD todo manualmente!

Implementando o reconciliador

Após ter o recurso Aplicacao definido, é possível implementar a classe que irá ser executada sempre que um recurso desse tipo for criado no cluster Kubernetes. É aqui que será definido o nosso control loop, ou reconciliation loop. No método reconcile é importante que o comportamento sempre siga o seguinte padrão: valide como está o estado atual dos recursos e, caso estejam diferentes do que deveriam, faça as modificações necessárias. No caso do nosso controller, será considerado um sucesso quando os 3 recursos estiverem criados e prontos: o Deployment, o Service e o Ingress.

@Override
public UpdateControl<Aplicacao> reconcile(Aplicacao aplicacao, Context<Aplicacao> context) {
    final ObjectMeta metadata = getMetadataPadrao(aplicacao);

    final Deployment deployment = criaDeployment(aplicacao, metadata);
    if (deployment == null) {
        return novaTentativaReconciliacao("deployment", metadata);
    }

    final Service service = criaService(aplicacao, metadata);
    if (service == null) {
        return novaTentativaReconciliacao("service", metadata);
    }

    final Ingress ingress = criaIngress(aplicacao, metadata);
    if (ingress == null) {
        return novaTentativaReconciliacao("ingress", metadata);
    }

    final String url = extraiURL(ingress);

    Log.infof("Aplicacao %s criada e exposta com sucesso em %s", metadata.getName(), url);
    aplicacao.setStatus(new AplicacaoStatus(url));
    return UpdateControl.updateStatus(aplicacao);
}

O método reconcile é chamado pela SDK sempre que um recurso do tipo Aplicacao é criado ou alterado, ou seja, ele é o nosso control loop, e essa classe é nosso controller.

A classe AplicacaoReconciler contém muito mais código do que isso, pois os métodos que criam os recursos são grandes e não estão apresentados aqui. Acesse o projeto completo para ver todo o código.

Benefícios do Quarkus

Por ser um projeto usando o Quarkus, todos os outros benefícios estão incluídos, como a geração automática do kubernetes.yml com todos as definições necessárias para nossa aplicação, inclusive as permissões para lidar com o recurso Aplicacao. Mas atenção, você ainda precisa definir manualmente as permissões necessárias para criar recursos do tipo Deploy, Service e Ingress, pois o Quarkus não sabe que estamos tentando criar esses recursos através da API do Kubernetes.

O Quarkus também pode ser usado nesse caso para a gerar a imagem da sua aplicação, basta usar a extensão jib que faz o build da imagem e pode inclusive enviar para um registro externo.

Também é possível usar a SDK para gerar os bundles, que são uma forma de distribuir seus Operators que não vou entrar em detalhes neste artigo.

Projeto completo

Eu tive que reduzir bastante os códigos apresentados aqui, pois as classes são grandes. Caso queira ver as classes completas, o exemplo está disponível no meu github.

Este exemplo foi baseado em um já existente dentro do repositório do Quarkus Operator SDK. Caso você queira seguir este exemplo para levar até produção, recomendo dar uma olhada lá no repositório oficial, pois existem features da SDK que eu não utilizei aqui e que são bem úteis para facilitar o control loop, como a dependência entre recursos, ou a utilização de um Matcher para validar o estado atual antes de alterar qualquer coisa.

Quando usar Operators?

Operators são uma ferramenta poderosa pois estendem as funcionalidades do próprio Kubernetes. Sendo assim, é óbvio que não é algo que deve ser utilizado em qualquer situação. Exemplos clássicos para o uso de Operators são:

  • Provisionamento controlado de infra: se você está no time de infra e quer permitir que os desenvolvedores criem recursos do Kubernetes, como um PersistentVolume ou um Service, mas não quer expor isso diretamente a eles, você pode criar um Operator que irá expor essa funcionalidade de forma controlada. O operator pode inclusive checar permissões, cotas de uso, etc.
  • Disponibilizar aplicações: se você ou sua empresa desenvolvem aplicações que serão instaladas por outros clientes, disponibilizar um Operator para realizar a instalação, manutenção e atualização automática dessa aplicação pode ser até mesmo uma vantagem competitiva. Vários fabricantes fazem isso com suas aplicações para facilitar a instalação e manutenção. Nesse caso, pode ser interessante utilizar bundles e catálogos para disponibilizar o Operator, algo que não mencionei neste artigo.
  • Deploy de aplicações complexas: se o processo de deploy da sua aplicação é muito complexo, tem muitas fases manuais, requer que status sejam checados antes de realizar um próximo passo, talvez seja interessante automatizar esse deploy criando um Operator e reduzir a chance de falha humana no processo.
  • Implantação mais encapsulada: se você por qualquer motivo não quer expor muito sobre o processo de deploy da sua aplicação, um Operator é a melhor forma de fazer isso. Você expõe apenas um CustomResourceDefinition para seu usuário e todo o restante do processo fica interno ao Operator.

E aí, você já conhecia Operators e todos esses conceitos? Já usa no seu projeto ou no seu trabalho? Gostou desse artigo?

Compartilhe esse artigo nas suas redes sociais (e me marque! @rinaldodev) e vamos espalhar o conhecimento sobre Operators!

Veja aqui todo o conteúdo que compartilho com vocês!

Share