Virtualization

How to Detect and Fix Kubernetes Configuration Drift with Argo CD and Terraform

17 min read

The Back Room Tech is reader-supported. We may earn a commission when you buy through links on our site. Learn more.

Configuration drift rarely announces itself. A workload stays healthy, the dashboard stays calm, and one live field no longer matches Git. Trouble comes later. A routine sync may overwrite an emergency fix or fight another controller every few minutes.

This setup uses Argo CD, Terraform, Kubernetes role-based access control (RBAC), and alerts. It helps you find, review, fix, and prevent drift. Field ownership is the key decision. Get it wrong, and two reconcilers can spend all day undoing each other’s work.

What Is Kubernetes Configuration Drift?

Desired state is the config in an approved Git repository or Terraform workspace. Live state is what the Kubernetes API stores and controllers run. Configuration drift is a meaningful difference between those states.

Drift can exist when every manifest lives in Git. Someone may run kubectl edit, apply an emergency fix, or trigger an old automation job. Operators, Horizontal Pod Autoscalers (HPAs), admission webhooks, service meshes, and other controllers also change fields after deployment.

Some differences are expected. Each resource, and sometimes each field, needs one owner. Otherwise, drift detection becomes a noisy YAML comparison that nobody trusts.

Prerequisites

Make sure you have:

  • A Kubernetes cluster with a supported version for your chosen Argo CD release
  • Cluster-admin access for the initial Argo CD installation
  • Read access to the workloads being monitored
  • A Git repository containing the desired Kubernetes manifests
  • A Terraform 1.x workspace if Terraform manages cluster resources
  • kubectl, argocd, git, and optionally terraform installed
  • A browser on Windows 11, macOS 14 or later, or another desktop OS
  • A backup, stable Git revision, or Terraform state version for rollback
  • At least 2 CPU cores and 4 GB of available memory for a small test cluster

The commands use an application named my-app in the production namespace. Replace both values with your own.

Initial setup takes about 20-30 minutes when cluster access and Git credentials already work. Repository login, ingress, and single sign-on can add another hour. Identity systems have a knack for making simple jobs memorable.

Warning: Test installation and reconciliation in a non-production cluster first. Sync, prune, and Terraform operations can replace or delete resources when ownership is unclear.

Step-by-Step Guide

Step 1: Install the Command-Line Tools

Install the clients on the workstation you use to manage Kubernetes. Keep kubectl close to the cluster version. One minor version on either side is the normal supported range.

Windows

Open Windows Terminal as a normal user and run:

winget install --exact --id Kubernetes.kubectl
winget install --exact --id ArgoCD.ArgoCD
winget install --exact --id Hashicorp.Terraform

The --exact flag stops winget from choosing a package with a similar name. Package name collisions are rare, but debugging the wrong binary is a poor use of an afternoon.

Close and reopen Windows Terminal so it loads the updated PATH. Then check the commands:

kubectl version --client
argocd version --client
terraform version

Expected output includes client version details for all three programs:

Client Version: v1.x.x
argocd: v3.x.x
Terraform v1.x.x

Windows Terminal showing successful kubectl, Argo CD CLI, and Terraform version checks

macOS

Install the tools with Homebrew:

brew install kubectl argocd terraform

Check them:

kubectl version --client
argocd version --client
terraform version

Homebrew may install newer clients than your cluster uses. That’s usually fine within Kubernetes’ supported version skew. Check compatibility before crossing more than one minor release.

macOS Terminal showing successful kubectl, Argo CD CLI, and Terraform version checks

Web

The Argo CD web interface needs no local installation. Use a current Chrome, Edge, Firefox, or Safari release. You’ll still need the command-line tools for installation and detailed checks.

Open your organization’s Argo CD URL after an administrator confirms the address and certificate. A certificate warning on an internal admin service needs investigation, not a reflexive click.

Browser prepared to open the organization-provided Argo CD address without showing sign-in challenges

Step 2: Confirm Cluster Access

Select the correct Kubernetes context before installing anything:

kubectl config current-context
kubectl cluster-info
kubectl auth can-i create namespace

Expected result:

your-production-context
Kubernetes control plane is running at https://…
yes

