Infrastructure as Code (IaC) is the practice of defining servers, networks, and environments in machine-readable files instead of configuring them by hand. Rather than SSHing into a server and running commands one at a time, you write a file that describes what the server should look like — its OS, packages, firewall rules, storage — and a tool reads that file and makes it happen. The infrastructure becomes code: versioned, reviewed, tested, and reproducible.
Why Infrastructure as Code Exists
Manual server setup works fine when you have one server. It falls apart at two.
The core problem is snowflake servers — machines configured by hand over weeks or months, each one slightly different from the next. One staging server has a different PHP version than production. Someone installed a debugging tool on server 3 and forgot to remove it. The firewall rules on the load balancer were tweaked during an incident at 2 AM and never documented.
These inconsistencies cause deployment failures, security gaps, and hours of debugging where the root cause turns out to be this server was set up differently.
IaC eliminates this class of problems entirely:
- Reproducibility: Spin up identical environments from the same config file. Staging matches production because they're defined by the same code.
- Version control: Infrastructure changes go through pull requests, code review, and commit history — the same workflow your application code uses.
- Auditability: You can answer
who changed what, when, and why
by reading the Git log instead of searching through SSH session histories. - Speed: Provisioning a new server takes minutes, not days of manual setup and configuration.
Declarative vs. Imperative IaC
IaC tools fall into two categories based on how you describe your infrastructure.
Declarative tools let you describe the desired end state. You say I want an Ubuntu server with 4 GB of RAM running nginx
and the tool figures out the steps to get there. If the server already exists but nginx is missing, it installs nginx. If everything matches, it does nothing.
# Terraform (declarative) — describe what you want
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
tags = {
Name = "web-server"
}
}
Imperative tools let you describe the steps to execute, in order. You control the sequence explicitly — install this package, then copy this file, then restart this service.
# Ansible (imperative) — describe what to do
- name: Set up web server
hosts: web
tasks:
- name: Install nginx
apt:
name: nginx
state: present
- name: Start nginx
service:
name: nginx
state: started
flowchart LR
subgraph Declarative
A[Define desired state] --> B[Tool calculates diff]
B --> C[Tool applies changes]
end
subgraph Imperative
D[Define step 1] --> E[Define step 2]
E --> F[Define step 3]
F --> G[Execute in order]
end
In practice, most teams use both. Terraform provisions the servers (declarative), then Ansible configures what runs on them (imperative). The declarative approach is generally safer for infrastructure — it prevents configuration drift because the tool continuously reconciles reality with the desired state.
The Major IaC Tools Compared
| Tool | Approach | Language | Cloud Support | Best For |
|---|---|---|---|---|
| Terraform | Declarative | HCL | Multi-cloud | Teams using multiple cloud providers |
| Pulumi | Declarative | Python, TypeScript, Go, C# | Multi-cloud | Teams that prefer general-purpose languages |
| AWS CloudFormation | Declarative | JSON/YAML | AWS only | AWS-only environments |
| Azure Bicep | Declarative | Bicep DSL | Azure only | Azure-only environments |
| Ansible | Imperative | YAML | Any (agentless SSH) | Server configuration and application deployment |
| Chef/Puppet | Imperative | Ruby DSL / Puppet DSL | Any (agent-based) | Large fleets with complex configuration policies |
Terraform is the default choice for most teams. It supports every major cloud provider, has a massive ecosystem of community modules, and its HCL syntax is readable without being a programmer. If you are unsure which tool to pick, start here.
Pulumi is for teams that find HCL limiting. Writing infrastructure in TypeScript or Python means you get IDE autocompletion, unit testing frameworks, and the ability to use loops and conditionals without workaround syntax.
CloudFormation and Bicep are vendor-specific tools that integrate tightly with their respective cloud platforms. If you are exclusively on AWS or Azure with no plans to change, the native tool gives you tighter integration and faster support for new services.
Ansible sits in a different category — it configures servers rather than provisioning them. It connects over SSH, runs tasks, and disconnects. No agent to install, no state file to manage. Many teams use Terraform to create the servers and Ansible to configure them.
Chef and Puppet are older tools designed for managing large server fleets. They use agents installed on each server that periodically check in and enforce the desired configuration. They are powerful but carry significant operational overhead.
How IaC Fits Into Your Deployment Workflow
IaC handles one half of the deployment equation — provisioning the infrastructure that your application runs on. The other half is getting your application code onto that infrastructure, which is where deployment tools and CI/CD pipelines take over.
flowchart LR
A[Code change] --> B[CI builds & tests]
B --> C[IaC provisions infra]
C --> D[CD deploys application]
D --> E[Monitoring confirms]
Think of it as two layers:
- Infrastructure layer: IaC tools provision servers, networks, load balancers, databases, DNS records, and firewall rules. This is the foundation.
- Application layer: Deployment tools like DeployHQ push your application code to those servers, run deployment scripts, and handle rollbacks if something goes wrong.
Separating these layers is important. Your Terraform config changes when you add a new server or modify a network rule — maybe once a week. Your application deploys every time a developer merges a pull request — maybe ten times a day. Different cadences, different tools, different risk profiles.
This separation is a core principle of DevOps: automate each layer independently so that a routine code deployment never accidentally modifies infrastructure, and an infrastructure change never accidentally triggers a redeployment.
A Real IaC Workflow
Here is what IaC looks like in practice for a team using Terraform and DeployHQ:
1. A developer needs a new staging server for load testing a feature branch.
2. They add a Terraform resource definition to the infra/ directory in the team's repository:
resource "aws_instance" "staging_loadtest" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.large"
subnet_id = aws_subnet.staging.id
tags = {
Name = "staging-loadtest"
Environment = "staging"
ManagedBy = "terraform"
}
}
3. They open a pull request. The team reviews the infrastructure change the same way they review application code — checking the instance size, subnet placement, and tagging conventions. This is where IaC connects to GitOps: infrastructure changes go through the same Git-based review process as everything else.
4. The PR merges. The CI pipeline runs terraform plan to show what will change, then terraform apply to provision the server. This step fits into your existing build pipeline alongside application builds and tests.
5. The new server is provisioned automatically with the exact same configuration as other staging servers.
6. DeployHQ picks up the new server and includes it in deployments, pushing application code to it alongside the rest of the staging environment.
When the load test is done, the developer opens another PR removing the resource definition. Terraform destroys the server. No orphaned infrastructure, no forgotten instances running up your AWS bill.
IaC and GitOps
GitOps takes IaC one step further. Where IaC says define your infrastructure in code,
GitOps says Git is the single source of truth for both infrastructure and application state, and automated systems continuously reconcile reality with what's in Git.
IaC is a prerequisite for GitOps — you cannot version-control your infrastructure if it is not defined in files. But GitOps adds the reconciliation loop: an operator watches your Git repository and automatically applies changes when it detects drift between what Git says and what is actually running.
If someone manually modifies a server, the GitOps operator detects the difference and reverts it. The only way to make a lasting change is through a Git commit. This closes the loop on configuration drift entirely.
Managing State and Drift
Two practical challenges trip up every team adopting IaC.
State Management
Declarative IaC tools like Terraform maintain a state file — a JSON document that maps your configuration to real resources. When you define an EC2 instance in HCL, Terraform records its AWS instance ID in the state file. On the next run, it uses that mapping to determine whether to create, update, or destroy the resource.
The state file is critical. If you lose it, Terraform no longer knows which real resources it manages. If two people run terraform apply simultaneously against the same state file, they can corrupt it.
The solution is remote state: store the state file in a shared location (S3 bucket, Terraform Cloud, Azure Blob Storage) with locking so only one person can modify it at a time.
Configuration Drift
Drift happens when someone changes infrastructure outside of the IaC workflow — a manual tweak in the AWS console, an SSH session that installs a package, a security group rule added during an incident. The IaC config says one thing; reality says another.
Every major IaC tool can detect drift. Terraform shows it on terraform plan. CloudFormation has drift detection built in. The question is what to do about it:
- Overwrite the drift: Run
terraform applyand force reality to match the code. Simple but can be disruptive if the manual change was intentional. - Absorb the drift: Update the IaC config to match reality. Preserves the change but requires understanding why it was made.
- Prevent drift entirely: Use GitOps reconciliation or restrict console access so manual changes cannot happen in the first place.
Getting Started with IaC
1. Start with what you have. Document your current server setup — what is running where, which packages are installed, what ports are open. You cannot codify what you do not understand.
2. Pick one tool. Terraform for multi-cloud or if you are unsure. CloudFormation if you are exclusively on AWS. Do not spend weeks evaluating tools — the concepts transfer between them.
3. Codify one environment first. Start with staging, not production. Mistakes in staging cost you time, not revenue.
4. Store configs in Git alongside your application code, using a branching strategy that works for your team. Infrastructure changes should go through pull requests.
5. Add IaC to your CI/CD pipeline. Run terraform plan on every PR so reviewers can see exactly what will change. Run terraform apply on merge.
6. Expand to production once your staging workflow is solid. Use the same configs with different variables (instance sizes, replica counts) to keep environments consistent.
If you are deploying to a VPS, the same principles apply — you can set up Git-based deployment on a VPS and layer IaC on top of it using Terraform's providers for DigitalOcean, Hetzner, Linode, or Vultr.
Common Mistakes
Starting with production instead of staging. IaC is powerful, which means mistakes are powerful too. A misconfigured terraform destroy can delete production resources in seconds. Always validate your workflow on a non-critical environment first.
Not using remote state. Storing terraform.tfstate locally or committing it to Git causes conflicts when multiple people work on infrastructure. Use remote state with locking from day one — it takes ten minutes to set up and prevents hours of debugging.
Mixing IaC tools without clear boundaries. Using Terraform for some resources and CloudFormation for others in the same environment creates confusion about which tool manages what. Define clear ownership: Terraform handles networking and compute, Ansible handles server configuration, and so on.
Treating IaC as write-once. Infrastructure configs need maintenance just like application code. Dependencies get outdated, provider versions need upgrading, and unused resources accumulate. Budget time for IaC maintenance alongside your regular zero-downtime deployment and operations work.
Over-engineering for a simple setup. Three servers behind a load balancer do not need Kubernetes, Terraform, Helm, ArgoCD, and a custom control plane. Match the tooling complexity to the infrastructure complexity. A small team can get started with a beginner-friendly deployment tool and a single Terraform file.
FAQs
Do I need IaC for a small project?
Not always. If you have one server that rarely changes, the overhead of IaC tooling may not pay off. The tipping point is usually when you need reproducible environments — staging that matches production, or the ability to spin up new servers quickly. Even for small projects, keeping a simple Terraform file in your repo documents your infrastructure setup, which has value even if you run terraform apply by hand.
What is the difference between IaC and configuration management?
IaC typically refers to provisioning — creating and destroying infrastructure resources like servers, networks, and databases. Configuration management refers to configuring what runs on those resources — installing packages, managing files, setting up services. Terraform provisions an EC2 instance; Ansible configures what runs on it. In practice, the line is blurry and some tools do both.
Can I use IaC with shared hosting or a VPS?
With shared hosting, generally no — you do not have enough control over the underlying infrastructure. With a VPS, yes. Terraform has providers for most VPS platforms (DigitalOcean, Linode, Hetzner, Vultr) and can provision droplets, volumes, firewalls, and DNS records. Ansible can then configure the VPS over SSH without needing an agent installed.
How does IaC relate to DevOps?
IaC is one of the foundational practices of DevOps. It bridges the gap between development and operations by applying software engineering practices (version control, code review, automated testing) to infrastructure management. A team practicing DevOps almost certainly uses some form of IaC, because manual infrastructure management does not scale with the deployment frequency that DevOps enables.
What is the difference between IaC and GitOps?
IaC is the practice of defining infrastructure in code files. GitOps is a workflow that uses Git as the single source of truth and adds automated reconciliation — a system that watches your Git repo and continuously ensures reality matches what is defined there. IaC is a prerequisite for GitOps: you need infrastructure defined in files before you can version-control those files in Git and build automation around them.
Infrastructure as Code removes the guesswork from server management and makes your environments reproducible from day one. If you are ready to pair IaC with automated deployments, sign up for DeployHQ and connect your repository in minutes.
For questions or help setting up your deployment workflow, reach out at support@deployhq.com or find us on X (@deployhq).