By Fatskills Exam Guides Team — the exam nerds behind 28,500+ quizzes and 2.1M practice questions across 500+ global exams.
For engineers who need to write, debug, and scale Terraform in production—fast.
HCL (HashiCorp Configuration Language) is Terraform’s native syntax. Blocks, arguments, and expressions are the DNA of your infrastructure code. If you don’t master them: - You’ll write brittle, unmaintainable Terraform that breaks in CI/CD.- You’ll waste hours debugging "invalid argument" errors because you misplaced a = or forgot a comma.- You’ll struggle to refactor legacy codebases (e.g., "Why does this aws_instance block have 50 arguments?").
=
aws_instance
Real-world scenario:You inherit a Terraform repo where a single main.tf file deploys 20 resources with hardcoded values. Your task: modularize it to support multi-region, multi-environment deployments (dev/stage/prod). Without understanding HCL syntax, you’ll either: - Break existing deployments by misplacing a block.- Duplicate code instead of using variables/expressions.- Fail to enforce security policies (e.g., "All S3 buckets must have encryption enabled").
main.tf
This guide gives you the exact syntax and patterns to write clean, scalable Terraform—today.
resource
variable
hcl resource "aws_instance" "web" { # Block type: "resource", labels: "aws_instance", "web" ami = "ami-123456" # Arguments inside the block instance_type = "t2.micro" }
module
dynamic
ami = "ami-123456"
ami
instance_type = "t2.micro"
var.ami_id
data.aws_ami.latest.id
sensitive = true
hcl variable "db_password" { type = string sensitive = true }
"${var.env}-bucket"
count.index
aws_instance.web.private_ip
"us-east-1"
42
true
var.env
aws_instance.web.id
+
-
==
&&
!
concat()
lookup()
file()
hcl tags = { Name = "${var.env}-app" Environment = var.env }
local
hcl locals { bucket_name = "${var.env}-${var.project}-bucket" } resource "aws_s3_bucket" "app" { bucket = local.bucket_name }
hcl dynamic "ingress" { for_each = var.ingress_ports content { from_port = ingress.value to_port = ingress.value protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } }
for_each
hcl dynamic "tag" { for_each = var.tags content { key = tag.key value = tag.value } }
count
depends_on
hcl resource "aws_instance" "web" { count = 3 ami = "ami-123456" }
hcl resource "aws_iam_user" "developers" { for_each = toset(["alice", "bob", "charlie"]) name = each.key }
hcl resource "aws_db_instance" "app" { depends_on = [aws_security_group.db] }
data.aws_ami.latest
hcl data "aws_ami" "ubuntu" { most_recent = true owners = ["099720109477"] # Canonical filter { name = "name" values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] } }
hcl locals { common_tags = { Project = "my-app" Environment = var.env } } resource "aws_instance" "web" { tags = local.common_tags }
hcl locals { subnet_cidr = cidrsubnet(var.vpc_cidr, 8, 1) }
output "instance_ip"
hcl output "instance_ip" { value = aws_instance.web.private_ip }
hcl output "alb_dns" { value = aws_lb.app.dns_name }
Task: Deploy an S3 bucket with: - A unique name (using variables).- Encryption enabled.- A dynamic IAM policy (based on a variable list of users).- Outputs for the bucket name and ARN.
terraform --version
aws sts get-caller-identity
Initialize the project: bash mkdir tf-s3-demo && cd tf-s3-demo touch main.tf variables.tf outputs.tf
bash mkdir tf-s3-demo && cd tf-s3-demo touch main.tf variables.tf outputs.tf
Define variables (variables.tf): ```hcl variable "bucket_name" { type = string description = "Name of the S3 bucket (must be globally unique)" }
variables.tf
variable "allowed_users" { type = list(string) description = "List of IAM user ARNs allowed to access the bucket" default = [] }
variable "env" { type = string description = "Environment (dev/stage/prod)" default = "dev" } ```
# Enable encryption resource "aws_s3_bucket_server_side_encryption_configuration" "app" { bucket = aws_s3_bucket.app.id rule { apply_server_side_encryption_by_default { sse_algorithm = "AES256" } } }
# Dynamic IAM policy for allowed users data "aws_iam_policy_document" "bucket_policy" { statement { actions = ["s3:GetObject", "s3:PutObject"] resources = ["${aws_s3_bucket.app.arn}/*"] principals { type = "AWS" identifiers = var.allowed_users } } }
resource "aws_s3_bucket_policy" "app" { bucket = aws_s3_bucket.app.id policy = data.aws_iam_policy_document.bucket_policy.json } ```
outputs.tf
output "bucket_arn" { value = aws_s3_bucket.app.arn } ```
Deploy the infrastructure: bash terraform init terraform plan -var="bucket_name=my-unique-bucket-123" -var="allowed_users=[\"arn:aws:iam::123456789012:user/alice\"]" terraform apply -var="bucket_name=my-unique-bucket-123" -var="allowed_users=[\"arn:aws:iam::123456789012:user/alice\"]"
bash terraform init terraform plan -var="bucket_name=my-unique-bucket-123" -var="allowed_users=[\"arn:aws:iam::123456789012:user/alice\"]" terraform apply -var="bucket_name=my-unique-bucket-123" -var="allowed_users=[\"arn:aws:iam::123456789012:user/alice\"]"
Verify the deployment:
bash terraform output
Log in as alice and test access: bash aws s3 ls s3://dev-my-unique-bucket-123
alice
bash aws s3 ls s3://dev-my-unique-bucket-123
Clean up: bash terraform destroy -var="bucket_name=my-unique-bucket-123"
bash terraform destroy -var="bucket_name=my-unique-bucket-123"
aws_iam_policy_document
var.env == "prod"
hcl tags = { CostCenter = "marketing" }
network.tf
compute.tf
storage.tf
locals
hcl locals { subnet_cidrs = [for i in range(3) : cidrsubnet(var.vpc_cidr, 8, i)] }
hcl resource "aws_s3_bucket" "app" { bucket = "${var.env}-${var.project}-bucket" }
hcl tags = { Monitoring = "enabled" }
"Which of the following is a block in HCL?"
resource "aws_instance" "web" { ... }
Meta-Arguments:
"You need to create 3 identical EC2 instances. Which meta-argument should you use?"
Expressions:
"Which expression correctly references a variable?"
"${var.env}"
Dynamic Blocks:
dynamic "ingress" { for_each = var.ports ... }
ingress
aws_instance.web[0]
aws_instance.web["alice"]
${}
Task: Write a Terraform configuration that: 1. Creates an IAM user for each name in var.users = ["alice", "bob"].2. Attaches a policy allowing s3:GetObject on a bucket named ${var.env}-app-bucket.3. Outputs the IAM user ARNs.
var.users = ["alice", "bob"]
s3:GetObject
${var.env}-app-bucket
Solution:
variable "users" { type = list(string) default = ["alice", "bob"] } variable "env" { type = string default = "dev" } resource "aws_iam_user" "app" { for_each = toset(var.users) name = each.key } resource "aws_iam_policy" "s3_read" { name = "${var.env}-s3-read-policy" description = "Allow S3 read access" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = ["s3:GetObject"] Effect = "Allow" Resource = "arn:aws:s3:::${var.env}-app-bucket/*" } ] }) } resource "aws_iam_user_policy_attachment" "s3_read" { for_each = aws_iam_user.app user = each.key policy_arn = aws_iam_policy.s3_read.arn } output "user_arns" { value = [for user in aws_iam_user.app : user.arn] }
Why it works:- for_each creates one IAM user per name in var.users.- The policy is attached to each user dynamically.- The output uses a for expression to list all ARNs.
var.users
for
resource "aws_s3_bucket" "app" { ... }
dynamic "ingress" { for_each = var.ports }
count = 3
for_each = var.users
data "aws_ami" "ubuntu" { ... }
locals { name = "${var.env}-bucket" }
output "ip" { value = aws_instance.web.ip }
for_each = toset(["a", "b"])
Join 4M+ learners. Unlock unlimited quizzes, wrong-answer tracking, flashcards + reminders, study guides, and 1-on-1 challenges.