These checks confirm the context name, API connection, and permission to create the namespace. Read the context name twice. Installing Argo CD into the wrong cluster works remarkably well.

If the context is wrong, list the available choices and select the intended cluster:

kubectl config get-contexts
kubectl config use-context YOUR_CONTEXT_NAME

Stop if cluster-info fails or the context points to the wrong environment. Fix a failed permission check before installation too.

Step 3: Install Argo CD in the Cluster

Create the namespace and apply the official stable installation manifest:

kubectl create namespace argocd
kubectl apply --namespace argocd --filename https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

The stable URL is handy in a lab, but its target changes with each release. Pin a reviewed Argo CD release in production. That keeps the bootstrap command repeatable next month.

Wait up to 300 seconds for every deployment to become available:

kubectl wait --namespace argocd --for=condition=Available deployment --all --timeout=300s
kubectl get pods --namespace argocd

Expected output shows each pod as Running, usually with all containers ready:

NAME READY STATUS RESTARTS
argocd-application-controller-… 1/1 Running 0
argocd-repo-server-… 1/1 Running 0
argocd-server-… 1/1 Running 0

A Running pod with 0/1 readiness still has a problem. Check readiness, restarts, events, and logs before you continue.

The official Argo CD installation documentation covers production ingress, high availability, and single sign-on. Don’t expose the server publicly with its default administrator account.

For a local test, forward the API server to https://localhost:8080:

kubectl port-forward service/argocd-server --namespace argocd 8080:443

Keep that terminal open. The tunnel ends when the process stops. Production installs should use your approved identity provider, TLS certificate, and ingress setup.

Step 4: Register a Git-Managed Application

Create an Argo CD Application that points to the directory containing your manifests:

# ./my-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/YOUR_ORGANIZATION/YOUR_REPOSITORY.git
    targetRevision: main
    path: kubernetes/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    syncOptions:
      - CreateNamespace=true

targetRevision: main tracks each new commit on that branch. Pin a tag or commit when you need a fixed release. Updates will then require a change to the application definition.

CreateNamespace=true lets Argo CD create production when it’s missing. Skip that option when Terraform or a platform team owns namespaces and their labels.

Apply the definition:

kubectl apply --filename ./my-app.yaml
kubectl get application my-app --namespace argocd

The first result may be OutOfSync because the application hasn’t been synced:

NAME SYNC STATUS HEALTH STATUS
my-app OutOfSync Missing

That result is expected at this stage. Missing means the desired resources aren’t present yet. It doesn’t prove the repository or rendered manifests are valid.

Open the Argo CD URL, select Applications, and find my-app. The dashboard shows Synced, OutOfSync, or Unknown beside each application.

Argo CD Applications dashboard with application names, health status, and sync status visible

Step 5: Establish Clear Ownership Boundaries

Document ownership before you enable correction. This feels slower than clicking Sync, but it prevents Terraform and Argo CD from trading the same object back and forth.

A practical boundary looks like this:

Object or fieldAuthoritative owner
Cloud network, cluster, node groupsTerraform
Argo CD installation and base servicesTerraform or a bootstrap process
Deployments, Services, and application configurationArgo CD
Deployment replica count when HPA is activeHPA
Injected service-mesh annotationsAdmission controller
Certificate status and generated datacert-manager

Never let Terraform and Argo CD manage the same Kubernetes object. Shared ownership causes noisy plans, repeated rollouts, and controllers that undo each other’s work.

Field ownership can help identify the writer:

kubectl get deployment my-app --namespace production --show-managed-fields --output yaml

Review metadata.managedFields[].manager. Values may include argocd-controller, kubectl-edit, an operator name, or an admission component. Those names provide useful evidence.

Managed fields show who claimed or updated fields through Kubernetes’ field-management system. They don’t always explain intent. Older client-side operations can also leave gaps. Check audit logs when the writer matters.

Step 6: Detect Terraform-Managed Drift

Change to the Terraform workspace that owns the resource. Then initialize it and create a saved plan:

cd /absolute/path/to/terraform/workspace
terraform init
terraform plan -out=drift-review.tfplan

terraform init installs the declared providers and sets up the backend. Review provider lock-file changes before accepting them. An unplanned provider upgrade can change value handling and muddy the drift review.

