Saltar al contenido

GitOps con Flux y ArgoCD: infraestructura por pull request

Guía práctica de GitOps con Flux y ArgoCD: estructura del repositorio, detección de deriva, reconciliación automática y entrega progresiva.

5 min de lectura
Diagrama de flujo de trabajo GitOps que muestra el repositorio Git como única fuente de verdad del estado de la infraestructura

Git como la única fuente de verdad

GitOps es la práctica de usar repositorios Git como fuente declarativa de verdad para la configuración de infraestructura y aplicaciones. En lugar de ejecutar comandos directamente contra los clústeres, confirmas el estado deseado en Git y un operador reconcilia el estado real para que coincida.

No se trata solo de control de versiones para la configuración: es un modelo de operación fundamentalmente distinto. Cada cambio es auditable, revisable y reversible a través del flujo de trabajo estándar de Git.

Estructura del repositorio para GitOps

La estructura del repositorio determina qué tan manejable será tu flujo de trabajo GitOps a medida que las aplicaciones y los entornos crecen.

tstypescript
// ❌ Monolithic repo — everything in one place
const badStructure = `
infra/
  deployment.yaml    # Which environment? Which app?
  service.yaml
  configmap.yaml
  ingress.yaml
  ... 200 more files
`;
 
// ✅ Structured by environment and application
interface GitOpsRepoStructure {
  apps: Record<string, {
    base: string[];
    overlays: Record<string, string[]>;
  }>;
  infrastructure: Record<string, string[]>;
}
 
const goodStructure: GitOpsRepoStructure = {
  apps: {
    "api-server": {
      base: [
        "deployment.yaml",
        "service.yaml",
        "hpa.yaml",
        "kustomization.yaml",
      ],
      overlays: {
        staging: ["kustomization.yaml", "replicas-patch.yaml"],
        production: [
          "kustomization.yaml",
          "replicas-patch.yaml",
          "resources-patch.yaml",
        ],
      },
    },
    "web-frontend": {
      base: [
        "deployment.yaml",
        "service.yaml",
        "ingress.yaml",
        "kustomization.yaml",
      ],
      overlays: {
        staging: ["kustomization.yaml"],
        production: ["kustomization.yaml", "cdn-config.yaml"],
      },
    },
  },
  infrastructure: {
    staging: ["namespace.yaml", "cert-manager.yaml", "ingress-controller.yaml"],
    production: [
      "namespace.yaml",
      "cert-manager.yaml",
      "ingress-controller.yaml",
      "monitoring.yaml",
    ],
  },
};

Los overlays de Kustomize permiten configuraciones específicas por entorno (número de réplicas, límites de recursos, reglas de ingress) compartiendo una base común. Esto elimina la deriva de configuración entre entornos al tiempo que permite las diferencias necesarias.

Configuración de aplicaciones en ArgoCD

ArgoCD observa los repositorios Git y sincroniza automáticamente el estado del clúster para que coincida con el estado deseado en Git.

ymlyaml
# argocd/applications/api-server-production.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: api-server-production
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: production
  source:
    repoURL: https://github.com/org/gitops-config.git
    targetRevision: main
    path: apps/api-server/overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
      allowEmpty: false
    syncOptions:
      - CreateNamespace=true
      - PrunePropagationPolicy=foreground
    retry:
      limit: 3
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m
tstypescript
// Programmatic ArgoCD application management
interface ArgoApplication {
  name: string;
  repoUrl: string;
  path: string;
  targetRevision: string;
  destination: {
    server: string;
    namespace: string;
  };
  syncPolicy: {
    automated: boolean;
    selfHeal: boolean;
    prune: boolean;
  };
}
 
function generateArgoApp(
  appName: string,
  environment: string,
  config: {
    repoUrl: string;
    autoSync: boolean;
  }
): ArgoApplication {
  return {
    name: `${appName}-${environment}`,
    repoUrl: config.repoUrl,
    path: `apps/${appName}/overlays/${environment}`,
    targetRevision: environment === "production" ? "main" : "develop",
    destination: {
      server: "https://kubernetes.default.svc",
      namespace: environment,
    },
    syncPolicy: {
      automated: config.autoSync,
      selfHeal: environment === "production",
      prune: true,
    },
  };
}

Detección de deriva y reconciliación

Los operadores GitOps comparan continuamente el estado deseado (Git) con el estado real (clúster). Cuando divergen —por cambios manuales, despliegues fallidos o modificaciones externas— el operador reconcilia.

tstypescript
interface DriftReport {
  application: string;
  status: "synced" | "out-of-sync" | "unknown";
  driftedResources: Array<{
    kind: string;
    name: string;
    namespace: string;
    diff: string;
    cause: "manual-edit" | "failed-sync" | "external-controller";
  }>;
  lastSyncAttempt: Date;
  lastSuccessfulSync: Date;
}
 
// ❌ Manual drift detection — checking randomly
async function manualDriftCheck(): Promise<void> {
  // Engineer remembers to check... sometimes
  // Runs kubectl diff manually
  // Misses drift in namespaces they do not check
}
 
// ✅ Automated drift detection with alerting
class DriftMonitor {
  constructor(
    private readonly argoClient: ArgoAPIClient,
    private readonly alerting: AlertingService
  ) {}
 
