Files
aiworker/docs/06-deployment/gitops.md
Hector Ros db71705842 Complete documentation for future sessions
- CLAUDE.md for AI agents to understand the codebase
- GITEA-GUIDE.md centralizes all Gitea operations (API, Registry, Auth)
- DEVELOPMENT-WORKFLOW.md explains complete dev process
- ROADMAP.md, NEXT-SESSION.md for planning
- QUICK-REFERENCE.md, TROUBLESHOOTING.md for daily use
- 40+ detailed docs in /docs folder
- Backend as submodule from Gitea

Everything documented for autonomous operation.

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2026-01-20 00:37:19 +01:00

11 KiB

GitOps con ArgoCD

¿Qué es GitOps?

GitOps usa Git como fuente única de verdad para infraestructura y aplicaciones. Los cambios se hacen via commits, y herramientas como ArgoCD sincronizan automáticamente el estado deseado en Git con el estado real en Kubernetes.

Instalación de ArgoCD

# Create namespace
kubectl create namespace argocd

# Install ArgoCD
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Wait for pods
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=argocd-server -n argocd --timeout=300s

# Get initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d

# Port forward to access UI
kubectl port-forward svc/argocd-server -n argocd 8080:443

# Access at: https://localhost:8080
# Username: admin
# Password: (from above command)

Estructura de Repositorio GitOps

gitops-repo/
├── projects/
│   ├── backend/
│   │   ├── base/
│   │   │   ├── deployment.yaml
│   │   │   ├── service.yaml
│   │   │   └── kustomization.yaml
│   │   ├── dev/
│   │   │   ├── kustomization.yaml
│   │   │   └── patches.yaml
│   │   ├── staging/
│   │   │   ├── kustomization.yaml
│   │   │   └── patches.yaml
│   │   └── production/
│   │       ├── kustomization.yaml
│   │       └── patches.yaml
│   │
│   └── my-project/
│       ├── base/
│       ├── dev/
│       ├── staging/
│       └── production/
│
└── argocd/
    ├── applications/
    │   ├── backend-dev.yaml
    │   ├── backend-staging.yaml
    │   ├── backend-production.yaml
    │   └── my-project-production.yaml
    └── app-of-apps.yaml

Base Manifests con Kustomize

# projects/backend/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 2
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
      - name: backend
        image: aiworker/backend:latest
        ports:
        - containerPort: 3000
        env:
        - name: NODE_ENV
          value: production
        resources:
          requests:
            cpu: 250m
            memory: 512Mi
          limits:
            cpu: 1
            memory: 2Gi
---
# projects/backend/base/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: backend
spec:
  selector:
    app: backend
  ports:
  - port: 3000
    targetPort: 3000
---
# projects/backend/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- deployment.yaml
- service.yaml

commonLabels:
  app: backend
  managed-by: argocd

Environment Overlays

# projects/backend/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: control-plane

bases:
- ../base

patchesStrategicMerge:
- patches.yaml

images:
- name: aiworker/backend
  newTag: v1.2.3  # This gets updated automatically

replicas:
- name: backend
  count: 3

configMapGenerator:
- name: backend-config
  literals:
  - NODE_ENV=production
  - LOG_LEVEL=info
---
# projects/backend/production/patches.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  template:
    spec:
      containers:
      - name: backend
        resources:
          requests:
            cpu: 500m
            memory: 1Gi
          limits:
            cpu: 2
            memory: 4Gi

ArgoCD Application

# argocd/applications/backend-production.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: backend-production
  namespace: argocd
spec:
  project: default

  source:
    repoURL: https://git.aiworker.dev/aiworker/gitops
    targetRevision: HEAD
    path: projects/backend/production

  destination:
    server: https://kubernetes.default.svc
    namespace: control-plane

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

  revisionHistoryLimit: 10

App of Apps Pattern

# argocd/app-of-apps.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: aiworker-apps
  namespace: argocd
spec:
  project: default

  source:
    repoURL: https://git.aiworker.dev/aiworker/gitops
    targetRevision: HEAD
    path: argocd/applications

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

  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Actualización de Imagen desde Backend

// services/gitops/updater.ts
import { Octokit } from '@octokit/rest'
import yaml from 'js-yaml'
import { logger } from '../../utils/logger'

export class GitOpsUpdater {
  private octokit: Octokit
  private repo: string
  private owner: string

  constructor() {
    this.octokit = new Octokit({
      baseUrl: process.env.GITEA_URL,
      auth: process.env.GITEA_TOKEN,
    })
    this.repo = 'gitops'
    this.owner = 'aiworker'
  }

  async updateImage(params: {
    project: string
    environment: string
    imageTag: string
  }) {
    const { project, environment, imageTag } = params
    const path = `projects/${project}/${environment}/kustomization.yaml`

    logger.info(`Updating GitOps: ${project}/${environment}${imageTag}`)

    try {
      // 1. Get current file
      const { data: fileData } = await this.octokit.repos.getContent({
        owner: this.owner,
        repo: this.repo,
        path,
      })

      if (Array.isArray(fileData) || fileData.type !== 'file') {
        throw new Error('Invalid file')
      }

      // 2. Decode content
      const content = Buffer.from(fileData.content, 'base64').toString('utf-8')
      const kustomization = yaml.load(content) as any

      // 3. Update image tag
      if (!kustomization.images) {
        kustomization.images = []
      }

      const imageIndex = kustomization.images.findIndex(
        (img: any) => img.name === `aiworker/${project}`
      )

      if (imageIndex >= 0) {
        kustomization.images[imageIndex].newTag = imageTag
      } else {
        kustomization.images.push({
          name: `aiworker/${project}`,
          newTag: imageTag,
        })
      }

      // 4. Encode new content
      const newContent = yaml.dump(kustomization)
      const newContentBase64 = Buffer.from(newContent).toString('base64')

      // 5. Commit changes
      await this.octokit.repos.createOrUpdateFileContents({
        owner: this.owner,
        repo: this.repo,
        path,
        message: `Update ${project} ${environment} to ${imageTag}`,
        content: newContentBase64,
        sha: fileData.sha,
      })

      logger.info(`GitOps updated: ${project}/${environment}`)

      return { success: true }
    } catch (error: any) {
      logger.error('Failed to update GitOps:', error)
      throw error
    }
  }
}

Integración con CI/CD

// services/queue/deploy-worker.ts
import { GitOpsUpdater } from '../gitops/updater'

const gitopsUpdater = new GitOpsUpdater()

export const deployWorker = new Worker('deploys', async (job) => {
  const { deploymentId, projectId, environment, commitHash } = job.data

  // ... deployment logic ...

  // Update GitOps repo
  await gitopsUpdater.updateImage({
    project: project.name,
    environment,
    imageTag: commitHash.slice(0, 7),
  })

  // ArgoCD will automatically sync within 3 minutes
  // Or trigger manual sync:
  await triggerArgoCDSync(project.name, environment)

  logger.info('GitOps updated, ArgoCD will sync')
})

Trigger ArgoCD Sync

// services/gitops/argocd.ts
export async function triggerArgoCDSync(project: string, environment: string) {
  const appName = `${project}-${environment}`
  const argoCDUrl = process.env.ARGOCD_URL || 'https://argocd.aiworker.dev'
  const token = process.env.ARGOCD_TOKEN

  const response = await fetch(`${argoCDUrl}/api/v1/applications/${appName}/sync`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      prune: false,
      dryRun: false,
      strategy: {
        hook: {},
      },
    }),
  })

  if (!response.ok) {
    throw new Error(`ArgoCD sync failed: ${response.statusText}`)
  }

  logger.info(`Triggered ArgoCD sync: ${appName}`)
}

Health Status from ArgoCD

// services/gitops/argocd.ts
export async function getApplicationStatus(appName: string) {
  const argoCDUrl = process.env.ARGOCD_URL
  const token = process.env.ARGOCD_TOKEN

  const response = await fetch(`${argoCDUrl}/api/v1/applications/${appName}`, {
    headers: {
      'Authorization': `Bearer ${token}`,
    },
  })

  const app = await response.json()

  return {
    syncStatus: app.status.sync.status, // Synced, OutOfSync
    healthStatus: app.status.health.status, // Healthy, Progressing, Degraded
    lastSyncedAt: app.status.operationState?.finishedAt,
  }
}

Monitoring Dashboard

// api/routes/gitops.ts
router.get('/gitops/status', async (req, res) => {
  const apps = ['backend-production', 'backend-staging', 'backend-dev']

  const statuses = await Promise.all(
    apps.map(async (app) => {
      const status = await getApplicationStatus(app)
      return {
        name: app,
        ...status,
      }
    })
  )

  res.json({ applications: statuses })
})

Benefits of GitOps

1. Declarative

Todo el estado deseado está en Git, versionado y auditable.

2. Auditabilidad

Cada cambio tiene un commit con autor, timestamp y descripción.

3. Rollback Fácil

# Rollback to previous version
git revert HEAD
git push

# ArgoCD automatically syncs back

4. Disaster Recovery

Cluster destruido? Simplemente:

# Reinstall ArgoCD
kubectl apply -f argocd-install.yaml

# Deploy app-of-apps
kubectl apply -f app-of-apps.yaml

# Todo vuelve al estado en Git

5. Multi-Cluster

# Deploy same app to multiple clusters
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: backend-cluster-2
spec:
  destination:
    server: https://cluster-2.example.com
    namespace: control-plane
  # ... same source

Best Practices

1. Separate Repo

Mantener GitOps separado del código de aplicación:

  • App repo: Código fuente
  • GitOps repo: Manifests de K8s

2. Environment Branches (Optional)

main → production
staging → staging environment
dev → dev environment

3. Secrets Management

No commitear secrets en Git. Usar:

  • Sealed Secrets
  • External Secrets Operator
  • Vault

4. Progressive Rollout

# Use Argo Rollouts for canary/blue-green
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: backend
spec:
  strategy:
    canary:
      steps:
      - setWeight: 20
      - pause: {duration: 5m}
      - setWeight: 50
      - pause: {duration: 5m}
      - setWeight: 100

Troubleshooting

# Ver estado de aplicación
argocd app get backend-production

# Ver diferencias
argocd app diff backend-production

# Sync manual
argocd app sync backend-production

# Ver logs
kubectl logs -n argocd deployment/argocd-application-controller

# Refresh (fetch latest from Git)
argocd app refresh backend-production