terraform plan refreshes managed objects before comparing live values with the config and state. A change made outside Terraform can appear in the plan. The -out option saves that exact plan for later review and use.

Expected drift output resembles:

# kubernetes_namespace_v1.production will be updated in-place
~ resource “kubernetes_namespace_v1” “production” {
metadata {
~ labels = {
– “temporary-access” = “enabled” -> null
}
}
}

Plan: 0 to add, 1 to change, 0 to destroy.

Sanitized terraform plan showing the resource address, changed field, proposed in-place action, and plan summary

Don’t apply it yet. Check for -/+, must be replaced, destroy actions, immutable fields, or controller-owned values. Provider value handling and stale state can also create apparent differences.

Terraform compares resources it owns well. It’s poor at settling shared ownership. An apply won’t decide which controller should own a field.

Step 7: Investigate Argo CD Drift

Inspect the status, exact changes, and deployment history:

argocd app get my-app
argocd app diff my-app
argocd app history my-app

Expected status fields include:

Sync Status: OutOfSync
Health Status: Healthy

OutOfSync with Healthy means the workload passes its health checks but differs from Git. Review it without panicking. A healthy deployment can still contain an unapproved image, role, or environment variable.

argocd app diff my-app shows the desired and live fields. A nonzero exit status often means it found differences. It doesn’t always mean the command failed. Account for that behavior in shell scripts and CI jobs.

Sanitized argocd app diff my-app output with desired and live sides and one harmless changed field highlighted
Application details page showing health, synchronization status, resource tree, and diff control

In the web interface, open my-app, select an affected resource, and choose Diff.

Desired-versus-live diff with one changed field highlighted and no secrets visible

Classify every difference:

  • Unauthorized drift: Restore the approved Git or Terraform value and remove the writer’s access.
  • Intentional but undocumented: If an emergency live change is still needed, commit it to Git first. Use review and the normal deployment path to keep it.
  • Legitimately controller-managed: Give that field to the controller. Exclude only that field from GitOps comparison or sync.

Restore the Git value when Git is correct and the live edit was accidental, expired, or unauthorized. Commit an emergency value when reverting it would recreate the incident or remove a needed fix.

This decision needs human judgment. Diff tools show two values, but they can’t decide which one keeps checkout working at 02:00.

Step 8: Correct Approved Drift Safely

For Terraform-managed drift, inspect the saved plan again:

terraform show drift-review.tfplan

This reads the saved artifact instead of making a fresh plan. If the environment changed after planning, create and review a new plan. Don’t apply stale assumptions.

Apply the exact reviewed plan after you approve any replacements and deletions:

terraform apply drift-review.tfplan

For Argo CD, open the application and click Sync. Select only resources owned by Argo CD. Review the options, and keep pruning clear unless you intend to delete resources.

Argo CD Sync dialog with affected resources selected and a warning to review replacements, deletions, and pruning before clicking Sync

Selective sync can correct one ConfigMap or Deployment without touching an unrelated healthy resource. You still need to inspect immutable fields and rollout effects.

Treat pruning with extra suspicion. It deletes managed resources removed from Git. That’s useful when planned and rather final when somebody moved a directory by mistake.

Step 9: Validate Health and Preserve Rollback

After correction, check sync status, workloads, events, and rollout state:

argocd app get my-app
kubectl get deployments,pods --namespace production
kubectl rollout status deployment/my-app --namespace production --timeout=300s
kubectl get events --namespace production --sort-by=.lastTimestamp

The 300-second timeout stops a stuck rollout from holding the terminal forever. Five minutes suits a small application. Increase it for slow image pulls or long readiness checks.

Expected results are Synced, Healthy, ready pods, and a completed rollout:

Sync Status: Synced
Health Status: Healthy
deployment “my-app” successfully rolled out

Also check application metrics, logs, readiness probes, error rates, and external dependencies. Kubernetes readiness confirms that the probe passed. It doesn’t prove users can complete a transaction.

Keep the previous Git commit, Argo CD history entry, backup, and Terraform state version until the workload stays stable. Set that window per service. An internal API may need 15 minutes, while a low-traffic batch job may need one full run.

Step 10: Restrict Manual Production Changes

