Free lesson · GenAI Platform Engineering

Compare GitOps controllers: ArgoCD ApplicationSet vs Flux CD

You will implement multi-environment promotion (dev → staging → prod) using both ArgoCD and Flux CD, then compare them. Both run entirely inside the GKE cluster as K8s controllers. ArgoCD approach (already built): ApplicationSet generates Application resources per environment. Promotion: merge to the staging branch → ArgoCD syncs staging. Merge to main → ArgoCD syncs prod. Flux CD approach: install Flux on GKE with flux bootstrap github. Create a Kustomization resource per environment pointing to different paths in the Git repo: clusters/dev/, clusters/staging/, clusters/prod/. Flux watches the repo and automatically reconciles when changes are pushed. Configure Flux image automation: ImageRepository scans Artifact Registry for new tags, ImagePolicy selects the latest semver tag, ImageUpdateAutomation commits the new tag to the repo. This creates a full image-to-deploy automation loop — push a new image and Flux updates the manifests automatically. Compare: ArgoCD (UI-driven, Application CRD, manual sync option) vs Flux CD (Git-first, no UI, full automation). Evaluate: GitOps purity (Flux is more strictly Git-driven), UI/UX (ArgoCD has a dashboard, Flux uses CLI), image automation (Flux has native support, ArgoCD needs Argo Image Updater), and multi-tenancy.

Course: DevOps Foundations for GenAI Engineers · Chapter 4 · ArgoCD & GitOps

Free to read — no subscription required.

Introduction

When you stand up a GitOps controller for an AI platform, picking between ArgoCD and Flux CD shapes how every model rollout, evaluation gate, and multi-environment promotion will work for years. Teams that pick on vibes — "ArgoCD has a UI" or "Flux is more cloud-native" — often discover months later that they need the other controller's primitive (ApplicationSet fan-out, native image automation) and end up bolting on third-party tools or running both. Get the comparison wrong and you either burn engineering time fighting the controller or you ship model images by hand. By the end of this lesson you will be able to map ArgoCD ApplicationSet and Flux CD onto concrete AI-platform requirements — multi-environment fan-out, image-driven model rollouts, and operator visibility — and justify which controller fits your constraints.

Key Terminology

  • ApplicationSet — an ArgoCD CRD that generates many Application resources from a single template using generators (list, cluster, git, matrix). It is how ArgoCD scales one declaration into per-environment or per-cluster deployments without copy-paste.
  • Kustomization (Flux) — Flux's reconciler CRD pointing at a path in a GitRepository; it applies Kustomize overlays continuously and is the unit Flux uses to promote and roll back.
  • Image automation pipeline — Flux's built-in trio of ImageRepository, ImagePolicy, and ImageUpdateAutomation that detects new container tags, picks one by semver/policy, and commits the update back to Git so the cluster reconciles automatically.
  • Reconciliation loop — the controller's continuous compare-and-apply between Git (desired) and cluster (actual). ArgoCD surfaces drift in a UI and lets you choose manual or auto sync; Flux always auto-reconciles on an interval.
  • Sync wave / dependency ordering — ArgoCD's mechanism for sequencing resource application (CRDs before CRs, namespaces before pods); the Flux equivalent is dependsOn between Kustomization resources.

Concepts

ArgoCD ApplicationSet — UI-first, templated fan-out

ArgoCD's ApplicationSet generates many Application resources from one template. For an AI inference service, a single ApplicationSet with a list generator produces one Application per environment (dev, staging, prod), each pointing at a different path or revision. The ArgoCD UI then shows topology, drift, and sync status for every generated Application — operators see exactly which Deployment, Service, and ConfigMap each environment is running and can trigger or roll back syncs from the dashboard. Sync can be manual (prod) or automated (dev) per Application, and Projects + RBAC partition who can touch which Applications (see Code Walkthrough).

Loading diagram...

Flux CD — Git-first reconciliation with native image automation

