IaC — State Corruption from Untagged S3 Buckets
Untagged S3 bucket manually deleted corrupts Terraform state.
20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.
- ✓Solid grasp of DevOps fundamentals
- ✓Comfortable with command-line tools
- ✓Basic Linux administration knowledge
- IaC defines infrastructure in version-controlled files — treat servers like code
- Declarative (Terraform) describes desired state; imperative (Ansible) lists steps
- Remote state storage (S3+DynamoDB) prevents team-wide state corruption
- Idempotency means 10 applies = same result as 1 — safe for CI/CD
- Production insight: manual changes in the cloud console cause drift; Terraform overwrites them
- Biggest mistake: committing terraform.tfstate to Git — exposes plaintext secrets permanently
Imagine you're building a LEGO city. Instead of photographing your city and hoping you can recreate it from memory, you keep the instruction booklet. Whenever a tornado (server crash) hits, you just follow the booklet and rebuild it perfectly in minutes. Infrastructure as Code is that instruction booklet — except for your servers, networks, and cloud resources. Your entire data centre, written down as files you can version, share, and replay on demand.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
Every modern software team has faced the same nightmare: a production server dies at 2am, and the engineer who built it left the company six months ago. Nobody wrote anything down. The replacement server gets rebuilt from memory, Slack messages, and guesswork — and it's never quite right. This isn't a people problem. It's a process problem, and Infrastructure as Code (IaC) exists specifically to eliminate it. When your infrastructure lives in code, it lives in Git, in pull requests, in code reviews, and in your CI/CD pipeline — just like the application it runs.
Before IaC, provisioning infrastructure meant logging into a cloud console, clicking through wizards, and hoping the person next to you was watching and taking notes. Every environment — dev, staging, production — drifted apart over time because human hands configured them differently. This 'configuration drift' is the silent killer of reliable deployments. IaC solves this by making infrastructure declarative and repeatable: you describe the desired state of your system, and a tool like Terraform or Ansible figures out how to get there. The same code that spins up your staging environment spins up production, byte for byte.
By the end of this article you'll understand why IaC exists at a systems level, know the difference between declarative and imperative approaches, and have a real working Terraform + GitHub Actions CI/CD pipeline you can adapt for your own projects. You'll also know the two mistakes that catch almost every intermediate engineer off guard when they go to use IaC in a team setting.
Why Infrastructure as Code Is a State Machine, Not a Script
Infrastructure as Code (IaC) is the practice of defining and managing infrastructure — servers, networks, databases — through machine-readable definition files, not manual CLI commands or click-ops. The core mechanic is declarative: you specify the desired end state (e.g., three EC2 instances, one RDS read replica), and the IaC tool computes the diff between current and desired state, then executes only the necessary create/update/delete operations. This turns infrastructure into a reproducible artifact, versioned alongside application code.
In practice, IaC tools like Terraform, CloudFormation, or Pulumi maintain a state file that maps real-world resources to your definitions. This state is the source of truth — it tracks resource IDs, dependencies, and metadata. When you run a plan, the tool compares your config against this state, not against live cloud APIs directly. That means state corruption (e.g., from manual changes or untagged resources) causes drift: the tool sees a resource as missing when it's actually present, or vice versa, leading to duplicate resources, deletion of production data, or failed applies.
Use IaC for any environment that outlives a single developer session — production, staging, even long-lived dev environments. The value compounds when you need to recreate an environment from scratch (disaster recovery, blue/green deployments) or audit changes across a team. Without IaC, you're one accidental click away from an irreproducible mess. With it, you get deterministic provisioning, change history, and the ability to roll back infrastructure changes like code.
Declarative vs Imperative IaC — Choosing the Right Mental Model
There are two ways to tell someone how to make a cup of coffee. The imperative way: 'Boil water. Measure 18g of beans. Grind them. Pour water at 94°C. Wait 4 minutes.' The declarative way: 'I want a black filter coffee in this cup.' The declarative approach lets the system figure out the steps.
This distinction is the most important conceptual split in IaC. Terraform is declarative — you describe what your infrastructure should look like, and Terraform calculates the diff between current state and desired state, then makes the changes. Ansible is imperative by default — you write a sequence of tasks that run top to bottom. Both are valid. The right choice depends on what you're managing.
Declarative tools shine for cloud resource provisioning: creating VPCs, EC2 instances, databases, and load balancers. You don't want to think about order of operations — you just want the result. Imperative tools shine for configuration management: installing packages, editing config files, restarting services. The order genuinely matters there.
In a mature DevOps pipeline you'll often use both: Terraform provisions the server, Ansible configures it. Understanding why they work differently stops you from fighting the tool when it doesn't behave the way you expect.
IaC Tools Comparison: Terraform, Ansible, and AWS CDK
Choosing the right IaC tool is like choosing the right hammer — they look similar but each is designed for a specific nail. The three most popular tools each have distinct strengths: Terraform for declarative cloud provisioning, Ansible for imperative configuration management, and AWS CDK for developers who want to write infrastructure in familiar programming languages.
| Feature | Terraform (Declarative) | Ansible (Imperative) | AWS CDK (Imperative-like) |
|---|---|---|---|
| Primary use | Cloud resource provisioning | Configuration and application deployment | Cloud resource provisioning (AWS only) |
| Approach | Declare desired state, tool computes steps | Write ordered tasks, tool executes | Write code (TypeScript, Python, etc.) that generates CloudFormation |
| State management | Explicit state file (local or remote) | Stateless — runs against live systems | CloudFormation stack (state managed by AWS) |
| Idempotency | Built-in via plan/apply | Manual — each task must be idempotent | Built-in via CloudFormation |
| Language | HCL | YAML | TypeScript, Python, Java, C#, Go |
| Learning curve | Moderate (must understand state, providers) | Gentle (YAML is easy, no agent required) | Steep (need to understand programming and cloud abstractions) |
| Best for | Multi-cloud, team with dedicated IaC knowledge | Quick automation, server configuration, hybrid environments | AWS-only teams with developers comfortable in TypeScript/Python |
CDK (Cloud Development Kit) is unique because it lets you define AWS resources using general-purpose languages. Under the hood, CDK synthesises CloudFormation templates — so you get the safety of declarative state management with the expressiveness of code. This is increasingly popular in DevOps-heavy teams that already use TypeScript for backend services.
In production, pick Terraform if you need multi-cloud or a mature state management story. Pick Ansible if you're configuring servers and don't want to manage state files. Pick CDK if your team lives in AWS and writes TypeScript daily. Mixing Terraform (cloud) + Ansible (config) is the most common pattern among teams that need flexibility.
Push vs Pull Model in IaC — How Agents and Agentless Architectures Compare
IaC tools fall into two operational models: push and pull. This distinction affects everything from security posture to network topology. Understanding it helps you choose the right tool for your environment — and debug failures when the model conflicts with your infrastructure.
Push model (Agentless): The orchestration server (or user's laptop) directly connects to target nodes via SSH or WinRM and executes commands. Ansible is the canonical example. No agent needs to be installed on the target — the orchestrator pushes configuration to the node. This is simple to set up initially but requires network connectivity from the orchestrator to every target. In production pipelines, this often means running Ansible from a CI runner that has SSH access to your fleet.
Pull model (Agent-based): An agent is installed on each target node. The agent periodically polls a central server (or service) for its desired configuration, pulls it down, and applies it locally. Chef and Puppet (in default mode) work this way. Pull models scale better at the cost of more complex initial setup. The agent handles retries, avoids single orchestrator bottlenecks, and works even when the target is behind NAT or a firewall.
The trade-off: push is simpler for small infrastructures, pull is more resilient at scale. Many mature DevOps teams adopt a hybrid approach: Terraform (push for provisioning) + Chef/Puppet (pull for ongoing config).
Idempotency vs Immutability — Two Pillars of Reliable Infrastructure
Two concepts are often confused but serve different purposes: idempotency and immutability. Both make infrastructure safer, but they achieve safety through different mechanisms.
Idempotency means running the same operation multiple times produces the same result as running it once. Terraform is idempotent: if you apply the same configuration twice, the second apply does nothing (no changes). Idempotent tools are forgiving — you can rerun them as often as needed without side effects. This is essential for CI/CD where multiple triggers may attempt an apply.
Immutability means you never modify a running resource. Instead, you replace it entirely. When a configuration change is needed, you build a new server (or container) and switch traffic to it, then destroy the old one. This guarantees that the running system always matches the artifact that was built in CI. No drift, no snowflake servers. Tools like Packer and Docker champion immutability — you bake the configuration into an image, then deploy the image.
| Aspect | Idempotency | Immutability |
|---|---|---|
| What it guarantees | Safe re-runs of the same apply | The running resource is exactly what was built |
| How it's achieved | State tracking, diff calculation | Blue/green deployments, image-based builds |
| Tool examples | Terraform, Ansible (if written idempotently) | Packer, Docker, AWS AMI pipelines |
| Conflict with manual changes | Overwrites drift (idempotent apply) | Manual changes are impossible — server is replaced |
| Production benefit | Quick recovery from partial failures | Predictable, no drift, easier rollback (switch to old image) |
| Drawback | State can still drift between applies | Slower deployment — build and test new image each time |
In a mature production environment you'll leverage both: use immutable images for core infrastructure (AMI pipelines) and idempotent configuration management for runtime adjustments (config files, secret rotation).
IaC in a Real CI/CD Pipeline — Automate the Infrastructure Itself
Knowing how to run Terraform locally is a starting point. But IaC's real power unlocks when it runs automatically inside your CI/CD pipeline. Think about it: your application code goes through automated testing before it deploys. Why should your infrastructure changes be any different? A pull request that adds a new RDS database should go through the same review process as a pull request that adds a new API endpoint.
The pattern that works in production is this: on every pull request, run terraform plan and post the output as a PR comment. This gives reviewers an exact, human-readable diff of what will change in the real cloud — before anyone approves it. On merge to main, run terraform apply automatically. No one runs Terraform from their laptop. Ever.
This approach solves three problems at once. It creates an audit trail (every infrastructure change is a Git commit with an author and a timestamp). It prevents 'works on my machine' infrastructure (the pipeline always runs from a clean state). And it forces infrastructure changes through code review, which catches mistakes before they hit production.
The GitHub Actions workflow below implements this exact pattern. It's the real thing — not a toy example.
terraform.tfstate file is created locally. If two engineers run Terraform against the same environment, they'll corrupt each other's state and create duplicate or orphaned resources. Always configure a remote backend — S3 + DynamoDB for AWS, or Terraform Cloud — before anyone else joins the project. Add .tfstate and .tfstate.backup to your .gitignore immediately. State files contain plaintext secrets.Remote State and Modules — The Patterns That Make IaC Scale
A single main.tf file works fine for a hobby project. It falls apart the moment you have two engineers, two environments, or two services. This is where two patterns become non-negotiable: remote state backends and modules.
Remote state is how Terraform remembers what it already built. Without it, every terraform apply is flying blind. With a remote backend — like an S3 bucket with a DynamoDB lock table — the state file lives in the cloud, is accessible to everyone on the team, and is locked during applies so two engineers can't run it simultaneously and corrupt each other's work.
Modules are reusable Terraform components. Think of them as functions for infrastructure. Instead of copy-pasting the same EC2 + security group + IAM role configuration for every service, you write it once as a module and call it with different variables for each service. This is the IaC equivalent of the DRY principle and it's what separates a professional IaC setup from a pile of disconnected config files.
Below is a minimal but real remote backend configuration alongside a module call pattern. This is the structure you'd actually find in a production repository.
terraform apply does nothing. If you can't guarantee this, your pipeline becomes dangerous — applying twice could duplicate resources and double your AWS bill. This is a concept interviewers probe hard on.IaC Security and Secrets Management — Don't Leak Your Infrastructure's Keys
Infrastructure code often requires secrets: API keys, database passwords, cloud provider credentials. A common rookie mistake is hardcoding these in the IaC files. Terraform state files, in particular, store resource attributes in plaintext, which can include sensitive values like database passwords or IAM secret keys.
The rule: IaC code should never contain secrets. Instead, use environment variables, encrypted variables in your CI/CD platform, or a dedicated secrets manager like HashiCorp Vault, AWS Secrets Manager, or GitHub Actions Secrets. For Terraform, use the sensitive = true attribute on outputs, and avoid outputting secrets in plan output. Use data sources that read from a secrets manager rather than embedding values.
Another critical practice: never commit .tfstate files to version control. Even if you delete them later, secrets are permanently exposed in Git history. Add .tfstate and .tfstate.backup to .gitignore on day one. Use a remote backend with encryption at rest.
Below is an example of using environment variables with Terraform, and a pattern for reading secrets from AWS Secrets Manager.
IaC Testing and Validation — Ensure Your Infrastructure Works Before You Apply
Writing infrastructure code without testing is like deploying a microservice without unit tests. You're one typo away from deleting a production database. IaC testing isn't as mature as application testing, but it's evolving fast. Here are three levels of validation every IaC pipeline needs:
- Syntax and static analysis: Use
terraform validateto catch basic HCL errors. Use tools liketflintfor style and potential bugs, andcheckovortfsecfor security policy violations. Run these on every PR before the plan step. - Plan review: The manual step where a human reviews the
terraform planoutput. This catches logical mistakes — like changing a security group that breaks connectivity, or accidentally destroying a stateful resource. - Integration testing: Tools like Terratest let you write Go tests that deploy real infrastructure, run assertions against it, and then destroy it. This is the gold standard, but it's expensive and slow. Use it sparingly for critical resources.
Below is a minimal GitHub Actions step that runs static analysis before the plan. Integrate this into your workflow to catch issues early.
Why Your Deployment Pipeline Should Fail When the State Lock Is Missing
Most teams treat state locking as optional. It's not. Without a lock, two engineers — or worse, two pipelines — can apply changes simultaneously. The result is state corruption, partial deployments, and an infrastructure that matches neither configuration. I've debugged a three-hour outage caused by concurrent Terraform applies fighting over the same S3 backend. The fix was one line: dynamodb_table = "terraform-lock". Treat state locking like a database transaction. If your CI/CD pipeline doesn't fail when the lock is missing, you're accepting risk. In production, configure your backend to reject concurrent operations. Check lock status before planning. If the lock exists, abort. Your staging environment isn't a rehearsal — it's the same code path. If you skip safety there, you'll skip it in production. Add a pre-flight check that verifies lock availability before any apply command.
Stop Patching Your VMs — Burn Them and Rebuild
Mutable infrastructure feels comfortable. You SSH in, run yum update, and move on. But after six months, that server has accumulated packages, configs, and cron jobs no one remembers. Configuration drift turns your 'pet' into a fragile mystery. Immutable infrastructure solves this. When you need to update a server, you don't patch it — you destroy it and create a fresh one from a golden image. Your CI/CD pipeline builds that image, runs security scans, and deploys it to staging. If tests pass, the image goes to production. The old server is terminated. This shifts your mindset: you're not managing servers, you're managing releases. Use tools like Packer to build AMIs or Azure Image Builder for VHDs. Store images in a registry with version tags. Never SSH into production. If something's wrong, roll back to the previous image — not a 'patch Tuesday' prayer.
The Terraform State Corruption That Took Down Production
- Always tag IaC-managed resources with ManagedBy: <tool> — it tells humans not to touch them manually.
- Enable versioning on state-critical resources so you can recover from accidental deletion.
- Use S3 bucket policies or IAM permissions to block console modifications for production resources.
- Run 'terraform plan' periodically in a CI job to catch drift before it becomes a crisis.
terraform init -input=false -lock=falseterraform providers mirror <path> # to cache providers offline| File | Command / Code | Purpose |
|---|---|---|
| main.tf | provider "aws" { | Declarative vs Imperative IaC |
| cdk-stack.ts | export class MyStack extends cdk.Stack { | IaC Tools Comparison |
| ansible-push-vs-chef-pull.md | - name: Ensure nginx is installed on web servers | Push vs Pull Model in IaC |
| immutable-vs-idempotent.tf | resource "aws_instance" "immutable_server" { | Idempotency vs Immutability |
| .github | name: Terraform Infrastructure Pipeline | IaC in a Real CI/CD Pipeline |
| backend.tf + modules | terraform { | Remote State and Modules |
| secrets.tf | provider "aws" { | IaC Security and Secrets Management |
| static-analysis.yml | - name: Run tflint (Terraform linter) | IaC Testing and Validation |
| preflight_lock_check.py | def check_lock(bucket, key, region='us-east-1'): | Why Your Deployment Pipeline Should Fail When the State Lock |
| packer_ami.pkr.hcl | source "amazon-ebs" "web" { | Stop Patching Your VMs |
Key takeaways
terraform plan on PR (posted as a comment for review) and terraform apply on merge to mainInterview Questions on This Topic
What is configuration drift, and how does Infrastructure as Code prevent it? Can you give a concrete example of how drift occurs without IaC?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.
That's CI/CD. Mark it forged?
10 min read · try the examples if you haven't