Infrastructure as Code stopped being a differentiator years ago; in 2026 it is table stakes. What still separates teams is how well they run it: whether state is a liability or a non-event, whether policy is enforced before apply or discovered after an incident, and whether "what is running in production" is answered by Git or by an archaeology session. This article covers the Terraform fundamentals that keep IaC boring, and the GitOps layer that extends the same discipline to Kubernetes workloads.

Terraform Fundamentals Done Right

State Is the Crown Jewels

Terraform state maps your code to real resources, and it frequently contains secrets in plain text. It belongs in an encrypted remote backend with locking, with access restricted as tightly as your production credentials — never in the repository. Since Terraform 1.10, the S3 backend supports native lockfile-based locking, so a separate DynamoDB table is no longer required for new configurations:

terraform {
  required_version = ">= 1.10"

  backend "s3" {
    bucket       = "org-terraform-state"
    key          = "network/prod/terraform.tfstate"
    region       = "eu-central-1"
    encrypt      = true
    use_lockfile = true
  }
}

Split state by blast radius: one state file per environment per component, so a mistake in a staging module cannot corrupt the production network. State surgery with terraform state mv is occasionally necessary; needing it weekly is a design smell.

Modules: Small, Versioned, Composable

Good modules encapsulate one decision — a VPC layout, a hardened database instance, a standard set of IAM roles — and expose a small variable surface. Pin module versions via registry constraints or Git tags so an upstream change never lands in production implicitly. Resist the giant "platform module" that wraps everything: it couples every consumer to every change and turns each upgrade into a coordination project.

Workspaces Have a Narrow Job

Terraform workspaces are fine for ephemeral copies of the same configuration — preview environments, per-branch test stacks. They are the wrong tool for separating dev from prod, because every workspace shares the same backend configuration, the same code path and, too often, the same credentials. For real environments, prefer separate directories (or repositories) with separate state and separate cloud accounts, and let the directory structure make the blast radius explicit.

Policy as Code

Code review does not scale as a compliance mechanism. Encode the rules — no publicly readable buckets, mandatory cost-allocation tags, approved instance families, TLS 1.2 or newer everywhere — as policies evaluated against the Terraform plan in CI, using tools such as OPA with Conftest, HashiCorp Sentinel, or Checkov. A failed policy check blocks the merge with a message the author can act on, which is both faster and less contentious than a human pointing out the same problem for the tenth time. Policy checks belong in the same pipeline as terraform plan; if you are building that pipeline, our primer on what CI/CD actually involves covers the foundations.

Drift Is a Signal, Not an Annoyance

Drift happens whenever reality diverges from code: a console hotfix during an incident, an operator "just checking something", an autoscaler doing its job. Detect it by running a scheduled terraform plan against every state and alerting on non-empty diffs. Then treat each finding as information — recurring drift in the same place usually means the code is missing a knob people genuinely need, and the fix is to add the variable, not to lecture the team.

GitOps with ArgoCD and Flux

For Kubernetes workloads, GitOps inverts the deployment model: instead of CI pushing manifests into the cluster, an in-cluster controller — ArgoCD or Flux — continuously pulls the desired state from Git and reconciles the cluster toward it. The benefits are structural. Drift correction is built in, because the controller re-applies Git's version of the truth on every sync. Credentials improve, because nothing outside the cluster needs apply access. And rollback is git revert, with the full audit trail that implies.

ArgoCD suits teams that want a UI, application-level health views and app-of-apps composition; Flux suits teams that want a leaner, CRD-driven toolkit that composes with Kustomize and Helm. Both are mature — the choice is ergonomics, not capability. Whichever you pick, enable automated sync with self-healing in production only after your alerting can tell you why a sync failed; the patterns in our Kubernetes in real life write-up apply directly here.

Promoting Changes Across Environments

Environment promotion is where IaC and GitOps discipline pays off or falls apart. The rule: promote artifacts, not rebuilds. The container image that passed staging is byte-for-byte the image that reaches production; only configuration — replica counts, resource limits, endpoints — differs, expressed as Kustomize overlays or per-environment Helm values. Promotion itself is a pull request that updates an image tag or a values file in the environment's directory, reviewed and merged like any other change. No environment is special: dev, staging and prod differ in data and scale, never in mechanism.

Teams that get this right ship infrastructure changes with the same cadence and confidence as application code. If yours is not there yet — state scattered across laptops, console changes nobody recorded, deployments that depend on one person — our DevOps solutions practice helps teams put Terraform, policy as code and GitOps on rails.