Give most operators read access and route lasting changes through pull requests. This example permits inspection in production. It denies create, patch, update, and delete operations:

# ./production-viewer-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: production-viewer
  namespace: production
rules:
  - apiGroups: ["", "apps", "batch"]
    resources:
      - pods
      - pods/log
      - services
      - configmaps
      - deployments
      - replicasets
      - jobs
      - cronjobs
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: production-viewers
  namespace: production
subjects:
  - kind: Group
    name: production-viewers
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: production-viewer
  apiGroup: rbac.authorization.k8s.io

The Role applies only to the production namespace. The three verbs support inspection and watch-based tools without allowing resource changes.

Apply and test it:

kubectl apply --filename ./production-viewer-rbac.yaml
kubectl auth can-i patch deployments --namespace production --as-group=production-viewers --as=example-user

Expected result:

no

Readable production-viewer Role and RoleBinding with namespace, resources, read-only verbs, and successful can-i denial visible

Test with impersonation before you assign the group broadly. Check allowed actions such as get pods too. A policy that blocks routine diagnosis encourages requests for wider access.

Keep tightly controlled break-glass access for incidents. Require an incident record, short-lived credentials, audit logs, and a follow-up Git commit or rollback. Emergency access without cleanup is permanent access with better branding.

Configuration

Configure Safe Argo CD Self-Healing

Enable automatic self-healing after ownership is clear and diffs are quiet. Health checks must work, and the workload should pass manual reconciliation tests. Start with one low-risk application. Watch it through several normal deployment cycles.

spec:
  syncPolicy:
    automated:
      enabled: true
      prune: false
      selfHeal: true
    syncOptions:
      - RespectIgnoreDifferences=true
SettingPurposePractical guidance
selfHealReapplies desired Git values after live driftEnable per tested application, not globally by assumption
pruneDeletes managed resources removed from GitLeave off until deletion and rollback behavior are tested
ignoreDifferencesExcludes fields legitimately owned elsewhereMatch the exact resource and JSON pointer
RespectIgnoreDifferences=truePreserves ignored fields during syncUse when ignored fields must not be overwritten
Server-side diffIncludes API-server defaulting and mutation behaviorUseful when client-side comparisons produce false positives

ignoreDifferences may hide comparison noise while sync still writes the desired value. Add an applicable RespectIgnoreDifferences=true setting when sync must preserve ignored fields. Otherwise, the ownership fight continues behind a greener dashboard.

For an HPA-owned replica count, use a narrow rule:

spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      name: my-app
      namespace: production
      jsonPointers:
        - /spec/replicas
  syncPolicy:
    syncOptions:
      - RespectIgnoreDifferences=true
Application synchronization-policy area showing automated sync, self-healing, and pruning controls without assuming their selected state

Ignoring /spec/replicas for one HPA-controlled Deployment is reasonable. Broad rules can hide security and operational drift. Avoid ignoring all annotations, container security contexts, images, or whole resource types.

Self-healing restores known values quickly. It’s bad at understanding emergency intent. Tie break-glass procedures to Git updates or a documented pause in reconciliation.

Monitor Recurring Drift

Expose Argo CD metrics to your existing Prometheus stack. Alert when an application stays out of sync. Metric labels vary by Argo CD version and monitoring setup. Check them in Prometheus before you deploy this rule:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: argocd-drift
  namespace: monitoring
spec:
  groups:
    - name: argocd-drift
      rules:
        - alert: ArgoCDApplicationOutOfSync
          expr: argocd_app_info{sync_status!="Synced"} == 1
          for: 10m
          labels:
            severity: warning
          annotations:
            summary: "Argo CD application is not synchronized"
            description: "Review ownership and the desired-versus-live diff before correction."
        - alert: ArgoCDApplicationUnhealthy
          expr: argocd_app_info{health_status=~"Degraded|Missing|Unknown"} == 1
          for: 5m
          labels:
            severity: critical
          annotations:
            summary: "Argo CD application health requires investigation"

The 10-minute delay filters short sync transitions. The 5-minute health threshold flags broken or missing workloads sooner. Match both values to normal rollout time. A 12-minute deployment will otherwise page on every release.

