Hedronite · Dev Lesson · Polyglot-Dev / HCL · Fri 2026-09-11

HCL backend blocks — partial config and init-time injection

A backend block is HCL that does not evaluate like the rest of the file. Learn the grammar that init understands.

Lesson Class: Dev (HCL depth · backend dialect)
Paired Ops: Terraform partial backend config: S3 keys at init time
Paired Cert: Associate remote backends, partial config, and state migration
Paired Go: DynamoDB DescribeTable Terraform lock inventory
Grounding: Lab 31 · Lab 04 · Brikman Limitations
Literals
Backend args are strings and bools only.
Partial
Omit args; init overlays merge them.
Reconfigure
Name -reconfigure vs -migrate-state.
The backend dialect has no variables.

A backend block is HCL that does not evaluate like the rest of the file. Learn the grammar that init understands.

§I — Frame

The last HCL-depth fire taught replace_triggered_by and terraform_data as address-sensitive lifecycle grammar. Today the grammar is quieter and earlier: the terraform { backend "s3" { ... } } block.

Ops shows why environments inject bucket and key at init. This lesson stays inside the language: which arguments are strings only, which may be omitted for partial configuration, how -backend-config merges, and why required_version sits in a sibling terraform block rather than inside the backend.

§II — The backend block is a closed dialect

terraform {
  required_version = ">= 1.6, < 2.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }

  backend "s3" {
    bucket         = "acme-tfstate"
    key            = "network/dev.tfstate"
    region         = "us-east-1"
    dynamodb_table = "acme-tf-locks"
    encrypt        = true
  }
}

Arguments take literal values. No var., no local., no module., no function calls. Booleans are bare true/false. Strings are quoted. That is the entire dialect.

Partial configuration omits arguments (or leaves empty strings, as Lab 31 does) so init can supply them:

terraform {
  backend "s3" {
    encrypt = true
  }
}
# backend.hcl (not imported by the graph; consumed by init)
bucket         = "acme-tfstate"
key            = "network/dev.tfstate"
region         = "us-east-1"
dynamodb_table = "acme-tf-locks"

Merge rule: values in -backend-config files or key=value flags overlay the partial block. Arguments set in both places: the init overlay wins for that init. Arguments only in the HCL remain.

§III — Illegal shapes that look like normal HCL

variable "environment" {
  type = string
}

locals {
  state_key = "network/${var.environment}.tfstate"
}

terraform {
  backend "s3" {
    bucket = "acme-tfstate"
    key    = local.state_key
    region = "us-east-1"
  }
}

This is ordinary HCL elsewhere. Inside backend it is invalid. Lab 04 encodes that failure. Write the key in backend.hcl or pass -backend-config="key=network/dev.tfstate".

String templates with ${} are still expressions. They fail the same way. Format the path in the shell or in the generator that writes backend.hcl, not in the backend block.

§IV — Multiple overlays and reconfigure

terraform init \
  -backend-config=backend-common.hcl \
  -backend-config="key=network/dev.tfstate"

You may pass -backend-config more than once. Later flags override earlier keys. This is useful when a shared file holds bucket/region/dynamodb_table and the pipeline adds only key.

When the working directory already has backend state under .terraform/, switching keys requires an explicit posture:

terraform init -backend-config=backend-prod.hcl -reconfigure

-reconfigure discards the prior backend settings and adopts the new ones without migrating the object. Use -migrate-state when you intend to copy state from the old backend address to the new one. Name the intent in the pipeline; do not rely on a prompt.

§V — Documentation locals versus backend keys

Lab 31 keeps a local hint:

locals {
  expected_backend_key = format("network/%s.tfstate", var.environment)
}

output "backend_key_hint" {
  value = local.expected_backend_key
}

output "init_example" {
  value = "terraform init -backend-config=backend.hcl"
}

That local is ordinary HCL. It evaluates at plan time. It does not configure the backend. Use it to fail tests when a human-supplied backend.hcl disagrees with the expected path, or to print operator guidance. Never assign it into backend.key.

§VI — Closing

Treat backend as a sealed map of literals with optional init overlays. Keep required_version and required_providers in the same terraform block family, outside the sealed map. When you want environment-specific keys, write HCL files for init, not expressions for the graph. Open any root that still interpolates inside backend and delete the interpolation today.

Related