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-syntax

kb/core ── Terraform basics ── beginner

HCL: the language you write Terraform in

HCL (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.

view as markdown

What HCL is

HCL stands for HashiCorp Configuration Language, a language HashiCorp built specifically to describe infrastructure. In it you write "I want one S3 bucket with these tags and one EC2 instance next to it," and Terraform turns that into API calls to the cloud.

HCL is not a programming language in the full sense. It is a declarative language: you describe the desired state, not a sequence of steps. That is the main difference from Python or bash.

What a .tf file is made of

An HCL file is a set of blocks. Each block describes one thing: a resource, a variable, a provider, an output. The basic structure of a block:

hcl
block_type "first_label" "second_label" {
  argument_one   = "value"
  argument_two   = 42
  nested_block {
    sub_argument = true
  }
}

A concrete example, the description of an S3 bucket:

hcl
resource "aws_s3_bucket" "demo" {
  bucket = "my-unique-name-12345"
  tags = {
    Owner   = "student"
    Project = "terraform-hello"
  }
}

Here:

  • resource is the block type (we are creating a resource).
  • "aws_s3_bucket" is the first label, which says what kind of resource this is.
  • "demo" is the second label, our internal name for this bucket. Inside the file you can refer to it as aws_s3_bucket.demo.
  • bucket and tags are the block's arguments.

The block types you see most often

  • resource: create something in the cloud (a bucket, a VM, a database).
  • data: read something from the cloud (find the ID of an existing subnet). See tf-resource-block and tf-data-source later.
  • variable: an input parameter for the configuration (the bucket name from outside).
  • output: what to return to the outside (the ID of the bucket you created).
  • provider: which provider is used and with which settings (AWS, GCP, Azure).
  • terraform: a meta block with settings for Terraform itself: which provider versions, where to store state.
  • locals: computed internal values.
  • module: pull in a reusable module (not used in the beginner track).

Expressions and interpolation

Values in HCL do not have to be strings. You can reference other resources, call functions, do arithmetic:

hcl
resource "aws_s3_bucket" "logs" {
  bucket = "${aws_s3_bucket.demo.bucket}-logs"
  # read this as: "take the bucket attribute of resource aws_s3_bucket.demo and append -logs"
}
output "bucket_count" {
  value = length(aws_s3_bucket.demo.tags)
  # length is a built-in function that counts the elements of a list or map
}

The ${...} construct is called interpolation, a substitution. In modern HCL you can often drop it when the expression is the only content of the string: bucket = aws_s3_bucket.demo.id works the same as bucket = "${aws_s3_bucket.demo.id}".

Comments

HCL has three ways to comment something out:

hcl
# single line (the most common)
// also single line (a C-style holdover)
/*
  multi-line
  block comment
*/

Use #: that is the HashiCorp convention.

How HCL differs from JSON

JSON is also a valid format for Terraform, called JSON syntax (.tf.json). But in real work nobody writes it by hand. The reasons:

  • In HCL you can write comments. In JSON you cannot.
  • In HCL you do not need quotes around keys or trailing commas in lists.
  • HCL has readable multi-line strings through heredoc (<<EOF ... EOF).
  • In HCL it is easier to reference other resources, without nested quotes.

The JSON format is useful only when the HCL file is generated by another program.

Gotchas

  • Block labels are case sensitive. Resource or RESOURCE is invalid; only resource works.
  • Resource names within one type must be unique. Two resource "aws_s3_bucket" "demo" blocks is an error, even if the bucket names differ.
  • A comma after the last element of a list or map is optional but allowed. Unlike JSON.
  • Heredoc keeps line breaks. If you need line breaks in the string, use <<-EOF (with a hyphen): it strips the leading indentation.

§ команды

bash
terraform fmt

Automatically brings your HCL to canonical formatting (indentation, alignment). Run it before every commit.

bash
terraform validate

Checks HCL syntax and basic logic without reaching out to the cloud. Useful in the IDE and in CI.

bash
terraform fmt -recursive

Formats every .tf file across all nested folders.

§ см. также

  • tf-initterraform init: the first command in any projectterraform init downloads the provider plugins (AWS, GCP, and so on), creates a lockfile that pins their versions, and prepares the working directory. Without it, neither plan nor apply will run.
  • tf-planterraform plan: see what Terraform is about to doplan is a dry run: Terraform reads your HCL, reads the state, and shows the diff between them. It changes nothing in the cloud. This is your main tool for not breaking prod by mistake.
  • hcl-typesData types in HCL: string, number, list, map, objectHCL 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).
  • 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