DevOps

Capstone: Pipeline Completo de CI/CD Já leu

16 min de leitura

Capstone: Pipeline Completo de CI/CD
Um pipeline de CI/CD de qualidade não é uma sequência de scripts colados — é um produto de engenharia com suas próprias preocupações de confiabilidade, manutenibilidade e experiência do usuário. O desenvolvedor que faz u

Um pipeline de CI/CD de qualidade não é uma sequência de scripts colados — é um produto de engenharia com suas próprias preocupações de confiabilidade, manutenibilidade e experiência do usuário. O desenvolvedor que faz um push é o usuário desse produto. O feedback que recebe — rápido ou lento, claro ou confuso, confiável ou instável — determina diretamente a velocidade e a segurança com que o time entrega software.

O pipeline completo do capstone integra todas as práticas abordadas na série: testes automatizados com cobertura mínima, scanning de segurança em múltiplas camadas, build de imagens otimizado com cache, deploy via GitOps com ArgoCD, verificação pós-deploy com rollback automático e notificações contextuais. Para um monorepo com cinco serviços, o pipeline detecta inteligentemente quais serviços foram modificados e executa apenas o necessário — evitando o custo de buildar e deployar tudo a cada commit.


Detecção de Mudanças no Monorepo

O primeiro desafio de um monorepo é determinar quais serviços foram afetados por um conjunto de commits. A solução usa git diff para comparar os arquivos modificados com os prefixos de cada serviço:

# .github/workflows/detectar-mudancas.yml
# Workflow reutilizável — detecta quais serviços foram modificados
name: Detectar Mudanças

on:
  workflow_call:
    outputs:
      catalog:
        value: ${{ jobs.detectar.outputs.catalog }}
      user:
        value: ${{ jobs.detectar.outputs.user }}
      order:
        value: ${{ jobs.detectar.outputs.order }}
      notification:
        value: ${{ jobs.detectar.outputs.notification }}
      api-gateway:
        value: ${{ jobs.detectar.outputs.api-gateway }}
      infraestrutura:
        value: ${{ jobs.detectar.outputs.infraestrutura }}

jobs:
  detectar:
    runs-on: ubuntu-latest
    outputs:
      catalog:       ${{ steps.diff.outputs.catalog }}
      user:          ${{ steps.diff.outputs.user }}
      order:         ${{ steps.diff.outputs.order }}
      notification:  ${{ steps.diff.outputs.notification }}
      api-gateway:   ${{ steps.diff.outputs.api-gateway }}
      infraestrutura: ${{ steps.diff.outputs.infraestrutura }}

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Detecta serviços modificados
        id: diff
        run: |
          # Compara com o commit anterior em push, ou com a base em PR
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            BASE="${{ github.event.pull_request.base.sha }}"
          else
            BASE="${{ github.event.before }}"
            # Primeiro push — compara com HEAD~1
            if [ "$BASE" = "0000000000000000000000000000000000000000" ]; then
              BASE="HEAD~1"
            fi
          fi

          ARQUIVOS_MODIFICADOS=$(git diff --name-only "$BASE" HEAD)
          echo "Arquivos modificados:"
          echo "$ARQUIVOS_MODIFICADOS"

          # Verifica cada serviço
          for SERVICO in catalog user order notification api-gateway; do
            if echo "$ARQUIVOS_MODIFICADOS" | grep -q "^services/${SERVICO}-service/\|^packages/"; then
              echo "${SERVICO}=true" >> $GITHUB_OUTPUT
              echo "✓ ${SERVICO}-service: modificado"
            else
              echo "${SERVICO}=false" >> $GITHUB_OUTPUT
              echo "  ${SERVICO}-service: sem mudanças"
            fi
          done

          # Verifica infraestrutura
          if echo "$ARQUIVOS_MODIFICADOS" | grep -q "^infrastructure/"; then
            echo "infraestrutura=true" >> $GITHUB_OUTPUT
            echo "✓ infraestrutura: modificada"
          else
            echo "infraestrutura=false" >> $GITHUB_OUTPUT
          fi

Pipeline Principal de CI

# .github/workflows/ci.yml
name: CI — Integração Contínua

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true  # Cancela runs anteriores do mesmo branch

