Terraform Basics #1 IaC and Terraform: Where Console Clicking Breaks Down, Installation, and the First apply
You launched an EC2 instance with a few clicks in the AWS console, opened a security group, created a bucket, and got the service running. A few months later you need the same setup again — and nobody remembers which screens were involved or which options were selected. Nothing was written down, and the only way to know what production actually looks like is to dig through the console page by page. Every team that builds infrastructure by hand runs into this eventually. Terraform is the tool that solves this problem with code, and this series covers it from the ground up in 9 parts: starting from the idea of IaC, then moving through HCL syntax, variables, state, and modules, up to the remote state a team shares. It is the first series of a track that continues with a 10-part practice series building a full AWS infrastructure as code, and a 7-part operations series on team workflows. It assumes you have an AWS account and have touched the basic services (roughly the level of AWS Basics).
How click-built infrastructure falls apart #
The problems with console operations show up not when you build, but later.
- You cannot reproduce it: recreating production in staging means repeating dozens of screens of choices exactly. Miss one and you get the “works in staging, fails in production” class of problems — and finding the difference means putting two consoles side by side and comparing by eye.
- There is no history: CloudTrail records API calls, but not who changed what or why. There is no way to tell whether port 22 being open in a security group is intentional configuration or the leftover of a temporary fix.
- There is no review: code gets a teammate’s eyes before merging, but a console click takes effect the moment it happens. One typo, one wrong assumption, and you have a production incident.
- It depends on one person: when the only person who knows the full setup leaves the team, the infrastructure becomes a black box nobody fully understands.
These four problems share a single root cause: the current state of the infrastructure is not written down anywhere.
IaC: declare the desired state, and the tool makes it so #
IaC (Infrastructure as Code) means defining infrastructure in code files instead of clicks. What matters is the kind of code. A shell script listing AWS CLI commands is code too, but it is a sequence of commands — “create the bucket” — so running it twice produces errors or duplicates. Terraform code is a declaration of the desired state — “a bucket with this name must exist.” When you run it, Terraform compares the declaration against what currently exists, creates only what is missing, removes only what is extra, and fixes only what differs. If reality already matches the declaration, it does nothing. In other words, you can run it any number of times and get the same result.
The moment infrastructure becomes a code file, every tool software development has built up applies to infrastructure too. Push it to Git and every change has a commit history, changes go through PR review, incidents can be rolled back to a previous commit, and the same code can be applied to another region or account to reproduce an identical environment. The answers to all four problems above come from exactly this.
Where Terraform sits: versus CloudFormation, CDK, and Pulumi #
Terraform is not the only IaC tool. On AWS, the usual candidates are four.
| Tool | Scope | Code format | Notes |
|---|---|---|---|
| Terraform | Multi-cloud, SaaS | HCL (declarative DSL) | De facto standard, largest provider ecosystem |
| CloudFormation | AWS only | JSON, YAML | AWS-managed, state kept by AWS |
| AWS CDK | AWS only | TypeScript, Python, etc. | Written in general languages, compiled to CloudFormation |
| Pulumi | Multi-cloud | TypeScript, Python, etc. | Suits teams that prefer general-purpose languages |
CloudFormation and CDK integrate deeply with AWS, but cannot define anything outside it. Terraform uses a plugin architecture called providers, which lets it manage not just clouds like AWS, GCP, and Azure but also GitHub repositories, Cloudflare DNS, and Datadog monitors with the same syntax. Real-world infrastructure rarely ends at a single cloud, and this generality is what made Terraform the de facto standard — and the name that appears most often under the IaC line in job postings.
One piece of background worth knowing: in 2023 HashiCorp changed Terraform’s license from open source to BSL, and the community that objected forked it as OpenTofu, which is still actively maintained. The two tools remain nearly identical in syntax, so most of this series applies to both; how to choose between them gets its own treatment in the final part of the operations series.
Installation and setup #
This series is written against Terraform 1.15 and AWS provider 6.x. On macOS, install with Homebrew.
brew tap hashicorp/tap
brew install hashicorp/tap/terraformOn Ubuntu and friends, register the HashiCorp repository and install with apt.
wget -O - https://apt.releases.hashicorp.com/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \
https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraformVerify the installation.
$ terraform version
Terraform v1.15.8Terraform needs credentials to create resources in AWS. If you have the AWS CLI configured (aws configure or an SSO login), Terraform picks up the same credentials automatically — no extra setup required.
The first resource: an S3 bucket as code #
Create an empty directory and write a main.tf file. An S3 bucket costs nothing just by existing, which makes it a safe first exercise.
terraform {
required_version = ">= 1.15.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_s3_bucket" "hello" {
bucket = "my-terraform-hello-20260822"
tags = {
ManagedBy = "terraform"
}
}We dissect the syntax in the next part; for now, just note what the three chunks do. The terraform block pins the versions of Terraform itself and the provider, the provider block decides which region to build in, and the resource block is the declaration: “an S3 bucket with this name must exist.” S3 bucket names are globally unique, so replace my-terraform-hello-20260822 with a value of your own.
The first time you work in a directory, run the initialization once.
$ terraform init
Initializing provider plugins...
- Installing hashicorp/aws v6.x.x...
Terraform has been initialized!init is the preparation step that downloads the provider plugins your code declares. Next comes the most important command in this entire series: plan.
$ terraform plan
Terraform will perform the following actions:
# aws_s3_bucket.hello will be created
+ resource "aws_s3_bucket" "hello" {
+ bucket = "my-terraform-hello-20260822"
...
}
Plan: 1 to add, 0 to change, 0 to destroy.plan does not create anything yet. It compares the declaration with the current state and shows you in advance what it would do. This is a checkpoint that console clicking never had: you review the changes with your own eyes before anything runs, and on a team, this output is what goes into review. If the plan matches your intent, apply it.
$ terraform apply
...
Do you want to perform these actions?
Enter a value: yes
aws_s3_bucket.hello: Creating...
aws_s3_bucket.hello: Creation complete after 2s
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.Open the S3 page in the console and the bucket is really there — infrastructure created with a code file and two commands instead of clicks. Run terraform plan again in this state and you get “No changes.” The declaration and reality already match, so there is nothing to do — which is exactly the declarative behavior described earlier.
Removing what you made, as code #
The exercise is done, so clean up. No need to hunt down the bucket in the console — one command is enough.
$ terraform destroy
Plan: 0 to add, 0 to change, 1 to destroy.
Enter a value: yes
Destroy complete! Resources: 1 destroyed.Terraform knows everything it created and removes it by walking the same path backwards. There is no better tool for building and tearing down practice infrastructure. But how did Terraform remember what it had created? Look in the directory and you will find a file named terraform.tfstate — a record of the resources Terraform manages and their state. Terraform’s entire mechanism hangs on this file, and #5 covers it in detail.
Recap #
What we covered in this post:
- Console-click operations create four problems — no reproducibility, no history, no review, dependence on individuals. The shared root cause is that the current state of the infrastructure is not written down anywhere
- IaC declares the desired state as code. The tool compares it against reality and applies only the difference, so any number of runs gives the same result, and Git history, review, and rollback all apply to infrastructure
- CloudFormation and CDK are AWS-only; Terraform and Pulumi are multi-cloud. By provider ecosystem and job market alike, Terraform is the de facto standard
terraform initprepares providers,planpreviews changes,applyapplies them, anddestroyremoves managed resources- Terraform records what it manages in
terraform.tfstate. What that file really is comes in #5
In the next post (#2 HCL Core Syntax), we properly dissect the main.tf we skimmed today — blocks, arguments, resource addresses, the skeleton of HCL — and get the plan, apply, destroy cycle into muscle memory.