Terraform Fundamentals
Provider, resource, variable, output, state — the five pieces, on infrastructure small enough to read in one screen.
- Time
- 45 min
- Level
- Beginner
- Objectives
- 5 objectives
- Cost
- Low cost
Where this fits in the platform
Already built
This lab adds
- Resources created from code, and a state file that tracks them
Which lets you
Before you start
You will need
- Terraform >= 1.6
- AWS CLI v2, configured
You do not need these already — the lab environment below provides them.
You will be able to
- Declare a provider and pin its version
- Read a plan and name what each symbol means
- Move a hardcoded value into a variable and out through an output
- Explain what the state file is and why it is not disposable
Cost — Low cost
— one `t3.micro` and an S3 bucket. Nothing here runs an hourly-billed resource beyond the instance itself.
Nothing to pay in the browser. Open the terminal runs this against a simulated cloud — the same API calls and the same commands, with no account and no bill. The figure above applies only if you build it in your own.
The scenario#
Before a VPC module makes any sense, the five pieces underneath it have to be concrete: what a provider is, what a resource declaration produces, where a value comes in, where a value goes out, and what Terraform remembers between runs.
This lab builds the smallest infrastructure that exercises all five.
Hands-on environment
Run this lab in a real terminal, free and in your browser. The environment is temporary and yours alone — break it as much as you like.
Open the terminalOpens in Killercoda, in a new tab — keep this page open for the steps.
Run it on AWS
This lab builds real cloud infrastructure, so it needs your own AWS account. Follow the cost and cleanup notes above — the resources are yours, and so is the bill.
Anything you tick here is your own record. EgyKode cannot see inside that terminal, so the success criteria stay self-assessed even when the environment checks your work for you.
The provider
Step 1 of 5
What you are proving: You can pin a provider so the same configuration behaves the same way next month
This step settles no success criterion on its own.
terraform {
required_version = ">= 1.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.40"
}
}
}
provider "aws" {
region = var.region
}Terraform knows nothing about AWS. The provider is a plugin that translates
resource blocks into API calls, and ~> 5.40 means "any 5.x from 5.40, never
6.0" — patch and minor updates are allowed, the breaking major is not.
terraform initinit downloads the plugin and writes .terraform.lock.hcl, which records the
exact version and its checksum. Commit that file — it is what makes your
build and the CI runner's build identical.
What you are proving: You can take values in through variables and hand them back out through outputs
Marking this settles success criterion 3.
variable "region" {
description = "Where this runs"
type = string
default = "us-east-1"
}
variable "instance_type" {
description = "Size of the demo instance"
type = string
default = "t3.micro"
}A variable with no default is required, and Terraform refuses to run without
it. That is the correct choice for anything environment-specific.
output "instance_ip" {
description = "Public address of the demo instance"
value = aws_instance.demo.public_ip
}Outputs are the module's public surface. Anything a caller needs must leave through one — there is no reaching inside.
What you are proving: You can create real resources from one configuration
Marking this settles success criterion 1.
data "aws_ami" "al2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
resource "aws_instance" "demo" {
ami = data.aws_ami.al2023.id
instance_type = var.instance_type
tags = {
Name = "tf-fundamentals"
Purpose = "learning"
}
}
resource "aws_s3_bucket" "demo" {
bucket = "tf-fundamentals-${random_id.suffix.hex}"
}
resource "random_id" "suffix" {
byte_length = 4
}A data block reads something that already exists; a resource block owns
something Terraform will create and destroy. Confusing the two is how people
accidentally destroy shared infrastructure.
Note aws_s3_bucket.demo depends on random_id.suffix without anyone saying
so. Terraform builds a dependency graph from the references themselves and
orders the work automatically.
What you are proving: You can read a plan before applying it, and explain why the second apply reports no changes
Marking this settles success criterion 2.
terraform fmt -recursive
terraform validate
terraform planEvery resource gets a symbol, and the symbol is the whole message:
| Symbol | Meaning | How worried to be |
|---|---|---|
+ | create | Normal for new infrastructure |
~ | update in place | Usually safe |
- | destroy | Read carefully |
-/+ | destroy then recreate | Stop and read every line |
terraform applyThen run it again:
terraform apply
# No changes. Your infrastructure matches the configuration.That second run is the point of the whole tool. The configuration describes an end state, so applying it to a system already in that state does nothing. This is idempotency, and it is what makes it safe to run continuously.
What you are proving: You can point at the line in state that maps your resource to its real cloud id
Marking this settles success criterion 4.
terraform state list
terraform state show aws_instance.demo | head -20
grep -o '"id": "i-[a-z0-9]*"' terraform.tfstate | head -1State is Terraform's memory: a map from your resource addresses to real AWS ids. Without it, Terraform cannot tell a resource it created from one it has never seen.
Two consequences to internalise now:
- It frequently contains secrets in plain text — an RDS password, a generated key — because the API returned them at creation. It never goes in Git.
- Losing it orphans everything it tracked. The resources keep running and billing; Terraform simply no longer knows about them. That is why the next lab moves it to a remote backend with locking.
terraform apply says the bucket name is already taken
S3 bucket names are globally unique across every AWS account. That is what random_id is for — check it is actually being interpolated into the name.
The second apply shows changes when nothing changed
Something outside Terraform modified the resource — configuration drift. terraform plan shows exactly which attribute differs.
terraform destroy leaves the bucket behind
S3 refuses to delete a bucket with objects in it. Empty it first: aws s3 rm s3://<bucket> --recursive.
init fails with a provider checksum mismatch
The lock file was written on a different platform. terraform providers lock -platform=linux_amd64 -platform=windows_amd64 records both.
Clean up#
Run this even if you did not finish.
Destructive — This removes real resources. Check which environment you are in first.
terraform destroy -auto-approve
aws ec2 describe-instances --filters Name=instance-state-name,Values=running --query 'Reservations[].Instances[].InstanceId'
aws s3 ls | grep tf-fundamentals # must print nothingCost of this lab: Free tier — one t3.micro and an S3 bucket. Nothing here runs an hourly-billed resource beyond the instance itself.
Success criteria
0 of 5
The concept behind it
Next up
Lab 20 of 59 on the project path