Terraform on AWS: Infrastructure as Code for Beginners
AWS·August 25, 2026·6 min read

Terraform on AWS: Infrastructure as Code for Beginners

Infrastructure as code means describing your cloud resources in files instead of clicking through a console. Rather than creating a VPC, then a subnet, then a security group, then an instance by hand, you write all of it down once and let the tool build it in the right order.

If that sounds like unnecessary work for a single VM, it is. The value arrives the third time you need that same setup, or the first time someone deletes a security group and nobody can remember exactly how it was configured.

Why Terraform specifically

Terraform is not the only option. AWS has CloudFormation, Azure has Bicep and ARM, Google has its own. Three things keep Terraform in the conversation regardless of which cloud you are on.

The workflow is the same everywhere. init, plan, apply behaves identically whether the target is AWS, Azure or GCP. The resource syntax changes; the mental model does not. Learning it once pays out across providers.

It tells you what it is about to do. terraform plan prints exactly what will be created, changed or destroyed before anything happens. That preview is the feature that makes the tool safe to use on real infrastructure.

It tracks state. Terraform keeps a record mapping your configuration to the real resources. Remove something from the config and it knows to destroy it. Change a property and it knows to update rather than recreate.

State files contain secrets

State can include database passwords and access keys in plain text. Never commit it to Git. For anything beyond solo learning, use a remote backend with locking.

Your first configuration

This creates a VPC, a subnet inside it, and an instance inside that.

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

provider "aws" {
  region = var.region
}

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "terraform-lab-vpc"
  }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "terraform-lab-public-subnet"
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type
  subnet_id     = aws_subnet.public.id

  tags = {
    Name = "terraform-lab-instance"
  }
}

Notice that the instance references aws_subnet.public.id, and the subnet references aws_vpc.main.id. Those references are how Terraform works out the order to build things in. You never specify it.

Do not hardcode AMI IDs

AMI IDs are specific to a region and change whenever the image is updated. A configuration with a literal ami-0c02... in it works in one region today and breaks elsewhere or later. Look the image up instead:

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

The workflow

Initialize

terraform init

Downloads the provider plugins and prepares the directory. Run it once per project, and again whenever you add a provider.

Plan

terraform plan

Terraform reads your files, compares them against the recorded state, and shows you the difference:

Plan: 3 to add, 0 to change, 0 to destroy.

Read that line every time. If it says fourteen to destroy and you expected one to add, stop and find out why before going further. This step is the whole safety mechanism.

Apply

terraform apply

Resources get created in dependency order. VPC, then subnet, then instance. You did not sequence that, and you should not try to.

Reading this and running it are different experiences, particularly the first time a plan shows something you did not expect. Introduction to infrastructure as code with Terraform on AWS runs the full loop against a real account.

That required_providers block at the top deserves more attention than it usually gets, because version drift is a common source of "it worked last week". Configuring, versioning and locking provider dependencies covers why the lock file matters.

Variables make it reusable

Hardcoded values are fine while you are learning. For anything real, pull them out:

variable "region" {
  description = "AWS region"
  default     = "us-east-1"
}

variable "instance_type" {
  description = "EC2 instance type"
  default     = "t3.micro"
}

Now the same configuration serves every environment:

terraform apply -var="instance_type=t3.medium" -var="region=eu-west-1"

One definition, different parameters. That is most of the argument for IaC in a single command.

State, which is where people actually get hurt

Applying writes a terraform.tfstate file mapping your config to real resources. Three failure modes follow from that.

Lose the file and Terraform no longer knows what exists, so the next apply creates duplicates. Corrupt it and Terraform may decide to destroy and rebuild things that were fine. Share it through Git and two people will overwrite each other's view of reality.

The fix is a remote backend with locking, so state lives in one place and only one apply can run at a time.

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

Set this up before your second deployment. Local state is fine while you are alone; the moment a colleague or a pipeline is involved it stops being fine, and migrating state after the fact is genuinely unpleasant.

If state still feels abstract, understanding Terraform state fundamentals has you inspect a real state file and see what it is actually storing.

Then managing state with a remote storage backend walks through the migration, which is the same procedure regardless of which cloud holds the bucket.

Mistakes worth skipping

Everything in one file. Fine at three resources. Past about five, split into main.tf, variables.tf, outputs.tf and providers.tf. Future you will be looking for something specific under time pressure.

Copy pasting instead of building modules. If the same VPC and subnet and security group block is appearing in a third project, it wants to be a module. A module is just a folder of .tf files with defined inputs and outputs, which is less ceremony than it sounds. Introduction to Terraform modules covers the structure.

Skimming the plan. When it shows unexpected changes, find out why before applying. The usual cause is drift: somebody changed something in the console, and Terraform intends to put it back. That is Terraform working correctly, and it is also how a well meaning manual fix gets silently reverted.

Where to go next

Terraform is provider agnostic, so the workflow you just learned carries into Azure with different resource names. If you work across both, Introduction to Terraform on Azure is the same loop against a different provider.

If certification is the goal, IaC appears throughout. AWS exams test CloudFormation and Azure tests ARM and Bicep, but the concepts you have just learned, declarative definitions, dependency graphs, state and drift, are what those questions are really about.