What is Infrastructure as Code?
Infrastructure as Code is the practice of managing and provisioning computing infrastructure through code rather than through manual processes. Instead of logging into a cloud console, clicking buttons, and filling in forms, you write configuration files that describe the desired state of your infrastructure. A tool then reads those files and makes the real-world infrastructure match the description.
There are two fundamental approaches to IaC. The declarative approach describes what the final state should look like, and the tool figures out the steps to get there. The imperative approach describes the specific steps to execute in order. Terraform uses the declarative model โ you describe the infrastructure you want, and Terraform calculates the changes needed to reach that state. This makes configurations easier to reason about, because you focus on the outcome rather than the procedure.
The benefits of IaC are substantial and well-documented. Teams that adopt IaC report dramatically fewer configuration errors, faster provisioning times, and the ability to reproduce entire environments in minutes rather than days. Because infrastructure definitions live in version control alongside application code, every change is tracked, reviewable, and reversible. This eliminates the "snowflake server" problem where no two environments are quite the same.
Terraform vs Other IaC Tools
Terraform is not the only IaC tool available, and choosing the right one depends on your cloud strategy, team skills, and specific requirements. The four most widely used tools each have distinct strengths and trade-offs. Understanding these differences helps you make an informed decision โ or recognise when a combination of tools is the best approach.
The table below compares the major IaC tools across the dimensions that matter most in practice: cloud support, configuration language, approach, and organisational fit.
IaC Tool Comparison
| Tool | Cloud Support | Language | Approach | Maintained By |
|---|---|---|---|---|
| Terraform | Multi-cloud (AWS, Azure, GCP, and 3,000+ providers) | HCL (HashiCorp Configuration Language) | Declarative | HashiCorp |
| AWS CloudFormation | AWS only | JSON / YAML | Declarative | AWS |
| Ansible | Multi-platform (cloud, on-premises, network devices) | YAML | Imperative / Declarative | Red Hat |
| Pulumi | Multi-cloud (AWS, Azure, GCP, Kubernetes) | General-purpose languages (Python, TypeScript, Go, C#) | Declarative | Pulumi |
Source: Official documentation from HashiCorp, AWS, Red Hat, and Pulumi (2025)
Terraform excels when you need to manage infrastructure across multiple cloud providers or want a single, consistent workflow regardless of where your resources live. CloudFormation is the natural choice for teams fully committed to AWS, offering deep integration with every AWS service. Ansible is strongest for configuration management and server provisioning, often used alongside Terraform rather than as a replacement. Pulumi appeals to teams that prefer writing infrastructure definitions in the same languages they use for application code.
Terraform's Cloud-Agnostic Advantage
The single most compelling reason teams choose Terraform is its cloud-agnostic provider model. A single Terraform codebase can provision an AWS VPC, an Azure database, a Cloudflare DNS record, and a Datadog monitoring dashboard. This is not theoretical โ it is how modern multi-cloud architectures are built in practice. The provider ecosystem includes over 3,000 integrations, covering everything from major cloud platforms to SaaS tools, databases, and even physical hardware.
HCL Basics: The HashiCorp Configuration Language
Terraform configurations are written in HCL (HashiCorp Configuration Language), a domain-specific language designed specifically for infrastructure definitions. HCL is not a general-purpose programming language โ it was created to be readable by humans and parseable by machines. If you can read JSON or YAML, you can read HCL within minutes.
HCL uses a block-based syntax where each block defines a piece of infrastructure or configuration. The building blocks are straightforward: providers connect you to cloud platforms, resources define the infrastructure components you want to create, variables allow you to parameterise your configurations, and outputs expose values for other tools or modules to consume.
Core HCL Constructs
| Construct | Description | Syntax Example |
|---|---|---|
| Provider | Connects Terraform to a cloud platform or service API | provider "aws" { region = "eu-west-2" } |
| Resource | Defines an infrastructure component to create and manage | resource "aws_instance" "web" { ami = "ami-0c55b159" } |
| Variable | Declares an input parameter for reusable configuration | variable "region" { default = "eu-west-2" } |
| Output | Exposes a value after resources are created | output "ip" { value = aws_instance.web.public_ip } |
| Data Source | Reads information from existing infrastructure without managing it | data "aws_ami" "latest" { most_recent = true } |
| Local | Defines a computed value for use within the configuration | locals { env_prefix = "${var.env}-app" } |
| Module | Groups related resources into a reusable, shareable package | module "vpc" { source = "./modules/vpc" } |
Source: Terraform Language Documentation โ HashiCorp (2025)
HCL supports expressions, functions, and conditional logic that make configurations flexible without requiring a full programming language. You can use string interpolation, for-each loops, conditional expressions, and built-in functions for tasks like formatting strings, calculating CIDR blocks, or encoding data. The language strikes a deliberate balance between power and simplicity โ expressive enough for real-world infrastructure, constrained enough to remain auditable.
HCL Is Designed for Readability
HashiCorp deliberately chose to create a new language rather than use JSON, YAML, or a general-purpose language. JSON lacks comments and is verbose. YAML has indentation pitfalls that cause subtle bugs. General-purpose languages like Python or TypeScript are powerful but make configurations harder to review and audit. HCL occupies the sweet spot: it supports comments, is concise, uses a consistent block structure, and can be parsed both by humans scanning a pull request and by automated tools running policy checks.
Your First Terraform Project
The best way to learn Terraform is to build something real. The Terraform workflow follows four core commands that you will use in every project, every day. Understanding this workflow is the foundation of everything else โ modules, state management, and CI/CD integration all build on top of these four steps.
The Terraform Workflow
| Command | What It Does | When You Run It |
|---|---|---|
| terraform init | Downloads provider plugins and initialises the backend for state storage | Once when starting a new project, or after adding a new provider or module |
| terraform plan | Previews the changes Terraform will make โ a dry run that shows what will be created, modified, or destroyed | Before every apply, to review and validate the intended changes |
| terraform apply | Executes the planned changes and creates, updates, or deletes real infrastructure resources | After reviewing the plan and confirming the changes are correct |
| terraform destroy | Tears down all infrastructure managed by the current configuration | When decommissioning an environment or cleaning up after testing |
Source: Terraform CLI Documentation โ HashiCorp (2025)
A typical Terraform project organises its configuration across several files, each with a specific purpose. While Terraform treats all .tf files in a directory as a single configuration, the convention of separating concerns into distinct files makes projects easier to navigate, review, and maintain as they grow.
Standard Terraform File Structure
| File | Purpose | What It Contains |
|---|---|---|
| main.tf | Primary resource definitions | The core infrastructure resources โ instances, databases, networks, load balancers |
| variables.tf | Input variable declarations | All variable blocks with descriptions, types, defaults, and validation rules |
| outputs.tf | Output value definitions | Values to expose after apply โ IP addresses, DNS names, resource IDs |
| terraform.tfvars | Variable values for the current environment | Concrete values for variables โ region, instance size, environment name |
| providers.tf | Provider configuration and version constraints | Provider blocks, required_providers, and version pinning |
Source: Terraform recommended project structure โ HashiCorp (2025)
To get started, create a new directory for your project, add a providers.tf file to configure your cloud provider, and a main.tf file to define your first resource. Run terraform init to download the provider plugin, then terraform plan to preview what will be created. Once you are satisfied with the plan, run terraform apply and confirm. Your infrastructure is now real, managed, and reproducible.
Always Review the Plan Before Applying
The terraform plan command is your safety net. It shows you exactly what Terraform intends to create, change, or destroy before any real infrastructure is touched. Never skip this step. In production workflows, the plan output should be reviewed by at least one other team member โ just like a code review. Many teams save the plan to a file with terraform plan -out=tfplan and then apply that exact plan with terraform apply tfplan, ensuring that what was reviewed is exactly what gets executed.
State Management
Terraform state is the single most important concept to understand after the basic workflow. When Terraform creates infrastructure, it records the mapping between your configuration and the real-world resources in a state file (terraform.tfstate). This file is how Terraform knows what it manages, what has changed, and what needs to be updated or destroyed.
By default, Terraform stores state locally in the working directory. This works for individual learning and experimentation, but it fails completely in team environments. If two developers run Terraform at the same time with different local state files, they will overwrite each other's changes and corrupt the infrastructure. Remote state backends solve this by storing the state in a shared, lockable location.
State locking is a critical feature that prevents concurrent operations from corrupting your infrastructure. When one team member runs terraform apply, the state is locked so that no one else can make changes until the operation completes. Without state locking, simultaneous applies can result in duplicate resources, orphaned infrastructure, or a state file that no longer reflects reality.
State Backend Options
| Backend | Best For | State Locking | Key Considerations |
|---|---|---|---|
| Local | Single developer, learning, experimentation | No | Default backend. Simple but not suitable for teams. State file lives on your machine only. |
| S3 + DynamoDB | AWS teams, production workloads | Yes (via DynamoDB) | The most widely used remote backend. S3 stores the state, DynamoDB provides locking. Enable versioning on the S3 bucket for recovery. |
| Azure Blob Storage | Azure teams, production workloads | Yes (native) | Azure's equivalent of S3. Supports native state locking without a separate locking service. |
| Terraform Cloud | Managed solution, small to mid-size teams | Yes (built-in) | Free tier for up to 5 users. Provides state management, locking, run history, and policy enforcement in a managed service. |
| GCS (Google Cloud Storage) | Google Cloud teams, production workloads | Yes (native) | Google Cloud's object storage backend. Supports native state locking and versioning for state recovery. |
Source: Terraform Backend Configuration Documentation โ HashiCorp (2025)
Migrating from local to remote state is straightforward. Add a backend block to your Terraform configuration, run terraform init, and Terraform will prompt you to migrate the existing state to the new backend. This is a one-time operation per project, and it should be done as early as possible โ ideally before any production infrastructure is created.
Never Commit State Files to Git
The Terraform state file contains sensitive information including resource IDs, IP addresses, database connection strings, and sometimes passwords or access keys. It should never be committed to version control. Add *.tfstate and *.tfstate.backup to your .gitignore file immediately when starting any Terraform project. Use a remote backend to store state securely, and rely on the backend's encryption and access controls to protect it.
Modules and Best Practices
As your Terraform codebase grows beyond a single project, modules become essential. A module is a self-contained package of Terraform configuration that can be reused across multiple projects, environments, and teams. Instead of copying and pasting resource definitions, you define them once in a module and call that module wherever you need it โ with different variables for each environment.
The Terraform community has established a set of best practices that help teams maintain clean, secure, and scalable infrastructure code. These practices are not optional nice-to-haves โ they are the difference between a Terraform codebase that scales gracefully and one that becomes an unmaintainable tangle of configuration files.
Terraform Best Practices
| Practice | Why It Matters | How to Implement |
|---|---|---|
| Use modules for reusability | Eliminates duplication and ensures consistent infrastructure across environments | Create a modules/ directory. Each module has its own main.tf, variables.tf, and outputs.tf. Call modules from root configurations. |
| Pin provider versions | Prevents breaking changes when providers release new versions | Use required_providers with version constraints: version = "~> 5.0" allows patch updates but blocks major version changes. |
| Use remote state | Enables team collaboration and prevents state corruption from concurrent operations | Configure an S3, Azure Blob, GCS, or Terraform Cloud backend. Migrate from local state as early as possible in the project. |
| Implement state locking | Prevents two team members from applying changes simultaneously and corrupting state | Use a backend that supports locking (DynamoDB for S3, native for Azure/GCS, built-in for Terraform Cloud). |
| Separate environments | Isolates dev, staging, and prod to prevent accidental changes to production infrastructure | Use separate directories or workspaces for each environment. Each environment has its own state file and variable values. |
| Use variables for everything configurable | Makes configurations reusable and prevents hardcoded values that differ between environments | Define variables with types, descriptions, and validation rules. Use .tfvars files for environment-specific values. |
| Tag all resources | Enables cost tracking, ownership identification, and automated management across cloud accounts | Use a default_tags block in the provider configuration or a local map of standard tags applied to every resource. |
| Use terraform fmt and terraform validate | Ensures consistent formatting and catches syntax errors before they reach a plan or apply | Run terraform fmt -recursive and terraform validate in CI pipelines and pre-commit hooks. |
Source: Terraform best practices โ HashiCorp and community standards (2025)
The Terraform Registry at registry.terraform.io is the official repository for community and partner modules. Before building a module from scratch, check the Registry โ there are well-maintained, battle-tested modules for common patterns like VPCs, Kubernetes clusters, databases, and monitoring stacks. Using Registry modules saves time and benefits from the collective experience of thousands of contributors.
Leverage the Terraform Registry
The Terraform Registry hosts thousands of community-maintained modules that codify best practices for common infrastructure patterns. The official AWS VPC module, for example, handles subnet calculations, NAT gateways, route tables, and network ACLs in a single, well-tested module call. Rather than reinventing these patterns, use the Registry as a starting point. Pin module versions just as you pin provider versions, and review the module source code before using it in production to ensure it meets your security and compliance requirements.
Master DevOps Engineering
Infrastructure as Code is a cornerstone of modern DevOps practice. Our accredited DevOps course covers Terraform, CI/CD pipelines, containerisation, monitoring, and the full engineering toolkit you need to build and operate reliable, scalable systems with confidence.
Explore Our DevOps Course