Time to write your first Terraform code: understanding the anatomy of HCL syntax from blocks, arguments, to identifiers, then practicing the four core workflow steps — init, plan, apply, and destroy.

In episode 1, we discussed the long history of why Terraform was born: from inconsistent manual ClickOps, fragile imperative shell scripting, to declarative Infrastructure as Code — and why Terraform with its state management and dependency graph became the primary choice in the modern world. Well, in this episode 2, we'll start touching real code.
In this episode we'll discuss two foundations you'll use in every remaining episode of this series. First, the HCL syntax anatomy (HashiCorp Configuration Language) — Terraform's configuration language — starting from block structure, arguments, and identifiers. Second, the four core workflow steps of Terraform: terraform init, terraform plan, terraform apply, and terraform destroy. These two foundations are like learning "the alphabet and grammar" before writing a long essay: without mastering them, the following episodes (providers, resources, variables, state, modules) will feel like reading a book without knowing the letters.
Why is this important in the real world? Because this is the workflow that engineering teams around the world execute hundreds of times every day — including in CI/CD pipelines we'll discuss in episode 13. Understanding what happens behind each command will differentiate you from an operator who just "runs it" into an engineer who can debug and explain Terraform's behavior.
HCL was designed with one philosophy: easy to read for humans, easy to understand for machines. Before writing configuration, let's break down the three core concepts that form every .tf file: Block, Argument, and Identifier.
A Block is the main unit in HCL. It starts with a keyword, followed by label pairs, then content wrapped in curly braces { }. Think of a block as a "box" that holds configuration for one specific thing.
resource "aws_instance" "web" {
# block content: argument
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
}The structure above reads as: I declare a resource of type aws_instance named web. The general form is:
<block_type> "<label_1>" "<label_2>" {
argument = value
}The number of labels varies depending on the block type:
| Block Type | Number of Labels | Example |
|---|---|---|
resource | 2 labels: type & name | resource "aws_instance" "web" |
data | 2 labels: type & name | data "aws_ami" "ubuntu" |
provider | 1 label: name | provider "aws" |
variable | 1 label: name | variable "region" |
output | 1 label: name | output "public_ip" |
terraform | 0 labels | terraform { } |
module | 1 label: name | module "vpc" |
An Argument is a key-value pair inside a block. It's the "material" that configures the block. In the resource "aws_instance" "web" block, the ami and instance_type arguments tell Terraform the desired instance specs.
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
tags = {
Name = "web-server"
Environment = "production"
}
}Notice that argument values can be simple (string, number, boolean) or complex (list, map, even nested blocks like tags). Arguments can also reference other resources — this is the basis of the automatic dependency graph discussed in episode 1.
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
subnet_id = aws_subnet.app.id
}The aws_subnet.app.id reference is the attribute reference syntax: aws_subnet (block type) → app (name) → id (exported attribute). As soon as Terraform sees this reference, it automatically creates a dependency: the instance waits for the subnet to be created first.
An Identifier is the label you give to name a block — in the examples above, web, app, and ubuntu. Identifiers serve two roles:
aws_instance.web.public_ip).Tip
Choose descriptive and consistent identifiers. web, db, bastion are much better than r1, r2, r3 — because infrastructure code is read more often than written, and these identifiers will appear many times as references between resources.
Caution
A common beginner mistake: writing arguments with names that don't exist in the provider documentation. Terraform is very strict — instancetype (wrong) vs instance_type (right) will immediately trigger an Unsupported argument error. Use VS Code autocomplete and always check the official provider documentation for the list of valid arguments.
Time to practice. As a first safe and free example, we'll use the random provider (built-in by HashiCorp, no cloud account needed) and the local provider to write files on your laptop. This example lets you experience Terraform's entire workflow without any cost risk.
Create a new project folder, then fill in the following main.tf:
terraform {
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
local = {
source = "hashicorp/local"
version = "~> 2.5"
}
}
}
resource "random_id" "server_id" {
byte_length = 4
}
resource "local_file" "server_info" {
filename = "server.txt"
content = "Server ID: ${random_id.server_id.hex}\n"
}What's happening in this code?
terraform block declares the providers needed — this will be studied in depth in episode 3.resource "random_id" "server_id" block generates a random 4-byte ID (8 hex characters).resource "local_file" "server_info" block writes a server.txt file containing that random ID. Notice the ${random_id.server_id.hex} reference — this is the interpolation that connects the two resources.Now comes the heart of this episode. Terraform has four main commands that form the full lifecycle of a project. Let's discuss them one by one, complete with their output.
terraform initThe first command that must be run in any Terraform project. Its job: download the declared providers, initialize the backend (where state is stored), and prepare modules. Without init, other commands will refuse to run with the Missing required provider message.
Note
terraform init is idempotent — safe to run repeatedly. It also automatically creates the hidden .terraform/ folder containing the downloaded provider cache. This folder must not be committed to Git (add it to .gitignore), because its contents can be regenerated at any time with init.
terraform planterraform plan is a preview of changes without touching the real environment. Terraform compares three things: your HCL code, the state file, and the actual condition in the cloud. The result is a list of operations that would be performed if you ran apply.
Notice the three symbols that appear in the plan output — these are Terraform's "universal language":
| Symbol | Meaning | When It Appears |
|---|---|---|
+ | Add (create) | New resource that doesn't exist in the cloud yet |
~ | Update in-place (change) | Resource exists, but its attributes differ from the code |
-/+ | Replace (replace) | Resource exists, but its attributes can't be changed in-place |
- | Destroy (delete) | Resource exists in state but is no longer in the code |
The summary line at the very bottom — Plan: 2 to add, 0 to change, 0 to destroy — is the summary of all operations. Reading this line quickly is an important everyday skill: before doing apply, you must make sure that what will be created/changed/deleted matches expectations. Why? Because a "0 to destroy" line you expected that turns out to be "5 to destroy" could mean you're about to delete production infrastructure unintentionally.
Important
The golden rule of a Terraform engineer: always read the terraform plan output before apply. Never blind-run apply without looking at the plan — especially in production environments. This is what separates a responsible engineer from a careless one.
terraform applyterraform apply executes the plan in the real world. Terraform will show the plan again, then ask for confirmation (yes) — unless you add the -auto-approve flag.
After apply succeeds, several things happen:
server.txt file is written on your laptop).terraform.tfstate file — Terraform's "memory" that maps your code to real resources.Apply complete! Resources: 2 added, 0 changed, 0 destroyed. output confirms the execution result.Try verifying the result:
cat server.txt
Server ID: 1a2b3c4d5e6fWarning
After apply, a terraform.tfstate file appears in your project folder. This file contains code-to-resource mappings and can hold sensitive data (IPs, IDs, even secrets in certain cases). Never commit it to Git. In episode 5 we'll discuss how to store it in a secure remote backend, and in episode 15 how to secure the secrets inside it.
terraform destroyterraform destroy deletes all infrastructure managed by the configuration. It's the opposite of apply: it reads the state, finds all resources, and deletes them one by one.
Warning
Terraform itself states it in its prompt: "There is no undo." Running destroy in the wrong environment (for example production) without reading the plan first is one of the biggest incidents in the IaC world. Always confirm the target environment before destroying, and in production, consider prevent_destroy = true for vital resources (we discuss this in episode 7).
| Command | Function | When to Run | Effect on Cloud |
|---|---|---|---|
terraform init | Download providers, init backend & modules | First time & when configuration changes | None |
terraform plan | Preview changes (dry-run) | Before every change | None |
terraform apply | Execute real changes | When code is ready & reviewed | Creates/changes/deletes resources |
terraform destroy | Delete all managed resources | When the environment is no longer needed | Deletes all resources |
Here are some mistakes beginners most often encounter — hopefully you can avoid them:
terraform init in a new project → Missing required provider error. Solution: run init every time there's a new provider or new project.apply directly without reading the plan → risk of deleting resources that shouldn't be deleted. Solution: get in the habit of reading the Plan: X to add, Y to change, Z to destroy. line.apply in the wrong directory → Terraform works per directory. Make sure you're in the folder containing the correct main.tf (pwd, ls *.tf).Unsupported argument error. Solution: use VS Code autocomplete and the provider documentation..terraform/ and terraform.tfstate to Git → bloats the repo and leaks data. Solution: add them to .gitignore..terraform/ or .tfstate carelessly → Terraform loses track of resources and can't manage them anymore. Solution: don't delete, understand first (episodes 5 & 8).Tip
The best exercise to master the workflow: repeat the init → plan → apply → (change configuration) → plan → apply → destroy cycle several times while paying attention to the changes in the Plan: X to add, Y to change, Z to destroy summary line. Also notice how the server.txt file changes. By repeating, your intuition will form on its own.
In this episode 2 we built the technical foundation that will be used throughout the series: understanding the HCL syntax anatomy — blocks as configuration boxes, arguments as the material filling them, and identifiers as the names connecting one resource to another — as well as mastering Terraform's four core workflow steps: init (prepare), plan (preview), apply (execute), and destroy (clean up).
Key takeaways you should bring along:
block { argument = value }, with the resource block being the one you'll use most often.terraform init downloads providers; terraform plan doesn't change anything; terraform apply executes; terraform destroy deletes everything.Plan: X to add, Y to change, Z to destroy line before applying.terraform.tfstate) is a valuable asset — don't commit it, don't delete it.How do you feel after running the init → apply → destroy cycle for the first time? It's a nice feeling, isn't it — the ability to create and delete infrastructure with just a few commands is a superpower you've just acquired.
Now that you understand the HCL "alphabet" and the basic workflow, in the next episode 3 we'll discuss a no less exciting topic: Providers, Resources & Data Sources — understanding the bridge between Terraform and cloud APIs, the difference between managed resources vs read-only data sources, and how to leverage existing infrastructure without recreating it. Stay excited, because from here you start building real infrastructure in the cloud!