7.3 Conhecendo os recursos e conceitos do Kubernetes

O Kubernetes permite a criação de um cluster de máquinas, cujo objetivo é a execução de aplicações com a utilização de contêineres. Além de criar o cluster, o Kubernetes permite a definição de recursos que juntos tornam possível a execução das aplicações. Nesse capítulo vamos conhecer as funções de 3 diferentes tipos de recursos: deployment, service e ingress.

Cada um deles oferece funcionalidades interessantes, e juntos vão nos ajudar a realizar o deploy de nossa aplicação em um cluster Kubernetes. De modo bem simples, o deployment permite configurar os contêineres necessários para executar a aplicação, o service expõe a aplicação como um serviço, e o ingress habilita o acesso externo ao serviço por meio dos protocolos HTTP e HTTPS.

Vejamos a seguir em mais detalhes as funções de cada um, e como são definidos.

Deployment

O deployment, como o nome sugere, é o recurso que permite a definição de como será realizada a implantação de uma aplicação em um cluster. Ele pode ser entendido como uma declaração de um estado desejado para uma aplicação, sendo possível por exemplo informar o número de instâncias que serão executadas, e as imagens utilizadas para a criação dos contêineres.

Em um único deployment, podem ser definidos contêineres com diferentes imagens, que podem por exemplo serem executados em paralelo. Para cada instância informada, o Kubernetes cria um pod, que é de fato, a menor unidade implantável que você pode definir e gerenciar com o Kubernetes. Portanto, o pod é um agrupamento de 1 ou mais contêineres, os quais são implantados em um mesmo nó do cluster. Além de estarem em um mesmo nó, eles conseguem fazer uso de recursos de armazenamento compartilhados e de rede.

Os recursos no Kubernetes, incluindo os deployments, podem ser definidos utilizando arquivos YAML. Esse tipo de arquivo é composto de chaves e valores, tal como o exemplo de deployment a seguir:

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: nginx
  name: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
        resources: {}
status: {}

Nesse exemplo, temos a chave kind, com o valor Deployment. Também podemos ter chaves aninhadas, como é o caso a seguir, em que replicas é uma chave aninhada na chave spec:

spec:
  replicas: 2

Nesse caso, é importante destacar que para cada replica do deployment, será criado um pod, que conterá os containers defindos no deployment.

Observe que o deployment (e grande parte dos recursos do Kubernetes) possui as seguintes seções (chaves) principais: apiVersion, kind, metadata, spec e status. As duas primeiras servem para definir qual a API e o recurso utilizado do Kubernetes.

A chave metadata serve para informar metadados do recurso, como por exemplo seu nome (name), data de criação (creationTimestamp) e labels, as quais são utilizadas para classificar o recurso e permitir sua busca e referência.

Já na chave spec, que vem da palavra em ingês specification (especificação), podemos definir de fato as configurações do recurso, como por exemplo número de réplicas (replicas) e os contêineres que serão executados (containers).

Por fim, temos a chave status, que mostra o estado atual do deployment.

Service

O service é um recurso do Kubernetes que permite expor de modo abstrato e como um serviço de rede uma aplicação executada em um conjunto de pods.

Desse modo, é possivel acessar a aplicação por um único nome DNS. O Kubernetes realiza o trabalho de atribuir um endereço de IP para cada pod, e realizar o balanceamento de carga entre os containers.

Para ficar mais claro, vamos analisar um caso no qual a aplicação A tenha um deployment definindo um único container, com 3 réplicas. Nessa situação serão criados 3 pods, cada um com um único container, o qual segue as especificações do deployment. Cada um desses pods (e consequentemente seu respectivo container) pode estar em uma máquina diferente. Cada container, recebe do Kubernetes um endereço único de IP. Sendo assim, sem a utilização de um service do Kubernetes, uma aplicação B que precise se comunicar com a aplicação A necessitaria tentar acessar um dos 3 endereços disponíveis.

No entanto, como os contêineres podem ser removidos e recriados dinamicanente pelo Kubernetes, os IPs dos contêineres rodando a aplicação A poderiam mudar, trazendo dificuldades para a aplicação B manter o contato com a aplicação A.

O service resolve justamente esse problema. Ele permite a definição de um nome para o serviço, por exemplo aplicacao-a. Quando algum contêiner é adicionado, o service incorpora o novo IP na lista de contêineres que fornecem o serviço, podendo enviar requisições para ele. De modo análogo, se um contêiner é removido, o service retira o IP do contêiner removido da sua lista, e não envia mais requisições para esse IP. Desse modo, para a aplicação B fica transparente quantos contêineres estão rodando a aplicação A, e seus respectivos IPs. Ela sempre se comunicará com o nome do serviço, ou seja aplicacao-a.