jobs:
  # ── Detecção de mudanças ──────────────────────────────────────────
  detectar:
    uses: ./.github/workflows/detectar-mudancas.yml

  # ── Segurança: secrets e SAST ─────────────────────────────────────
  seguranca-base:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Detecta segredos com Gitleaks
        uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Análise estática com Semgrep
        uses: returntocorp/semgrep-action@v1
        with:
          config: >
            p/nodejs
            p/secrets
            p/owasp-top-ten
            .semgrep/regras-customizadas.yml

      - name: Verifica IaC com Checkov
        uses: bridgecrewio/checkov-action@master
        with:
          directory: infrastructure/
          framework: terraform,kubernetes,dockerfile
          soft_fail: false

  # ── CI de cada serviço (executado em paralelo) ────────────────────
  ci-catalog:
    needs: [detectar, seguranca-base]
    if: needs.detectar.outputs.catalog == 'true'
    uses: ./.github/workflows/ci-servico.yml
    with:
      servico: catalog
      porta: 3001
    secrets: inherit

  ci-user:
    needs: [detectar, seguranca-base]
    if: needs.detectar.outputs.user == 'true'
    uses: ./.github/workflows/ci-servico.yml
    with:
      servico: user
      porta: 3002
    secrets: inherit

  ci-order:
    needs: [detectar, seguranca-base]
    if: needs.detectar.outputs.order == 'true'
    uses: ./.github/workflows/ci-servico.yml
    with:
      servico: order
      porta: 3003
    secrets: inherit

  ci-notification:
    needs: [detectar, seguranca-base]
    if: needs.detectar.outputs.notification == 'true'
    uses: ./.github/workflows/ci-servico.yml
    with:
      servico: notification
      porta: 3004
    secrets: inherit

  ci-api-gateway:
    needs: [detectar, seguranca-base]
    if: needs.detectar.outputs.api-gateway == 'true'
    uses: ./.github/workflows/ci-servico.yml
    with:
      servico: api-gateway
      porta: 3000
    secrets: inherit

  # ── Gate de qualidade — só prossegue se tudo passou ───────────────
  ci-concluido:
    needs: [ci-catalog, ci-user, ci-order, ci-notification, ci-api-gateway]
    if: always()
    runs-on: ubuntu-latest
    steps:
      - name: Verifica resultado de todos os CIs
        run: |
          RESULTADOS=(
            "${{ needs.ci-catalog.result }}"
            "${{ needs.ci-user.result }}"
            "${{ needs.ci-order.result }}"
            "${{ needs.ci-notification.result }}"
            "${{ needs.ci-api-gateway.result }}"
          )

          for RESULTADO in "${RESULTADOS[@]}"; do
            if [ "$RESULTADO" = "failure" ]; then
              echo "::error::CI falhou — deploy bloqueado"
              exit 1
            fi
          done

          echo "✅ Todos os CIs passaram ou foram pulados"

Workflow Reutilizável de CI por Serviço

# .github/workflows/ci-servico.yml
name: CI Serviço (Reutilizável)

on:
  workflow_call:
    inputs:
      servico:
        required: true
        type: string
      porta:
        required: true
        type: number

