Skip to content

Your First Terraform Configuration


Terraform becomes much easier to understand once you stop treating its syntax as a collection of special commands. A Terraform configuration is a description of infrastructure written in a configuration language. You create blocks, give them arguments, and use expressions to connect values together.

In this chapter, you will build a tiny Terraform configuration and use it to see the basic structure of the language.

Terraform Configuration Is Written in HCL

Terraform configurations are normally written using HashiCorp Configuration Language (HCL).

A minimal example looks like this:

resource "example_resource" "example" {
  name = "hello"
}

There are several things happening here.

resource "example_resource" "example"
β”‚        β”‚                 β”‚
β”‚        β”‚                 └── local name
β”‚        └── resource type
└── block type

Inside the block is an argument:

name = "hello"

The left side is the argument name and the right side is its value.

This gives you the basic shape:

block
β”œβ”€β”€ labels
└── arguments

Terraform configurations can also contain expressions. An expression is something Terraform evaluates to produce a value.

For example:

name = var.project_name

Here var.project_name is an expression rather than a literal string.

Your First Working Configuration

For a self-contained first exercise, use Terraform’s random provider. It lets you create a deterministic Terraform-managed resource without needing a cloud account.

Create a directory:

mkdir terraform-first-config
cd terraform-first-config

Create main.tf with this exact content:

terraform {
  required_providers {
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
  }
}

provider "random" {}

resource "random_pet" "name" {
  length = 2
}

output "generated_name" {
  value       = random_pet.name.id
  description = "The generated name."
}

This small configuration contains several important pieces.

The terraform block declares the provider dependency.

The provider block configures the provider. This particular provider does not need any configuration, so the block is empty.

The resource block asks Terraform to manage a random_pet resource.

The output block exposes the resulting generated name after Terraform creates it.

Now initialize the directory:

terraform init

Terraform downloads the required provider and creates the dependency lock file.

Validate the configuration:

terraform validate

You should see a successful validation message.

Format the configuration:

terraform fmt

Then create a plan:

terraform plan

Terraform should show that it intends to create one resource.

Apply it:

terraform apply

Terraform will show the proposed changes and ask for confirmation. Enter:

yes

After the apply completes, Terraform should display the generated output.

You can also ask Terraform to display the output later:

terraform output

When you are finished with the example, clean it up:

terraform destroy

Confirm the destruction when Terraform asks.

Understanding the Resource Address

The resource declaration is:

resource "random_pet" "name" {
  length = 2
}

Terraform identifies this resource using two labels:

random_pet.name

The first part is the resource type:

random_pet

The second part is the local name:

name

The local name does not need to be the same as the resource’s actual name in an external system. It is Terraform’s name for referring to this resource inside the configuration.

That is why the output can say:

value = random_pet.name.id

The expression means, roughly:

resource type
    β”‚
    β–Ό
random_pet
    β”‚
    └── local name: name
             β”‚
             └── attribute: id

This resource-reference pattern becomes one of the most important pieces of Terraform syntax you will learn.

Blocks and Arguments

Terraform uses different block types for different purposes.

For example:

resource "random_pet" "name" {
  length = 2
}

The resource is a block.

Its labels are:

"random_pet"
"name"

Its argument is:

length = 2

Other Terraform blocks have different structures.

For example:

output "generated_name" {
  value = random_pet.name.id
}

The output block has one label:

"generated_name"

and an argument:

value = random_pet.name.id

The syntax is similar, but the meaning depends on the block type.

Comments

You can add comments to Terraform configuration.

A single-line comment can use #:

# Generate a short random name.
resource "random_pet" "name" {
  length = 2
}

You can also use // for a single-line comment, and HCL supports block comments:

/*
  This resource is only used
  for demonstration.
*/
resource "random_pet" "name" {
  length = 2
}

Use comments to explain decisions or non-obvious behavior. Do not comment every obvious line.

Multiple Terraform Files

As a configuration grows, keeping everything in one file becomes inconvenient.

You can split the same configuration across several .tf files.

For example:

terraform-first-config/
β”œβ”€β”€ main.tf
β”œβ”€β”€ outputs.tf
└── terraform.tf

You could move the terraform block into terraform.tf and the output into outputs.tf.

Terraform still evaluates the files together.

For example, this terraform.tf:

terraform {
  required_providers {
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
  }
}

provider "random" {}

and this main.tf:

resource "random_pet" "name" {
  length = 2
}

and this outputs.tf:

output "generated_name" {
  value       = random_pet.name.id
  description = "The generated name."
}

represent one Terraform configuration.

Terraform does not execute terraform.tf first and then main.tf.

Note

Splitting configuration into files is primarily an organizational decision. Terraform evaluates all top-level .tf files in the directory as one module.

A Practical Configuration Structure

Terraform does not require a particular filename for every block.

You can put the entire first example into main.tf, and it works.

As projects grow, however, a common organization is:

.
β”œβ”€β”€ main.tf
β”œβ”€β”€ providers.tf
β”œβ”€β”€ variables.tf
β”œβ”€β”€ outputs.tf
└── locals.tf

The names communicate what a reader is likely to find in each file.

For example:

  • providers.tf contains provider configuration.
  • variables.tf contains input variable declarations.
  • outputs.tf contains outputs.
  • locals.tf contains local values.
  • main.tf can contain the primary resources.

You do not have to follow this exact structure for Terraform to work. The goal is readability.

A larger configuration can also be divided by infrastructure concern:

.
β”œβ”€β”€ network.tf
β”œβ”€β”€ compute.tf
β”œβ”€β”€ storage.tf
└── outputs.tf

The important rule is that the structure should help someone find a resource quickly.

Configuration Is Not a Script

One of the easiest mistakes to make when first learning Terraform is to imagine this:

main.tf
  β”‚
  β”œβ”€β”€ line 1
  β”œβ”€β”€ line 2
  β”œβ”€β”€ line 3
  └── line 4
       ↓
Terraform executes each line in order

That is not the right mental model.

Terraform reads the configuration as a collection of declarations and expressions. It determines relationships between values and resources and later uses that information when planning changes.

This is why the following does not mean “create A, then create B” merely because A appears first in the file:

resource "..." "a" {
  ...
}

resource "..." "b" {
  ...
}

Terraform’s behavior is based on the configuration’s meaning and dependencies, not simply on file order.

You will explore this idea in much greater depth later in the course.

The Basic Shape to Remember

At this point, you do not need to memorize every Terraform block.

Keep this mental model:

Terraform configuration
β”‚
β”œβ”€β”€ blocks
β”‚   β”œβ”€β”€ resource
β”‚   β”œβ”€β”€ provider
β”‚   β”œβ”€β”€ variable
β”‚   β”œβ”€β”€ output
β”‚   └── others
β”‚
β”œβ”€β”€ arguments
β”‚   └── name = value
β”‚
└── expressions
    └── values Terraform evaluates

That structure is the foundation for everything that follows.

You have now created a real Terraform configuration, initialized its provider, validated it, planned it, applied it, inspected its output, and destroyed it. The next chapter will slow down that workflow and examine what each Terraform command actually does.

Last updated on