beginner~4h

Terraform Fundamentals

What Infrastructure as Code actually solves compared to manually clicking through a cloud console, how providers and resources fit together, and the init/plan/apply/destroy workflow you'll run constantly.

Learning objectives

  • Beginner: Explain why declarative, repeatable infrastructure definitions solve problems that manual console changes cannot.
  • Beginner: Describe what a provider and a resource are, and how Terraform uses them to talk to a cloud API.
  • Intermediate: Run the init, plan, apply, and destroy workflow and explain what each step actually does.

Provisioning infrastructure by clicking through a cloud console works fine the first time. It falls apart the moment you need to do it again: build a second environment that matches the first, recover after someone accidentally deletes a resource, or simply remember six months later exactly what settings were chosen and why. Manual provisioning has no memory, no repeatability, and no record — the console shows you the current state, but nothing shows you how it got there or what it looked like last week.

Infrastructure as Code (IaC) solves this by describing infrastructure as text files, checked into version control exactly like application code. Terraform, specifically, is a declarative IaC tool: instead of writing a script that issues a sequence of imperative API calls ("create a VPC, then create a subnet inside it, then create a security group..."), you write a description of the end state you want ("there should exist a VPC with this CIDR block, a subnet inside it, a security group with these rules"), and Terraform figures out what API calls are needed to make reality match that description. This distinction matters enormously in practice: a declarative definition can be safely re-run any number of times — running it again when nothing has changed does nothing, because reality already matches the description — whereas a naive imperative script re-run from the top would try to create the same VPC a second time and fail, or worse, create a duplicate.

Because the infrastructure definition is just text, it inherits everything version control already gives application code: a full history of every change with who made it and why (via commit messages), the ability to review a proposed infrastructure change before it's applied (exactly like a pull request on code), and the ability to revert a bad change. It also means the infrastructure for a new environment — a staging environment that should mirror production, a disaster-recovery region — can be built by running the same definition against a different target, rather than by a person trying to manually reproduce dozens of console settings from memory and hoping nothing was missed.

Terraform itself is cloud-agnostic at its core: the same Terraform CLI and the same HashiCorp Configuration Language (HCL) are used whether you're provisioning AWS, Azure, Google Cloud, Kubernetes clusters, or even non-cloud systems like DNS records or monitoring dashboards. What makes any of that possible is the next concept: providers.

💻 Code example

# The smallest complete Terraform configuration: one provider, one resource terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } provider "aws" { region = "us-east-1" } resource "aws_s3_bucket" "logs" { bucket = "my-company-app-logs-prod" }

A provider is a plugin that Terraform downloads and uses to translate your configuration into actual API calls against a specific platform. The 'aws' provider knows how to talk to AWS's API; the 'google' provider knows Google Cloud's API; the 'kubernetes' provider knows the Kubernetes API, and so on. Each provider is versioned independently and published to the Terraform Registry, and a configuration declares which providers it needs, and which version constraints to accept, in a 'required_providers' block. This separation matters because it's what lets Terraform's core engine — the part that computes dependency graphs, plans changes, and manages state — stay entirely generic, while all platform-specific knowledge lives in swappable plugins.

A resource is a single managed object: one S3 bucket, one virtual machine, one DNS record, one IAM role. Every resource block has the form 'resource "<provider_type>" "<local_name>" { ... }', where the provider_type (like 'aws_instance' or 'aws_s3_bucket') tells Terraform which provider is responsible for it and which API operations apply, and the local_name is just a name you choose for referring to this resource elsewhere within your own configuration — it never appears in the actual cloud platform. Inside the block's body, each provider type defines its own set of arguments: an 'aws_instance' resource has arguments like 'ami' and 'instance_type'; an 'aws_s3_bucket' has arguments like 'bucket' and 'versioning'. These arguments map directly to fields the provider sends to the underlying API, and the provider's documentation (auto-generated from its schema and published on the Terraform Registry) is the authoritative reference for exactly what's available for any given resource type.