Alert on lasting drift and failed reconciliation. Don’t alert on each brief state change. Track repeat drift by application, resource, and field owner. Kubernetes audit logs can identify manual and automated writers that keep returning.

Raw whole-cluster YAML snapshots make poor primary baselines. They include status, timestamps, generated metadata, and controller output. They also become stale after approved changes. Keep Git or infrastructure as code as the maintained baseline.

Snapshots still help during incident response and forensic review. Treat them as evidence from one point in time. Don’t feed them back into the API as configuration.

Tips and Troubleshooting

Application repeatedly becomes OutOfSync

Why it happens: An HPA, operator, webhook, automation job, or person keeps rewriting a field.

Fix: Run argocd app diff my-app, inspect metadata.managedFields, and identify the writer. Stop unauthorized automation, commit an intended change, or add a narrow ownership rule for a trusted controller.

If the same field changes on a fixed schedule, check CronJobs and external automation first. A five-minute drift cycle often has a five-minute scheduler behind it.

Terraform proposes unexpected replacement or deletion

Why it happens: State may be stale, a provider may change the value’s format, or another tool may own the object.

Fix: Stop before applying. Run terraform show drift-review.tfplan, confirm the resource owner, inspect the live object, and review the state backend history. Move ownership instead of accepting a permanent Terraform-versus-Argo CD conflict.

A replacement marker needs more attention than the final plan count. One replaced namespace or cluster resource can affect hundreds of objects, even when Terraform reports one change.

The diff contains defaulted or generated values

Why it happens: The API server, an admission webhook, or a controller changed the submitted object.

Fix: Identify the field manager and use server-side diff where supported. Ignore only the exact valid field. Don’t hide security contexts, image tags, role permissions, or network policy changes to make the dashboard green.

Defaults can change after Kubernetes or webhook upgrades. Keep ignore rules narrow so new security-related differences still appear.

Sync may disrupt a healthy workload

Why it happens: The proposed change touches immutable fields, triggers replacement, includes pruning, or changes rollout behavior.

Fix: Use selective sync, leave pruning disabled, check readiness and disruption budgets, and test outside production. Correct the smallest owned resource set first. Then watch rollout status and application metrics.

For one Deployment, check maxUnavailable, maxSurge, and the PodDisruptionBudget before syncing. A valid manifest can still remove too much capacity during rollout.

Drift returns after every correction

Why it happens: The correction fixed the changed value, but the original writer stayed active.

Fix: Review audit logs, managed fields, scheduled jobs, webhooks, and RBAC bindings. Remove unneeded write access and require production changes through Git-based review.

Repeated correction can cause constant pod restarts or API load. Pause automatic self-healing while you identify the writer if the live change poses no immediate risk.

Argo CD shows Unknown instead of OutOfSync

Why it happens: Repository access, manifest generation, cluster credentials, or the application controller may have failed.

Fix: Inspect application conditions and controller logs:

argocd app get my-app
kubectl logs deployment/argocd-application-controller --namespace argocd --tail=200
kubectl logs deployment/argocd-repo-server --namespace argocd --tail=200

The --tail=200 flag keeps the first check readable. Increase it or add --since=30m when the failure started earlier.

Repository login failures usually appear in repo-server logs. Cluster access and reconciliation failures tend to appear in application-controller logs. That split saves a fair amount of random log browsing.

For Amazon EKS integrations, also consult the official EKS Argo CD troubleshooting guide.

Wrapping Up

StepActionApplies To
DetectRun a reviewed plan or desired-versus-live diffTerraform and Argo CD
ReviewClassify drift and confirm field ownershipEvery difference
CorrectApply the saved plan or selectively syncApproved owned resources
ValidateCheck health, rollouts, events, and metricsCorrected workloads
PreventUse RBAC, Git review, audit logs, and alertsProduction clusters

Drift monitoring works when ownership is clear. Argo CD handles Git-managed application resources well. Terraform handles its infrastructure, and controllers keep their assigned fields. Cross those boundaries and you’ll get noisy plans, repeated changes, and occasional outages.

Start with manual review, narrow exceptions, and pruning disabled. After several clean sync cycles, enable self-healing per application. Alert on drift that lasts 10 minutes. Automatic correction everywhere sounds handy, but a slower rollout tends to survive production.