jobs:
  testar:
    runs-on: ubuntu-latest
    name: Testar ${{ inputs.servico }}-service

    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: test_db
          POSTGRES_USER: test_user
          POSTGRES_PASSWORD: test_password
        options: >-
          --health-cmd pg_isready
          --health-interval 5s
          --health-timeout 3s
          --health-retries 10
        ports:
          - 5432:5432

      redis:
        image: redis:7-alpine
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 5s
          --health-timeout 3s
          --health-retries 10
        ports:
          - 6379:6379

    defaults:
      run:
        working-directory: services/${{ inputs.servico }}-service

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
          cache-dependency-path: |
            services/${{ inputs.servico }}-service/package-lock.json
            packages/core/package-lock.json

      - name: Instala dependências do core
        run: npm ci
        working-directory: packages/core

      - name: Instala dependências do serviço
        run: npm ci

      - name: Verifica lint
        run: npm run lint

      - name: Executa testes unitários
        run: npm run test:unit -- --coverage

      - name: Executa testes de integração
        run: npm run test:integration
        env:
          DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
          REDIS_URL:    redis://localhost:6379
          NODE_ENV:     test

      - name: Verifica cobertura mínima
        run: |
          COBERTURA=$(cat coverage/coverage-summary.json \
            | jq '.total.lines.pct')
          echo "Cobertura: ${COBERTURA}%"

          if (( $(echo "$COBERTURA < 80" | bc -l) )); then
            echo "::error::Cobertura abaixo do mínimo: ${COBERTURA}% (mínimo: 80%)"
            exit 1
          fi

      - name: Upload cobertura para Codecov
        uses: codecov/codecov-action@v4
        with:
          flags: ${{ inputs.servico }}
          token: ${{ secrets.CODECOV_TOKEN }}

  construir-imagem:
    runs-on: ubuntu-latest
    needs: testar
    name: Construir imagem ${{ inputs.servico }}-service
    outputs:
      imagem: ${{ steps.build.outputs.imagem }}
      digest: ${{ steps.build.outputs.digest }}

    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: actions/cache@v4
        with:
          path: /tmp/.buildx-cache-${{ inputs.servico }}
          key: ${{ runner.os }}-buildx-${{ inputs.servico }}-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-${{ inputs.servico }}-

      - name: Extrai metadados da imagem
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository_owner }}/${{ inputs.servico }}-service
          tags: |
            type=sha,prefix=sha-,format=short
            type=ref,event=branch
            type=semver,pattern={{version}}

      - name: Constrói e publica imagem
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          file: services/${{ inputs.servico }}-service/Dockerfile
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=local,src=/tmp/.buildx-cache-${{ inputs.servico }}
          cache-to: type=local,dest=/tmp/.buildx-cache-${{ inputs.servico }}-new,mode=max
          provenance: true
          sbom: true  # Gera Software Bill of Materials

      - name: Rotaciona cache do Buildx
        run: |
          rm -rf /tmp/.buildx-cache-${{ inputs.servico }}
          mv /tmp/.buildx-cache-${{ inputs.servico }}-new \
             /tmp/.buildx-cache-${{ inputs.servico }}

      - name: Escaneia imagem com Trivy
        if: github.event_name != 'pull_request'
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ghcr.io/${{ github.repository_owner }}/${{ inputs.servico }}-service:sha-${{ github.sha }}
          format: sarif
          output: trivy-${{ inputs.servico }}.sarif
          severity: CRITICAL,HIGH
          exit-code: '1'
          ignore-unfixed: true

      - name: Upload resultados Trivy
        if: always() && github.event_name != 'pull_request'
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy-${{ inputs.servico }}.sarif

      - name: Exporta outputs
        id: exportar
        run: |
          IMAGEM="ghcr.io/${{ github.repository_owner }}/${{ inputs.servico }}-service:sha-${{ github.sha }}"
          echo "imagem=${IMAGEM}" >> $GITHUB_OUTPUT

Pipeline de Deploy via GitOps

# .github/workflows/deploy.yml
name: Deploy — Produção

on:
  push:
    branches: [main]

