Terraform Blocks: Syntax, Types & Examples

Blocks are the core building units of every configuration file, and everything from a simple virtual machine to a complex multicloud deployment is defined through them.

Mariusz Michalowski

In this article, we'll cover the Terraform block syntax, walk through the most common block types, and provide some practical examples.

What is a Terraform block?

A Terraform block is the fundamental building unit of every Terraform configuration. Each block is a container that defines a specific piece of infrastructure, a behavior, or a setting. Blocks use a consistent syntax based on the HashiCorp Configuration Language (HCL), and they are what allow Terraform to understand what you want to create, manage, or configure in your environment.

Every Terraform project is essentially a collection of blocks working together. Some blocks describe cloud resources, others pull in external data, and others control how Terraform itself behaves. Understanding what each block does and how to write it correctly is one of the first steps toward becoming productive with infrastructure as code.

Terraform block syntax

The general syntax of a Terraform block follows this pattern:

block_type "label_one" "label_two" { argument_name = "argument_value" nested_block { nested_argument = "nested_value" } }
  • The block type tells Terraform what kind of object you are defining.
  • The labels that follow vary depending on the block type.

Some blocks require two labels (like resource), some require one (like provider), and some require none (like terraform). Inside the curly braces, you place arguments and, optionally, nested blocks that further configure the object.

Types of blocks in Terraform

Terraform configurations can contain several types of blocks. Here is an overview of the most common ones you will encounter.

  • terraform — This block configures settings for Terraform itself, such as the required version of Terraform and the providers your configuration depends on. It does not create any infrastructure on its own.
  • provider — This block configures a specific provider, which is a plugin that Terraform uses to interact with a cloud platform or service like AWS, Azure, or Google Cloud.
  • resource — This is the most common block type. It declares a single infrastructure object that Terraform should manage, such as a virtual machine, a database, or a DNS record.
  • data — A data block lets you fetch information from an existing resource or external source without creating or modifying anything. It is read-only.
  • variable — This block defines an input variable that makes your configuration flexible and reusable by accepting values from the user or from other modules.
  • output — An output block exposes a value from your configuration so it can be used by other modules or displayed to the user after a terraform apply.
  • module — This block lets you call a reusable group of Terraform configurations, either from a local directory or from a remote registry.
  • locals — This block defines local values, which are named expressions you can reference throughout your configuration to avoid repetition.
  • import — This block lets you bring an existing infrastructure object under Terraform management without recreating it. It was introduced in Terraform 1.5.0 and is now the recommended way to import resources.

Example 1: Locking down provider versions with the Terraform block

One of the first things you should do in any Terraform project is define which version of Terraform and which provider versions your configuration supports. This is done inside the terraform block, and it helps prevent unexpected behavior caused by automatic provider upgrades.

terraform { required_version = ">= 1.5.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } backend "s3" { bucket = "my-terraform-state" key = "prod/terraform.tfstate" region = "eu-west-1" } }

The required_version argument ensures everyone on the team is running Terraform 1.5.0 or higher. The required_providers block pins the AWS provider to the 5.x range, preventing unexpected breaking changes from a major version jump.

The backend block stores the state file in an S3 bucket instead of on your local machine. This is a best practice for teams because it allows multiple people to collaborate without overwriting each other's changes.

Example 2: Creating a virtual network on Azure with a resource block

After setting up the foundational configuration, the next step is to start defining the actual infrastructure. The resource block is where you describe the cloud objects Terraform should create and manage.

In this example, we define an Azure resource group along with a virtual network inside it.

provider "azurerm" { features {} } resource "azurerm_resource_group" "main" { name = "production-rg" location = "West Europe" } resource "azurerm_virtual_network" "main" { name = "production-vnet" address_space = ["10.0.0.0/16"] location = azurerm_resource_group.main.location resource_group_name = azurerm_resource_group.main.name tags = { environment = "production" managed_by = "terraform" } }

The first resource block creates an Azure resource group in West Europe, which acts as a logical container for related resources.

The second block creates a virtual network inside that group. The location and resource_group_name arguments reference the first resource directly using azurerm_resource_group.main.location. This creates an implicit dependency, meaning Terraform knows to create the resource group first without you specifying the order manually.

The tags argument adds metadata that makes it easier to find and organize resources in the Azure portal or through cost management tools.

Example 3: Looking up an existing AMI with a data block and using variables

Not everything in a Terraform configuration needs to be created from scratch. Sometimes you need to reference something that already exists in your environment, such as a machine image.

The data block is designed for this exact purpose, and when combined with variable and output blocks, it creates a flexible and reusable configuration.

variable "instance_type" { description = "The EC2 instance size to use" type = string default = "t3.micro" } data "aws_ami" "ubuntu" { most_recent = true owners = ["099720109477"] filter { name = "name" values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] } } resource "aws_instance" "web" { ami = data.aws_ami.ubuntu.id instance_type = var.instance_type tags = { Name = "web-server" } } output "instance_public_ip" { description = "The public IP address of the web server" value = aws_instance.web.public_ip }

The variable block defines an input called instance_type with a default of t3.micro. You can override it at runtime through the command line or a .tfvars file without editing the configuration.

The data block queries AWS for the most recent Ubuntu 22.04 AMI owned by Canonical. Instead of hardcoding an AMI ID that could become outdated, this always retrieves the latest image. The filter nested block narrows the search to a specific naming pattern.

The resource block brings these together, using the AMI from the data source and the size from the variable to launch an EC2 instance. The output block then exposes the public IP so you can grab it right after deployment without logging into the console.

Key points

Terraform blocks are the foundation of every infrastructure-as-code configuration. Once you understand how each block type works and how they connect to one another, writing and reading Terraform code becomes significantly easier. Start with a solid terraform block, build out your resources, and use variables and outputs to keep everything flexible.

Terraform is powerful, but to achieve an end-to-end secure GitOps approach, you need a product that can run your Terraform workflows. Spacelift takes managing Terraform to the next level by giving you access to a powerful CI/CD workflow and unlocking features such as:

  • Policies (based on Open Policy Agent)
  • Multi-IaC workflows
  • Self-service infrastructure
  • Integrations with any third-party tools

If you want to learn more about Spacelift, create a free account today or book a demo with one of our engineers.

Note: HashiCorp changed Terraform’s license for newer releases, while earlier versions remain under their previous open-source license. OpenTofu is an open-source fork based on Terraform 1.5.6 and is a viable alternative for teams that want a community-governed option.

Frequently Asked Questions

  • What is the difference between a resource block and a data block in Terraform?

    A resource block creates and manages a new infrastructure object, whereas a data block reads information from something that already exists. Use a data block when you need to reference an external resource without letting Terraform control its lifecycle.

  • Can you use multiple provider blocks in one Terraform configuration?

    Yes. You can define multiple provider blocks to manage resources across different regions or even different cloud platforms within the same configuration. Each resource block can then specify which provider it should use through the provider argument.

  • What happens if you skip the terraform block in your configuration?

    Terraform will still run, but it will use whatever provider and Terraform versions are available on your machine. This can lead to inconsistent behavior across team members or CI pipelines, which is why explicitly defining the terraform block is considered a best practice.

Manage Terraform better and faster

If you are struggling with Terraform automation and management, check out Spacelift. It helps you manage Terraform state, build more complex workflows, and adds several must-have capabilities for end-to-end infrastructure management.

Learn more