---
title: "GitOps and Infrastructure as Code: Reconciliation Patterns for Terraform, ArgoCD, and Crossplane"
description: "How GitOps principles apply to infrastructure management. Covers the reconciliation gap between Terraform (push-based, stateful) and GitOps (pull-based, continuous), patterns for combining Terraform with ArgoCD and Crossplane, when each approach fits, and how to avoid the common failure modes of mixing paradigms."
url: https://agent-zone.ai/knowledge/cicd/gitops-iac-reconciliation/
section: knowledge
date: 2026-02-22
categories: ["cicd"]
tags: ["gitops","terraform","argocd","crossplane","reconciliation","infrastructure-as-code","drift-detection","continuous-reconciliation","push-vs-pull"]
skills: ["gitops-iac-integration","reconciliation-design","terraform-argocd-patterns","crossplane-patterns"]
tools: ["terraform","argocd","crossplane","flux"]
levels: ["intermediate","advanced"]
word_count: 1175
formats:
  json: https://agent-zone.ai/knowledge/cicd/gitops-iac-reconciliation/index.json
  html: https://agent-zone.ai/knowledge/cicd/gitops-iac-reconciliation/?format=html
  api: https://api.agent-zone.ai/api/v1/knowledge/search?q=GitOps+and+Infrastructure+as+Code%3A+Reconciliation+Patterns+for+Terraform%2C+ArgoCD%2C+and+Crossplane
---


# GitOps and Infrastructure as Code

GitOps says: the desired state is in Git. A controller continuously reconciles the real state to match. Infrastructure as Code says: the desired state is in code. A human (or agent) runs `apply` to push changes.

These two paradigms overlap but do not align perfectly. Kubernetes resources fit the GitOps model well — ArgoCD/Flux watch Git, detect differences, and apply changes continuously. Cloud infrastructure (VPCs, databases, IAM roles) fits the IaC model better — Terraform tracks state, computes diffs, and applies on command.

Most real systems need both. This article covers how to combine them without creating reconciliation conflicts, state drift, or operational confusion.

## The Reconciliation Gap

| Property | Terraform (IaC) | ArgoCD/Flux (GitOps) |
|---|---|---|
| **State tracking** | Explicit state file (S3, Azure Blob, GCS) | Kubernetes API server is the state |
| **Reconciliation** | On-demand (`terraform apply`) | Continuous (every 3 minutes by default) |
| **Drift response** | Detected on next `plan`, requires manual fix | Automatically corrected to match Git |
| **Scope** | Any cloud resource (AWS, Azure, GCP, K8s, DNS, etc.) | Kubernetes resources (manifests, Helm, Kustomize) |
| **Rollback** | Git revert + apply (manual) | Git revert → auto-reconciled (automatic) |
| **Concurrency** | State lock prevents concurrent applies | Multiple sources can conflict |

The gap: Terraform does not continuously reconcile. ArgoCD cannot manage non-Kubernetes resources directly. You need both, and you need clear ownership boundaries.

## Pattern 1: Terraform for Cloud, GitOps for Kubernetes

The most common and safest pattern: use each tool for what it does best.

```
Terraform manages:
  ├── VPC / VNET / VPC Network
  ├── Subnets, NAT, routes
  ├── EKS / AKS / GKE cluster
  ├── RDS / Azure SQL / Cloud SQL
  ├── IAM roles and policies
  ├── S3 buckets, KMS keys
  └── DNS zones

ArgoCD/Flux manages:
  ├── Kubernetes namespaces
  ├── Deployments, Services, Ingress
  ├── Helm releases
  ├── ConfigMaps, Secrets (sealed/external)
  ├── CRDs (cert-manager, external-dns)
  ├── Network Policies
  └── RBAC (ClusterRoles, RoleBindings)
```

### How They Connect

Terraform creates the cluster and outputs connection information. ArgoCD/Flux is bootstrapped onto the cluster and takes over Kubernetes resource management:

```hcl
# Terraform creates the cluster
resource "aws_eks_cluster" "main" {
  name    = "production"
  version = "1.29"
  # ...
}

# Terraform bootstraps ArgoCD
resource "helm_release" "argocd" {
  name       = "argocd"
  namespace  = "argocd"
  repository = "https://argoproj.github.io/argo-helm"
  chart      = "argo-cd"
  version    = "6.0.0"

  depends_on = [aws_eks_node_group.main]
}
```

After this, Terraform does not manage any Kubernetes resources except the cluster itself. ArgoCD manages everything inside the cluster from Git.

### Boundary Rules

- **Terraform owns the cluster lifecycle**: creation, version upgrades, node group changes, networking
- **ArgoCD owns the cluster content**: all resources running inside the cluster
- **Neither manages the other's resources**: Terraform does not create Deployments, ArgoCD does not modify the VPC
- **Outputs bridge the gap**: Terraform outputs (cluster endpoint, database URL, IAM role ARNs) are consumed by ArgoCD apps via ConfigMaps or ExternalSecrets

## Pattern 2: Crossplane for Cloud Resources in GitOps

Crossplane lets you manage cloud resources (RDS, S3, IAM) as Kubernetes CRDs, bringing them into the GitOps reconciliation loop.