jobs:
  # ── Reutiliza o CI completo ────────────────────────────────────────
  detectar:
    uses: ./.github/workflows/detectar-mudancas.yml

  ci:
    needs: detectar
    uses: ./.github/workflows/ci.yml
    secrets: inherit

  # ── Deploy em staging primeiro ────────────────────────────────────
  deploy-staging:
    needs: ci
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://staging.loja.empresa.com
    outputs:
      sha-curto: ${{ steps.vars.outputs.sha-curto }}

    steps:
      - uses: actions/checkout@v4

      - name: Exporta variáveis
        id: vars
        run: echo "sha-curto=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT

      - name: Clona repositório GitOps
        uses: actions/checkout@v4
        with:
          repository: empresa/gitops-loja
          token: ${{ secrets.GITOPS_TOKEN }}
          path: gitops

      - name: Atualiza imagens no staging
        run: |
          cd gitops
          SHA="${{ steps.vars.outputs.sha-curto }}"

          # Atualiza a tag de cada serviço modificado
          for SERVICO in catalog user order notification api-gateway; do
            MODIFICADO="needs.detectar.outputs.${SERVICO}"
            if [ "${!MODIFICADO}" = "true" ] || true; then
              cd workloads/${SERVICO}-service/staging
              kustomize edit set image \
                ghcr.io/empresa/${SERVICO}-service:sha-${SHA}
              cd ../../..
            fi
          done

          git config user.email "pipeline@empresa.com"
          git config user.name "Pipeline CI/CD"
          git add .
          git diff --staged --quiet || git commit -m \
            "chore(staging): deploy sha-${SHA}

          Serviços atualizados pelo pipeline.
          Repositório: ${{ github.repository }}
          Run: ${{ github.run_id }}"
          git push

      - name: Aguarda sincronização do ArgoCD em staging
        run: |
          # Instala argocd CLI
          curl -sSL -o argocd \
            https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
          chmod +x argocd
          sudo mv argocd /usr/local/bin/

          argocd login ${{ vars.ARGOCD_SERVER }} \
            --auth-token ${{ secrets.ARGOCD_TOKEN }} \
            --grpc-web

          # Aguarda cada aplicação sincronizar e ficar saudável
          for SERVICO in catalog user order notification api-gateway; do
            argocd app wait loja-staging-${SERVICO} \
              --sync \
              --health \
              --timeout 300
          done

      - name: Executa smoke tests em staging
        run: |
          BASE_URL="https://staging.loja.empresa.com"

          echo "Verificando saúde dos serviços em staging..."

          verificar() {
            local URL="$1"
            local DESCRICAO="$2"
            local STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
              --max-time 10 "$URL")

            if [ "$STATUS" = "200" ]; then
              echo "  ✓ $DESCRICAO ($STATUS)"
            else
              echo "  ✗ $DESCRICAO ($STATUS)"
              return 1
            fi
          }

          verificar "${BASE_URL}/api/health"        "API Gateway health"
          verificar "${BASE_URL}/api/v1/produtos"   "Catálogo — listar produtos"
          verificar "${BASE_URL}/api/v1/health/live" "Order service liveness"

          echo "✅ Smoke tests em staging passaram"

  # ── Deploy em produção com aprovação ─────────────────────────────
  deploy-producao:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://loja.empresa.com

    steps:
      - uses: actions/checkout@v4

      - name: Clona repositório GitOps
        uses: actions/checkout@v4
        with:
          repository: empresa/gitops-loja
          token: ${{ secrets.GITOPS_TOKEN }}
          path: gitops

      - name: Atualiza imagens em produção
        run: |
          cd gitops
          SHA="${{ needs.deploy-staging.outputs.sha-curto }}"

          for SERVICO in catalog user order notification api-gateway; do
            cd workloads/${SERVICO}-service/producao
            kustomize edit set image \
              ghcr.io/empresa/${SERVICO}-service:sha-${SHA}
            cd ../../..
          done

          git config user.email "pipeline@empresa.com"
          git config user.name "Pipeline CI/CD"
          git add .
          git diff --staged --quiet || git commit -m \
            "chore(producao): deploy sha-${SHA}

          Deploy promovido do staging após aprovação.
          Aprovador: ${{ github.actor }}
          Run: ${{ github.run_id }}"
          git push

      - name: Aguarda sincronização do ArgoCD em produção
        run: |
          argocd login ${{ vars.ARGOCD_SERVER }} \
            --auth-token ${{ secrets.ARGOCD_TOKEN }} \
            --grpc-web

          for SERVICO in catalog user order notification api-gateway; do
            echo "Aguardando loja-producao-${SERVICO}..."
            argocd app wait loja-producao-${SERVICO} \
              --sync \
              --health \
              --timeout 600
          done

      - name: Verifica métricas pós-deploy
        id: verificar-metricas
        run: |
          # Aguarda 2 minutos para métricas estabilizarem
          echo "Aguardando estabilização das métricas..."
          sleep 120

          PROMETHEUS="${{ vars.PROMETHEUS_URL }}"

          # Verifica taxa de erros (deve ser < 1%)
          TAXA_ERROS=$(curl -s "${PROMETHEUS}/api/v1/query" \
            --data-urlencode 'query=
              sum(rate(http_requests_total{
                status=~"5..",
                namespace="producao"
              }[2m]))
              /
              sum(rate(http_requests_total{
                namespace="producao"
              }[2m])) * 100
            ' | jq '.data.result[0].value[1] | tonumber')

          echo "Taxa de erros: ${TAXA_ERROS}%"

          # Verifica latência p99 (deve ser < 1s)
          LATENCIA_P99=$(curl -s "${PROMETHEUS}/api/v1/query" \
            --data-urlencode 'query=
              histogram_quantile(0.99,
                sum(rate(http_request_duration_seconds_bucket{
                  namespace="producao"
                }[2m])) by (le)
              )
            ' | jq '.data.result[0].value[1] | tonumber')

          echo "Latência p99: ${LATENCIA_P99}s"

          # Falha se métricas estiverem fora do threshold
          if (( $(echo "$TAXA_ERROS > 1" | bc -l) )); then
            echo "::error::Taxa de erros elevada: ${TAXA_ERROS}%"
            echo "rollback=true" >> $GITHUB_OUTPUT
            exit 1
          fi

          if (( $(echo "$LATENCIA_P99 > 1" | bc -l) )); then
            echo "::error::Latência p99 elevada: ${LATENCIA_P99}s"
            echo "rollback=true" >> $GITHUB_OUTPUT
            exit 1
          fi

          echo "✅ Métricas pós-deploy dentro dos thresholds"
          echo "rollback=false" >> $GITHUB_OUTPUT

      - name: Rollback automático em caso de falha
        if: failure() && steps.verificar-metricas.outputs.rollback == 'true'
        run: |
          echo "⚠️  Iniciando rollback automático..."

          argocd login ${{ vars.ARGOCD_SERVER }} \
            --auth-token ${{ secrets.ARGOCD_TOKEN }} \
            --grpc-web

          for SERVICO in catalog user order notification api-gateway; do
            argocd app rollback loja-producao-${SERVICO} --hard-refresh || true
          done

          echo "Rollback concluído"

      - name: Notifica resultado do deploy
        if: always()
        uses: slackapi/slack-github-action@v1.26.0
        with:
          channel-id: ${{ vars.SLACK_DEPLOYS_CHANNEL }}
          payload: |
            {
              "blocks": [
                {
                  "type": "header",
                  "text": {
                    "type": "plain_text",
                    "text": "${{ job.status == 'success' && '✅ Deploy em Produção — Sucesso' || '❌ Deploy em Produção — Falha' }}"
                  }
                },
                {
                  "type": "section",
                  "fields": [
                    {
                      "type": "mrkdwn",
                      "text": "*SHA:*\n`${{ needs.deploy-staging.outputs.sha-curto }}`"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Deployado por:*\n${{ github.actor }}"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Branch:*\n${{ github.ref_name }}"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "*Status:*\n${{ job.status }}"
                    }
                  ]
                },
                {
                  "type": "actions",
                  "elements": [
                    {
                      "type": "button",
                      "text": { "type": "plain_text", "text": "Ver Pipeline" },
                      "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
                    },
                    {
                      "type": "button",
                      "text": { "type": "plain_text", "text": "Grafana" },
                      "url": "${{ vars.GRAFANA_URL }}/d/loja-overview"
                    }
                  ]
                }
              ]
            }
        env:
          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}

Pipeline de Infraestrutura

# .github/workflows/deploy-infraestrutura.yml
name: Deploy — Infraestrutura

on:
  push:
    branches: [main]
    paths:
      - 'infrastructure/terraform/**'
  pull_request:
    branches: [main]
    paths:
      - 'infrastructure/terraform/**'
  workflow_dispatch:
    inputs:
      ambiente:
        description: 'Ambiente alvo'
        required: true
        type: choice
        options: [staging, production]
      acao:
        description: 'Ação Terraform'
        required: true
        type: choice
        options: [plan, apply]
        default: plan

jobs:
  validar:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "~1.7"

      - name: Terraform Format Check
        run: terraform fmt -check -recursive infrastructure/

      - name: Terraform Validate — Staging
        run: |
          cd infrastructure/terraform/environments/staging
          terraform init -backend=false
          terraform validate

      - name: Terraform Validate — Production
        run: |
          cd infrastructure/terraform/environments/production
          terraform init -backend=false
          terraform validate

      - name: Checkov
        uses: bridgecrewio/checkov-action@master
        with:
          directory: infrastructure/terraform/
          framework: terraform

  plan-staging:
    needs: validar
    runs-on: ubuntu-latest
    environment: staging
    if: github.event_name == 'pull_request' || inputs.ambiente == 'staging'

    permissions:
      contents: read
      id-token: write
      pull-requests: write

    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_TERRAFORM_ROLE_ARN }}
          aws-region: us-east-1

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: "~1.7"

      - name: Terraform Init
        run: |
          cd infrastructure/terraform/environments/staging
          terraform init \
            -backend-config="bucket=${{ vars.TF_STATE_BUCKET }}" \
            -backend-config="key=staging/terraform.tfstate" \
            -backend-config="region=us-east-1" \
            -backend-config="dynamodb_table=${{ vars.TF_LOCK_TABLE }}"

      - name: Terraform Plan
        id: plan
        run: |
          cd infrastructure/terraform/environments/staging
          terraform plan \
            -var="environment=staging" \
            -no-color \
            -out=tfplan \
            2>&1 | tee plan-output.txt

          # Extrai sumário do plano
          echo "SUMARIO<<EOF" >> $GITHUB_ENV
          grep -E "^Plan:|^No changes" plan-output.txt || echo "Plano gerado"
          echo "EOF" >> $GITHUB_ENV

      - name: Comenta plano no PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const planOutput = fs.readFileSync(
              'infrastructure/terraform/environments/staging/plan-output.txt',
              'utf8'
            );

            // Trunca se muito longo
            const truncated = planOutput.length > 65000
              ? planOutput.substring(0, 65000) + '\n... (truncado)'
              : planOutput;

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## Terraform Plan — Staging\n\`\`\`\n${truncated}\n\`\`\``
            });

  apply-producao:
    needs: validar
    runs-on: ubuntu-latest
    environment: production
    if: |
      github.event_name == 'push' && github.ref == 'refs/heads/main' ||
      (inputs.ambiente == 'production' && inputs.acao == 'apply')

    permissions:
      contents: read
      id-token: write

    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_TERRAFORM_ROLE_ARN }}
          aws-region: us-east-1

      - uses: hashicorp/setup-terraform@v3

      - name: Terraform Init
        run: |
          cd infrastructure/terraform/environments/production
          terraform init \
            -backend-config="bucket=${{ vars.TF_STATE_BUCKET }}" \
            -backend-config="key=production/terraform.tfstate" \
            -backend-config="region=us-east-1"

      - name: Terraform Plan
        run: |
          cd infrastructure/terraform/environments/production
          terraform plan \
            -var="environment=production" \
            -out=tfplan

      - name: Terraform Apply
        run: |
          cd infrastructure/terraform/environments/production
          terraform apply -auto-approve tfplan

