Skip to content

Infrastructure as Code with Terraform: The Basics

Cloud & DevOps6 min read

Learn how Terraform works: providers, resources, state, plan and apply, with a working AWS example, remote state and common mistakes.

In this article
  1. Why bother with infrastructure as code
  2. How Terraform works
  3. A small example
  4. State: the part that bites
  5. Structuring a real project
  6. Terraform and its alternatives
  7. Common mistakes
  8. A getting-started checklist
  9. Summary

Infrastructure as code (IaC) means describing your servers, networks, databases and permissions in text files, then letting a tool create and update them to match. Terraform is the most widely used tool for this. Instead of clicking through a cloud console and hoping you remember what you did, you write a configuration, review it like any other code change, and apply it. The result is infrastructure you can reproduce, audit, roll back and hand to a colleague.

This guide explains how Terraform works, walks through a small but realistic example, and covers the habits that stop a first project turning into a mess. It assumes no prior Terraform knowledge.

Why bother with infrastructure as code

Hand-built infrastructure has a predictable failure pattern. A system is set up under pressure, a few settings are tweaked later, and a year on nobody knows exactly how production differs from staging. Recreating it after a disaster becomes guesswork. IaC addresses this directly:

  • Repeatability. The same code creates the same environment, whether it is for development, testing or production.
  • Review. Changes go through pull requests, so a second pair of eyes sees a firewall change before it happens.
  • History. Version control records who changed what and why.
  • Recovery. If an environment is lost, you rebuild it from code. This is central to disaster recovery in the cloud.
  • Cost control. Temporary environments can be created and destroyed on demand, which helps with cloud spend.

How Terraform works

Terraform uses a declarative language called HCL. You describe the end state you want, not the steps to reach it. The core concepts are few:

  • Providers are plugins that talk to a platform: AWS, Azure, Cloudflare, GitHub and many more.
  • Resources are the things you want to exist, such as a storage bucket or a virtual network.
  • Variables make a configuration reusable by parameterising values like environment name or region.
  • Outputs expose useful values, such as an address, after creation.
  • State is a record of what Terraform has created, which it compares against your code to decide what to change.
  • Modules are reusable bundles of resources, so a pattern can be written once and used many times.

The daily workflow has four commands:

  1. terraform init downloads providers and prepares the working directory.
  2. terraform fmt and terraform validate tidy and check the code.
  3. terraform plan shows exactly what would be created, changed or destroyed, without doing it.
  4. terraform apply carries out the plan after you confirm.

The plan step is the heart of the safety model. Read it every time, especially lines marked for destruction or replacement.

A small example

This configuration creates a private, versioned storage bucket on AWS. The bucket name must be globally unique, so it takes a variable.

terraform {
  required_version = ">= 1.6"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.region
}

variable "region" {
  type    = string
  default = "eu-west-1"
}

variable "bucket_name" {
  type        = string
  description = "Globally unique bucket name"
}

resource "aws_s3_bucket" "assets" {
  bucket = var.bucket_name
  tags = {
    Environment = "dev"
    ManagedBy   = "terraform"
  }
}

