Introduction — The Common Mistake of Lining Up All Three
If you have run Kubernetes for a while, you have probably received or asked questions like "We use Helm, so do we also need an Operator?" or "If we adopt GitOps, do we throw away Helm?" These questions confuse people because Operator, Helm, and GitOps appear to be three candidates competing for the same seat.
But these three solve fundamentally different problems. In a single line each:
- **Helm** is a tool that packages and templates Kubernetes manifests. It defines "what to deploy" as a bundle.
- **GitOps** is an operating model that continuously synchronizes declarations in a Git repository to cluster state. It defines "how to deploy and maintain."
- **An Operator** is a controller that encodes the operational knowledge of a specific application. It defines "who handles operations after deployment."
In other words, they are mostly complementary, not competitive. In real production, using all three together is the most common scenario. The goal of this article is to clearly separate the essence of each tool and to build judgment criteria for which to choose in which situation.
The Essence of Each — Packaging, Synchronization, Controller
Helm — Packaging and Templating
The core of Helm is the package called a "chart." A chart bundles many Kubernetes manifests (Deployment, Service, ConfigMap, etc.) into one unit and injects values to produce different results per environment.
templates/deployment.yaml (part of a Helm template)
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 2
template:
spec:
containers:
- name: app
image: my-app:1.2.3
Producing dev/stage/prod from the same chart by only changing values is the typical Helm usage. The important point is that Helm itself is a tool that "renders once and sends to the cluster." It acts only at the moment you run helm install or helm upgrade, and if someone later changes the cluster by hand, Helm does not know. Helm is a static deployment tool.
GitOps — Declarative Continuous Synchronization
GitOps is an operating model rather than a tool. Its core principles are four:
1. Express the entire desired state of the system as declarations.
2. Version those declarations in Git. Git becomes the single source of truth.
3. Approved changes are automatically applied to the cluster.
4. An agent continuously compares actual state with declarations, and alerts or reverts when drift appears.
Tools like Argo CD and Flux implement this model. The decisive difference is point four, **continuous reconcile**. Even if someone manually changes replicas in the cluster, Argo CD soon detects it as drift and reverts to the Git state. If Helm is "fire and forget," GitOps is "constant aim correction."
Operator — A Controller That Encodes Operational Knowledge
An Operator goes one level deeper. While Helm and GitOps focus on handling Kubernetes built-in resources, an Operator adds a new resource kind (a CRD, Custom Resource Definition) and deploys a dedicated controller that tends it.
For example, installing a PostgreSQL Operator creates a new resource type such as PostgresCluster in the cluster. A user declares this:
apiVersion: postgres.example.com/v1
kind: PostgresCluster
metadata:
name: orders-db
spec:
instances: 3
version: "16"
backup:
schedule: "0 2 * * *"
For this single declaration, the Operator handles StatefulSet creation, replication setup, failover, backup scheduling, and even minor version upgrades on its own. It automates the operational procedures (Day-2 operations) a human would otherwise perform.
The core difference, drawn out:
Helm: values -> [render once] -> manifests -> cluster
(acts only at execution time)
GitOps: Git declarations -> [agent keeps comparing] -> cluster
(auto drift correction, continuous reconcile)
Operator: CR declaration -> [dedicated controller keeps reconciling] -> built-in resources + operational actions
(automates backup/failover/upgrade too)
Static Deployment vs Continuous Reconcile
The most important axis separating the three tools is "does it act once, or does it keep acting?"
| Category | When it acts | Drift correction | Analogy |
| --- | --- | --- | --- |
| Helm alone | At command execution | None | Taking a single photo |
| GitOps | Always (periodic + event) | Automatic | Autofocus camera |
| Operator | Always (event-driven) | Automatic + operational actions | Hiring a dedicated operator |
If you use Helm alone, cluster state gradually diverges from declarations. Someone hotfixes with kubectl edit, someone else temporarily bumps replicas, and over time no one knows exactly "what is running in the cluster now." GitOps structurally prevents this drift problem. The Operator goes one step further: beyond matching manifests, it handles application-specific operational actions (backup, failover, etc.) inside the reconcile loop.
Day-1 vs Day-2 — Where Is the Boundary
The operations industry divides work into Day-0/Day-1/Day-2.
- **Day-0**: Design and planning. Architecture decisions, capacity sizing.
- **Day-1**: Installation and initial deployment. "First boot."
- **Day-2**: Operations. Upgrades, backup/restore, scaling, incident response, patching. The phase that occupies most of a system's lifetime.
Viewing the three tools through this lens makes their strengths clear.
- **Helm** is strong at Day-1. It is optimal for cleanly installing a complex application in one go. But Day-2 operational actions are not part of Helm itself.
- **GitOps** spans both Day-1 and Day-2. It manages everything from initial deployment to all subsequent changes through the same Git workflow. However, GitOps too goes only as far as "matching declarations" and does not by itself know an application's complex internal operations (such as a DB failover procedure).
- **An Operator** is strong at Day-2. The application-specific operational knowledge lives in the controller code, eliminating the need for human intervention each time.
In conclusion, an Operator shines for stateful applications whose Day-2 operations are complex and repetitive (databases, message queues, search engines). For a stateless web application with simple operations, an Operator may be an overkill choice.
Combination Patterns — In Practice, Use All Three Together
The most common and recommended setup places the three roles so they do not overlap.
Pattern 1 — Deploy a Helm Chart via GitOps
Argo CD and Flux can handle Helm charts directly as a source. The chart is used as the packaging unit, while applying and maintaining it in the cluster is handled by the GitOps agent.
Argo CD Application — managing a Helm chart via GitOps
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/example/my-app-chart
targetRevision: main
path: charts/my-app
helm:
valueFiles:
- values-prod.yaml
destination:
server: https://kubernetes.default.svc
namespace: my-app
syncPolicy:
automated:
prune: true
selfHeal: true
This gives you Helm's templating power and GitOps's continuous synchronization and drift correction at the same time. Humans no longer run helm install directly, and all changes flow through Git PRs.
Pattern 2 — Install the Operator with Helm, Manage CRs with GitOps
The Operator itself is usually installed via a Helm chart or OLM (Operator Lifecycle Manager). So Helm handles deploying the Operator controller, while GitOps manages the custom resources (CRs) the Operator handles.
[Platform team]
Install PostgreSQL Operator via Helm chart (controller + CRD)
|
v
[Application team]
Declare PostgresCluster CR in Git -> Argo CD synchronizes
|
v
Operator reconciles the CR -> automates StatefulSet/backup/failover
In this setup the responsibilities split cleanly. Helm "installs the Operator software," GitOps "keeps declarations matched to the cluster," and the Operator "actually operates the declared DB." The three tools do not invade each other's territory.
Pattern 3 — App-of-Apps and Bootstrap
In large platforms, the Argo CD App-of-Apps pattern lets "Argo CD manage all other Applications," and those Applications in turn deploy Helm charts (shared infrastructure), Operator installations, and workloads. The entire cluster bootstrap flows out of a single Git tree.
Decision Table — What to Use When
The following table summarizes which tool fits each situation.
| Situation | Recommended tool | Reason |
| --- | --- | --- |
| Deploying a stateless web app | Helm + GitOps | Simple ops, Operator unnecessary |
| Many deployments differing only by env values | Helm + GitOps | Templating + continuous sync |
| Operating a production database | Operator | Needs backup/failover/upgrade automation |
| Operating message queues, search engines | Operator | Complex Day-2 operations |
| Consistent management across many clusters | GitOps | Git single source of truth |
| Environments with frequent drift | GitOps | Auto-correction via selfHeal |
| Automating an internal standard workflow | Operator (or simple controller) | Codifying operational knowledge |
| One-off experiment/PoC | Helm alone | Fastest, no governance needed |
Rather than memorizing the table, compress it into one question. **"Are the operations (Day-2) of this system repetitive and requiring application-specific knowledge?"** If so, consider an Operator; if not, look first at the Helm + GitOps combination.
Recommendations by Case
- **Backend API of an early-stage SaaS**: One Helm chart + Argo CD. Deployment is simple and changes are frequent, so the GitOps PR-based flow gives the biggest benefit. An Operator is not yet needed.
- **PostgreSQL in an air-gapped financial environment**: Install a proven Operator with Helm and manage CRs with GitOps. Since backup and failover cannot be left to human hands in this environment, the Operator's automation is the key.
- **Deploying a common agent across dozens of edge clusters**: GitOps-centric (Flux multi-tenancy or Argo CD ApplicationSet). Consistently applying the same declaration to every cluster is the top priority.
- **Model serving on an ML platform**: Use the Operator (CRD-based) provided by the serving framework, and manage the workloads on top with GitOps.
When an Operator Is Overkill — Facing the Cost
An Operator is powerful but not free. In the following cases, an Operator is likely over-engineering.
- When operational actions are simple enough that editing one line of a Deployment suffices. There is no reason to build/operate a CRD and controller.
- When the team lacks the Go skills or staffing to maintain a controller. A homegrown Operator becomes yet another maintenance target.
- When change frequency is low and the automation investment never pays back.
The real cost of an Operator is in **long-term maintenance** rather than writing code. Every time the Kubernetes version goes up you must follow the controller-runtime dependency, you must manage RBAC permissions precisely, and a bug in the reconcile logic translates directly into an operational incident. You must clearly distinguish "using a well-maintained external Operator" from "building an Operator yourself" — they have entirely different cost structures.
Migration — From a Helm Chart to an Operator
Mature applications often start as a Helm chart and evolve toward an Operator as operational complexity grows. The typical path:
1. **Inventory the current chart's operational burden.** Write down all the procedures humans do manually (backup, scaling, incident recovery). These are candidates for the Operator to automate.
2. **Design the CRD schema.** Keep only the minimal intent a user declares (e.g., instance count, version, backup schedule) and let the controller decide the rest.
3. **Migrate incrementally.** First make the Operator create the same resources the existing chart produced, then add operational actions like backup/failover in stages.
4. **Allow a coexistence period.** Run the existing Helm deployment and the Operator-based deployment in parallel for a while, verify equivalence, then switch.
Conversely, if the conclusion is "there is no real need to move to an Operator," staying with Helm + GitOps is a perfectly correct choice. Migration itself must not become the goal.
Operational Cost Comparison
| Item | Helm + GitOps | Operator (external) | Operator (in-house) |
| --- | --- | --- | --- |
| Initial adoption difficulty | Low | Medium | High |
| Day-2 automation level | Limited | High | High |
| Maintenance burden | Low | Medium (track upstream) | High (manage code directly) |
| Required skills | YAML, GitOps | Adoption/config | Go, controller-runtime |
| Debugging on failure | Trace manifests | CR + controller logs | Down to controller code |
The key message of this table is "the higher the automation level, the higher the maintenance burden rises with it." Automation is not free; it is closer to moving cost into the future.
Observability and Troubleshooting — Where to Look per Tool
When an incident occurs, where to look differs by tool. Having this map in advance greatly speeds up your response.
| Symptom | Helm view | GitOps view | Operator view |
| --- | --- | --- | --- |
| Deployment not reflected | Check helm history | Check Application sync status | CR status and controller logs |
| Resource keeps reverting | (N/A) | Suspect selfHeal drift correction | Suspect reconcile loop |
| Only some envs differ | Compare values files | Check per-env source paths | Check CR spec differences |
| Resource not deleted | helm uninstall remnants | Suspect prune disabled | Suspect unfinished finalizer |
The key is to first grasp "who currently owns this resource." Depending on whether Helm created it, GitOps synced it, or the Operator reconciled it, the place you act changes entirely. If two parties manage the same resource, that very spot is the epicenter of conflict.
Tracking Ownership via Labels
All three tools leave identifying metadata on the resources they create. Helm marks releases with annotations and labels, Argo CD leaves a tracking label indicating which Application owns it, and an Operator usually points to the parent CR via an ownerReference. The first step of incident analysis is to read the metadata of the problem resource and confirm "whose it is."
The order for tracing a resource's origin
1. Check metadata with kubectl get <resource> -o yaml
2. If there is an ownerReference -> owned by an Operator (or a built-in controller)
3. If there is an Argo CD tracking label -> owned by GitOps
4. If there is a Helm release annotation -> owned by Helm
A Collection of Pitfalls
- **Conflict between GitOps and Helm release**: If Argo CD later tries to manage a release you installed directly with helm install, an ownership conflict arises. Unify on one side from the start.
- **CRD ordering when installing an Operator via GitOps**: Trying to apply a CR before its CRD is created makes sync fail. Guarantee ordering with Argo CD sync waves or Flux dependencies.
- **selfHeal reverting the Operator's normal behavior**: If the Operator updates status or some fields and GitOps mistakes that for drift and reverts it, you get an infinite conflict. Exclude controller-managed fields with ignoreDifferences.
- **Secrets in values**: Committing plaintext secrets in Helm values to Git is a common accident. Use Sealed Secrets, External Secrets Operator, SOPS, etc.
A Decision Flowchart
Turning the table into a flowchart is more useful at the actual moment of choice.
Start: What are you deploying/operating?
|
+- A one-off experiment? --yes--> Helm alone is enough
|
+- No
|
+- Do you need consistent application across many envs/clusters? --yes--> Adopt GitOps
| (with Helm/Kustomize as source)
|
+- Are this system's Day-2 operations complex?
(Are backup/failover/version upgrade repetitive?)
|
+- Yes --> Is there a well-maintained external Operator?
| +- Yes --> Install that Operator with Helm + manage CRs with GitOps
| +- No --> Consider building your own Operator (face the cost)
|
+- No --> Helm + GitOps is enough
This flowchart has only two branch points. First, "do you need consistency" (-> GitOps); second, "is Day-2 complex" (-> Operator). Most decisions end with these two questions.
Common Anti-patterns
A collection of pitfalls people often fall into while handling the three tools.
- **The urge to build everything as an Operator**: the fact that something can be automated does not mean it should be. Introducing a CRD for a simple workload only increases cognitive load and maintenance burden.
- **Adopting only an Operator without GitOps**: an Operator reconciles a CR, but if that CR itself is managed arbitrarily by hand, you get drift and untraceability again. Put the CR in Git too.
- **Using Helm imperatively**: running helm install/upgrade by hand, and differently per environment, breaks reproducibility. Use the chart only for packaging and leave application to GitOps.
- **Secrets as plaintext values**: the most common security accident. Always encrypt secrets with a dedicated tool.
- **Mistaking the tool for the goal**: motivations like "let's build an Operator too" or "GitOps is cool, let's adopt it" are dangerous. The starting point must always be an operational problem to solve.
A Practical Scenario — Following One Service's Evolution
Abstract comparison alone is hard to grasp. Let us follow how a hypothetical order service adopts the three tools in turn over time.
Stage 1 — Helm Alone (Right After Launch)
Early on, the service is just one Deployment, one Service, and one ConfigMap. A developer deploys by running helm upgrade directly from a laptop. At this stage, this is enough. Governance and drift worries are not yet a big problem. Still, the following symptoms start to appear.
- There is no record to answer "who deployed what to prod yesterday?"
- The staging and production values have subtly diverged, and no one knows why.
- Deploy authority is tied to a specific developer's laptop.
Stage 2 — Adopting GitOps (As the Team Grows)
Here you introduce Argo CD. You keep the chart as-is and move only the deploy trigger to Git PRs. Now every change goes through PR review, who changed what and when remains in Git history, and drift is auto-corrected by selfHeal. The helm command on developer laptops disappears.
Change: "humans apply directly to the cluster"
-> "humans open a PR in Git, the agent applies to the cluster"
The key benefit of this shift is not deployment itself but **auditability and consistency**.
Stage 3 — Adopting an Operator (As the Data Layer Grows Heavy)
As order volume grows, you begin operating PostgreSQL yourself. Backup, failover, and minor version upgrades reach a level that must leave human hands. Here you adopt a proven PostgreSQL Operator. You install the Operator controller with Helm and manage the PostgresCluster CR with GitOps. The combination Pattern 2 seen earlier is exactly this stage.
The lesson of this evolution is clear. **You do not need to adopt all three from the start.** Add them one at a time when operational complexity actually justifies the tool. Excessive upfront investment is itself debt.
Frequently Asked Questions
**Q. Is Helm now outdated? Do Kustomize or GitOps replace it?**
No. Helm is still the most widely used packaging tool, and GitOps agents support Helm charts as first-class. Kustomize is a different approach that mutates via overlays without templates; rather than competing with Helm, it is chosen by taste and situation. All three can coexist under GitOps.
**Q. If I use an Operator, do I not need GitOps?**
On the contrary, using them together is the standard. The Operator knows "how to operate the CR," and GitOps knows "how to match that CR declaration to the cluster." Their roles differ.
**Q. We are a small team — do we have to learn all three?**
No. A small team can run most stateless workloads well with just Helm + GitOps. Adopt an Operator when truly complex stateful operations arise, and preferably start by adopting a well-maintained external Operator.
**Q. Which is easier to maintain, a homegrown Operator or a Helm chart?**
Generally a Helm chart is far easier. An Operator adds a continuous maintenance surface of Go code, the controller-runtime dependency, RBAC, and reconcile logic. It is justified only when the value of automation exceeds that burden.
**Q. Between Argo CD and Flux as the GitOps agent, which should I pick?**
Both faithfully implement the OpenGitOps principles, and their core features are nearly equivalent. Argo CD's strengths are a rich web UI and intuitive sync visualization, while Flux's strengths are the modular composition of the GitOps Toolkit and its lightness. Teams that value a UI often pick Argo CD, and teams that prefer code/CRD-centric assembly often pick Flux. The conclusion of this article (the division of roles among the three) applies the same either way.
**Q. Doesn't adopting an Operator make cluster permissions too broad?**
A valid concern. An Operator needs permissions over the resources it manages, and for a database Operator, Secret access is often unavoidable. So use Operators from trustworthy sources, always review the RBAC granted at install, and where possible limit it to a namespace scope. Permission review is one of the key criteria in choosing an Operator.
An Example Repository Structure Holding All Three
Moving theory into an actual directory makes it clearer. The following is a typical structure for operating Helm + GitOps + Operator in one repository.
platform-gitops/
├── bootstrap/
│ └── argocd-apps.yaml # App-of-Apps: manages everything below
├── infrastructure/
│ ├── postgres-operator/ # Operator install (references a Helm chart)
│ │ └── application.yaml
│ └── cert-manager/ # shared infrastructure (Helm)
│ └── application.yaml
├── databases/
│ └── orders-db/
│ └── postgrescluster.yaml # the CR the Operator reconciles (GitOps-managed)
└── apps/
└── orders-api/
├── application.yaml # Argo CD Application pointing to a Helm chart
└── values-prod.yaml # per-environment values
In this structure, each layer's role is revealed by the directory.
- infrastructure/ installs the Operator and shared components with Helm.
- databases/ declares the CRs the Operator handles, via GitOps.
- apps/ deploys ordinary workloads with Helm charts + GitOps.
- bootstrap/'s App-of-Apps ties this whole thing together, so applying it once to an empty cluster makes the rest flow out automatically.
The key is guaranteeing ordering with sync waves. The Operator (and its CRD) must be installed first, then the CRs in databases/ are applied. With Argo CD, you express this dependency by assigning wave numbers via annotations. This single directory structure visually summarizes the whole conclusion of this article: the three tools coexisting in their respective places, without conflict, under one Git tree.
Closing
Summarizing the three tools again in one sentence: Helm **bundles**, GitOps **matches**, and the Operator **tends**. They are not competitors but different layers of the operational stack. Most production setups converge on the combination "package with Helm, synchronize with GitOps, automate operations with an Operator only where needed."
The starting point of choice is always operational complexity. Do not drag an Operator into a simple workload, and do not leave complex stateful operations to human hands. Once you know the essence of each tool, "what to use when" follows naturally.
References
- Kubernetes Operator pattern official docs: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
- Kubebuilder Book: https://book.kubebuilder.io/
- Operator SDK: https://sdk.operatorframework.io/
- Operator Lifecycle Manager (OLM): https://olm.operatorframework.io/
- Helm official docs: https://helm.sh/docs/
- Argo CD official docs: https://argo-cd.readthedocs.io/
- Flux official docs: https://fluxcd.io/flux/
- OpenGitOps principles: https://opengitops.dev/
- controller-runtime: https://pkg.go.dev/sigs.k8s.io/controller-runtime
- Argo CD App-of-Apps pattern: https://argo-cd.readthedocs.io/en/stable/operator-manual/cluster-bootstrapping/
- Argo CD Sync Waves: https://argo-cd.readthedocs.io/en/stable/user-guide/sync-waves/
- Kustomize official docs: https://kustomize.io/
- kubebuilder GitHub: https://github.com/kubernetes-sigs/kubebuilder
현재 단락 (1/234)
If you have run Kubernetes for a while, you have probably received or asked questions like "We use H...