Every .tf file is written in HCL. HCL is made of blocks. A block looks like:
block_type "label_one" "label_two" {
argument = value
nested_block {
other = value
}
}There are only a handful of block types you need to know. Here are all of the ones used in this repository.
5.1 resource - a thing to create
A resource block declares one piece of infrastructure. This is the heart of HCL.
resource "aws_kms_key" "state" {
description = "OpenTofu state encryption"
enable_key_rotation = true
}"aws_kms_key"is the resource type (defined by the AWS provider)."state"is a local name you pick, used to refer to it elsewhere in the code. It is not visible in AWS.- Inside the braces are the resource's arguments.
To reference an attribute of this resource elsewhere, you write
aws_kms_key.state.arn (type.name.attribute). Real example from
bootstrap/main.tf, where the bucket encryption rule points at
the key created above:
kms_master_key_id = aws_kms_key.state.arnThis reference is also how OpenTofu learns the order to create things: because the bucket refers to the key, OpenTofu knows the key must exist first. You almost never specify ordering manually - it is inferred from references.
5.2 variable - an input
A variable is an input you can set from outside, so the same code can behave
differently per environment. From envs/dev/variables.tf:
variable "az_count" {
description = "Number of AZs. Dev runs lean at 2."
type = number
default = 2
}typeconstrains what's allowed (string,number,bool,list(...),map(...), etc.).defaultmakes it optional. With no default, the value must be supplied.- You read a variable's value with
var.az_count.
Variables can also validate their input. From
bootstrap/variables.tf:
variable "environment" {
type = string
validation {
condition = contains(["management", "dev", "staging", "prod"], var.environment)
error_message = "environment must be one of: management, dev, staging, prod."
}
}If you pass anything else, OpenTofu stops with that error message before touching AWS.
5.3 output - a result to expose
An output publishes a value after apply - for humans to read, or for other
layers to consume. From bootstrap/outputs.tf:
output "state_kms_key_arn" {
description = "KMS key ARN encrypting state."
value = aws_kms_key.state.arn
}After tofu apply, you can run tofu output state_kms_key_arn to print it. The
Makefile uses exactly this to copy the KMS key ARN into the env backends.
5.4 local - a named intermediate value
locals are like local constants/variables inside the code - computed once,
reused for readability. From envs/dev/main.tf:
locals {
name_prefix = "${var.project}-${var.environment}" # e.g. "alphaform-dev"
common_tags = merge(
{
Project = var.project
Environment = var.environment
ManagedBy = "opentofu"
},
var.tags,
)
}You read them with local.name_prefix. The ${...} syntax is string
interpolation - inserting a value into a string. merge(...) is a built-in
function that combines maps (see section 14).
5.5 data - reading something that already exists
A data source looks up information without creating anything. From
modules/networking/main.tf:
data "aws_availability_zones" "available" {
state = "available"
}This asks AWS "which AZs are available in this region?" so the code can adapt to
whatever region it runs in, instead of hard-coding zone names. You read it with
data.aws_availability_zones.available.names.
5.6 module - a reusable package of resources
A module is a folder of .tf files you can call from elsewhere, passing inputs
and receiving outputs - like calling a function. From
envs/dev/main.tf:
module "networking" {
source = "../../modules/networking" # where the module's code lives
name_prefix = local.name_prefix # inputs (the module's variables)
vpc_cidr = var.vpc_cidr
az_count = var.az_count
single_nat_gateway = var.single_nat_gateway
tags = local.common_tags
}You then use the module's outputs as module.networking.vpc_id. Modules are how
this repo avoids copy-pasting the same network definition into dev, staging, and
prod - the definition lives once in modules/networking/, and each environment
calls it with different inputs. This is the single most important structural idea
in the repo; section 11 covers it in depth.
5.7 provider - configuring the connection to AWS
A provider block configures how to talk to AWS - chiefly which region and
which account. From envs/dev/providers.tf:
provider "aws" {
region = var.region
allowed_account_ids = var.allowed_account_ids # safety guardrail
default_tags {
tags = local.common_tags # auto-tag every resource
}
}Two things worth noting here, both real safety/quality features used throughout this repo:
allowed_account_idsmakes OpenTofu fail fast if your credentials point at the wrong AWS account - a guardrail against accidentally changing prod while aiming at dev.default_tagsautomatically stamps every resource with the same tags (Project, Environment, ManagedBy), so resources are easy to identify and bill.
A root can declare multiple providers using an alias, to work in more than
one region at once. Same file, second provider:
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
# ...
}This exists because some AWS features (CloudFront certificates, CloudFront-scoped
WAF) must be created in the us-east-1 region regardless of where the rest of
your stack lives. The edge module is handed both providers so it can place those
resources correctly (see section 12).
5.8 terraform - settings for OpenTofu itself
The terraform { ... } block configures OpenTofu, not AWS. Two uses in this repo:
- Declaring required versions and providers (
versions.tf, shown in section 4). - Declaring the backend - where state is stored (
backend.tf, covered next).
That is the entire set of block types this repository uses: resource,
variable, output, locals, data, module, provider, terraform. Once
these click, every file becomes readable.