resource "aws_s3_bucket_versioning" "assets" {
  bucket = aws_s3_bucket.assets.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_public_access_block" "assets" {
  bucket                  = aws_s3_bucket.assets.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

output "bucket_arn" {
  value = aws_s3_bucket.assets.arn
}

Run it like this:

terraform init
terraform plan -var="bucket_name=example-assets-0001"
terraform apply -var="bucket_name=example-assets-0001"

Notice that the versioning and public-access settings are separate resources that refer to the bucket by reference. Terraform builds a dependency graph from those references, so it creates things in the right order automatically.

State: the part that bites

State is a file mapping your code to real resources. By default it is stored locally, which is fine for experiments and dangerous for teams. If it is lost, Terraform no longer knows what it manages; if two people apply at once, state can be corrupted. For any shared work, use a remote backend with locking. On AWS a common setup is an S3 bucket for state with locking enabled; Azure Storage and Terraform's own hosted services are alternatives.

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

State can contain sensitive values in plain text, so restrict access to the state bucket tightly, enable encryption and versioning, and never commit state files to Git. Check your Terraform version's documentation for the current locking options, since backend features evolve.

Structuring a real project

  • Separate environments. Keep development, staging and production in separate state files, and ideally separate cloud accounts or subscriptions.
  • Use modules sparingly at first. Copying a pattern twice is fine. Extract a module when the third use makes the shape obvious.
  • Pin versions. Pin Terraform and provider versions so a surprise upgrade does not change your plan.
  • Tag everything. Environment, owner and purpose tags make cost reporting and clean-up possible.
  • Run it in a pipeline. Plan on every pull request, apply on merge, with approval for production. See our GitHub Actions guide for the pipeline mechanics.

Terraform and its alternatives

CloudWiss lists both Terraform and CloudFormation among its infrastructure-as-code tools. CloudFormation is AWS-only and tightly integrated with it; Azure has its own native language, Bicep. Terraform works across providers with one syntax, which is attractive if you use more than one platform or may do so later, as discussed in AWS versus Azure. The best tool is the one your team will actually keep using consistently, so the choice matters less than the discipline.

Common mistakes

  • Editing resources in the console afterwards. This creates drift between code and reality. If an emergency change is unavoidable, record it and bring the code back into line.
  • Keeping state locally in a team. Use a remote, locked backend from the start.
  • Committing secrets. Never put passwords or keys in .tf or .tfvars files that go to Git. Reference a secret manager.
  • Applying without reading the plan. A rename can show up as destroy-and-create, which deletes a database.
  • One enormous configuration. A single state for everything makes every change slow and risky. Split by lifecycle, such as network, data and applications.
  • Not protecting critical resources. Use lifecycle { prevent_destroy = true } on things like production databases, and enable deletion protection at the provider level too.
  • Ignoring drift. Run a scheduled plan to detect changes made outside Terraform.
  • Importing never. Existing hand-built resources can be brought under management with terraform import or import blocks; you do not have to start from nothing.

A getting-started checklist

  1. Choose one small, low-risk area, such as a storage bucket or a test network.
  2. Write it in Terraform, with variables and tags, in a Git repository.
  3. Configure a remote backend with locking and encryption.
  4. Add fmt, validate and plan to a pull-request check.
  5. Apply through a pipeline with a restricted role, not a personal admin account.
  6. Document how to bootstrap the state bucket and the pipeline itself.
  7. Expand gradually, importing existing resources as you go.

Summary

Terraform turns infrastructure into reviewable, repeatable code. Start small, protect your state, read every plan and let a pipeline do the applying. If you would rather have an experienced team set the foundations, CloudWiss's cloud and DevOps services include infrastructure as code with Terraform and CloudFormation, together with the pipelines, monitoring and documentation around it.

Share this article

General educational information, not legal or professional advice for your specific situation.

Frequently asked questions

What is infrastructure as code?

It is the practice of describing servers, networks, databases and permissions in text files that a tool applies, so infrastructure can be reviewed, versioned, reproduced and rebuilt like software.

What is Terraform state and why does it matter?

State is the record Terraform keeps of the resources it manages. It compares state with your code to plan changes. Teams should store it in a remote, locked, encrypted backend and never commit it to Git.

Is Terraform only for AWS?

No. Terraform uses providers and supports many platforms, including AWS, Azure and others, with one syntax. CloudFormation is AWS-specific, and Azure has its own native language called Bicep.

Can I manage existing resources with Terraform?

Yes. You can bring hand-built resources under management using terraform import or import blocks, so you do not have to start from nothing.

Keep reading

Let’s build it

Ready to build with CloudWiss?

Tell us what you want to build, move to the cloud or keep running. We reply with a clear next step.