Infrastructure as Code, What Terraform Is, and Getting to Know the CLI
This is the first part
Cloud infrastructure is always easy at the start. You open the AWS console, click a few buttons, an EC2 spins up, an S3 bucket gets created, a security group opens port 443. Everything is fine until you need to do it a second time: stand up that exact configuration in staging, or recover after someone deletes it by accident, or explain to a colleague why the security group opens precisely those ports. That's when you realize you have no record of what you clicked, other than memory.
This series teaches Terraform hands-on: every concept is tied to a real terraform apply on AWS, we inspect real state and clean up afterward. The first article runs no infrastructure-creating commands at all. We need to understand first what this tool actually is, what problem it solves, and how it works inside, so that the later articles type commands while knowing what they're doing.
The problem: hand-built infrastructure can't be repeated
Configuring infrastructure by clicking the console (or running scattered aws ec2 run-instances commands) hits three problems that grow over time.
The first is that it can't be reproduced. The thing you built last month exists, but how you built it is recorded nowhere. To get an identical copy for a test environment, you have to recall every step, and memory is wrong.
The second is configuration drift. Someone tweaks a parameter by hand at 2 a.m. while handling an incident, then forgets. A few weeks later nobody knows how the actual system differs from the original intent, because no "original intent" was ever written down to compare against.
The third is that it can't be reviewed. A dangerous infrastructure change (opening port 22 to the whole Internet, deleting a subnet) looks exactly like a harmless click. No diff, no pull request, no trail for someone to approve before it happens.
Infrastructure as Code (IaC) solves all three with a simple idea: describe the infrastructure you want in configuration files, store them in git, and let a tool turn those files into real infrastructure. The files become the source of truth. Reproducing is re-running. Drift becomes detectable, because there's something to compare against. Review becomes reading a diff, like reviewing ordinary code.
What Terraform is
Per the HashiCorp docs, Terraform is "an infrastructure as code tool that lets you define both cloud and on-prem resources in human-readable configuration files that you can version, reuse, and share." You write out the desired state of the infrastructure; Terraform handles turning the current state into that state.
The core point to grasp from the start: Terraform is declarative. You don't write "create an EC2, then attach a security group, then create an elastic IP." You write "I want an EC2 like this, a security group like that, an elastic IP attached to it." Terraform figures out what to do, in what order, to make reality match the description. On the first run, it creates everything. On the next, if you change one line, it modifies only what changed instead of rebuilding from scratch.
Three concepts will follow you throughout the series:
Provider is the plugin that lets Terraform talk to a specific platform. hashicorp/aws knows how to call the AWS API; there are providers for Google Cloud, Azure, Cloudflare, GitHub, Kubernetes, and thousands of other services on the Terraform Registry. Terraform core itself knows nothing about AWS — all the knowledge of "what parameters an EC2 has, which API to call to create it" lives in the provider.
Resource is a piece of infrastructure you declare: an aws_instance, an aws_s3_bucket, an aws_security_group. This is the unit Terraform manages.
State is the file Terraform uses to remember "what actually exists." The docs describe it as "the source of truth for your environment" — Terraform compares the configuration file against state to know what needs to change. This is the part most prone to confusion, and it gets several articles of its own (Part II).
The lifecycle: write, plan, apply
HashiCorp packages the Terraform workflow into three steps.
Write — you define resources in .tf files, possibly spanning multiple providers.
Plan — Terraform creates an execution plan describing what it will create, modify, or destroy, based on the current state and your configuration. This is the preview step: nothing has actually changed yet, you read the plan and decide.
Apply — once you approve, Terraform performs those operations "in the correct order, respecting any resource dependencies." That sentence matters: you don't state the order, Terraform infers it (Article 5 dissects this dependency graph in detail).
Add destroy to tear down what's been created, and that's the basic lifecycle. The whole series revolves around mastering these four verbs for situations that grow ever more complex.
Inside: how core talks to the provider
This is the part beginners rarely notice, but it explains nearly all of Terraform's later behavior. Terraform is not one monolith. It consists of two separate parts running as two distinct processes:
terraform plan / apply
│
┌──────────────▼───────────────┐
│ Terraform Core │ reads *.tf + terraform.tfstate
│ - builds dependency graph │ compares config ⟷ state ⟷ reality
│ - computes diff (the plan) │ decides what to create/modify/delete
└──────────────┬───────────────┘
│ gRPC (go-plugin, runs locally)
┌──────────────▼───────────────┐
│ Provider plugin │ e.g. hashicorp/aws 6.46.0
│ - knows each resource schema │ downloaded to .terraform/ on init
│ - translates into API calls │
└──────────────┬───────────────┘
│ HTTPS (AWS SDK, SigV4-signed)
┌──────────────▼───────────────┐
│ AWS API │ EC2, S3, IAM, ...
└───────────────────────────────┘
terraform.tfstate ◄─ Core records the real id + attributes of every resource
Terraform core is the binary you install. It reads the .tf files, builds a graph of resources and the relationships between them, compares against state, then computes the plan. Core contains not a single line of knowledge about AWS.
The provider is a separate binary, downloaded at terraform init and living in the .terraform/ directory. It declares the schema (what fields an aws_instance resource has, of what type, which are required) and knows how to translate "create this resource" into real API calls. Core launches the provider as a child process and communicates over gRPC on localhost — this is HashiCorp's go-plugin mechanism. When core needs to "create an EC2," it sends a request over gRPC to the AWS provider; the provider translates it into an AWS SDK RunInstances call, signs it with SigV4 using the credentials you configured, receives back an instance id, then returns that id up to core.
Core records the real id along with all attributes into terraform.tfstate. On the next run, core reads state to know "this resource already exists, id is i-0abc...", asks the provider to query AWS again whether it still exists and still matches the configuration, then computes the diff. Separating core from the provider is exactly why a single tool can manage AWS, Cloudflare, and GitHub in the same apply: core orchestrates the graph, each provider handles its own API.
A word on licensing
As of version 1.6 (August 2023), Terraform switched from the MPL 2.0 license to the Business Source License 1.1 (BUSL). For ordinary users — individuals and companies using Terraform to manage their own infrastructure — nothing changes: you still use it for free. BUSL only restricts taking Terraform itself and turning it into a product that competes commercially with HashiCorp. This led to the birth of OpenTofu, a fork that keeps the open-source license; this series uses official Terraform, but most of the knowledge here applies to both because the HCL syntax is nearly identical. We won't return to the licensing topic again.
Installation
On macOS, use HashiCorp's Homebrew tap:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
On Ubuntu/Debian, add the official apt repo:
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) 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 terraform
If you need to run multiple Terraform versions on the same machine (an old project pins an old version), tfenv is a version manager worth using. For a single shared install, the two methods above are enough.
Check:
$ terraform version
Terraform v1.15.4
on darwin_arm64
This whole series uses Terraform 1.15.4 and AWS provider 6.46.0. The version matters more than you'd think: many features we'll use (state lock via use_lockfile, ephemeral resources, the import/moved/removed blocks) only exist in recent releases, and each article will note which feature needs which version.
A tour of the CLI commands
Run terraform -help to see them all. The first part lists the main workflow commands, in the order you'll use them daily:
$ terraform -help
Usage: terraform [global options] <subcommand> [args]
...
Main commands:
init Prepare your working directory for other commands
validate Check whether the configuration is valid
plan Show changes required by the current configuration
apply Create or update infrastructure
destroy Destroy previously-created infrastructure
These five commands are the backbone. init prepares the working directory — downloads providers and modules. validate checks syntax and consistency without calling any API. plan previews changes. apply executes. destroy tears down.
Below are the less common commands you'll need once you go deeper:
All other commands:
console Try Terraform expressions at an interactive command prompt
fmt Reformat your configuration in the standard style
graph Generate a Graphviz graph of the steps in an operation
import Associate existing infrastructure with a Terraform resource
output Show output values from your root module
providers Show the providers required for this configuration
show Show the current state or a saved plan
state Advanced state management
test Execute integration tests for Terraform modules
...
fmt standardizes formatting (you'll run it often, so put it in CI in Part VII). console lets you try HCL expressions interactively — very handy when learning functions and variables in Part III. state gathers advanced state operations, import brings existing infrastructure under management, test runs tests — each gets its own article later. For now just know they exist and roughly what they do.
One small thing worth doing right away: enable shell autocomplete; type terraform then Tab to get command suggestions.
terraform -install-autocomplete
This command adds configuration to ~/.bashrc or ~/.zshrc; open a new shell for it to take effect.
Wrap-up
Terraform is a tool that turns a declarative infrastructure description in .tf files into real infrastructure, through the write → plan → apply lifecycle. Underneath, core handles the graph and the diff, the provider handles translating into API calls, and state is the ledger that records what actually exists — these three pieces explain nearly all the behavior we'll meet later. Your machine now has Terraform 1.15 and you know what the main commands do.
Next article we stop talking theory and stand up our first real resource on AWS: declare a provider, pin a version, create an S3 bucket, then walk the full init → plan → apply → destroy cycle to see what each step actually does.
This is the first part