Terraform Language and Features¶
HashiCorp Configuration Files¶
Terraform configuration files (with .tf or .tf.json extensions) contain
provider, resource, variable, and module definitions. They are written using
the Terraform Language, which is based on HCL.
resource "aws_vpc" "main" {
cidr_block = var.base_cidr_block
}
<BLOCK_TYPE> "<BLOCK_LABEL>" "<BLOCK_LABEL>" {
# Block Body
<IDENTIFIER> = <EXPRESSION> # Argument
}
Terraform can use JSON as an alternative to HCL. This is useful when generating configuration dynamically, because you can rely on existing JSON tooling libraries.
{
"resource": {
"aws_instance": {
"example": {
"instance_type": "t2.micro",
"ami": "ami-abc123"
}
}
}
}
HashiCorp Configuration Language (HCL)¶
HCL is an open-source configuration language designed to be both human-readable and machine-friendly. Many HashiCorp tools use HCL:
- Terraform (.tf), including extended features like dynamic blocks
- Packer (.pkr.hcl)
- Vault policies
- Boundary configuration
- Consul configuration
- Waypoint configs
- Nomad job specs (.nomad)
- Shipyard blueprints
Terraform Settings¶
The special top-level terraform block is used to configure Terraform's own behavior — not provider or resource behavior.
terraform {
required_providers {
aws ={
version = ">= 2.7.0"
source = "hashicorp/aws"
}
}
}
Additional terraform {} settings include:
- required_version — the required Terraform CLI version
- experiments — opt-in to experimental language features
- provider_meta — module-specific metadata for certain providers
Version Constraints¶
Terraform uses Semantic Versioning (MAJOR.MINOR.PATCH).
Version constraint operators:
- = or no operator → exact version (e.g.: "=1.0.0" or "1.0.0")
- != → exclude version (e.g.: "!=1.0.0")
-
, >=, \<, \<= → comparisons (e.g.: ">=1.0.0")
- ~> → “pessimistic constraint”:
- ~> 1.0.0 → allows only patch upgrades
- ~> 1.2 → allows minor & patch upgrades
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
Use the latest stable versions of Terraform, providers, and modules to maintain security, modernity, and development agility.
Named Values¶
Named values are built-in namespaces that expose various values:
- Resources:
<resource_type>.<name>, e.g.: aws_instance_my_server - Variables:
var.<name> - Locals:
local.<name> - Module outputs:
module.<name> - Data sources:
data.<type>.<name>
Filesystem/workspace info:
path.module— path to the module containing the expressionpath.root— root module pathpath.cwd— current working directoryterraform.workspace— name of the currently selected workspace
Block-local values:
count.index(when using the count meta argument)each.key/each.value(when using the for_each meta argument)self.<attribute>- self reference information within the block (provisioners and connections)
Named values look like maps but are not objects — you cannot index them with [].
Input Variables¶
Input variables act as parameters for Terraform modules. They are defined using the variable block:
variable "whatever" {
type = "string"
default = "whatever"
description = "whatever whatever"
validation = ... # block to define validation rules, usually in addition to type contraints
sensitive = ... # limits Terraform UI output when the variable is used in configuration
}
Variable definition files allow you to specify values for multiple variables at once. They must end in:
.tfvars.tfvars.json
Terraform automatically loads a file named terraform.tfvars if present in the
root module.
You can set variables using environment variables prefixed with TF_VAR_:
export TF_VAR_image_id="ami_abc123"
Terraform loads variable values in the following precedence (highest overrides lowest):
- Environment variables:
TF_VAR_<name> - Autoloaded file:
terraform.tfvars - Manually loaded files:
terraform.tfvars.json - Automatically loaded files:
*.auto.tfvarsor*.auto.tfvars.json - Inline CLI variables:
-varand-var-fileCLI arguments
Local Values¶
Local values (defined in locals blocks) assign names to expressions so they
can be reused without repetition. You can define multiple locals blocks in a single
file (even nested - used within itself).
locals {
service_name = "forum"
owner = "whoever"
}
locals {
common_tags = {
service = local.service_name
owner = local.owner
}
}
Notes:
- Use
local.<name>(singular) to reference them. - Best practice: Use locals sparingly. Excessive locals reduce clarity in declarative infrastructure.
Data Sources¶
Data sources allow Terraform to retrieve read-only information from external systems or other Terraform states.
data "aws_ami" "web" { # You specify what kind of external resource you want to select
filter { # You use filters to narrow down the selection
name = "state"
values = ["available"]
}
filter {
name = "tag:component"
values = "web"
}
most_recent = true
}
resource "aws_instance" "web" {
ami = data.aws_ami.web.id
instance_type = "t3.micro"
}
Resource Meta-Arguments¶
Meta-arguments modify how resources behave. They apply to all resource types.
I. depends_on
Manually declare an explicit dependency:
resource "aws_iam_role_policy" "example" {
name = "example"
role = "whatever"
}
resource "aws_instance" "web" {
# ...
depends_on = [
aws_iam_role_policy.example
]
}
II. count
Create multiple resource instances based on a count:
resource "aws_instance" "server" {
count = 4 # create four similar ec2 instances
# ...
tags = {
Name = "Server ${count.index}" # get the current count value (index) - starts at 0
}
}
- count can accept numeric expressions (e.g.: count = length(var.subnet_ids))
III. for_each
Used to create multiple resource instances according to a map, or set of strings. You can iterate over a map for more dynamic values.
resource "azurerm_resource_group" "rg" {
for_each = {
first_group = "eastus"
second_group = "westus2"
}
name = each.key
location = each.value
# or
for_each = toset( ["first_group", "second_group"] )
name = each.key # but there won't be values since they are not provided - only keys
}
IV. lifecycle
Control how Terraform handles create/update/destroy:
resource "azurerm_resource_group" "rg" {
# ...
lifecycle {
create_before_destroy = true # (bool) when replacing a resource, first create the new resource before deleting it (by default, it destroys the old one first)
# or
prevent_destroy = true # (bool) ensures a resource is not destroyed
# or
ignore_changes = [] # (list of attributes) don't change the resource if a change occurs for the listed attributes
}
}
V. provider and alias
Used for selecting a non-default provider configuration
provider "aws" {
alias = "west"
region = "us-west-2"
}
resource "aws_instance" "foo" {
provider = aws.west
}
VI. provisioner and connection
Used for taking extra actions after resource creation (not recommended for most cases; use cloud-init or configuration management tools instead).
Expressions¶
Terraform expressions are used to compute values or reference other values within a configuration. Every expression evaluates to a value, and every value has a type.
I. Primitive Types
A. string
- Uses double quotes:
"hello" - Supports escape sequences:
"\n","\t","\\","$${","%%{" - Supports multi-line heredoc strings:
<<EOT
multi
line
text
EOT
- String interpolation:
"hello, ${var.name}" - String directives (inline conditional logic):
"hello, %{ if var.name != "" }${var.name}%{ else }%some_value%{ endif }" - Directives can also be used in heredocs.
- Strip whitespace using
~:"${var.name ~}"
B. number: numeric types (integers or floats).
C. bool: boolean values: true, false.
II. The null Type
Represents “no value” or “use the default”. Used when you want to use the underlying default of a provider's resource configuration option. It does not necessarily mean “empty.”
III. Complex Types
Complex types group multiple values into a single structure. There are two categories of complex types:
A. Collection Types
Store multiple values of the same type.
- list(type): Ordered, indexable sequence.
- map(type): Key–value pairs.
- set(type): Unordered, unique values; coerced based on the first element’s type.
B. Structural Types
Group multiple values of different types. Require a schema.
- object({ … })
variable "human" {
type = object ({
name = string
surname = optional(string)
age = number
})
default = {
name = "John"
age = 52
}
}
- tuple([ … ])
variable "human" {
type = tuple([string, number, bool])
default = ["John", 52, true]
}
Operators¶
- multiplication: a*b
- division: a/b
- modulus: a%b
- addition: a+b
- subtraction: a-b
- flip to negative (*-1): -a
- equals: a==b
- does not equal: a != b
- less than: a\<b
- less than or equal: a\<=b
- and: a && b
- or: a || b
- flip boolean: !a
Conditional Expressions¶
Terraform supports ternary if else conditions:
condition ? true_val : false_value
The return types of both branches must match.
For expressions¶
Allows you to iterate over a complex type and apply transformations (can iterate over a list, set, tuple, map or an object).
[for s in var.list: upper(s)] # Uppercase each string in the list
# or
[for k, v in var.map: length(k) + length(v)] # sum of key+value
# or
[for i, v in var.list: "${i}: ${v}"] # you can also get the index from a list
# or
{for s in var.list: s => upper(s)} # {hello = "HELLO", world = "WORLD" }
# or
[for s in var.list: upper(s) if s != ""]
Splat expressions¶
Provide a shorter expression for for expressions. A splat operator is represented by an asterisk (*) and it originates from the ruby language. Splats in Terraform are used to rollup or soak up a bunch of iterations in a for expression.
[for o in var.list: o.id] <=> var.list[*].id
# and
[for o in var.list: o.interfaces[0].name] <=> var.list[*].interfaces[0].name
Special behavior:
- If the value is non-null, the result becomes a single-element list.
- If the value is null, the result is an empty tuple.
Dynamic Blocks¶
Used to programmatically create repeated nested blocks:
locals {
ingress_rules = [
{ port = 443 },
{ port = 80 },
]
}
resource "aws_security_group" "main" {
#...
dynamic "ingress" {
for_each = local.ingress_rules
content = {
# ...
from_port = ingress.value.port
to_port = ingress.value.port
}
}
}