Problem Teams that create servers and cloud resources by clicking in a console cannot reproduce environments, review changes or recover quickly from disaster. This guide covers managing infrastructure as reviewed files and its risks.
Declare servers, networks, DNS and permissions in versioned files and let a tool converge reality to them; Terraform/OpenTofu for most teams, Pulumi or CDK when you want a real language.
terraformopentofupulumicdkansiblegitopsstate
Overview
Infrastructure as code (IaC) replaces console clicking with files reviewed in git. A declarative tool compares desired state with a recorded state file and the provider's real resources, prints a plan (create, change, destroy), and applies it. The payoff is reproducible environments, reviewed changes and fast disaster recovery; the risk is a shared state file that, if lost or corrupted, disconnects the tool from reality.
Key facts
Terraform (BSL licence since 2023) and its open fork OpenTofu (MPL) share HCL and providers for AWS, GCP, Azure, Hetzner, Cloudflare and hundreds more.
Pulumi, AWS CDK and CDKTF use TypeScript/Python/Go; CloudFormation and ARM/Bicep are single-cloud.
State holds every resource ID and often secrets in plain text: store it remotely, encrypted, locked.
A plan for a large stack takes order of seconds to minutes; split stacks by blast radius.
Use when
More than a handful of resources, more than one environment, or more than one person changing infrastructure.
Avoid when
One throwaway VM. Even then, a cloud-init file in git is cheap.
Deep dive
Tool map
Tool
Language
State
Best at
Terraform / OpenTofu
HCL
Remote backend (S3, GCS, azurerm, HTTP), lock
Multi-provider, biggest ecosystem
Pulumi
TS, Python, Go, C#
Pulumi Cloud or self-managed backend
Loops, abstractions, testing in a real language
AWS CDK
TS, Python, Java
CloudFormation stacks
AWS-only teams who like constructs
CloudFormation / Bicep
YAML / Bicep
Managed by the cloud
No state file to run; single cloud
Ansible / cloud-init
YAML
None (imperative-ish)
Configuring inside servers
Crossplane / k8s operators
YAML CRDs
Kubernetes etcd
GitOps-driven cloud resources
Decision table
Situation
Choice
Why
Mixed providers (Hetzner + Cloudflare + AWS)
OpenTofu/Terraform
One tool, one workflow
AWS-only, developers write TypeScript
CDK or Pulumi
Real language, type checks
Server configuration (packages, files, users)
cloud-init for first boot, Ansible or image builds (Packer) after
IaC tools are bad at in-OS config
Kubernetes app manifests
Helm/Kustomize + Argo CD/Flux (GitOps)
Continuous reconciliation from git
Secrets
Secret manager referenced by ID, never values in code
CI gate: plan on pull request, apply only from main with the reviewed plan:
tofu init -input=false
tofu plan -input=false -out=plan.bin -lock-timeout=60s
tofu show -no-color plan.bin > plan.txt # post as PR comment
# on merge, apply exactly the reviewed plan
tofu apply -input=false plan.bin
Efficiency
Cost: a plan refreshes every resource through provider API calls and then diffs it against the configuration, so plan time grows with resource count times API latency, and large stacks hit provider rate limits. OpenTofu walks the graph with 10 concurrent operations by default (-parallelism, OpenTofu docs). State storage is negligible. Bound: provider API latency and rate limits.
Levers:
split stacks by layer and environment — smaller graphs, shorter plans, smaller blast radius (this page) — large estates
-refresh=false — fewer remote API requests, but external changes are ignored (OpenTofu plan docs) — local iteration only, never the CI gate
-parallelism=n — more concurrent operations than the default 10 (OpenTofu plan docs) — until rate limits answer
apply the saved plan file — no second plan between review and apply (Code above; OpenTofu docs) — CI pipelines
Naive → standard → state of the art:
console clicks · minutes per change, no review, not repeatable
one stack per environment, plan on pull request, apply the saved plan · one refresh pass per stack
layered stacks, scheduled drift plans, continuous reconciliation (Crossplane, Argo CD) · only the touched layer is refreshed on a change
Implementation notes
Split state per environment and per layer (network, data, apps); a bad apply should not be able to touch the database layer from the app stack.
prevent_destroy on stateful resources; deletion protection on databases at the provider level too.
Pin provider versions; upgrade deliberately with a plan diff.
Import existing hand-made resources (import blocks) instead of recreating them.
Run drift detection on a schedule (plan -detailed-exitcode, exit 2 = drift) and alert.
CI credentials: short-lived OIDC federation from the CI system to the cloud, least privilege (Least privilege and isolation).
Pitfalls
Local state files on laptops: lost, conflicting, containing secrets.
count index shifts: removing item 0 recreates every later resource; use for_each with stable keys.
Renaming a resource destroys and recreates it unless you add a moved block.
Console hot-fixes create drift that the next apply silently reverts.
One giant state for everything: slow plans, huge blast radius, lock contention.