Skip to main content
Terraform · lesson 5 of 17
Terraform beginner Lesson 5 of 17

Providers and Resources (the building blocks of Terraform)

How providers supply resource types, how arguments differ from attributes, how references order an apply, and what a provider version mismatch looks like.

By SkillByExample · Updated

terraform providers resources

Terraform core can’t create anything by itself. Providers are plugins that know an API, and each one supplies resource types. A resource block declares one object of one of those types. These examples use Terraform 1.16.5 with hashicorp/tls 4.4.1, which generates keys locally, and hashicorp/local 2.9.1.

Two providers, two resources, one reference

terraform {
  required_providers {
    tls = {
      source  = "hashicorp/tls"
      version = "~> 4.4"
    }
    local = {
      source  = "hashicorp/local"
      version = "~> 2.9"
    }
  }
}

resource "tls_private_key" "deploy" {
  algorithm = "ED25519"
}

resource "local_file" "public_key" {
  filename        = "${path.module}/deploy.pub"
  content         = tls_private_key.deploy.public_key_openssh
  file_permission = "0644"
}

output "fingerprint" {
  value = tls_private_key.deploy.public_key_fingerprint_sha256
}
$ terraform providers -no-color

Providers required by configuration:
.
├── provider[registry.terraform.io/hashicorp/tls] ~> 4.4
└── provider[registry.terraform.io/hashicorp/local] ~> 2.9