  async checkAllApplications(): Promise<DriftReport[]> {
    const apps = await this.argoClient.listApplications();
    const reports: DriftReport[] = [];
 
    for (const app of apps) {
      const status = await this.argoClient.getAppStatus(app.name);
 
      if (status.sync.status !== "Synced") {
        const report: DriftReport = {
          application: app.name,
          status: "out-of-sync",
          driftedResources: status.resources
            .filter((r: any) => r.status !== "Synced")
            .map((r: any) => ({
              kind: r.kind,
              name: r.name,
              namespace: r.namespace,
              diff: r.diff || "unknown",
              cause: this.classifyDriftCause(r),
            })),
          lastSyncAttempt: new Date(status.operationState?.startedAt),
          lastSuccessfulSync: new Date(status.history?.[0]?.deployedAt),
        };
 
        reports.push(report);
 
        if (report.driftedResources.length > 0) {
          await this.alerting.send({
            severity: "warning",
            title: `Drift detected: ${app.name}`,
            message: `${report.driftedResources.length} resources out of sync`,
          });
        }
      }
    }
 
    return reports;
  }
 
  private classifyDriftCause(resource: any): string {
    if (resource.hook) return "external-controller";
    if (resource.status === "OutOfSync") return "manual-edit";
    return "failed-sync";
  }
}

Flujo de promoción mediante pull requests

En un flujo de trabajo GitOps, promover un cambio de staging a producción significa abrir un pull request que actualice el overlay de producción.

tstypescript
interface PromotionRequest {
  application: string;
  fromEnvironment: string;
  toEnvironment: string;
  imageTag: string;
  changeDescription: string;
  author: string;
}
 
function generatePromotionPR(request: PromotionRequest): {
  title: string;
  body: string;
  files: Array<{ path: string; content: string }>;
} {
  const kustomizationPatch = `
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
images:
  - name: ${request.application}
    newTag: ${request.imageTag}
`;
 
  return {
    title: `Promote ${request.application} ${request.imageTag} to ${request.toEnvironment}`,
    body: `
## Promotion Request
 
**Application**: ${request.application}
**From**: ${request.fromEnvironment}
**To**: ${request.toEnvironment}
**Image Tag**: \`${request.imageTag}\`
 
### Change Description
${request.changeDescription}
 
### Verification
- [ ] Staging deployment healthy for 24+ hours
- [ ] No error rate increase in staging
- [ ] Load test results reviewed
- [ ] Rollback plan confirmed
    `.trim(),
    files: [
      {
        path: `apps/${request.application}/overlays/${request.toEnvironment}/kustomization.yaml`,
        content: kustomizationPatch.trim(),
      },
    ],
  };
}

Gestión de secretos en GitOps

Los secretos no pueden almacenarse en texto plano en Git. Sealed Secrets u operadores de secretos externos descifran los secretos dentro del clúster a partir de datos de origen cifrados.

tstypescript
interface SecretStrategy {
  approach: string;
  pros: string[];
  cons: string[];
  bestFor: string;
}
 
const secretStrategies: SecretStrategy[] = [
  {
    approach: "Sealed Secrets",
    pros: [
      "Encrypted secrets stored in Git",
      "Decryption only happens in-cluster",
      "Works with standard GitOps workflow",
    ],
    cons: [
      "Cluster-specific encryption keys",
      "Key rotation requires re-sealing all secrets",
    ],
    bestFor: "Single-cluster setups with moderate secret volume",
  },
  {
    approach: "External Secrets Operator",
    pros: [
      "Integrates with AWS Secrets Manager, Vault, etc.",
      "Centralized secret management",
      "Automatic rotation support",
    ],
    cons: [
      "Adds external dependency",
      "Requires network access to secret store",
    ],
    bestFor: "Multi-cluster setups using cloud secret managers",
  },
  {
    approach: "SOPS with Age/PGP",
    pros: [
      "Encrypts specific values within YAML files",
      "Diff-friendly — structure remains visible",
      "Works with any Git hosting",
    ],
    cons: [
      "Requires key management",
      "Manual encryption workflow",
    ],
    bestFor: "Teams wanting encrypted-at-rest secrets in Git with visible structure",
  },
];

Rollback mediante git revert

Uno de los aspectos más poderosos de GitOps es que el rollback es simplemente git revert. Todo el historial de despliegues está en Git.

tstypescript
interface RollbackPlan {
  application: string;
  currentCommit: string;
  targetCommit: string;
  affectedFiles: string[];
  estimatedDowntime: string;
}
 
async function executeGitOpsRollback(
  plan: RollbackPlan
): Promise<{ success: boolean; revertCommit: string }> {
  // In GitOps, rollback = git revert
  // ArgoCD detects the revert and applies the previous state
 
  console.log(`Rolling back ${plan.application}`);
  console.log(`  Current: ${plan.currentCommit}`);
  console.log(`  Target: ${plan.targetCommit}`);
  console.log(`  Files: ${plan.affectedFiles.join(", ")}`);
 
  // Create revert commit
  // ArgoCD auto-syncs to the reverted state
  // Previous deployment manifests are applied
  // Traffic shifts to the previous version
 
  return {
    success: true,
    revertCommit: "abc1234",
  };
}

Conclusiones clave

GitOps transforma la gestión de infraestructura de comandos imperativos en configuraciones declarativas versionadas en Git. Cada cambio pasa por una revisión en pull request, cada despliegue es auditable y cada rollback es un git revert.

Estructura tu repositorio GitOps con bases y overlays de Kustomize para gestionar configuraciones específicas por entorno sin duplicación. Usa ArgoCD o Flux para la reconciliación automatizada: garantizan continuamente que el estado del clúster coincida con el estado de Git. Maneja los secretos por separado usando Sealed Secrets o External Secrets Operator para mantener los datos sensibles cifrados.

El flujo de promoción mediante pull requests otorga a los cambios de producción la misma rigurosidad de revisión que a los cambios de código. Cuando surgen problemas, todo el historial de despliegues está en Git, lo que hace que el análisis de causa raíz sea sencillo y el rollback inmediato.

Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX