Exponha aplicações Kubernetes

KubernetesBeginner
Pratique Agora

Introdução

Agora você já consegue implantar e solucionar problemas em aplicações, mas os clientes ainda precisam de uma forma confiável de acessá-las. O IP de um Pod é temporário: um Deployment pode substituir um Pod a qualquer momento, atribuindo ao substituto um endereço IP diferente.

O Kubernetes resolve isso com um Service. Um Service seleciona um grupo de Pods que pode mudar e oferece aos clientes uma identidade de rede estável. Neste laboratório, você acompanhará o caminho completo da conexão: dos rótulos aos EndpointSlices, passando pelo DNS do cluster, e conhecerá dois tipos de Service: ClusterIP, para acesso dentro do cluster, e NodePort, para acesso por meio de um nó.

Este laboratório se concentra deliberadamente em Services. O Ingress adiciona roteamento HTTP e um controlador separado; ele fica mais fácil de entender depois que a seleção e a acessibilidade dos Services estiverem claras.

Implante os backends do Service

Inicialização do ambiente: Este laboratório inicia um cluster Kubernetes completo para você. A configuração do plano de controle, do nó e dos componentes de rede normalmente leva 2–3 minutos. Aguarde pacientemente até que o ambiente termine de carregar antes de começar.

Nesta etapa, você criará a aplicação replicada que os Services exporão posteriormente. Aqui, um backend é um Pod capaz de receber tráfego destinado a um Service.

Vá para o diretório de trabalho. cd altera o diretório atual do shell; normalmente, não exibe nada quando é executado com sucesso:

cd /home/labex/project/service-lab

Crie um Deployment com duas réplicas. O rótulo app: course-nginx dos Pods é especialmente importante, pois os Services o utilizarão como regra de seleção.

A sintaxe de shell cat <<'EOF' > filename é um here-document. Todo o conteúdo até o EOF de fechamento é gravado no arquivo, e > cria ou substitui esse arquivo. Colocar o primeiro EOF entre aspas impede a expansão acidental do shell dentro do YAML.

cat <<'EOF' > course-nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: course-nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: course-nginx
  template:
    metadata:
      labels:
        app: course-nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.27-alpine
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
EOF

Aplique o manifesto e aguarde os dois Pods. -f informa ao apply qual arquivo deve ser lido; rollout status aguarda o controlador do Deployment; --timeout=60s limita esse tempo de espera. No comando final, -l filtra pelo rótulo e -o wide adiciona as colunas de IP do Pod e do nó:

kubectl apply -f course-nginx-deployment.yaml
kubectl rollout status deployment/course-nginx --timeout=60s
kubectl get pods -l app=course-nginx -o wide

A porta nomeada http documenta a porta TCP 80 em cada contêiner. As duas linhas devem estar com o status Running e exibir IPs de Pod diferentes. Esses IPs são reais, mas não são endereços duráveis para os clientes; nas próximas etapas, você adicionará uma identidade estável de Service à frente deles.

Conecte os rótulos à seleção do Service

Nesta etapa, você inspecionará os metadados que conectam um Service aos Pods. Um Service não seleciona um Deployment pelo nome; ele procura independentemente os Pods cujos rótulos correspondem ao seu seletor.

Exiba os rótulos dos Pods. -l app=course-nginx é um seletor de rótulo, enquanto --show-labels adiciona o conjunto completo de rótulos como a última coluna:

kubectl get pods -l app=course-nginx --show-labels

Cada Pod tem app=course-nginx e um pod-template-hash gerado automaticamente. Seu Service deve selecionar apenas o rótulo estável da aplicação.

Compare nomes, rótulos e IPs em uma visualização compacta. -o custom-columns cria uma tabela a partir de campos específicos dos objetos. Cada cabeçalho antes de : é seguido pelo caminho do campo usado naquela coluna; a barra invertida continua o mesmo comando na linha seguinte da tela:

kubectl get pods -l app=course-nginx \
  -o custom-columns='NAME:.metadata.name,LABEL:.metadata.labels.app,IP:.status.podIP,READY:.status.containerStatuses[0].ready'

Os nomes e IPs gerados distinguem os Pods individualmente; o rótulo compartilhado descreve a função que eles desempenham. Esse nível de indireção permite que um Service continue funcionando quando um Pod é substituído.

Confirme se o seletor do Deployment corresponde ao rótulo do modelo de Pod. -o jsonpath='...' extrai apenas os campos solicitados. O texto fora das chaves vira um rótulo, os caminhos de campo dentro das chaves retornam valores, e {"\n"} insere uma quebra de linha:

kubectl get deployment course-nginx \
  -o jsonpath='Selector: {.spec.selector.matchLabels.app}{"\n"}Pod label: {.spec.template.metadata.labels.app}{"\n"}'

