Terraform Fundamentals

Infrastructure as Code & Terraform

18 min Lesson 1 of 30

Infrastructure as Code & Terraform

Before Terraform, provisioning a cloud environment meant clicking through a web console, running ad-hoc AWS CLI commands, or writing fragile shell scripts that worked once and then became tribal knowledge no one dared touch. The result was snowflake infrastructure — every environment subtly different, impossible to reproduce reliably, and terrifying to change.

Infrastructure as Code (IaC) is the practice of describing your infrastructure in version-controlled, human-readable files and letting a tool provision and manage it from that description. At Google, Meta, and every serious cloud-native shop, no production resource is created by clicking. Everything — VPCs, IAM roles, EKS clusters, RDS instances, DNS records — is declared in code, reviewed via pull request, and applied through an automated pipeline. The audit trail is in Git; the blast radius of any change is visible before it lands.

Declarative vs Imperative IaC

There are two approaches to IaC: imperative and declarative. Ansible playbooks and shell scripts are imperative — you specify the steps to reach a desired state. Terraform is declarative — you specify what the end state should look like and let Terraform figure out the steps.

The declarative model has a decisive advantage at scale: idempotency. Running terraform apply ten times against the same config produces the same infrastructure every time. No side-effects, no cumulative drift. With an imperative script, running it twice might create duplicate resources, fail on already-existing objects, or leave the environment half-configured.

Why Terraform dominates IaC in 2025: Terraform (and its open-source fork OpenTofu) is provider-agnostic — the same workflow manages AWS, GCP, Azure, Cloudflare, GitHub, Datadog, Kubernetes, and 3,000+ other providers through a consistent interface. Unlike AWS CloudFormation (AWS-only) or Azure ARM templates (Azure-only), a single Terraform codebase can orchestrate your entire multi-cloud and SaaS footprint. This is why it is the default IaC tool at large engineering organizations running on more than one cloud.

The Plan / Apply Loop

Terraform's core workflow is a three-phase loop: Write → Plan → Apply. This is the discipline that separates professional IaC from script-and-pray.

Write: you author .tf files describing the resources you want. Terraform uses HCL (HashiCorp Configuration Language), a configuration language designed to be readable by both humans and machines.

Plan: terraform plan computes the difference between your declared configuration and the current real-world state (tracked in a state file). It prints a precise diff — which resources will be created, changed, or destroyed — without touching anything. This is your pre-flight check. In production CI pipelines, the plan output is posted as a PR comment so reviewers can approve the exact changes before they land.

Apply: terraform apply executes the plan. Terraform calls provider APIs in dependency order, creating resources in parallel where possible, and updates the state file to record what now exists.

# 1. Initialize the working directory (download providers, set up backend) terraform init # 2. See exactly what will change — no side effects terraform plan # 3. Apply the changes (prompts for confirmation; use -auto-approve in CI) terraform apply # 4. Destroy all managed resources (use with extreme caution in production) terraform destroy
Always run terraform plan before apply, even locally. In production, make the plan artifact mandatory: generate it with terraform plan -out=tfplan, store it as a CI artifact, then apply that exact plan with terraform apply tfplan. This guarantees what was reviewed is what gets applied — no race conditions where the environment changes between plan and apply.

Providers: Terraform's Extension Model

Terraform itself has no knowledge of AWS, Kubernetes, or Cloudflare. All resource types live in providers — plugins that translate Terraform's declarative resource definitions into real API calls. The AWS provider makes calls to the AWS APIs; the Kubernetes provider talks to the cluster's API server; the GitHub provider manages repositories and team memberships.

A provider block in your config declares which provider to use and at which version. Terraform downloads providers from the Terraform Registry during terraform init and caches them locally in .terraform/.

# main.tf — declare required providers and configure AWS terraform { required_version = ">= 1.6.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" # allow 5.x, block 6.x (breaking changes) } } } provider "aws" { region = "us-east-1" # credentials come from environment variables or ~/.aws/credentials # never hard-code secrets here } # A simple S3 bucket resource resource "aws_s3_bucket" "app_assets" { bucket = "myorg-app-assets-prod" tags = { Environment = "production" ManagedBy = "terraform" Team = "platform" } }

