Pular para conteúdo

4. GitOps com Argo CD

Até agora você fez deploy com kubectl apply a partir do seu notebook: quem tem as credenciais muda o cluster, e nada registra o que está rodando. O GitOps inverte a direção: o estado desejado vive no Git, e um agente dentro do cluster — o Argo CD — o puxa e mantém o cluster igual a ele, para sempre. O CI nunca toca no cluster; ele só escreve no Git. A teoria está na aula de entrega.

flowchart LR
    dev[git push] --> ci[CI: testes, scan, imagem]
    ci -->|imagem sha-…| ghcr[(ghcr.io)]
    ci -->|sucesso| deploy[workflow Deploy]
    deploy -->|"commit: nova tag em k8s/overlays/gitops"| git[(repositório no GitHub)]
    argo[Argo CD no cluster] -->|consulta a cada 3 min| git
    argo -->|apply, prune, self-heal| k8s[namespace boxoffice]
    k8s -->|pull da imagem| ghcr

Os componentes da plataforma

Use o cluster kind do lab 10 (recrie-o com o passo 1 do lab 10 se você o apagou; Traefik e metrics-server são necessários). Primeiro remova o que o lab 10 implantou à mão — daqui em diante o Argo CD é dono do namespace:

kubectl config use-context kind-boxoffice
kubectl delete namespace boxoffice --wait=true

Instale o Argo CD, o Argo Rollouts (o passo 5 o usa, e o overlay de GitOps já contém um Rollout) e o pequeno Prometheus do passo 5. As CRDs grandes precisam de server-side apply:

kubectl create namespace argocd
kubectl apply -n argocd --server-side --force-conflicts \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/v3.5.5/manifests/install.yaml
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts --server-side \
  -f https://github.com/argoproj/argo-rollouts/releases/download/v1.10.1/install.yaml
kubectl apply -f k8s/monitoring/prometheus.yaml
kubectl -n argocd rollout status deployment/argocd-server --timeout=300s

Você deve ver deployment "argocd-server" successfully rolled out, e kubectl get pods -n argo-rollouts e kubectl get pods -n monitoring devem mostrar um pod rodando cada.

O Argo CD só considera um Ingress Healthy quando o controller escreveu um endereço no status dele, e um Traefik exposto como NodePort não escreve nenhum: a Application ficaria em Progressing para sempre. Faça o Traefik publicar 127.0.0.1:

helm upgrade traefik traefik/traefik --version 42.0.0 --namespace traefik --reuse-values \
  --set providers.kubernetesIngress.publishedService.enabled=false \
  --set providers.kubernetesIngress.ingressEndpoint.ip=127.0.0.1

Depois de um minuto, kubectl -n boxoffice get ingress mostra 127.0.0.1 em ADDRESS (quando a Application tiver criado o Ingress, abaixo).

Abra a interface do Argo CD num segundo terminal e entre como admin:

kubectl -n argocd port-forward svc/argocd-server 8443:443          # keep it running
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d; echo

Acesse https://localhost:8443 (aceite o certificado autoassinado) e use essa senha.

Acesso ao seu repositório e às suas imagens

O seu repositório e os seus pacotes são privados, então o cluster precisa de credenciais só de leitura. Crie um fine-grained personal access token no GitHub (Settings → Developer settings → Fine-grained tokens) para o seu repositório boxoffice com Contents: read, e um token clássico com read:packages para as imagens (ou torne o pacote boxoffice-catalog público e pule o segundo segredo). Nunca faça commit deles:

export GH_USER=<your GitHub user, lowercase>
export REPO_TOKEN=<the fine-grained token>
export PKG_TOKEN=<the read:packages token>

kubectl -n argocd create secret generic boxoffice-repo \
  --from-literal=type=git \
  --from-literal=url=https://github.com/$GH_USER/boxoffice.git \
  --from-literal=username=$GH_USER --from-literal=password=$REPO_TOKEN
kubectl -n argocd label secret boxoffice-repo argocd.argoproj.io/secret-type=repository

kubectl create namespace boxoffice
kubectl -n boxoffice create secret docker-registry ghcr-pull \
  --docker-server=ghcr.io --docker-username=$GH_USER --docker-password=$PKG_TOKEN

O overlay de GitOps