Os dois valores devem ser course-nginx. Um seletor incompatível faria com que o controlador ou o Service perdesse a conexão com os Pods pretendidos.

Crie um Service ClusterIP

Nesta etapa, você criará o tipo padrão de Service, ClusterIP. Ele fornece um IP virtual e um nome DNS acessível pelas cargas de trabalho dentro do cluster.

Crie o manifesto usando o mesmo padrão de here-document apresentado neste laboratório:

cat <<'EOF' > course-nginx-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: course-nginx
spec:
  type: ClusterIP
  selector:
    app: course-nginx
  ports:
    - name: http
      port: 80
      targetPort: http
      protocol: TCP
EOF

Leia atentamente o mapeamento de portas:

  • port: 80 é a porta que os clientes usam no Service.
  • targetPort: http refere-se à porta nomeada do contêiner em cada Pod selecionado.
  • O seletor escolhe os Pods de backend; ele não é um endereço de rede.

Valide, aplique e inspecione o Service. --dry-run=client analisa o arquivo localmente sem criar nada; removê-lo executa a aplicação real; em seguida, get service lê o objeto ativo:

kubectl apply --dry-run=client -f course-nginx-service.yaml
kubectl apply -f course-nginx-service.yaml
kubectl get service course-nginx

O valor de CLUSTER-IP é atribuído pelo Kubernetes. Ele permanece estável durante toda a existência deste Service, mesmo quando os Pods de backend são alterados.

Rastreie o Service até os EndpointSlices

Nesta etapa, você acompanhará o seletor do Service até os endereços reais dos backends. O Kubernetes registra esses endereços em objetos EndpointSlice.

Descreva o Service. describe expande um objeto nomeado para mostrar configuração, status e informações relacionadas aos endpoints, sendo útil depois da tabela resumida exibida por get:

kubectl describe service course-nginx

Procure Selector: app=course-nginx e uma linha Endpoints contendo dois IPs de Pod na porta 80.

Liste o EndpointSlice selecionado pelo rótulo que identifica o nome do Service. Este seletor -l usa o rótulo adicionado automaticamente kubernetes.io/service-name=course-nginx:

kubectl get endpointslices -l kubernetes.io/service-name=course-nginx

Inspecione os endereços e o estado de prontidão. Este JSONPath usa range para repetir o modelo delimitado para cada endpoint. Ele exibe o primeiro endereço, o texto literal ready=, o valor de prontidão e uma quebra de linha:

kubectl get endpointslices -l kubernetes.io/service-name=course-nginx \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'

Você deverá ver dois endereços com ready=true. Agora a cadeia está concreta:

Service selector -> matching Pod labels -> EndpointSlice addresses -> ready Pods

Se um Service existir, mas não tiver endpoints, primeiro compare o seletor dele com os rótulos e o estado de prontidão dos Pods.

Acesse o Service pelo DNS do cluster

Nesta etapa, você atuará como um cliente dentro do cluster. O DNS do Kubernetes permite que um Pod no mesmo namespace use o nome do Service course-nginx, sem precisar memorizar seu IP virtual.

Inicie um Pod temporário do BusyBox, solicite a página do NGINX e remova o cliente automaticamente quando ele terminar. Leia as opções de cima para baixo:

  • --image escolhe a imagem do contêiner e --image-pull-policy=IfNotPresent reutiliza a cópia armazenada em cache.
  • --restart=Never cria um Pod independente, em vez de uma carga de trabalho gerenciada por um controlador.
  • --rm remove o Pod depois que o comando termina, e -i mantém a entrada e a saída do comando conectadas.
  • O separador -- encerra as opções do kubectl; tudo depois dele é o comando executado dentro do contêiner.
  • wget -qO- solicita a URL silenciosamente e grava o corpo da resposta no terminal.
kubectl run service-client \
  --image=busybox:1.36 \
  --image-pull-policy=IfNotPresent \
  --restart=Never \
  --rm -i \
  -- wget -qO- http://course-nginx

A resposta contém a página de boas-vindas do NGINX. O tráfego passou pelo Service, e não diretamente por um IP de Pod escolhido.

Execute uma verificação de sucesso mais silenciosa. >/dev/null descarta o corpo HTML, e && exibe a mensagem apenas se o comando de solicitação for concluído com sucesso:

kubectl run service-client-check \
  --image=busybox:1.36 \
  --image-pull-policy=IfNotPresent \
  --restart=Never \
  --rm -i \
  -- wget -qO- http://course-nginx >/dev/null && echo "ClusterIP Service responded"

A mensagem de sucesso comprova tanto a resolução DNS quanto a acessibilidade HTTP. O Pod cliente é temporário; o Service e seus dois Pods de backend permanecem.