Resources reference each other by their local names, and this is how Terraform discovers dependencies without you declaring them explicitly. Writing 'subnet_id = aws_subnet.main.id' inside an 'aws_instance' resource both wires the actual subnet ID into that instance's configuration and tells Terraform's dependency graph that the instance depends on the subnet — Terraform will always create the subnet first and the instance second, and will destroy them in the reverse order, entirely inferred from these references rather than from any ordering you had to specify manually.

A single Terraform configuration can combine any number of providers, which is routine in real infrastructure — a configuration might use the 'aws' provider to create compute resources, the 'cloudflare' provider to manage DNS records pointing at them, and the 'random' provider to generate a unique suffix for a globally-unique bucket name, all in the same set of files, with Terraform coordinating the dependency graph across all of them uniformly.

💻 Code example

# Two resources with an implicit dependency via reference resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" } resource "aws_subnet" "app" { vpc_id = aws_vpc.main.id # <- creates the dependency: subnet needs the VPC's id cidr_block = "10.0.1.0/24" } resource "aws_instance" "web" { ami = "ami-0abcdef1234567890" instance_type = "t3.micro" subnet_id = aws_subnet.app.id # <- instance depends on subnet, which depends on VPC }

'terraform init' is the first command run in any new or freshly cloned configuration. It reads the 'required_providers' block, downloads the matching provider plugins (caching them locally so subsequent runs don't re-download), and initializes the backend that will store state (covered in depth separately — by default this is just a local file, but production setups almost always configure a remote backend here instead). 'init' needs to be re-run any time providers change, a module source changes, or the backend configuration changes; running it again when nothing's changed is harmless and fast, since it reuses what's already cached.

'terraform plan' is the command that makes Terraform genuinely useful rather than just risky. It compares your configuration files against the current state file (Terraform's record of what it believes already exists) and, for anything that differs, queries the real provider API to refresh that belief if needed, then computes exactly what would need to change to make reality match your configuration — without actually changing anything yet. The output lists every resource to be created (prefixed '+'), changed in place (prefixed '~'), or destroyed (prefixed '-'), along with the specific attributes changing on each. This is the single most important habit in Terraform usage: read the plan output carefully before applying, especially watching for unexpected destroys, since a plan that says it will destroy and recreate a database because of an immutable attribute change is a very different risk than one that's only adding a new, unrelated resource.

'terraform apply' re-runs the same comparison plan does, shows you the same plan output, and then — after you type 'yes' at the confirmation prompt (or if you pass a previously saved plan file, applies that exact plan without re-prompting) — actually calls the provider APIs to create, update, or destroy resources to match. Applying updates the state file to reflect the new reality as each change succeeds, which is what lets the next 'plan' correctly compare against an accurate baseline. In automated pipelines, 'terraform apply -auto-approve' skips the interactive confirmation, but this should only ever run against a plan that a human or a policy check already reviewed — never as a way to skip review entirely.

'terraform destroy' computes and applies the opposite operation: it plans the removal of every resource currently tracked in state, prompts for confirmation, and then deletes them all for real. It's standard practice for tearing down temporary environments (a feature-branch preview environment, a throwaway testing sandbox) cleanly, but it is irreversible against the actual infrastructure — there's no "undo" once a database has actually been deleted via the provider's API, even though the Terraform operation itself is simple and simply follows the plan it printed first.

💻 Code example

# A full local workflow from a fresh checkout terraform init # Initializing the backend... # Installing hashicorp/aws v5.31.0... terraform plan -out=tfplan # Terraform will perform the following actions: # + aws_s3_bucket.logs will be created # Plan: 1 to add, 0 to change, 0 to destroy. terraform apply tfplan # applies exactly the reviewed plan, no re-prompt needed # Later, tearing down a throwaway environment entirely: terraform destroy # Plan: 0 to add, 0 to change, 1 to destroy. # Do you really want to destroy all resources? yes

Want a visual for this concept?

Generate a diagram tailored to “Terraform Fundamentals” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to Terraform State Management →← Back to all Terraform: Infrastructure as Code chapters