Flux installs via flux bootstrap, which commits Flux's own configuration into your Git repo so Flux manages itself via GitOps. There is no UI; every action is a Git commit or a kubectl get against Flux CRDs. Where Flux pulls ahead for AI platforms is its native image automation: ImageRepository scans the registry, ImagePolicy picks the next tag by semver (>=1.0.0 <2.0.0) or alphabetical order, and ImageUpdateAutomation writes the chosen tag back into the Git manifest and pushes a commit. The cluster reconciles the new commit on the next interval — fully closed loop, no human in the path. This matters for AI inference where a passing evaluation gate produces a new image tag that should flow to staging within minutes. ArgoCD requires a separate Argo Image Updater add-on to do the same thing (see Code Walkthrough).

Loading diagram...

Selection criteria for AI platforms

Pick ArgoCD ApplicationSet when operators need a dashboard for drift and rollout status, when prod requires manual approval before sync, when many teams share clusters and you need Project-scoped RBAC, or when you fan out one manifest set across many clusters. Pick Flux CD when high-frequency model image rollouts need a hands-off image-to-deploy loop, when the team has standardized on Kustomize overlays, or when you want the controller itself to be GitOps-managed. The two are not mutually exclusive — running ArgoCD in shared platform clusters and Flux in single-team inference clusters is a common pattern.

Code Walkthrough

The two snippets below demonstrate the defining primitive of each controller from the Concepts section: ArgoCD's ApplicationSet generating per-environment Applications, and Flux's image automation trio committing new tags back to Git. Read them side by side — the contrast is the lesson.

Code snippetpython
1import json 2import subprocess 3from dataclasses import dataclass 4 5@dataclass 6class EnvironmentSpec: 7 name: str 8 namespace: str 9 path: str 10 auto_sync: bool = False 11 12@dataclass 13class ApplicationSetConfig: 14 name: str 15 repo_url: str 16 target_revision: str 17 project: str 18 environments: list[EnvironmentSpec] 19 20def create_applicationset(config: ApplicationSetConfig) -> dict: 21 LB, RB = "{" * 2, "}" * 2 22 def tmpl(var: str) -> str: 23 return f"{LB}{var}{RB}" 24 elements = [ 25 { 26 "env": env.name, 27 "namespace": env.namespace, 28 "path": env.path, 29 "autoSync": str(env.auto_sync).lower(), 30 } 31 for env in config.environments 32 ] 33 appset = { 34 "apiVersion": "argoproj.io/v1alpha1", 35 "kind": "ApplicationSet", 36 "metadata": {"name": config.name, "namespace": "argocd"}, 37 "spec": { 38 "generators": [{"list": {"elements": elements}}], 39 "template": { 40 "metadata": {"name": f"{config.name}-{tmpl('env')}"}, 41 "spec": { 42 "project": config.project, 43 "source": { 44 "repoURL": config.repo_url, 45 "targetRevision": config.target_revision, 46 "path": tmpl("path"), 47 }, 48 "destination": { 49 "server": "https://kubernetes.default.svc", 50 "namespace": tmpl("namespace"), 51 }, 52 }, 53 }, 54 }, 55 } 56 subprocess.run( 57 ["kubectl", "apply", "-f", "-"], 58 input=json.dumps(appset), text=True, check=True, 59 ) 60 return appset 61 62create_applicationset(ApplicationSetConfig( 63 name="ai-inference", 64 repo_url="https://github.com/team/ai-manifests.git", 65 target_revision="main", 66 project="ai-team", 67 environments=[ 68 EnvironmentSpec("dev", "ai-dev", "envs/dev", auto_sync=True), 69 EnvironmentSpec("staging", "ai-staging", "envs/staging", auto_sync=True), 70 EnvironmentSpec("prod", "ai-prod", "envs/prod", auto_sync=False), 71 ], 72))

