# 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 ```bash # 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 ```yaml # 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 ```yaml # 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 ```yaml # 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 ```yaml # 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 ```typescript // 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 ```typescript // 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 ```typescript // 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 ```typescript // 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 ```typescript // 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 ```bash # Rollback to previous version git revert HEAD git push # ArgoCD automatically syncs back ``` ### 4. Disaster Recovery Cluster destruido? Simplemente: ```bash # 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 ```yaml # 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 ```yaml # 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 ```bash # 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 ```