The ~> 5.0 version constraint is a production-critical pattern. It pins the major version, allowing automatic patch and minor updates (5.1, 5.2...) while blocking 6.0 which may have breaking changes. Omitting version constraints causes terraform init to download whatever is latest — a silent way to import breaking changes into your infrastructure codebase.

Terraform plan/apply workflow with provider plugin model Write .tf config files Plan terraform plan Apply terraform apply State File terraform.tfstate state informs diff Provider Plugin Model Terraform Core plan + state engine AWS Provider hashicorp/aws K8s Provider hashicorp/kubernetes GitHub Provider integrations/github Datadog Provider datadog/datadog → AWS APIs → K8s API server → GitHub API → Datadog API 3,000+ providers on the Terraform Registry — one workflow manages your entire infrastructure footprint
The Terraform plan/apply loop (top) and provider plugin model (bottom) — providers translate declarative config into real API calls.

How Terraform Knows What Exists: The State File

Terraform tracks every resource it manages in a state file (terraform.tfstate). The state file is the bridge between your HCL configuration and real-world infrastructure. When you run terraform plan, Terraform reads both the config and the state, calls the provider to refresh live resource attributes, and computes the diff.

This has an important consequence: resources created outside Terraform (by clicking in the console, or by another team's script) are invisible to Terraform unless you explicitly import them. This is both a feature (clear ownership) and a common trap (engineers who bypass Terraform create untracked resources that later cause confusing plan diffs or orphaned costs).

Never commit terraform.tfstate to Git, and never store it locally for shared infrastructure. The state file contains sensitive data — resource IDs, IP addresses, and sometimes secrets output from resources. More critically, if two engineers apply against the same local state file simultaneously, the state becomes corrupted and your infrastructure is in an unknown condition. The solution is a remote backend (S3 + DynamoDB for locking, or Terraform Cloud) — covered in Lesson 6. On every real team, day one of the Terraform setup is configuring the remote backend. Local state is only acceptable for personal experiments.

Your First Terraform Workflow End-to-End

Here is a minimal but complete example that creates an AWS S3 bucket and outputs its ARN. Run this against a personal AWS account to get the muscle memory for the workflow:

# Directory layout # . # ├── main.tf # └── outputs.tf # main.tf terraform { required_version = ">= 1.6.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } provider "aws" { region = "us-east-1" } resource "aws_s3_bucket" "example" { bucket = "myorg-terraform-intro-demo" tags = { ManagedBy = "terraform" } } # outputs.tf output "bucket_arn" { description = "ARN of the demo S3 bucket" value = aws_s3_bucket.example.arn } # --- Commands --- # export AWS_ACCESS_KEY_ID=... # export AWS_SECRET_ACCESS_KEY=... # Download the AWS provider plugin terraform init # Preview: shows "+ aws_s3_bucket.example will be created" terraform plan # Create the resource; type "yes" when prompted terraform apply # Verify output terraform output bucket_arn # Clean up (destroys the bucket) terraform destroy

Notice that aws_s3_bucket.example.arn references the bucket's ARN attribute before the resource exists. Terraform resolves these references at apply time, building a dependency graph to determine execution order. This reference syntax — resource_type.name.attribute — is fundamental to everything in Terraform and is covered in depth in Lesson 7.

Enable the Terraform language server in your editor immediately. Install the official HashiCorp Terraform extension for VS Code (or the equivalent for your editor). It provides autocompletion for every resource attribute, inline documentation, and syntax validation. Writing HCL without it is like writing code without a type checker — you will catch errors at terraform plan time instead of in the editor, which is a slower feedback loop.

The next lesson dives into HCL syntax in depth — blocks, arguments, expressions, type system, and how to write your first real multi-resource configuration. By Lesson 10 you will have provisioned a full web stack — load balancer, EC2 instances, RDS, and Route 53 — entirely from Terraform code.