linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
Intro
Lessons
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Knowledge base
  • Cheat sheet
  • Capstone
  • Interview prep
home/terraform/kb/Terraform basics/hcl-types

kb/core ── Terraform basics ── beginner

Data types in HCL: string, number, list, map, object

HCL supports primitives (string, number, bool) and complex types: list, set, map, tuple, object. This article covers the syntax of each one and the difference between the look-alikes (list vs tuple, map vs object).

view as markdownaka: hcl-data-types

Primitive types

There are three base types:

TypeLiteralExample
stringin double quotes"us-east-1"
numberinteger or decimal42, 3.14
booltrue / falsetrue

In a variable you declare the type explicitly:

hcl
variable "region" {
  type    = string
  default = "us-east-1"
}
variable "retention_days" {
  type    = number
  default = 30
}
variable "enable_versioning" {
  type    = bool
  default = false
}

Complex collection types

A collection is "many of the same." Every element has the same type.

list (ordered)

hcl
variable "az_names" {
  type    = list(string)
  default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

You access elements by index: var.az_names[0] gives "us-east-1a". Order matters and stays stable.

set (no order, no duplicates)

hcl
variable "allowed_cidrs" {
  type    = set(string)
  default = ["10.0.0.0/8", "192.168.0.0/16"]
}

There is no order, so you cannot access elements by index. A set is used with for_each, which needs unique keys with no order.

map (key-value, string keys)

hcl
variable "tags" {
  type = map(string)
  default = {
    Environment = "dev"
    Owner       = "platform"
  }
}

You access values by key: var.tags["Environment"] gives "dev". All values have the same type (string in this example).

Structural types

A structural type is "many different values of a shape known in advance."

object (fields of different types)

hcl
variable "bucket_settings" {
  type = object({
    name           = string
    versioning     = bool
    retention_days = number
  })
  default = {
    name           = "my-bucket"
    versioning     = true
    retention_days = 90
  }
}

Fields can have different types. You access them with a dot: var.bucket_settings.name. If the default leaves out a field or adds an extra one, terraform fails on validate.

tuple (an ordered pair or triple)

hcl
variable "name_and_size" {
  type    = tuple([string, number])
  default = ["primary", 100]
}

A tuple is like a list, but each element has its own type and the length is fixed. In practice you rarely need one: an object is usually clearer.

How list differs from tuple

  • list(T) holds any number of elements of one type T.
  • tuple([T1, T2, T3]) holds exactly three elements, each of its own type.

If your colleagues keep writing the same shape, that is a list. If the structure has several fields, an object is preferable to a tuple.

How map differs from object

  • map(T) has an arbitrary number of keys, all values of type T.
  • object({...}) has a fixed set of keys, each value of its own type.

A tag set is a map(string). A resource config is an object({...}). A simple rule of thumb: if the keys come from outside (from the user, not from your code), use map; if you write the key names yourself, use object.

any, opting out of typing

hcl
variable "anything" {
  type = any
}

Terraform will not check the structure at all. This is an escape hatch for cases where the type changes at runtime, and it is almost always an anti-pattern. If you use any, write a comment explaining why typing was genuinely impossible.

Optional fields in an object

Since Terraform 1.3+ you can make object fields optional:

hcl
variable "logging" {
  type = object({
    enabled    = bool
    target_arn = optional(string)
    prefix     = optional(string, "logs/")  # default
  })
  default = {
    enabled = true
  }
}
  • optional(string) means the field can be omitted; its value will be null.
  • optional(string, "logs/") sets a default value.

This saves you from long object schemas where half the fields are rarely needed.

Gotchas

  • null is not the same as a missing field. In an object with required fields you must pass them all, even null explicitly.
  • map(string) versus object({a=string}) in a variable. With map(string) the keys can be anything; with object, only the ones you listed.
  • A set loses order. If order matters (for example, in outputs), switch to a list.
  • Numbers in HCL have no fixed precision. terraform stores a number internally as cty.Number (BigFloat). Large values may round, but this is usually not a problem.
  • A boolean from a string. "true" (a string) and true (a bool) are different types. terraform does not convert automatically, so you get a type error.

§ команды

bash
terraform console

An interactive REPL for checking types. Type `type(var.tags)` there to see the type that was inferred.

bash
echo 'type(var.tags)' | terraform console

A non-interactive variant: print the type of a single variable.

bash
terraform validate

Checks that the default values match the declared types, without reaching out to the cloud.

§ см. также

  • hcl-syntaxHCL: the language you write Terraform inHCL (HashiCorp Configuration Language) is the language you use to describe the desired state of your infrastructure. It looks like JSON, but it is easier to read: you can write comments, variables, and loops.
  • tf-provider-blockThe provider block: who Terraform will callThe provider block configures the plugin: which AWS region to talk to, which endpoints to use, which credentials to take. One block per provider is usually enough.
  • tf-version-constraintsVersion constraints in Terraform: required_version and providersrequired_version pins which versions of terraform may run this code. required_providers.version does the same for providers. The pessimistic operator ~> 5.60 is the standard: it allows minor updates and blocks major ones.
Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies