Primeiro deployment em cinco minutos

8 min
Público
iniciante, cluster Docker Desktop ativo, kubectl v1.34.1 verificado (lição 03)
Duração
30 a 40 min
Módulo
1/8
Competência alvo
fazer deploy de uma aplicação sem YAML com kubectl create deployment, expô-la em http://localhost:8080, levá-la a três réplicas, observar balanceamento de carga e auto-reparação, depois deletar tudo corretamente

Em uma imagem

Você abre uma barraca com um único vendedor e um cartaz "API". O vendedor usa um crachá com seu nome, e a cada cliente diz "olá, sou jv2xw". Quando a fila alonga, você diz ao gerente "quero três vendedores", e dois novos crachás aparecem. Os clientes se apresentam no mesmo balcão, mas é alternadamente 4pl9z, nvshh quem responde: a carga se distribui. Se um vendedor sai, o gerente já chamou outro antes que você perceba, porque sua instrução é "três vendedores", não "esses três específicos". É exatamente o que você verá: traefik/whoami é um vendedor que exibe seu nome (o Pod), o Deployment é o gerente, o Service é o balcão, e localhost:8080 é a porta da barraca.

Como funciona

Quando você digita kubectl create deployment api --image=traefik/whoami:v1.10, não é criado um Pod mas três objetos encaixados. O Deployment api carrega sua intenção ("essa imagem, esse número de réplicas") e sabe gerenciar atualizações. Cria um ReplicaSet api-9bfb55fc6, cujo único trabalho é manter exatamente N Pods idênticos; o sufixo 9bfb55fc6 é um hash do modelo Pod (imagem, portas, labels), então uma nova imagem dará novo ReplicaSet. O ReplicaSet finalmente cria os Pods api-9bfb55fc6-jv2xw, -4pl9z… com um sufixo aleatório de cinco caracteres. Cada Pod recebe uma IP interna (10.1.0.210) inacessível de seu navegador.

Para acessá-la, dois caminhos. kubectl port-forward abre um túnel temporário de seu terminal para um Pod: perfeito para verificar, inútil para distribuir carga, e ele fecha com Ctrl+C. Um Service é o objeto permanente: uma IP estável no cluster (CLUSTER-IP), um nome DNS (api.primeiros-passos.svc), e uma lista de endereços alvo mantida atualizada por um controlador a partir dos labels (app=api). De tipo LoadBalancer, pede além uma adressIP externa; no Docker Desktop, o componente vpnkit-controller responde "localhost" e publica a porta do Service em sua máquina. É o que torna http://localhost:8080 possível sem túnel.

Esta lição trabalha de forma imperativa: cada comando diz ao cluster o que fazer, agora mesmo. É a forma certa de conseguir uma vitória rápida e ver os objetos aparecerem. A partir do módulo seguinte, você escreverá os mesmos objetos em YAML e os aplicará com kubectl apply: é o modo declarativo, repetível e versionável, aquele da vida real. Os dois produzem exatamente os mesmos objetos; kubectl get deployment api -o yaml já te mostra o que create deployment escreveu por você.

Comando imperativoO que cria ou mudaEquivalente declarativo (próximo módulo)
kubectl create namespace primeiros-passosum Namespacekind: Namespace
kubectl create deployment api --image=… --port=80um Deployment (+ ReplicaSet + Pod)kind: Deployment, spec.template.spec.containers[0].image
kubectl expose deployment api --type=LoadBalancer --port=8080 --target-port=80um Service que visa app=apikind: Service, spec.type: LoadBalancer, ports[0].port: 8080, targetPort: 80
kubectl scale deployment api --replicas=3spec.replicas do Deploymentmudar replicas: 3 depois kubectl apply
kubectl delete pod <nome>deleta um Pod; o ReplicaSet o substituinada a escrever: é a reconciliação
ColunaEm get deployEm get rsEm get pods
READYPods prontos / desejados (3/3)Pods prontoscontêineres prontos / total (1/1)
UP-TO-DATEPods no último modelo
DESIRED / CURRENTdesejados / existentes
STATUSRunning, ContainerCreating, ImagePullBackOff

Passo a passo