```yaml
# Cloud SQL database managed as a Kubernetes resource
apiVersion: sql.gcp.upbound.io/v1beta1
kind: DatabaseInstance
metadata:
  name: production-postgres
spec:
  forProvider:
    databaseVersion: POSTGRES_15
    region: us-central1
    settings:
      - tier: db-custom-2-8192
        diskSize: 50
        ipConfiguration:
          - ipv4Enabled: false
            privateNetwork: projects/myproject/global/networks/production-vpc
```

ArgoCD deploys this manifest. Crossplane reconciles it to a real Cloud SQL instance. Drift is automatically corrected.

### When Crossplane Makes Sense

| Scenario | Use Crossplane | Use Terraform |
|---|---|---|
| Databases created per team/namespace | Yes — teams self-serve via K8s manifests | No — too many Terraform PRs for per-team resources |
| Core networking (VPC, subnets, routes) | No — networking is foundational, not per-team | Yes — managed once, rarely changes |
| Per-service S3 buckets | Yes — each service declares its bucket | Maybe — if few services, Terraform is simpler |
| IAM roles for workload identity | Either — Crossplane or Terraform both work | Either |
| EKS/AKS/GKE cluster lifecycle | No — cluster is foundational | Yes — cluster creation is a one-time operation |
| Environment-specific databases | Yes if many environments | Either |

### The Hybrid Architecture

```
Terraform:
  VPC, subnets, NAT, routes
  EKS cluster + node groups
  IAM foundational roles
  S3 state bucket, KMS keys
  DNS zone
      ↓ (cluster exists, Crossplane is installed)

Crossplane + ArgoCD:
  Team databases (per-namespace RDS/Cloud SQL)
  Team S3 buckets
  Per-service IAM roles
  Application Kubernetes resources (Deployments, Services, etc.)
```

Terraform handles the **platform layer** (things that exist once and rarely change). Crossplane handles the **application layer** (things created per team, per service, per environment).

## Pattern 3: Terraform Controller in Kubernetes

Run Terraform from inside Kubernetes, triggered by ArgoCD:

```yaml
apiVersion: infra.contrib.fluxcd.io/v1alpha2
kind: Terraform
metadata:
  name: networking
  namespace: terraform-system
spec:
  path: ./infrastructure/networking
  sourceRef:
    kind: GitRepository
    name: infrastructure
  approvePlan: auto  # or "manual" for human approval
  interval: 10m
  retryInterval: 5m
```

This brings Terraform into the GitOps reconciliation loop — changes in Git trigger Terraform plans and applies automatically.

### Tradeoffs

| Advantage | Disadvantage |
|---|---|
| Single reconciliation model (everything is GitOps) | Terraform state management adds complexity inside K8s |
| Drift detected and corrected automatically | `auto` approve is dangerous for destructive changes |
| No external CI/CD pipeline for infrastructure | Debugging Terraform failures is harder inside a pod |
| Consistent with how applications are deployed | Terraform state lock conflicts if multiple controllers run |

**Recommendation**: Use this pattern only if your team is already deeply invested in GitOps and wants a single operational model. For most teams, Pattern 1 (Terraform in CI/CD, ArgoCD for K8s) is simpler and safer.

## Anti-Patterns

### Dual Management

The most dangerous anti-pattern: both Terraform and ArgoCD/Crossplane manage the same resource.

```
Terraform creates aws_db_instance.main → state tracks it
Crossplane creates the same RDS instance via CRD → Crossplane tracks it

Result: Two controllers fight over the same resource.
Terraform sees "unexpected changes" on every plan.
Crossplane sees "drift" every reconciliation cycle.
One overwrites the other's changes in an infinite loop.
```

**Fix**: Clear ownership. Every resource is managed by exactly one tool. Draw the boundary and document it.

### Crossplane for Everything

Using Crossplane to manage foundational infrastructure (VPC, clusters, IAM) that changes once and needs careful, reviewed changes:

```
Problem: Crossplane auto-reconciles, including destructive changes.
If the VPC CIDR changes in Git (typo, merge conflict), Crossplane
recreates the VPC — destroying every resource in it.
```

**Fix**: Use Crossplane for resources where continuous reconciliation is valuable (per-team databases, per-service buckets). Use Terraform with manual approval for foundational resources where a mistake is catastrophic.

### Terraform for Kubernetes Application Resources

Using Terraform to manage Deployments, Services, ConfigMaps:

```
Problem: Terraform applies once but does not reconcile.
If someone `kubectl edit`s a Deployment, Terraform does not notice
until the next `terraform plan`. ArgoCD would immediately detect
and revert the change.
```

**Fix**: Use ArgoCD/Flux for resources that benefit from continuous reconciliation. Use Terraform for resources that should only change through reviewed PRs.

## Choosing Your Pattern

| Team Situation | Recommended Pattern |
|---|---|
| Starting fresh, small team | Pattern 1: Terraform for cloud, ArgoCD for K8s |
| Platform team serving many app teams | Pattern 2: Terraform for platform, Crossplane + ArgoCD for team resources |
| Deep GitOps investment, wants single model | Pattern 3: Terraform controller (with caution) |
| Single developer or small project | Terraform only — add GitOps when the team grows |
| Enterprise with change advisory boards | Pattern 1 with strict PR approval gates on Terraform |

The goal is clear ownership, not tool purity. Every resource has exactly one controller. Boundaries are documented. Agents and humans know which tool manages which resource without checking.

