Providers, Resources, and Data Sources in Depth
Providers, Resources, and Data Sources in Depth
A provider is a plugin with credentials. A resource is one object that plugin owns on your behalf. A data source is a question. This chapter is how those three show up for AWS, Azure, and GCP, and how you stop a floating provider version from rewriting the plan.
Learning goals
- Configure a provider without putting a long-lived key in the configuration
- Read a resource schema as required, optional, computed, or deprecated
- Write a small sketch for each of the three large clouds
- Pin provider versions and commit the lock file
How Providers Work: Plugins, Authentication, and Configuration Blocks
Init downloads a binary and records a checksum in .terraform.lock.hcl. Plan and apply start that binary and speak the plugin protocol. The provider configuration is the settings every resource of that type inherits:
terraform {
required_version = "~> 1.13.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "eu-west-1"
}
Leave the access key out of the block. The AWS provider uses the standard credential chain: environment variables, a shared config file, or a role from the instance or the pipeline. Azure uses a service principal or workload identity. GCP uses application-default credentials or a workload identity pool. Chapter 12 lists those patterns as the security rule. The configuration above is the whole provider block when the environment already has credentials.
tofu providers shows what is required and what was installed. tofu providers lock updates the lock file for the platforms you name, so a developer on ARM64 and a pipeline on AMD64 do not fight over checksums. Lock the platforms you actually run.
Resource Arguments: Required, Optional, Computed, and Deprecated
The provider schema, not this book, decides which arguments exist. The categories are stable:
| Kind | In configuration | In state after apply |
|---|---|---|
| Required | You must set it | Stored |
| Optional | You may set it. Null usually means “leave the provider default” | Stored if set |
| Computed | You cannot set it, or you can set a related optional and the provider may override | Filled in by the provider |
| Optional and computed | If you set it, it is yours. If you do not, the provider chooses and stores the choice | The next plan may want to adopt the provider’s value |
Computed-only attributes (an instance id, a bucket domain name) are outputs of the create call. Referencing them in the same plan is fine. Using them as a for_each key is the unknown-value problem from chapter 4.
Deprecated arguments still work until a provider major version removes them. A plan that warns is a plan you schedule a follow-up for. Ignoring deprecations is how a provider bump becomes a rewrite. Pin the provider, read the changelog at the bump, and do not combine a provider major bump with an OpenTofu minor bump in the same change.
Creating Resources Across Clouds: AWS, Azure, GCP Examples
These are sketches of the same idea: one named object, tags or labels from a variable, no embedded secrets. They will not apply until the matching provider is configured and the account exists. The pet project in chapter 3 is the one that applies as written.
resource "aws_s3_bucket" "state" {
bucket = "example-tofu-state-203-0-113"
}
resource "azurerm_resource_group" "edge" {
name = "example-tofu-rg"
location = "westeurope"
}
resource "google_storage_bucket" "state" {
name = "example-tofu-state-203-0-113"
location = "EU"
}
The shape to copy is not the resource type. It is the discipline around it. The name is explicit, not generated from an unknown. The region is an input. Tags that mark the owning team and the state key are how a human in the console knows the object is managed. Deleting the block later deletes the object. If the object must survive, it does not belong in this configuration, or it belongs behind prevent_destroy with a written reason.
On-premises follows the same shape with a different provider: a hypervisor, a DNS server, a load balancer, an IPAM. If the provider is a community plugin, pin it harder and read who publishes it. The registry namespace is the publisher’s identity.
Data Sources: Querying Existing Infrastructure Safely
A data source is for an object you do not want this state to own.
data "aws_caller_identity" "current" {}
output "account_id" {
value = data.aws_caller_identity.current.account_id
}
Use a data source when another team owns the object and you need its id. Do not use a data source to look up an object this same state created. Use that object’s attribute. A data source that searches by a moving filter (“the most recent image”) changes plans under you. Pin the identifier when the filter is only a convenience.
A data source that fails because the object is missing fails the plan. That is the right failure when the dependency is real. It is the wrong tool for “create this if it does not exist.” Existence-as-create is a resource.
depends_on on a data source is occasionally required when the object is created in the same configuration and the data source would otherwise read too early. If you need that, you are usually one refactor away from passing the attribute directly.
Provider Aliases: Managing Multiple Configurations Simultaneously
One default provider configuration is the unnamed block. An alias is a second configuration of the same provider, almost always for a second region or a second account.
provider "aws" {
region = "eu-west-1"
}
provider "aws" {
alias = "backup"
region = "eu-central-1"
}
resource "aws_s3_bucket" "backup" {
provider = aws.backup
bucket = "example-tofu-backup-203-0-113"
}
Resources use the default unless provider says otherwise. Modules do not inherit aliases. You pass them:
module "backup" {
source = "./modules/bucket"
providers = {
aws = aws.backup
}
}
The child declares configuration_aliases only when it must. A module that quietly creates objects in a region the caller did not pass is a bug. OpenTofu 1.9’s provider for_each is how you build one module instance per region from a map without copying the alias block by hand. The map keys must be static.
Version Constraints: Pinning Providers for Stability
The constraint in required_providers is a range. The lock file is the selected build.
| Constraint | Meaning |
|---|---|
= 5.70.0 |
This build only. Heavy for a library module, right for a root you do not want surprised |
~> 5.70 |
Any 5.70.z. A reasonable root-module pin |
~> 5.0 |
Any 5.x. Fine for a reusable module that promises compatibility. Loose for a root |
>= 5.0.0 |
Anything newer, including 6.0.0, which may break. Avoid in roots |
Commit .terraform.lock.hcl. A pipeline that runs tofu init -upgrade on every apply will take provider upgrades you did not review. Init without -upgrade uses the lock. Upgrade in a commit whose diff is the lock file and a plan you read.
Provider major versions are allowed to change arguments and to force resource replacement. Schedule them. The OpenTofu pin and the provider pin move in separate commits so a bad plan has one cause.
Summary
- Providers are plugins. Credentials come from the environment, not from the
.tffile - Required, optional, and computed describe who is allowed to set an argument
- AWS, Azure, and GCP sketches share a shape: explicit name, no embedded secret, owned by this state
- Data sources read objects this state does not own
- Aliases are extra configurations you pass into modules on purpose
- The lock file is the pin. The constraint is only the allowed range