Configuração do ArgoCD para o Capstone

# infrastructure/kubernetes/platform/argocd/apps-loja.yaml
# App of Apps — ArgoCD gerencia todas as aplicações da loja
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: loja-apps
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: https://github.com/empresa/gitops-loja
    targetRevision: main
    path: apps
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
---
# apps/loja-producao-order.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: loja-producao-order
  namespace: argocd
spec:
  project: loja-producao

  source:
    repoURL: https://github.com/empresa/gitops-loja
    targetRevision: main
    path: workloads/order-service/producao

  destination:
    server: https://kubernetes.default.svc
    namespace: producao

  syncPolicy:
    automated:
      prune: true
      selfHeal: true
      allowEmpty: false
    syncOptions:
      - CreateNamespace=false
      - PrunePropagationPolicy=foreground
    retry:
      limit: 3
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas  # Gerenciado pelo HPA

O Que Vem a Seguir

O próximo e último artigo do capstone — e da série — implementa as operações em produção: dashboards de observabilidade calibrados para a plataforma de e-commerce, alertas com runbooks detalhados, um experimento de Chaos Engineering executado no sistema completo e uma retrospectiva sobre a jornada de doze meses desta série.


Referências para Aprofundamento

GitHub Actions - GitHub Actions Documentation — docs.github.com — Documentação completa do GitHub Actions com referência de sintaxe de workflows, contexts, expressions e os runners disponíveis. - Reusable Workflows — docs.github.com — Guia completo de workflows reutilizáveis no GitHub Actions com exemplos de passagem de inputs, outputs e secrets entre workflows.

GitOps e ArgoCD - ArgoCD App of Apps Pattern — argo-cd.readthedocs.io — Documentação do padrão App of Apps no ArgoCD para gerenciamento de múltiplas aplicações a partir de um único repositório GitOps. - GitOps with ArgoCD and Kustomize — codefresh.io — Guia prático de GitOps combinando ArgoCD e Kustomize para gerenciamento de múltiplos ambientes com overlays.

Comentários

Mais em DevOps

Capstone: Operações em Produção e Retrospectiva da Jornada
Capstone: Operações em Produção e Retrospectiva da Jornada

Um sistema de software não termina quando o último deploy é feito. Ele começa...

Kubernetes em Produção: Segurança, GitOps e Deploys Avançados
Kubernetes em Produção: Segurança, GitOps e Deploys Avançados

Criar um cluster EKS e fazer uma aplicação rodar nele é relativamente simples...

EKS: Kubernetes Gerenciado na AWS
EKS: Kubernetes Gerenciado na AWS

Operar o plano de controle do Kubernetes — manter o etcd replicado, atualizar...