Terraform Infrastructure as Code: Beginner's Guide

Consider a familiar situation: standing up a test environment for a project usually means clicking around the console for a long while — creating a VPC, security groups, a few instances, attaching a load balancer, then manually filling in config files on each machine. The first time takes half an hour; the second and third times take half an hour too, and the two results are rarely identical. One port off, one rule missing, and debugging becomes a slog. Terraform exists to solve exactly this problem: describe infrastructure as declarative code, then run a single command to create it repeatedly and consistently.

Terraform is HashiCorp's infrastructure as code (IaC) tool. It uses one unified HCL syntax to manage resources on almost every major cloud platform — AWS, Azure, Google Cloud, Alibaba Cloud, and Tencent Cloud included.

1. Installation and Initialization

On macOS the easiest path is Homebrew:

# macOS
brew install terraform
terraform version

Once installed, a project usually moves through the same command loop:

terraform init    # Initialize project, download provider plugins
terraform plan    # Preview changes without applying
terraform apply   # Apply changes, create/modify resources
terraform destroy # Destroy resources

The distinction between plan and apply is the habit most worth building. plan lists every add, change, and removal that would happen, so you can scan it with your own eyes before deciding to actually touch anything. In production this step is what prevents a lot of accidental damage.

2. Your First Configuration

Create a main.tf in the project directory declaring an AWS provider and a web server:

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

provider "aws" {
  region = "us-east-1"
}

resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  tags = { Name = "WebServer" }
}

resource "aws_security_group" "web_sg" {
  name = "web-sg"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

This configuration declares two things: one t2.micro instance and a security group that only opens ports 80 and 443. Note the declarative nature of Terraform — you describe what you want, not the step-by-step procedure to build it. The relationship between the instance and the security group is visible in the code itself, which is far clearer than clicking through the console in sequence.

3. Core Concepts

Concept Description
Provider Cloud platform plugin (AWS, Azure, GCP, etc.) that talks to that platform's API
Resource A managed resource (instance, network, bucket, etc.)
Data Source Read-only lookup of existing resources; creates nothing
State Infrastructure state file (terraform.tfstate)
Module A reusable configuration block that packages a group of resources

The difference between a Resource and a Data Source is worth internalizing: a Resource creates or modifies real infrastructure; a Data Source just looks up what already exists. To read the ID of an existing VPC, a Data Source is enough — no need to recreate it.

4. State Management: The Single-Machine Trap

After terraform apply, Terraform writes the actual state of your resources into terraform.tfstate. That file is the baseline for every future plan and apply — without it, Terraform does not know which resources it manages. Local state is fine for a solo project, but as soon as two people work on the same config, they start overwriting each other's state, causing duplicate resources at best and accidental deletions at worst.

That is why team collaboration defaults to a remote backend. On AWS the most common setup is S3 with DynamoDB for locking:

terraform {
  backend "s3" {
    bucket = "my-terraform-state"
    key    = "prod/terraform.tfstate"
    region = "us-east-1"
  }
}

Remote state plus a lock guarantees only one apply runs at a time, and the storage is safer and cheaper than a local file. Turn on versioning for that S3 bucket as well, so the state itself can be rolled back.

5. Variables and Outputs

Extract what changes often into variables and expose what is useful as outputs:

# variables.tf
variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t2.micro"
}

# outputs.tf
output "instance_ip" {
  value = aws_instance.web.public_ip
}

You can override variables on the command line with -var or collect them in a terraform.tfvars file. The output block prints things like the instance's public IP so later scripts can pick them up.

6. Formatting and Validation

Two habits save a lot of time when writing Terraform configs. First, terraform fmt formats the code into the official style and ends the "should this be indented" arguments. Second, terraform validate performs a static check before apply, catching variable type errors and block syntax mistakes early. Put both into CI so every merge request is checked automatically — much faster than discovering a typo halfway through a deployment.

Variable files have conventions too: put environment-specific values in terraform.tfvars (like test.tfvars, prod.tfvars) and select them with -var-file, so you never pass parameters by hand and different environments can reuse the same code.

A Practical Example

A small team needs test and production environments for a blog system. Building them by hand in the console takes about 40 minutes each time and tends to miss configuration. Writing a module that creates the VPC, security group, two instances, and an RDS database, then running terraform apply -var-file=test.tfvars yields an environment identical to the last one in under five minutes. Tearing it down is equally clean — terraform destroy removes everything in one go, leaving no orphaned instances that keep generating bills. Going further, defining the whole environment as a module means test, staging, and production are just three variable files and three commands apart — which is how many teams take their first step from clicking in the console to writing code.

Common Questions

  • apply fails halfway through? Run terraform plan first to see what the state file records, then decide whether to retry apply or fix things manually. Do not bypass Terraform and edit resources in the console — the state will drift.
  • Accidentally modified someone else's resource? This is exactly why the state file exists. Do not delete tfstate outright; use terraform state list and terraform state rm to manage it precisely.
  • Should tfstate be committed to Git? No. State files often contain sensitive information and are tied to a machine. Put them in a remote backend protected by a lock.

Reference: Terraform documentation https://developer.hashicorp.com/terraform/docs ; AWS provider docs https://registry.terraform.io/providers/hashicorp/aws/latest