$ terraform apply -auto-approve -no-color

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # local_file.public_key will be created
  + resource "local_file" "public_key" {
      + content              = (known after apply)
      + content_base64sha256 = (known after apply)
      + content_base64sha512 = (known after apply)
      + content_md5          = (known after apply)
      + content_sha1         = (known after apply)
      + content_sha256       = (known after apply)
      + content_sha512       = (known after apply)
      + directory_permission = "0777"
      + file_permission      = "0644"
      + filename             = "./deploy.pub"
      + id                   = (known after apply)
    }

  # tls_private_key.deploy will be created
  + resource "tls_private_key" "deploy" {
      + algorithm                     = "ED25519"
      + ecdsa_curve                   = "P224"
      + id                            = (known after apply)
      + private_key_openssh           = (sensitive value)
      + private_key_pem               = (sensitive value)
      + private_key_pem_pkcs8         = (sensitive value)
      + public_key_fingerprint_md5    = (known after apply)
      + public_key_fingerprint_sha256 = (known after apply)
      + public_key_openssh            = (known after apply)
      + public_key_pem                = (known after apply)
      + rsa_bits                      = 2048
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + fingerprint = (known after apply)
tls_private_key.deploy: Creating...
tls_private_key.deploy: Creation complete after 0s [id=326c983f49d516fe21443965e46a53f376e69b8a]
local_file.public_key: Creating...
local_file.public_key: Creation complete after 0s [id=3c811d52ba68748759f707775a4b1ebe6abc1744]

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

fingerprint = "SHA256:8PniQvkxlD5eGfOfHBNAzdd3evjjMFbCMwB+vfog3cs"
$ cat deploy.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJhQi/hTjnuXUp3aEK2/XBQKnVyTgv53Z7eD2tncT542

The resource type’s prefix picks the provider: tls_private_key comes from the tls provider. The block’s address is type.name, here tls_private_key.deploy. You set one argument, algorithm, but the plan lists eleven attributes. The provider fills in defaults (rsa_bits = 2048 is irrelevant for ED25519 but still present) and computes the key material. The private key fields are marked (sensitive value) by the provider itself. Referring to public_key_openssh is what made Terraform create the key before the file.

A typo is caught by the provider’s schema

Change tls_private_key to tls_privatekey in the resource block only:

$ terraform validate -no-color

Error: Invalid resource type

  on main.tf line 14, in resource "tls_privatekey" "deploy":
  14: resource "tls_privatekey" "deploy" {

The provider hashicorp/tls does not support resource type "tls_privatekey".
Did you mean "tls_private_key"?

Error: Reference to undeclared resource

  on main.tf line 20, in resource "local_file" "public_key":
  20:   content         = tls_private_key.deploy.public_key_openssh

A managed resource "tls_private_key" "deploy" has not been declared in the
root module.

Error: Reference to undeclared resource

  on main.tf line 25, in output "fingerprint":
  25:   value = tls_private_key.deploy.public_key_fingerprint_sha256

A managed resource "tls_private_key" "deploy" has not been declared in the
root module.

The first error is the real one. The provider binary carries a schema listing every resource type and argument, and Terraform uses it to suggest the correct name. The other two errors follow from it: every reference to the correct address now points at nothing. Fix the first error in a list and re-run before chasing the rest.

Changing the version constraint

Edit the tls constraint from ~> 4.4 to ~> 3.0 after applying:

$ terraform plan -no-color

Error: Inconsistent dependency lock file

The following dependency selections recorded in the lock file are
inconsistent with the current configuration:
  - provider registry.terraform.io/hashicorp/tls: locked version selection 4.4.1 doesn't match the updated version constraints "~> 3.0"

To update the locked dependency selections to match a changed configuration,
run:
  terraform init -upgrade
$ terraform init -upgrade -no-color
Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/tls versions matching "~> 3.0"...
- Finding hashicorp/local versions matching "~> 2.9"...
- Installing hashicorp/tls v3.4.0...
- Installed hashicorp/tls v3.4.0 (signed by HashiCorp)
- Using previously-installed hashicorp/local v2.9.1

Terraform has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.
$ terraform plan -no-color

Planning failed. Terraform encountered an error while generating this plan.


Error: Resource instance managed by newer provider version

The current state of tls_private_key.deploy was created by a newer provider
version than is currently selected. Upgrade the tls provider to work with
this state.

There are two separate guards here. The lock file won’t silently switch versions, so changing the constraint requires an explicit init -upgrade. Then the state refuses an older provider, because 4.4.1 wrote the key in a format 3.4.0 may not understand. Upgrades go forward easily, but going back to an older version is blocked. Put the constraint back to ~> 4.4 and run terraform init -upgrade again to recover.

Practice

1. Ask for an attribute the resource doesn’t have: change the output to tls_private_key.deploy.fingerprint.

Solution
$ terraform validate -no-color

Error: Unsupported attribute

  on main.tf line 25, in output "fingerprint":
  25:   value = tls_private_key.deploy.fingerprint

This object has no argument, nested block, or exported attribute named
"fingerprint".

Attribute names come from the same schema as resource types, so a wrong one never reaches the plan. The provider’s documentation page lists every attribute under “Attributes Reference”.

2. Read attributes interactively with terraform console instead of adding outputs.

Solution
$ echo "tls_private_key.deploy.algorithm" | terraform console
"ED25519"
$ echo 'split(" ", local_file.public_key.content)[0]' | terraform console
"ssh-ed25519"

terraform console evaluates any expression against the current state, including function calls. It’s the fastest way to check an expression before writing it into a resource.

3. Switch algorithm to "RSA" and predict how many resources the plan replaces.

Solution
$ terraform plan -no-color | grep -E "forces|Plan:"
        EOT -> (known after apply) # forces replacement
      ~ algorithm                     = "ED25519" -> "RSA" # forces replacement
Plan: 2 to add, 0 to change, 2 to destroy.

Two. A key can’t change algorithm in place, and the file’s content comes from the key, so the file has to be replaced too. With a cloud provider, the same chain would also replace every server that has the old key installed.

Next: locals, which compute a value once and reuse it everywhere.

Frequently Asked Questions

What is a Terraform provider?
A provider is a plugin binary that knows one API, such as AWS, GitHub or your local filesystem, and supplies the resource types and data sources for it. Terraform core does planning and state; the provider does the actual create, read, update and delete calls. terraform init downloads the providers listed in required_providers.
What's the difference between an argument and an attribute?
Arguments are values you set inside the resource block, like algorithm = "ED25519". Attributes are everything the resource exposes for other expressions to read, which includes the arguments plus values the provider computes, like public_key_openssh. Computed attributes show as (known after apply) in a plan until the resource exists.
How does Terraform decide what order to create resources in?
From references. When local_file.public_key's content refers to tls_private_key.deploy.public_key_openssh, Terraform adds an edge to its dependency graph and creates the key first. Resources with no reference between them are created in parallel. depends_on exists for the rare dependency that no expression expresses.
Why pin provider versions with ~>?
~> 4.4 allows 4.4 and any later 4.x but never 5.0, so you get bug fixes without a surprise major version that renames or removes arguments. The exact version chosen is recorded in .terraform.lock.hcl, and changing the constraint later requires terraform init -upgrade.
Can I downgrade a provider after applying?
Not freely. State records the schema version each resource was written with, and an older provider may not understand it. Terraform refuses with 'Resource instance managed by newer provider version', so test provider upgrades on a non-production state first.