Skip to content

Expert · Infrastructure as code · topic 37 of 50 · 35 min

Terraform for the cloud

Explanation

Terraform is a declarative IaC tool that works with AWS, Azure, Google Cloud and hundreds of other services through plugins called providers. You write .tf files in HCL, run terraform plan to preview the changes and terraform apply to make them. Terraform remembers what it built in a state file, which teams keep in shared, locked remote storage.

Why Terraform

CloudFormation only speaks AWS. Many companies use more than one cloud, plus services like GitHub, Cloudflare or Datadog. Terraform uses one language and one workflow for all of them. You write files ending in .tf in HCL (HashiCorp Configuration Language), a simple block-based language. OpenTofu, the open-source fork, reads the same files with the command tofu instead of terraform, so almost everything in this lesson applies to both.

The building blocks

  • Provider: a plugin that knows one platform's API, such as hashicorp/aws or hashicorp/azurerm. terraform init downloads it.
  • Resource: something Terraform creates and manages, such as aws_s3_bucket. It has a type and a local name.
  • Data source: something Terraform only reads, such as the current account ID or an existing VPC. It never creates it.
  • Variable: an input, like a parameter, with a type and an optional default.
  • Output: a value printed after apply, or read by other configurations.
  • Module: a folder of .tf files that you reuse, like a function.

The same versioned bucket in HCL

output
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"   # pin a major version; check the registry for the current one
    }
  }
}

provider "aws" {
  region = "ap-south-1"   # Mumbai
}

variable "env" {
  type        = string
  description = "Environment name: dev or prod"
  default     = "dev"
}

data "aws_caller_identity" "current" {}

resource "aws_s3_bucket" "results" {
  bucket = "results-${var.env}-${data.aws_caller_identity.current.account_id}"
  tags = {
    env = var.env
  }
}

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