Além dessa facilidade, o service também realiza o balanceamento de carga entre os containers, ou seja, ele utiliza uma política de distribuição das requisições do serviço entre os containers, diminuindo a chance de que um determinado contêiner fique sobrecarregado, enquanto os demais fiquem ociosos.

A seguir temos um exemplo de service, definido no formato YAML:

apiVersion: v1
kind: Service
metadata:
  name: aplicacao-a
  labels:
    app: nginx
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
      name: http
  selector:
    app: nginx

Alguns pontos de atenção na definição dos services são o seu tipo (spec.type), a definição de portas (spec.ports) e os seletores utilizados (spec.selector).

Um service pode ser de 4 tipos diferentes: ClusterIP, NodePort, LoadBalancer e External Name.

Um serviço do tipo ClusterIP recebe um IP interno ao cluster, ou seja, só pode ser acessado de dentro do próprio cluster.

Já o tipo NodePort expõe o serviço em uma porta estática do nó do Kubernetes. Nesse caso, automaticamente também é criado um serviço do tipo ClusterIP, para o qual são roteadas as requisições que chegam a porta do nó. O serviço nesse caso pode ser acessado por meio do endereço NODE_IP:NODE_PORT.

O tipo LoadBalancer expõe o serviço externamente, por meio de um load balancer do provedor de nuvem. O Kubernetes automaticamente cria dois outros services, um do tipo NodePort e outro do tipo ClusterIP, para os quais são roteadas as requisições que chegam no load balancer externo.

Por último, temos o tipo ExternalName, o qual é responsável por mapear o serviço para um nome externo, por exemplo (meuservico.ufscar.br). Ele retorna um registro CNAME com esse nome e nenhum tipo de proxy é configurado nessa situação.

É​ interessante perceber que com exeção do caso do ExternalName, o tipo ClusterIP sempre é criado, e utilizado pelos demais. O tipo NodePort também é utilizado pelo tipo LoadBalancer, mostrando que o Kubernetes faz reúso de seus tipos mais básicos.

Já na definição de portas, temos a porta do próprio serviço (port), pela qual será acessado, e a porta do container para o qual será mapeada a requisição (targetPort), além do protocolo utilizado (protocol). É possível acessar mais de uma porta, e poderíamos, por exemplo, ter a porta HTTP (80) e HTTPS (443) do serviço expostas.

Por último temos os seletores. Eles definem as labels que os containers devem ter para que sejam reconhecidos pelo service como containers para os quais devem ser encaminhadas as requisições que chegam ao serviço. No exemplo vemos que o service seleciona os containers que apresentem a label app: nginx, como o container definido no exemplo anterior.

Ingress

Para completar os recursos que serão utilizados, temos o ingress. Ele é utilizado para gerenciar o acesso externo aos serviços presentes no cluster, em geral por meio dos protocolos HTTP/HTTPS.

O ingress pode permitir a utilização de SSL, além de prover balanceamento de carga e a definição de URL para acesso aos serviços de maneira externa. A seguir um exemplo de ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: minimal-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: "meuservico.ufscar.br"
    http:
      paths:
      - path: /app-a
        pathType: Prefix
        backend:
          service:
            name: aplicacao-a
            port:
              number: 80

Observe que o ingress possui um conjunto de regras (rules) que devem ser atendidas para que ele realize o roteamento da requisição para um determinado service. No exemplo, temos uma única regra, que determina que requisições que cheguem tentando acessar o host meuservico.ufscar.br, no caminho /app-a sejam enviadas para o serviço aplicacao-a (definido anteriormente), na porta 80.

Arquivos estáticos

É importante mencionar que como vimos nos exemplos anteriores, os arquivos YAML utilizados para definir os recursos são estáticos, ou seja, os valores das configurações do recurso estão fixados nos próprios arquivos.

Para testarmos localmente, não há problemas nessa abordagem, no entanto, caso nossa aplicação possa ser implantada em diversos ambientes, com configurações diferentes, é interessante permitir que sejam passados valores diferentes para preencher os arquivos dos recursos, de acordo com as ​necessidades de cada ambiente.

Nesse sentido, podemos utilizar charts, que são pacotes de distribuição de aplicações no Kubernetes e que permitem essa flexibilidade. No próximo capítulo, vamos entender o que é um chart, sua estrutura básica e ver como ele facilita a implantação de aplicações em diferentes ambientes.

Last updated