The list generator emits one element per environment; the template interpolates the env, path, and namespace placeholders (built via the tmpl() helper above) to produce three Application resources (ai-inference-dev, ai-inference-staging, ai-inference-prod). Dev and staging auto-sync; prod waits for operator approval in the ArgoCD UI. Edit the dataclass, re-run, and ArgoCD reconciles the diff — you do not touch three Application YAMLs by hand.

Code snippetpython
1import json 2import subprocess 3from dataclasses import dataclass 4 5@dataclass 6class FluxImageConfig: 7 name: str 8 registry: str 9 image: str 10 semver_range: str 11 git_repo_name: str 12 update_path: str 13 interval: str = "1m" 14 15def create_image_automation(cfg: FluxImageConfig) -> list[dict]: 16 image_repo = { 17 "apiVersion": "image.toolkit.fluxcd.io/v1beta2", 18 "kind": "ImageRepository", 19 "metadata": {"name": cfg.name, "namespace": "flux-system"}, 20 "spec": {"image": f"{cfg.registry}/{cfg.image}", "interval": cfg.interval}, 21 } 22 image_policy = { 23 "apiVersion": "image.toolkit.fluxcd.io/v1beta2", 24 "kind": "ImagePolicy", 25 "metadata": {"name": cfg.name, "namespace": "flux-system"}, 26 "spec": { 27 "imageRepositoryRef": {"name": cfg.name}, 28 "policy": {"semver": {"range": cfg.semver_range}}, 29 }, 30 } 31 image_update = { 32 "apiVersion": "image.toolkit.fluxcd.io/v1beta2", 33 "kind": "ImageUpdateAutomation", 34 "metadata": {"name": cfg.name, "namespace": "flux-system"}, 35 "spec": { 36 "interval": cfg.interval, 37 "sourceRef": {"kind": "GitRepository", "name": cfg.git_repo_name}, 38 "git": { 39 "commit": {"author": {"name": "flux", "email": "flux@cluster.local"}}, 40 "push": {"branch": "main"}, 41 }, 42 "update": {"path": cfg.update_path, "strategy": "Setters"}, 43 }, 44 } 45 for m in (image_repo, image_policy, image_update): 46 subprocess.run( 47 ["kubectl", "apply", "-f", "-"], 48 input=json.dumps(m), text=True, check=True, 49 ) 50 return [image_repo, image_policy, image_update]

ImageRepository polls the registry every minute, ImagePolicy selects the highest tag matching the semver range, and ImageUpdateAutomation rewrites the manifest at update_path (using Setters markers in the YAML) and pushes a commit. From there, the cluster's Kustomization reconciles the new tag — no Argo Image Updater required.

You'll know it works when: (1) kubectl get applicationset -n argocd ai-inference lists three child Applications matching your environments, and (2) kubectl get imagepolicy -n flux-system <name> shows status.latestImage updating as you push new tags, with a matching commit in your Git repo log.

Do's and Don'ts

Do's

  1. Do match the controller to the rollout pattern — pick ArgoCD ApplicationSet when you fan one manifest across many envs/clusters with operator visibility; pick Flux when image-driven auto-promotion is the dominant flow.
  2. Do use semver ImagePolicy ranges for model images — pin a major+minor (>=2.3.0 <2.4.0) so a bad model train cannot auto-deploy across a major boundary.
  3. Do gate prod sync — set auto_sync=False on the prod Application (ArgoCD) or split prod into a separate Kustomization with suspend: true (Flux) so a human approves the final hop.

Don'ts

  1. Don't run both controllers against the same namespace — they will fight over ownership and one will revert the other's reconciliation continuously.
  2. Don't hand-edit generated Applications — edit the ApplicationSet template; the controller will overwrite ad-hoc edits on the next reconcile and you'll think the change "didn't apply".
  3. Don't bolt Argo Image Updater on without measuring — if image-driven rollouts dominate your traffic, Flux's native pipeline is fewer moving parts than ArgoCD + Image Updater + webhook glue.

This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Platform Engineering subscription.

From · cancel anytime

More free lessons in DevOps Foundations for GenAI Engineers

All free lessons in GenAI Platform Engineering