Adicione um Service NodePort

Nesta etapa, você criará um segundo Service para os mesmos Pods usando o tipo NodePort. Um NodePort abre uma porta do intervalo padrão 30000–32767 em cada nó e encaminha o tráfego para os backends do Service.

O Kubernetes pode ler vários objetos de um único arquivo YAML com vários documentos. Cada objeto mantém seu próprio apiVersion, kind, metadata e spec; uma linha contendo --- separa um documento YAML do seguinte.

Crie um arquivo reutilizável contendo o Service ClusterIP existente e o novo Service NodePort. Repetir a definição do ClusterIP é seguro: aplicar o mesmo estado desejado não o altera.

cat <<'EOF' > course-nginx-services.yaml
apiVersion: v1
kind: Service
metadata:
  name: course-nginx
spec:
  type: ClusterIP
  selector:
    app: course-nginx
  ports:
    - name: http
      port: 80
      targetPort: http
      protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  name: course-nginx-nodeport
spec:
  type: NodePort
  selector:
    app: course-nginx
  ports:
    - name: http
      port: 80
      targetPort: http
      nodePort: 30080
      protocol: TCP
EOF

Valide os dois documentos YAML em conjunto antes de alterar o cluster. A saída deve mencionar service/course-nginx e service/course-nginx-nodeport, seguidos de (dry run):

kubectl apply --dry-run=client -f course-nginx-services.yaml

Aplique e inspecione os Services. O primeiro comando lê os dois documentos de um único arquivo; o Service ClusterIP existente deve aparecer como unchanged, enquanto o Service NodePort será criado. O segundo comando lê o novo Service ativo:

kubectl apply -f course-nginx-services.yaml
kubectl get service course-nginx-nodeport

A coluna PORT(S) exibe 80:30080/TCP: a porta 80 é a porta do Service e 30080 é a porta voltada para o nó. Os dois Services selecionam os mesmos Pods e, portanto, podem ter os mesmos endereços de backend.

Compare os dois limites de acesso

Nesta etapa, você testará o NodePort e resumirá quando cada tipo de Service é apropriado.

Obtenha o IP do nó do Minikube. $(...) é uma substituição de comando: o shell executa minikube ip e armazena sua saída na variável NODE_IP. -p labex-v135 seleciona o perfil preparado, e echo permite verificar o valor armazenado:

NODE_IP=$(minikube ip -p labex-v135)
echo "$NODE_IP"

Solicite a aplicação pela porta voltada para o nó. O Kubernetes pode precisar de alguns segundos para configurar a nova regra de rede no nível do nó depois de aceitar o Service. As opções de repetição fazem o curl aguardar esse curto período de convergência, em vez de falhar na primeira conexão recusada:

curl -s --retry 5 --retry-connrefused --retry-delay 2 "http://${NODE_IP}:30080" | grep 'Welcome to nginx'

Aqui, -s oculta o indicador de progresso, --retry 5 permite até cinco novas tentativas, --retry-connrefused trata uma conexão recusada inicialmente como passível de repetição, e --retry-delay 2 aguarda dois segundos entre as tentativas. As aspas duplas permitem que ${NODE_IP} seja expandido dentro da URL. O pipe | envia o HTML retornado para o grep, que exibe a linha de boas-vindas correspondente como evidência.

O título HTML correspondente comprova que a solicitação chegou a um Pod de backend. Compare os dois caminhos construídos:

in-cluster Pod -> course-nginx:80 -> ready backend Pod
VM/node client -> NODE_IP:30080 -> course-nginx-nodeport:80 -> ready backend Pod

Inspecione os dois Services juntos:

kubectl get services course-nginx course-nginx-nodeport

Use ClusterIP para comunicação estável dentro do cluster; ele é o padrão e a base comum para outros mecanismos de exposição. NodePort adiciona um ponto de entrada no nível do nó e é útil para aprendizado, desenvolvimento ou integração com balanceadores de carga externos. Ambos dependem de seletores corretos e de EndpointSlices prontos.

No próximo desafio, você aplicará essas ideias de forma independente ao expor mais de uma carga de trabalho web.

Resumo

Você criou uma identidade de rede estável à frente de Pods substituíveis. Conectou os seletores do Service aos rótulos dos Pods, rastreou os backends selecionados por meio dos EndpointSlices, acessou um Service ClusterIP usando o DNS do cluster e adicionou um ponto de entrada NodePort pelo nó.

O principal modelo mental é que um Service não é a aplicação e não contém Pods. Ele representa continuamente um conjunto selecionado de backends prontos. Quando houver uma falha de conectividade, siga a cadeia na ordem: portas do Service, seletor, rótulos dos Pods, EndpointSlices, prontidão dos backends e, por fim, o limite do cliente.