Table of Contents
Understanding Providers and Resources in Terraform
In Infrastructure as Code with Terraform, providers and resources are the two core building blocks you work with every day. This chapter focuses on what they are, how they relate to each other, and how to use them correctly in real Terraform configurations.
The Role of Providers
A provider is a Terraform plugin that knows how to talk to an external system. That system might be a cloud platform, a virtualization platform, a SaaS service, or even a local tool. The provider translates Terraform configuration into real API calls and then returns information back to Terraform.
When you write Terraform code to create a virtual machine on a cloud platform, Terraform itself does not know how to talk to that platform. Instead, it loads a provider for that platform, and the provider handles the details of authentication, API endpoints, and the sequence of operations.
Terraform makes you declare providers explicitly. This happens in a terraform block and in one or more provider blocks. For example, you might declare that you are using the aws provider at a certain version, then configure it with a region.
Terraform keeps providers separate from core features so that the same Terraform tool can manage many kinds of infrastructure simply by adding or updating provider plugins.
Configuring Providers
Provider configuration tells Terraform how to connect to the external system. Common settings include region, endpoint URLs, profiles, or organization identifiers. Credentials are often provided indirectly, for example through environment variables or external files, although they can also be specified directly.
A minimal provider declaration usually has two parts. First, you declare the requirement so Terraform knows which plugin and which version it must install. Second, you configure how that provider instance should behave.
A basic pattern looks like this:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
The required_providers block tells Terraform which provider plugin to download and what version constraints apply. The provider "aws" block creates a configured instance of that provider. When you run terraform init, Terraform will download and install that provider and store it in the working directory. Later commands such as terraform plan and terraform apply will then use that provider.
Most cloud providers support a mixture of configuration methods. For example, a provider may read credentials from environment variables such as AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, from a credentials file, or from direct arguments in the provider block. It is common practice to keep secrets out of configuration files and rely on external mechanisms instead.
Provider configuration should avoid hard coding secrets such as passwords or long‑lived access keys. Prefer environment variables, short‑lived tokens, or secret management systems to reduce risk.
When multiple providers are used in a single Terraform project, each provider must either have its own provider block or rely on default settings. Terraform will automatically infer which provider a resource needs based on its type, for example aws_instance uses the aws provider, azurerm_resource_group uses the azurerm provider, and so on.
Provider Versions and Upgrades
Provider versions are important because changes in provider behavior can affect your infrastructure. Terraform lets you pin versions or specify version ranges. A common pattern is to use a pessimistic constraint that allows compatible updates while blocking major breaking changes.
For example:
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.7"
}
}
}
This allows updates that stay within the same major version, for example from 5.7.0 to 5.9.3, but prevents upgrades to 6.0.0 without an explicit change in the configuration. This gives you control over when you adopt potentially breaking changes.
When you change provider version constraints and run terraform init -upgrade, Terraform will fetch newer versions that satisfy the new constraint. After that, it is good practice to run terraform plan and review any changes caused by provider behavior updates, especially default values or deprecated fields.
Provider Aliases and Multiple Configurations
Sometimes a single project needs multiple configurations of the same provider. For example, you might create resources in two different regions or two different accounts. In this case you can use provider aliases to create more than one provider instance.
The alias argument in a provider block names an additional instance. Later, individual resources can reference the specific provider they need using the provider meta‑argument.
A simple illustration looks like this:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
provider "aws" {
alias = "eu"
region = "eu-central-1"
}
resource "aws_s3_bucket" "us_logs" {
bucket = "my-us-logs-bucket"
}
resource "aws_s3_bucket" "eu_logs" {
provider = aws.eu
bucket = "my-eu-logs-bucket"
}
Here Terraform uses the default aws provider for aws_s3_bucket.us_logs and the aliased provider aws.eu for aws_s3_bucket.eu_logs. Each provider instance manages resources against the region or account it is configured for.
Aliased providers are also common in modules. A module can declare that it expects certain providers and the root module can pass in aliased providers so that the same module code can be used with different configurations.
What Resources Represent
A resource is a block in Terraform that describes one object that Terraform manages. That object could be a virtual machine, a network, a database instance, a DNS record, or any other manageable item that the provider exposes.
Each resource has a type and a local name. The type comes from the provider, such as aws_instance, azurerm_virtual_network, or google_compute_network. The local name identifies a specific resource of that type within your configuration. Taken together, the type and name form a resource address, for example aws_instance.web.
Inside a resource block, arguments describe the desired state for that object. When you run terraform apply, Terraform compares the desired state with the current remote state, then creates, updates, or destroys the object to match what you have declared.
A minimal resource example might look like this:
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t3.micro"
}
Here, Terraform will ask the aws provider to create or maintain an EC2 instance with the given AMI and instance type. The provider knows which API calls to use. Terraform tracks the result in its state so that future runs can detect drift or identify necessary changes.
Every resource block describes the desired state, not specific create or delete commands. Terraform decides whether to create, change, or destroy based on the difference between the configuration and the current state.
Resources are always tied to a single provider. The prefix of the resource type, such as aws_, azurerm_, or google_, determines which provider must be installed and configured. If Terraform sees a resource type whose provider is not declared, it will prompt you to add the corresponding provider requirement.
Resource Arguments and Attributes
Resource blocks contain arguments that set properties of the managed object. Some arguments are required, others are optional with defaults, and some are computed by the provider after creation.
In the configuration, you specify input arguments such as names, sizes, and configuration options. After terraform apply, Terraform records output attributes, such as IDs, IP addresses, and computed values that come from the provider.
You can reference attributes from one resource in another using the resource address and attribute name. For example:
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = true
}
Here, the aws_subnet.public resource uses aws_vpc.main.id. The provider sets the id attribute when the VPC is created. Terraform uses this reference to build a dependency graph so that it creates the VPC before the subnet and passes the correct identifier to the provider.
Some attributes are marked as computed, which means they are not known until after apply. In a plan, Terraform will show them as known after apply. These values can then feed into other resources or outputs.
Provider Data Sources vs Resources
Providers often expose two kinds of blocks in Terraform, resources and data sources. Both come from the provider, but they have different purposes.
A resource manages the lifecycle of an object. Terraform can create, change, and destroy it. A data source lets Terraform read information about objects that exist outside its control, such as an existing image, an existing network, or a remote configuration value.
Although this chapter focuses on resources, it is common to use both side by side. For example, you might use a data source to look up the ID of a standard image, then pass that ID into a resource that creates a virtual machine. The provider uses different blocks for these roles, for example data "aws_ami" "ubuntu" for reading an existing AMI and resource "aws_instance" "web" for creating an instance.
Both resources and data sources rely on the same provider configuration. They use the same authentication and endpoint settings, but Terraform treats them differently when planning changes. Data sources are read only and do not appear as created or destroyed in your plan.
Resource Dependencies and Ordering
In practical use, resources often depend on one another. A subnet depends on a network, a database instance depends on a subnet, and so on. Terraform infers these dependencies from references between resource attributes. Any time you use an expression like aws_vpc.main.id inside another resource, Terraform establishes a dependency.
This implicit dependency graph determines the order in which Terraform creates and destroys resources. Terraform will create all prerequisites first, then the dependent resources. When destroying infrastructure, it reverses the order, so that dependent resources are removed before the ones they rely on.
In more complex situations, you may also use the depends_on meta‑argument to declare an explicit dependency when no direct attribute reference exists. Because this changes how Terraform plans and applies changes, you should only use depends_on when attribute references are not possible or when a provider's API requires a very specific order of operations that Terraform would not otherwise infer.
Meta‑arguments for Resources
Resources support several built‑in meta‑arguments that change how Terraform manages them. These meta‑arguments are handled by Terraform itself, not by the provider. Several of the most common are:
depends_on allows an explicit list of dependencies.
count allows you to create multiple instances of a resource with the same configuration pattern, indexed by a numeric counter.
for_each lets you create multiple resources based on a map or set of strings, with each instance keyed by an element of the collection.
provider allows a resource to select a specific provider instance, including aliased providers.
For example, for_each can create one resource per key in a map:
variable "subnets" {
type = map(string)
default = {
public = "10.0.1.0/24"
private = "10.0.2.0/24"
}
}
resource "aws_subnet" "this" {
for_each = var.subnets
vpc_id = aws_vpc.main.id
cidr_block = each.value
tags = {
Name = each.key
}
}
Terraform will create two subnet resources, aws_subnet.this["public"] and aws_subnet.this["private"], each with its own CIDR block and tag. The provider still sees individual resources, but Terraform uses the meta‑arguments to derive them from a single block in configuration.
Custom and Third‑Party Providers
Although many providers are maintained by HashiCorp or major cloud vendors, Terraform also supports third‑party and community providers. These providers are distributed through registries and are selected by their source, which includes a namespace, a name, and sometimes a hostname.
A third‑party provider might look like this:
terraform {
required_providers {
mycloud = {
source = "myorg/mycloud"
version = "~> 1.2"
}
}
}
provider "mycloud" {
api_endpoint = "https://api.mycloud.example.com"
}
Behind the scenes, this is a separate plugin binary that implements the provider protocol. From your perspective as a user, it behaves like any other provider. It offers resource types such as mycloud_server or mycloud_network that you can manage through Terraform.
Because third‑party providers may have different levels of support and testing, version pinning and careful review of documentation and release notes become even more important.
Provider and Resource Lifecycle in Practice
In a typical workflow, providers and resources follow a consistent lifecycle. At the start, you declare provider requirements in your Terraform configuration. You then run terraform init to install or update providers. After that, you write resource blocks that describe the desired state of your infrastructure.
When you run terraform plan, Terraform loads the providers, queries their information about existing resources, and computes the changes needed. The providers handle read operations, such as fetching the current state of each managed object.
When you run terraform apply, Terraform calls the provider to create, update, or delete resources in the order defined by the dependency graph. As it does this, it updates the state file with the latest attributes so that future plans are accurate.
Over time, you may update provider versions, add new providers, or change provider configuration. You may also add, modify, or remove resources. Throughout these changes, Terraform coordinates with the providers to keep the actual infrastructure aligned with the configuration you have declared.
Understanding how providers and resources interact is the key to using Terraform effectively. Providers give Terraform reach into many systems, and resources let you describe those systems in a consistent and reusable way.