| Tested on | Ubuntu 26.04 LTS (Resolute Raccoon) |
|---|---|
| Package | terraform 1.15.8-1 |
| Applies to | Any host with Terraform installed |
| Lab environment | Single Ubuntu VM with Terraform — Terraform lab environment on Ubuntu |
| Privilege | Normal user; sudo only if Terraform is not installed yet |
| Scope | Terraform configuration language basics — blocks, labels, arguments, expressions, comments, primitive and collection syntax, major block types, multi-file layout, terraform fmt and validate, and common syntax errors. Does not cover variables in depth, provider version locking, modules, or remote state. |
| Related guides | Terraform lab environment on Ubuntu Install Terraform on Ubuntu apt command check Ubuntu version |
Terraform configurations are written in HashiCorp Configuration Language (HCL) and saved as .tf files (or .tf.json when JSON is required). Searchers often say Terraform HCL syntax; this guide uses that term while focusing on how Terraform configuration is structured, not on HCL's implementation history.
Here is a complete minimal example you will break down and run later:
terraform {
required_version = ">= 1.12.0"
}
resource "terraform_data" "example" {
input = "Hello Terraform"
}
output "message" {
value = terraform_data.example.output
}The sections below explain each construct. Examples use the built-in terraform_data resource so you can practice Terraform syntax without cloud credentials or external providers.
Terraform configuration file structure
Create a dedicated directory for these exercises under your lab tree:
mkdir -p ~/terraform-labs/hcl-syntaxMove into that directory — every command in this guide assumes you are here:
cd ~/terraform-labs/hcl-syntaxCreate an empty starting file:
touch main.tfCommon filenames teams use in one module directory:
main.tf
variables.tf
outputs.tf
providers.tf
versions.tfThese names are conventions only. Terraform does not require main.tf or any particular filename. Terraform combines the .tf and .tf.json configuration files in the directory into one module.
Anatomy of a Terraform block
A typical resource block looks like this:
resource "terraform_data" "example" {
input = "hello"
}Read it left to right:
resource block type (keyword)
"terraform_data" first block label (resource type)
"example" second block label (local name in this module)
input argument name
"hello" argument value (a string expression)Every block has:
- a block type (
resource,variable,output, and others) - zero or more block labels in quotes (resource blocks require two labels)
- a body in
{ }containing arguments asname = valuepairs
Argument values can be literals or more complex expressions — covered briefly below and in depth in a dedicated expressions guide later in this course.
Terraform arguments vs attributes
In configuration, you set arguments:
input = "production"After Terraform creates a resource, you can read exported attributes in references:
terraform_data.example.outputDo not call every key = value pair an attribute:
- Arguments are what you assign in your
.tffiles - Attributes are what Terraform (or the provider) exposes for use in other expressions
Full resource reference rules belong in a separate resource-dependencies article — here the distinction is terminology only.
Terraform identifiers
Local names must start with a letter or underscore and contain letters, digits, underscores, and hyphens. Use descriptive snake_case names:
resource "terraform_data" "web_server" {
input = "ok"
}Avoid spaces, dots in local names, or names that read like provider types (aws_instance as a local name is confusing). The first block label is always the resource type string; the second is your chosen name within the module.
Terraform strings, numbers and booleans
Primitive argument values use familiar literal syntax:
name = "web"
count = 3
enabled = trueStructural types (lists, maps, objects, tuples) have their own rules — see a dedicated data-types guide for constraints and conversions. This article shows only how they look in configuration.
Lists and maps in Terraform syntax
A list uses square brackets:
ports = [80, 443]A map uses curly braces with key = value entries:
tags = {
environment = "dev"
owner = "admin"
}You are learning Terraform configuration syntax here, not the full type system. Tuple, set, and object semantics are out of scope.
Terraform expressions
Argument values are not limited to literals. They can reference other symbols:
input = var.environmentString templates combine values inside a quoted string — ${...} embeds an expression:
input = "${var.environment}-server"When the entire value is only a reference, use the reference directly:
input = var.environmentExpression operators, functions, and precedence are covered in a separate expressions article.
Terraform comments
HCL supports line and block comments:
# line comment
// also a line comment
/*
block comment
across lines
*/Use comments to document non-obvious intent — not to restate every argument name.
Terraform block types
Major block types you will see across the course:
| Block type | Role |
|---|---|
terraform |
Terraform version and provider requirements |
provider |
Provider configuration |
resource |
Managed infrastructure objects |
data |
Read-only data sources |
variable |
Input variables |
locals |
Named local values |
output |
Exported values after apply |
module |
Child module calls |
Each block type has its own tutorial in this course. This page introduces the vocabulary only.
The terraform block
Pin the Terraform CLI version your configuration expects:
terraform {
required_version = ">= 1.12.0"
}Declare which providers the module may use (provider configuration itself is a separate article):
terraform {
required_providers {
local = {
source = "hashicorp/local"
}
}
}The top-level terraform block controls Terraform and provider requirements — not individual resources. Provider version resolution and .terraform.lock.hcl belong in a dependency-lock guide.
Terraform .tf vs .tf.json files
| Extension | Format |
|---|---|
*.tf |
Native HCL syntax (default for hand-written config) |
*.tf.json |
JSON representation of the same structure |
A minimal JSON equivalent of a terraform_data resource:
{
"resource": {
"terraform_data": {
"json_example": {
"input": "from json"
}
}
}
}Most teams author .tf files. JSON is common for:
- Generated configuration
- Tools that emit Terraform language in JSON form
Does Terraform process files in order?
Terraform evaluates a module as a whole, not as a procedural script that runs main.tf before outputs.tf.
Split the example across files in your lab directory. Save the version constraint:
cat > versions.tf <<'EOF'
terraform {
required_version = ">= 1.12.0"
}
EOFPut the resource and a local value in main.tf:
cat > main.tf <<'EOF'
resource "terraform_data" "example" {
input = local.greeting
}
locals {
greeting = "${var.environment}-server"
}
EOFDefine the variable in variables.tf:
cat > variables.tf <<'EOF'
variable "environment" {
type = string
default = "dev"
}
EOFReference the resource output from outputs.tf:
cat > outputs.tf <<'EOF'
output "message" {
value = terraform_data.example.output
}
EOFoutputs.tf can reference terraform_data.example even though the resource is declared in main.tf. For ordinary .tf files, filename order does not control resource evaluation — Terraform evaluates the combined module configuration and dependency graph.
Format Terraform configuration
terraform fmt rewrites .tf files to canonical spacing and alignment. Create a deliberately messy main.tf fragment to see the diff:
cat > /tmp/fmt-demo.tf <<'EOF'
resource "terraform_data" "fmt_demo" {
input = "messy"
}
EOFPreview what fmt would change:
terraform fmt -diff /tmp/fmt-demo.tfSample output:
fmt-demo.tf
--- old/fmt-demo.tf
+++ new/fmt-demo.tf
@@ -1,3 +1,3 @@
resource "terraform_data" "fmt_demo" {
-input = "messy"
+ input = "messy"
}Apply formatting in place:
terraform fmt /tmp/fmt-demo.tfterraform fmt exits silently on success. Flag details and CI usage belong in a dedicated terraform fmt command article.
Validate the example configuration
Return to the multi-file lab directory:
cd ~/terraform-labs/hcl-syntaxInitialize the working directory (required before validate when providers or modules are involved; harmless for built-in resources):
terraform initSample output:
Initializing the backend...
Initializing provider plugins...
- terraform.io/builtin/terraform is built in to Terraform
Terraform has been successfully initialized!Check syntax and internal references:
terraform validateSample output:
Success! The configuration is valid.terraform validate catches many language errors before you run plan. Deeper validation rules (preconditions, custom conditions) are separate topics.
Run the example
Review what Terraform would change:
terraform planSample output (trimmed):
# terraform_data.example will be created
+ resource "terraform_data" "example" {
+ input = "dev-server"
}
Plan: 1 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ message = (known after apply)Apply the plan:
terraform applyType yes when prompted.
Read the exported output:
terraform outputSample output:
message = "dev-server"The value shows your expression ${var.environment}-server resolved correctly across files.
Clean up:
terraform destroyApprove with yes. This short run confirms your Terraform configuration syntax forms a valid, runnable module — command flag details live in dedicated init, plan, apply, and destroy articles.
Common Terraform syntax errors
Create that directory once and move into it for every error example below:
mkdir -p ~/terraform-labs/hcl-syntax-errors && cd ~/terraform-labs/hcl-syntax-errorsInitialize the working directory once before you start overwriting main.tf with broken examples:
terraform initMissing closing brace
Save a block without }:
cat > main.tf <<'EOF'
resource "terraform_data" "example" {
input = "test"
EOFAsk Terraform to parse the broken file:
terraform validateSample output:
Error: Unclosed configuration block
on main.tf line 1, in resource "terraform_data" "example":
1: resource "terraform_data" "example" {
There is no closing brace for this block before the end of the file.Add the missing brace and re-run terraform validate until you see Success!.
Unquoted string value
Use a bare word where Terraform expects a string:
cat > main.tf <<'EOF'
resource "terraform_data" "example" {
input = hello
}
EOFRun validate again on the updated file:
terraform validateSample output:
Error: Invalid reference
on main.tf line 2, in resource "terraform_data" "example":
2: input = hello
A reference to a resource type must be followed by at least one attribute
access, specifying the resource name.Wrap the value in quotes: input = "hello".
Invalid block labels
A resource block needs two labels (type and name):
cat > main.tf <<'EOF'
resource "terraform_data" {
input = "test"
}
EOFValidate to see the label error:
terraform validateSample output:
Error: Missing name for resource
on main.tf line 1, in resource "terraform_data":
1: resource "terraform_data" {
All resource blocks must have 2 labels (type, name).Add the local name: resource "terraform_data" "example".
Invalid reference
Point at a resource that does not exist:
cat > main.tf <<'EOF'
resource "terraform_data" "example" {
input = terraform_data.missing.output
}
EOFCheck whether the reference resolves:
terraform validateSample output:
Error: Reference to undeclared resource
on main.tf line 2, in resource "terraform_data" "example":
2: input = terraform_data.missing.output
A managed resource "terraform_data" "missing" has not been declared in the
root module.Fix the resource name or declare terraform_data.missing.
Unsupported argument
Add an argument the resource type does not accept:
cat > main.tf <<'EOF'
resource "terraform_data" "example" {
input = "test"
not_a_real = "value"
}
EOFValidate to confirm Terraform rejects the unknown argument:
terraform validateSample output:
Error: Unsupported argument
on main.tf line 3, in resource "terraform_data" "example":
3: not_a_real = "value"
An argument named "not_a_real" is not expected here.Remove the unknown argument or switch to a resource type that supports it.
Terraform file organization example
A small realistic layout for one module:
example/
├── versions.tf # terraform { required_version ... }
├── main.tf # primary resources and locals
├── variables.tf # input variables
├── locals.tf # optional: locals-only file
└── outputs.tf # output valuesSeparation is for human readability. Terraform still combines the configuration files in that directory into a single module.
- You can merge everything into one
main.tffor tiny labs - Split files as the configuration grows
References
- Terraform language documentation
- Terraform files and configuration structure
- Syntax overview
- Configuration syntax
- terraform fmt command
- terraform validate command
- terraform_data resource
Summary
You now know how Terraform HCL syntax fits together:
- Blocks with labels
- Arguments with expressions
- Comments, primitive and collection literals
- The major block types you will meet in later lessons
Multiple .tf files in one directory form one module — Terraform does not execute them top to bottom like a shell script.
You formatted configuration with terraform fmt, caught mistakes with terraform validate, and ran a multi-file terraform_data example through plan, apply, output, and destroy on Ubuntu.
The most common beginner slip is confusing arguments you set in configuration with attributes you read from resources — keep that distinction in mind when you move on to variables, providers, and resource references.
Next steps in this course track: provider configuration, the variable and output system, and expression depth — each in its own article so this page stays focused on language shape.