Um novo overlay, ao lado do local do lab 10: a mesma base, a imagem do GHCR, o Rollout do passo 5 e um patch que expõe /actuator/prometheus.

k8s/overlays/gitops/kustomization.yaml
# The environment Argo CD deploys (lab 11). CI changes only the "images" entry below.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: boxoffice
resources:
  - ../../base
  - catalog-services.yaml
  - catalog-rollout.yaml
  - catalog-analysis.yaml
images:
  - name: boxoffice/catalog
    newName: ghcr.io/your-user/boxoffice-catalog   # your GitHub user, lowercase
    newTag: sha-0000000000000000000000000000000000000000   # written by .github/workflows/deploy.yaml
secretGenerator:
  - name: db-credentials             # lab only: use Sealed Secrets / External Secrets in real projects
    namespace: boxoffice
    literals:
      - DB_USER=boxoffice
      - DB_PASSWORD=boxoffice
patches:
  # the Rollout runs the pods from now on: the Deployment only lends it its pod template
  - target: { kind: Deployment, name: catalog }
    patch: |-
      - op: replace
        path: /spec/replicas
        value: 0
      - op: add
        path: /spec/template/spec/imagePullSecrets
        value: [{ name: ghcr-pull }]
      - op: add                      # expose /actuator/prometheus for the canary analysis
        path: /spec/template/spec/containers/0/env/-
        value: { name: MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE, value: "health,info,metrics,prometheus" }
      - op: add                      # publish the latency buckets (_bucket) that the p99 analysis needs
        path: /spec/template/spec/containers/0/env/-
        value: { name: MANAGEMENT_METRICS_DISTRIBUTION_PERCENTILESHISTOGRAM_HTTP_SERVER_REQUESTS, value: "true" }
  # the HPA now scales the Rollout
  - target: { kind: HorizontalPodAutoscaler, name: catalog }
    patch: |-
      - op: replace
        path: /spec/scaleTargetRef
        value: { apiVersion: argoproj.io/v1alpha1, kind: Rollout, name: catalog }
k8s/overlays/gitops/catalog-services.yaml
# Argo Rollouts points these two Services at the stable and the canary pods (it adds the
# rollouts-pod-template-hash to their selectors). Prometheus scrapes them, so every series
# carries service="catalog-stable" or service="catalog-canary".
apiVersion: v1
kind: Service
metadata:
  name: catalog-stable
  annotations:
    prometheus.io/scrape: "true"
spec:
  selector:
    app: catalog
  ports:
    - name: http
      port: 8082
      targetPort: http
---
apiVersion: v1
kind: Service
metadata:
  name: catalog-canary
  annotations:
    prometheus.io/scrape: "true"
spec:
  selector:
    app: catalog
  ports:
    - name: http
      port: 8082
      targetPort: http
k8s/overlays/gitops/catalog-rollout.yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: catalog
spec:
  # no "replicas": the HPA owns it
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: catalog
  workloadRef:                       # reuse the pod template of the Deployment in base/
    apiVersion: apps/v1
    kind: Deployment
    name: catalog
    scaleDown: never                 # Git already sets the Deployment to 0 replicas
  strategy:
    canary:
      stableService: catalog-stable
      canaryService: catalog-canary
      analysis:                      # runs in the background from the first step
        templates:
          - templateName: catalog-slo
        startingStep: 1
      steps:
        - setWeight: 25
        - pause: { duration: 2m }
        - setWeight: 50
        - pause: { duration: 2m }
        # after the last step the canary becomes the new stable
k8s/overlays/gitops/catalog-analysis.yaml
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: catalog-slo
spec:
  metrics:
    - name: error-rate
      interval: 30s
      failureLimit: 1                # a second bad measurement aborts and rolls back
      successCondition: len(result) == 0 || isNaN(result[0]) || result[0] < 0.01
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            sum(rate(http_server_requests_seconds_count{service="catalog-canary", status=~"5.."}[1m]))
            /
            sum(rate(http_server_requests_seconds_count{service="catalog-canary"}[1m]))
    - name: p99-latency
      interval: 30s
      failureLimit: 1
      successCondition: len(result) == 0 || isNaN(result[0]) || result[0] < 0.5
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            histogram_quantile(0.99, sum by (le) (
              rate(http_server_requests_seconds_bucket{service="catalog-canary"}[1m])))