Todos os comandos usam -n primeiros-passos. Se você usar alias k da lição 03, substitua kubectl por k.

  1. Criar o namespace do módulo. Uma gaveta vazia na qual tudo que segue será guardado, e que a última etapa va deletar de uma vez.

    bash
    kubectl create namespace primeiros-passos

    Saída real:

    text
    namespace/primeiros-passos created

    O que observar: created. Se ler AlreadyExists, você o guardou de lição 03: sem problema, continue.

  2. Criar o Deployment api. Uma imagem pública, uma tag fixa (v1.10, nunca latest: sem tag precisa, duas máquinas podem baixar duas versões diferentes e "funciona aqui" volta), e a porta que o contêiner escuta.

    bash
    kubectl create deployment api --image=traefik/whoami:v1.10 --port=80 -n primeiros-passos
    kubectl get deploy,rs,pods -n primeiros-passos

    Saída real (oito segundos após criação):

    text
    deployment.apps/api created
    NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
    deployment.apps/api   1/1     1            1           8s
    
    NAME                            DESIRED   CURRENT   READY   AGE
    replicaset.apps/api-9bfb55fc6   1         1         1       8s
    
    NAME                      READY   STATUS    RESTARTS   AGE
    pod/api-9bfb55fc6-jv2xw   1/1     Running   0          8s

    O que observar: três objetos por um comando. Leia o nome do Pod de direita a esquerda: jv2xw = esse Pod preciso (aleatório), 9bfb55fc6 = ReplicaSet, então modelo Pod, api = Deployment. Se STATUS diz ContainerCreating, imagem está baixando (alguns segundos para 10,6 MB); digite comando novamente.

  3. Ver onde o Pod vive. -o wide adiciona IP do Pod e nó que o hospeda.

    bash
    kubectl get pods -n primeiros-passos -o wide

    Saída real:

    text
    NAME                  READY   STATUS    RESTARTS   AGE   IP           NODE             NOMINATED NODE   READINESS GATES
    api-9bfb55fc6-jv2xw   1/1     Running   0          8s    10.1.0.210   docker-desktop   <none>           <none>

    O que observar: IP em 10.1.0.x, rede interna do cluster, nó docker-desktop. Abra http://10.1.0.210 no navegador: nada responde, IP existe apenas na VM. Esse é todo o assunto dos dois passos seguintes.

  4. Abrir túnel com port-forward. Terminal bloqueia enquanto túnel está aberto; é proposital.

    bash
    kubectl port-forward deployment/api 8080:80 -n primeiros-passos

    Saída real:

    text
    Forwarding from 127.0.0.1:8080 -> 80
    Forwarding from [::1]:8080 -> 80
    Handling connection for 8080

    Abra http://localhost:8080 no navegador (linha Handling connection aparece a cada requisição). Página exibida, como curl -s http://localhost:8080 a exibe:

    text
    Hostname: api-9bfb55fc6-jv2xw
    IP: 127.0.0.1
    IP: ::1
    IP: 10.1.0.210
    IP: fe80::48b:d4ff:fe47:6adb
    RemoteAddr: 127.0.0.1:43450
    GET / HTTP/1.1
    Host: localhost:8080
    User-Agent: curl/8.21.0
    Accept: */*

    O que observar: Hostname: api-9bfb55fc6-jv2xw, nome do Pod, e IP 10.1.0.210. Volta ao terminal e digita Ctrl+C: túnel fecha, http://localhost:8080 não responde. Túnel não é exposição.

  5. Expor com Service LoadBalancer. --port é porta do Service (o que você digita no navegador), --target-port aquela do contêiner.

    bash
    kubectl expose deployment api --type=LoadBalancer --port=8080 --target-port=80 -n primeiros-passos
    kubectl get svc -n primeiros-passos

    Saída real:

    text
    service/api exposed
    NAME   TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
    api    LoadBalancer   10.101.121.221   localhost     8080:30153/TCP   3s

    O que observar: EXTERNAL-IP vale localhost, assinatura do Docker Desktop (em nuvem você leria endereço público após minuto; em kind, <pending> para sempre). PORT(S) diz 8080:30153: 8080 é porta publicada em sua máquina, 30153 porta nó atribuída automaticamente cuja você não precisa aqui. Recarregue http://localhost:8080 sem qualquer port-forward: página whoami volta.

  6. Passar para três réplicas e ver carga se distribuir. Deployment muda instrução; ReplicaSet cria dois Pods; Service adiciona a sua lista assim que prontos.

    bash
    kubectl scale deployment api --replicas=3 -n primeiros-passos
    kubectl get pods -n primeiros-passos

    Saída real (10 segundos após escalar):

    text
    NAME                  READY   STATUS    RESTARTS   AGE
    api-9bfb55fc6-4pl9z   1/1     Running   0          10s
    api-9bfb55fc6-jv2xw   1/1     Running   0          15s
    api-9bfb55fc6-nvshh   1/1     Running   0          10s

    O que observar: três Pods diferentes. Recarregue http://localhost:8080 cinco vezes e leia Hostname: alternadamente jv2xw, depois nvshh, depois 4pl9z. Service equilibra a carga redondinha. Se todos fossem jv2xw, o Service não estaria listando todos corretamente ou apenas um Pod.

  7. Matar um Pod e vê-lo renascer. O ReplicaSet vê 2 em vez de 3 e imediatamente cria outro.

    bash
    kubectl delete pod api-9bfb55fc6-4pl9z -n primeiros-passos
    kubectl get pods -n primeiros-passos -w

    Saída real (a flag -w = "watch", fica conectado e mostra mudanças):

    text
    NAME                  READY   STATUS    RESTARTS   AGE
    api-9bfb55fc6-4pl9z   1/1     Running   0          10s
    api-9bfb55fc6-jv2xw   1/1     Running   0          15s
    api-9bfb55fc6-nvshh   1/1     Running   0          10s
    api-9bfb55fc6-4pl9z   1/1     Terminating   0          15s
    api-9bfb55fc6-xq7v6   0/1     Pending       0          0s
    api-9bfb55fc6-xq7v6   0/1     ContainerCreating   0          1s
    api-9bfb55fc6-4pl9z   1/1     Terminating        0          16s
    api-9bfb55fc6-xq7v6   1/1     Running            0          3s

    O que observar: 4pl9z terminando, novo xq7v6 criando, após três segundos de novo 1/1 Running. Atualize http://localhost:8080 durante os três segundos: sem erro, Service redireciona para os outros dois Pods. Pressione Ctrl+C para sair do watch.

  8. Deletar tudo do module com um comando. Deployment, ReplicaSet, Pods, Service — tudo na mesma gaveta.

    bash
    kubectl delete namespace primeiros-passos

    Saída real:

    text
    namespace "primeiros-passos" deleted

    O que observar: tudo deletado de uma vez. Verifique:

    bash
    kubectl get namespace primeiros-passos

    Saída real:

    text
    Error from server (NotFound): namespaces "primeiros-passos" not found

    Perfeito: namespace e tudo dentro foi limpo.

Se der problema

  • Etapa 2: ImagePullBackOff após 30 segundos → Kubernetes não consegue baixar traefik/whoami:v1.10. Possíveis causa: tag errada (verifique no Docker Hub), sem acesso a Docker Hub (rede corporativa), ou seu kubeconfig aponta para cluster errado (lição 03). Se for um verdadeiro problema de rede, você verá ErrImagePull depois ImagePullBackOff; tente docker pull traefik/whoami:v1.10 em seu terminal.
  • Etapa 5: Service criado mas EXTERNAL-IP fica <pending> → Kubernetes procura um balanceador externo. No Docker Desktop depois alguns segundos deve aparecer localhost. Se ficar <pending> para sempre, você pode estar em minikube ou kind: use kubectl port-forward permanentemente ou minikube tunnel / kind load docker-image.
  • http://localhost:8080 não carrega após criar Service → Service foi criado mas ainda não está roteando. Verifique: kubectl get endpoints -n primeiros-passos deve listar api com IPs. Se estiver vazio, o Service não encontrou Pods com labels app=api — etapa 2 talvez tenha falhado, veja kubectl describe pod <name> -n primeiros-passos | grep -i label.

Para reter

  • Deployment cria Pods e ReplicaSet; ReplicaSet mantém número de réplicas; cada Pod é efêmero.
  • Três objetos por kubectl create deployment: Deployment, ReplicaSet, Pod(s).
  • Service é endereço permanente: uma IP interna (CLUSTER-IP, DNS <nome>.<namespace>.svc), opcionalmente IP ou nome externo.
  • LoadBalancer em Docker Desktop retorna localhost; escala de 1 para N réplicas com kubectl scale; Pod morto é substituído automaticamente.
  • Dois modos: imperativo (comandos diretos, para entender); declarativo (YAML, para reproduzir).
  • Próxima lição: prática guiada — criar seu próprio kit, instalar no cluster, e consertar duas falhas intencionais.

Para ir mais longe

A página Kubernetes Objects Lifecycle explica cascata de criação (Deployment → ReplicaSet → Pod). Deployments oferece referência completa de campos de um Deployment.