This is the first post in our Terraform fundamentals series. Later posts cover variables and outputs, modules, remote state, and workspaces.
Most Terraform tutorials start with syntax. That's backwards. HCL (HashiCorp Configuration Language) is genuinely simple — the part that actually trips people up later is the mental model underneath it: what a provider is, what state actually represents, and why Terraform behaves the way it does when those two things disagree with each other.
This post builds that model by provisioning one real resource, end to end.
What Terraform actually does
Terraform is a declarative provisioning tool. You describe the infrastructure you want in configuration files, and Terraform figures out the sequence of API calls needed to make reality match that description — whether that means creating something from nothing, updating an existing resource in place, or tearing something down.
The critical word there is declarative. You're not writing a script that says "create a VPC, then create a subnet, then create an instance." You're writing a description of the end state, and Terraform's dependency graph figures out the order.
Providers: how Terraform talks to the outside world
Terraform itself has no built-in knowledge of AWS, Azure, Kubernetes, or anything else. That knowledge lives in providers — plugins that translate HCL into API calls for a specific platform.
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "eu-west-2"
}The required_providers block pins which provider and version range you depend on — this matters more than it looks. Provider APIs change between major versions, and an unpinned provider can silently start behaving differently on someone else's machine or in CI, weeks after you wrote the config. Pin it the same way you'd pin a language runtime version.
Run terraform init after writing this, and Terraform downloads the AWS provider plugin into a local .terraform/ directory. Nothing has touched AWS yet — init only sets up the tooling.
Your first resource
A resource block is where you actually declare something you want to exist.
resource "aws_instance" "web" {
ami = "ami-0c1a7f89451184c8b"
instance_type = "t3.micro"
tags = {
Name = "terraform-101-example"
}
}The syntax is resource "<provider_type>" "<local_name>". aws_instance tells Terraform which provider resource type this is; web is a name you choose, used only to refer to this resource elsewhere in your own configuration — it never appears in AWS itself.
Run terraform plan. Terraform reaches out to AWS (read-only), compares what it finds against your configuration, and prints exactly what it intends to change:
Terraform will perform the following actions:
# aws_instance.web will be created
+ resource "aws_instance" "web" {
+ ami = "ami-0c1a7f89451184c8b"
+ instance_type = "t3.micro"
+ id = (known after apply)
...
}
Plan: 1 to add, 0 to change, 0 to destroy.
Always read the plan before applying it. This is the single habit that prevents the most expensive Terraform mistakes — a plan that says "1 to destroy" on a resource you didn't expect is a warning, not a formality to skip past.
Run terraform apply, confirm, and Terraform creates the EC2 instance and reports back.
State: the part nobody explains well
Here's the question every Terraform tutorial glosses over: how does terraform plan know what currently exists, without you telling it?
The answer is a file called terraform.tfstate, created automatically the moment you first apply. It's a JSON record mapping every resource in your configuration to the real-world object it corresponds to — for aws_instance.web, that means the actual EC2 instance ID, its current attributes, and everything else AWS returned when it was created.
State is not a cache for convenience. It is Terraform's only record of what it manages. Delete it, and Terraform has no memory that aws_instance.web was ever created — running apply again would try to create a second, duplicate instance, because as far as Terraform's next plan is concerned, nothing exists yet.
This has a few direct consequences worth internalizing now, before they cause a real problem later:
- Never hand-edit resources that Terraform manages. If someone changes the instance type in the AWS console directly, Terraform's state no longer matches reality. The next
planwill show a diff trying to revert that change — which is either exactly what you want (config is the source of truth) or a nasty surprise, depending on whether that console change was intentional. - State often contains sensitive data. Database passwords, private keys, connection strings passed as resource attributes — all of it can end up in plaintext inside
terraform.tfstate. Treat that file with the same care as a secrets file, because it frequently is one. - Local state doesn't survive a team. By default, that state file sits on whoever's laptop ran
applyfirst. The moment a second person needs to run Terraform against the same infrastructure, local state becomes actively dangerous — two people, two state files, both believing they have the authoritative picture. We cover the fix for this — remote backends and locking — in the fourth post in this series.
Cleaning up
terraform destroydestroy walks the state file in reverse dependency order and tears down everything Terraform currently tracks. On a learning environment, run this before you close the laptop — an idle t3.micro is cheap, but "cheap and forgotten for six months" is how AWS bills quietly creep up.
What to actually remember from this post
- Providers translate HCL into API calls for a specific platform, and should always be version-pinned.
- Resources are declarations of desired state, not imperative steps.
planbeforeapply, every time — it's the cheapest insurance Terraform gives you.- State is the ground truth, not a cache — protect it, don't hand-edit around it, and don't let it live only on one person's machine once more than one person is involved.
Next in the series: Mastering Terraform Variables and Outputs, where this same configuration stops being hardcoded and starts being reusable.