Troque your-user em newName pelo seu usuário do GitHub (em minúsculas) e deixe newTag como está: o workflow Deploy o escreve depois da próxima execução bem-sucedida do CI. kubectl kustomize k8s/overlays/gitops deve imprimir os manifestos sem erros.

O workflow Deploy

.github/workflows/deploy.yaml
# Continuous delivery by GitOps (lab 11): when CI succeeds on main, point the GitOps overlay at
# the image built from that commit. Argo CD, inside the cluster, sees the commit and deploys it.
# This workflow never talks to the cluster: it only writes to Git.
name: Deploy

on:
  workflow_run:
    workflows: [CI]
    types: [completed]
    branches: [main]

permissions:
  contents: write           # commit the new image tag

concurrency:
  group: deploy
  cancel-in-progress: false # deploys run one after the other, never in parallel

jobs:
  promote:
    if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push'
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          ref: main
      - name: Set the catalog image of the gitops overlay to this commit
        env:
          SHA: ${{ github.event.workflow_run.head_sha }}
        run: |
          OWNER="${GITHUB_REPOSITORY_OWNER,,}"
          cd k8s/overlays/gitops
          kustomize edit set image "boxoffice/catalog=ghcr.io/${OWNER}/boxoffice-catalog:sha-${SHA}"
          git diff --stat
      - name: Commit and push (a commit made with GITHUB_TOKEN does not trigger CI again)
        env:
          SHA: ${{ github.event.workflow_run.head_sha }}
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add k8s/overlays/gitops/kustomization.yaml
          git diff --cached --quiet && { echo "already deployed"; exit 0; }
          git commit -m "deploy: catalog sha-${SHA::7}"
          git push

Ele roda quando o CI termina na main, edita a entrada images do overlay com kustomize edit set image (o Kustomize vem pré-instalado nos runners Ubuntu do GitHub) e faz push do commit. O Kustomize reescreve o arquivo inteiro ao editá-lo: os comentários somem e as listas são reindentadas, o que é esperado. Um commit enviado com o GITHUB_TOKEN do workflow não inicia novas execuções de workflow, e o CI ignora mudanças em k8s/** de qualquer forma, então não há loop. Como o Deploy faz commit na main, a sua main local fica para trás depois de cada deploy: daqui em diante, rode git pull --rebase antes de cada git push.

Proteção de branch

Se a main exige pull requests, o bot não consegue fazer push nela. Ou permita que o bot do GitHub Actions faça bypass da regra, ou mude o workflow para abrir um pull request com a nova tag (um humano — ou uma política — aprova cada deploy).

A Application

k8s/argocd/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: boxoffice
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/your-user/boxoffice.git
    targetRevision: main
    path: k8s/overlays/gitops
  destination:
    server: https://kubernetes.default.svc
    namespace: boxoffice
  syncPolicy:
    automated:
      prune: true                    # delete what was removed from Git
      selfHeal: true                 # revert manual changes in the cluster
    syncOptions:
      - CreateNamespace=true

Troque your-user em repoURL, faça commit e push de tudo, e então registre a Application (o único kubectl apply do fluxo — daqui em diante quem aplica é o Argo CD):

git add .github k8s && git commit -m "lab 11: gitops" && git push
kubectl apply -f k8s/argocd/application.yaml
kubectl -n argocd get applications -w

Até o primeiro commit do Deploy chegar, o catalog não consegue subir (newTag ainda é o placeholder) e a Application aparece como Degraded — esperado. Quando o push dispara o CI e depois o Deploy, o Argo CD pega o novo commit (ele consulta a cada 3 minutos; Refresh na interface força a consulta). Você deve ver a Application passar para Synced e Healthy, a árvore de recursos na interface (Namespace, StatefulSet db, Deployment redis, Rollout catalog com seus pods…) e curl localhost/catalog/events/1 respondendo 200 pelo Traefik.

Drift e autocorreção

Mude o cluster à mão e veja o Argo CD desfazer:

kubectl -n boxoffice scale deployment redis --replicas=0
kubectl -n boxoffice get deployment redis -w

Em segundos o Argo CD reporta OutOfSync e, com selfHeal: true, restaura 1 réplica: o cluster só pode ser mudado pelo Git. Para fazer rollback de uma release, você faz git revert do commit de deploy.

Próximo: Canary com Argo Rollouts