Skip to content
C Codeloom
DevOps

Terraform Basics: HCL, Providers, and Your First Resource

Learn the foundations of Terraform: writing HCL, configuring providers, managing state, and creating your first cloud resource the right way.

·5 min read · By Yash Kesharwani
Intermediate 11 min read

What you'll learn

  • What HCL syntax looks like and how to read it
  • How providers and resources fit together
  • How Terraform state tracks the real world
  • How to use variables and outputs cleanly
  • How plan, apply, and destroy actually work

Prerequisites

  • Comfortable on the command line
  • Familiar with cloud concepts — see What is AWS

Infrastructure as code is the practice of describing cloud resources in text files that you check into git, review, and apply. Terraform is the most common tool for it. The language is HCL, the unit of work is a resource, and the engine compares your code to a state file and figures out the minimum changes needed. This post walks through the parts and builds a small example.

Install and verify

Install via your package manager or the official tarball.

brew install terraform
terraform -v

You should see a version near 1.9.x or newer. The CLI is the entry point for every operation.

The shape of a config

Terraform reads every .tf file in the current directory and treats them as one module. A minimal config has three sections.

terraform {
  required_version = ">= 1.6"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

resource "aws_s3_bucket" "logs" {
  bucket = "codeloom-logs-2026"
  tags = {
    Environment = "prod"
    Owner       = "platform"
  }
}
  • terraform block — pins the CLI version and lists the providers you need.
  • provider block — configures one provider, here AWS in a region.
  • resource block — declares a piece of infrastructure. The labels are TYPE NAME. aws_s3_bucket is the type, logs is the local name you use to reference it.

Initialize

Run terraform init once per directory. It downloads provider plugins and sets up the backend.

terraform init

You will see a .terraform directory and a .terraform.lock.hcl file. Commit the lock file so teammates get the same provider versions.

Plan and apply

terraform plan shows what will change. terraform apply makes it happen.

terraform plan -out tfplan
terraform apply tfplan

The plan output uses three symbols:

  • + create
  • - destroy
  • ~ modify in place

If something would be destroyed and recreated, you see -/+. Read every plan before applying. Treat surprise destroys as a stop-the-line event.

State, the source of truth

Terraform records what it created in a state file (terraform.tfstate). On the next run, it compares state, config, and live infrastructure to compute the diff. The local file is fine for solo experiments but dangerous for teams. Use a remote backend like S3 plus DynamoDB locking, or HCP Terraform.

terraform {
  backend "s3" {
    bucket         = "codeloom-tfstate"
    key            = "prod/network.tfstate"
    region         = "us-east-1"
    dynamodb_table = "codeloom-tflock"
    encrypt        = true
  }
}

Never edit state by hand. Use terraform state subcommands when you must.

Variables and outputs

Hardcoded values are fine until they are not. Use variables for things that change between environments and outputs for values you need to share.

variable "region" {
  type    = string
  default = "us-east-1"
}

variable "bucket_name" {
  type        = string
  description = "Globally unique S3 bucket name"
}

output "bucket_arn" {
  value = aws_s3_bucket.logs.arn
}

Set values via a terraform.tfvars file:

bucket_name = "codeloom-logs-2026"

Or via the command line: terraform apply -var bucket_name=foo.

References and dependencies

Terraform builds a dependency graph automatically from references. If resource B mentions aws_s3_bucket.logs.arn, B will be created after the bucket. You almost never need depends_on — only when the relationship is hidden from the graph (IAM policies, eventual consistency).

resource "aws_s3_bucket_versioning" "logs" {
  bucket = aws_s3_bucket.logs.id
  versioning_configuration {
    status = "Enabled"
  }
}

Modules

A module is a folder of .tf files. The root directory is one. Call other modules with module blocks.

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"

  name = "codeloom-vpc"
  cidr = "10.0.0.0/16"
}

Use modules for parts you reuse across projects. Do not over-modularize early; flat configs are easier to read until you have proven duplication.

Lifecycle controls

Inside a resource, the lifecycle block gives you safety knobs.

resource "aws_db_instance" "primary" {
  identifier = "primary"

  lifecycle {
    prevent_destroy       = true
    create_before_destroy = true
    ignore_changes        = [password]
  }
}

prevent_destroy stops terraform destroy and replacement plans for that resource. Use it on stateful systems.

A realistic workflow

A team workflow looks like:

  1. Open a branch, edit .tf files.
  2. Run terraform fmt and terraform validate locally.
  3. Push and let CI run terraform plan against a per-branch workspace.
  4. Review the plan in the pull request comment.
  5. Merge and let CI run terraform apply against the target environment.

This is just CI/CD applied to infrastructure. The plan is the artifact, the apply is the deploy.

Destroying

For sandbox environments, terraform destroy tears everything down. Always run a plan -destroy first, especially against shared accounts.

terraform plan -destroy -out destroy.plan
terraform apply destroy.plan

Common mistakes

  • Editing infrastructure in the cloud console after Terraform owns it. The next plan will try to revert your change. Either bring the change into code or remove the resource from state.
  • Mixing many environments in one root module. Use workspaces or separate directories per environment.
  • Storing secrets in .tf files. Use a secret manager and reference values via data sources.

Wrap up

Terraform is a small language and a big idea. HCL describes the world you want. Providers know how to talk to APIs. State is the ledger. Plan and apply are the verbs. Once you have a remote backend, a CI pipeline, and the habit of reading every plan, infrastructure stops being a thing that “someone configured once” and becomes the same kind of artifact as your application code.