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:
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.
# 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 }
# 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
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
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
# 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
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.