resource "aws_s3_bucket_public_access_block" "results" {
  bucket                  = aws_s3_bucket.results.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

output "bucket_name" {
  value = aws_s3_bucket.results.bucket
}

output "bucket_arn" {
  value = aws_s3_bucket.results.arn
}
Terraform (HCL), main.tf: in the current AWS provider, versioning and public access block are separate resources linked by the bucket ID.

Read references from left to right: aws_s3_bucket.results.id means "the id attribute of the resource of type aws_s3_bucket named results". Because the versioning resource uses that reference, Terraform knows it must create the bucket first. You rarely need to set the order by hand.

The workflow

bash
terraform init                 # download providers, set up the backend (run once per folder, and after changes)
terraform fmt                  # format the files neatly
terraform validate             # check syntax and references
terraform plan -out=tfplan     # preview the changes and save the plan
terraform apply tfplan         # make exactly the saved changes
terraform output bucket_name   # read an output
terraform destroy              # delete everything in this configuration (asks for confirmation)
Saving the plan with -out and applying that file means apply does exactly what you reviewed.

The plan uses symbols: + create, ~ update in place, - destroy, and -/+ destroy and then create a replacement. It ends with a summary such as "Plan: 3 to add, 0 to change, 0 to destroy." A replacement counts as one add and one destroy. Read every -/+ carefully: replacing a database means a new, empty database unless you planned for it.

The state file

Terraform keeps a state file (terraform.tfstate), a JSON record that maps each resource in your code to the real object in the cloud, such as aws_s3_bucket.results to the bucket results-dev-123456789012. Without it Terraform would not know what it already built. The state can contain secrets in plain text, such as database passwords, so never commit it to Git.

On a team, state must be shared and locked. Shared, so everyone plans against the same truth. Locked, so two people cannot apply at the same moment and corrupt it. A common AWS setup stores state in an S3 bucket with versioning and encryption. Newer Terraform versions can lock using a lock file in the same S3 bucket (use_lockfile); older setups used a DynamoDB table for locking. Check the current backend documentation before you choose.

output
terraform {
  backend "s3" {
    bucket       = "acme-terraform-state"
    key          = "results/dev/terraform.tfstate"
    region       = "ap-south-1"
    encrypt      = true
    use_lockfile = true   # S3-native locking in newer versions; older setups used dynamodb_table
  }
}
Terraform (HCL), backend.tf: a remote backend in S3. Each environment gets its own key, so dev and prod state never mix.

Modules

Once the bucket code works, wrap it in a module (a folder such as modules/results-bucket with its own variables and outputs) and call it for each use: module "results" { source = "./modules/results-bucket" env = "prod" }. The public Terraform Registry also has well-tested community modules, for example for a whole VPC. Pin module versions just like provider versions.

Terraform or CloudFormation?

CloudFormationTerraform
CloudsAWS onlyAWS, Azure, Google Cloud and many other services
LanguageYAML or JSONHCL
StateKept for you by AWS in the stackA state file you store and protect (or a managed service such as HCP Terraform)
PreviewChange setsterraform plan
When an apply failsAutomatic rollback to the last good stateNo automatic rollback: some changes may be done; fix the code and apply again
Licence and costPart of AWS, no extra charge for AWS resourcesBSL 1.1 since 2023, free for most users; OpenTofu is open source
Careful: Common mistakes: (1) Committing terraform.tfstate or .terraform/ to Git. Fix: add them to .gitignore and use a remote backend. (2) Running apply without reading the plan. Fix: plan -out, review, then apply the saved file. (3) Two people applying with local state at the same time. Fix: remote state with locking. (4) Not pinning versions, so a new provider release changes behaviour. Fix: version constraints plus the .terraform.lock.hcl file committed to Git.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Terraform for the cloud · 1/8

Terraform one language, many clouds

Terraform is a declarative tool that works with almost every cloud through plugins called providers.

Prefer a video by a teacher? Find free videos on “terraform aws tutorial for beginners s3 bucket state backend” on YouTube ↗

In real life: A fintech startup in Bengaluru manages AWS and Cloudflare together

A small payments startup runs its APIs on AWS in Mumbai and its DNS and web firewall on Cloudflare. With Terraform, one pull request can add an S3 bucket, a new API host on AWS and the matching DNS record on Cloudflare. The CI pipeline posts the plan ("Plan: 4 to add, 1 to change, 0 to destroy.") in the pull request, a second engineer reviews it, and the state lives in an encrypted, versioned S3 bucket with locking, so two engineers can never apply at the same moment.

Practice (0/3 done)

Task 1 · Code · Python

Write symbol(action) that returns the symbol Terraform shows in a plan: "create" gives "+", "update" gives "~", "delete" gives "-", "replace" gives "-/+". Any other action gives "" (empty string).

Task 2 · Code · Python

Write plan_summary(actions), where actions is a list such as ["create", "update", "replace", "no-op"]. Return the summary line that Terraform prints: "Plan: X to add, Y to change, Z to destroy." A "create" adds 1, an "update" changes 1, a "delete" destroys 1, and a "replace" counts as 1 add AND 1 destroy. "no-op" counts for nothing.

Task 3 · Question

Two engineers run terraform apply at the same time on the same configuration, with state on their own laptops. What is the main risk?

Quick quiz

Question 1

1.What does terraform init do?

Question 2

2.In a plan, a resource is marked -/+. What will happen?

Question 3

3.Why should terraform.tfstate not be committed to Git?

Question 4

4.What is the difference between a resource block and a data block?

Question 5

5.Which command checks the syntax and references of your .tf files without contacting the cloud to plan changes?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does terraform init do?

Answer: Downloads the providers and sets up the backend

init prepares the working folder: it downloads providers and modules and configures the backend.

In a plan, a resource is marked -/+. What will happen?

Answer: It will be destroyed and a new one created

-/+ means replace: Terraform destroys the old object and creates a new one. Check this carefully for databases.

Why should terraform.tfstate not be committed to Git?

Answer: It can contain secrets in plain text and it must be shared with locking, which Git cannot do

State can hold passwords and keys, and it needs locking to stop two applies at once. Use an encrypted remote backend.

What is the difference between a resource block and a data block?

Answer: A resource block is created and managed by Terraform; a data block only reads something that already exists

Data sources read existing information, like the account ID, without managing it.

Which command checks the syntax and references of your .tf files without contacting the cloud to plan changes?

Answer: terraform validate

validate checks the configuration itself. plan is the step that compares it with real infrastructure.

How do you refer to the id of resource "aws_s3_bucket" "logs"?Related

Answer: aws_s3_bucket.logs.id

A resource address is TYPE.NAME, and you add the attribute after another dot.

From Terraform basics: HCL, providers, resources and the workflow

What does terraform state rm aws_s3_bucket.legacy do to the real bucket?Related

Answer: Nothing: Terraform just stops tracking it

state rm only removes the record. The real resource keeps existing.

From Terraform state, backends and drift

What should happen to .terraform.lock.hcl?Related

Answer: Commit it to Git

The lock file pins exact provider versions and checksums for everyone, so it belongs in Git.

From Modules, environments and Terraform in CI

Your company has 200 resources that were created in the console. What is the usual way to bring them under Terraform?Related

Answer: Import them (import blocks or terraform import), then plan until it shows no changes

Importing adds existing resources to the state; a clean plan proves the code matches them.

From IaC in a team: reviews, pipelines and policy

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Infrastructure as code” before its exam: IaC in a team: reviews, pipelines and policy.
  2. Module 9 exam: Infrastructure as code · 12 questions · 20 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, High availability, scaling and disaster recovery. 50 topics left in the course before the final exam.

← Previous: AWS CloudFormation