Terraform connects four elements:
In the lesson 01 picture: the configuration is the blueprint, the state is the record, the providers are the trades, and the CLI is the architect.
Terraform uses primarily the HashiCorp Configuration Language, or HCL. Code is organized into blocks.
resource "aws_s3_bucket" "logs" {
bucket = var.bucket_name
tags = {
Environment = var.environment
ManagedBy = "Terraform"
}
}In this example:
resource announces an object Terraform must manage;aws_s3_bucket is the type provided by the AWS provider;logs is the local name you choose in this module;bucket and tags are arguments;var.bucket_name and var.environment come from variables.The type and local name together form the address of the resource: aws_s3_bucket.logs. That is the name you will find it under in a plan, in terraform state list and in references from other blocks.
A provider is a plugin that knows the API of a platform and provides resource and data source types. Terraform installs providers during terraform init. Version constraints and the lock file make runs more predictable. Official reference — Provider requirements
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "ca-central-1"
}The version of Terraform CLI and the version of a provider are two separate things. You control them separately: terraform version shows the first (Terraform v1.12.2 on the course machine), .terraform.lock.hcl freezes the second.
A resource generally represents an object Terraform creates or manages.
resource "aws_security_group" "web" {
name = "web"
}A data source reads an object or information that already exists without automatically becoming its owner.
data "aws_vpc" "default" {
default = true
}You can then reference data.aws_vpc.default.id in a resource.
Variables are configurable inputs:
variable "environment" {
type = string
description = "Name of the environment"
validation {
condition = contains(["dev", "test", "prod"], var.environment)
error_message = "Environment must be dev, test or prod."
}
}Locals calculate internal values to avoid repetition:
locals {
prefix = "application-${var.environment}"
}Outputs expose useful results:
output "bucket_id" {
description = "Bucket identifier"
value = aws_s3_bucket.logs.id
}A project with no output block has nothing to display: terraform output then replies Warning: No outputs found. That is the case in Project 01; outputs arrive in Project 02.
Terraform detects a dependency when one resource references another.
resource "aws_s3_object" "log" {
bucket = aws_s3_bucket.logs.id
key = "log.txt"
source = "log.txt"
}The object depends on the bucket. Terraform must therefore create the bucket before sending the file. Resources without dependencies between them can be handled in parallel. Block order in the file does not matter: only the reference counts.
HashiCorp sums up the Terraform workflow in three steps: Write, Plan, Apply. Official reference — Core workflow
Before the first of these steps, there is a unique gesture per folder: terraform init. The outputs below are those from Project 01 of this module, captured with Terraform 1.12.2 on the course machine; you will see them word-for-word when you do it yourself.
terraform initInitializing the backend...
Initializing provider plugins...
- Finding hashicorp/local versions matching "~> 2.5"...
- Installing hashicorp/local v2.9.1...
- Installed hashicorp/local v2.9.1 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. …
Terraform has been successfully initialized!Three things happen: Terraform downloads the provider (Installing hashicorp/local v2.9.1), puts it in the hidden folder .terraform/, and writes .terraform.lock.hcl to retain the version chosen. The sentence to expect: Terraform has been successfully initialized!. Without init, any other command stops: Error: Inconsistent dependency lock file for plan, Error: Missing required provider for validate, and both tell you what to do (run: terraform init).
You write or modify .tf files, then usually run:
terraform fmt
terraform validateterraform fmt realigns spaces and indentation; it displays the names of files it modified, and nothing if everything was already clean. terraform validate checks syntax and block consistency without touching anything, and replies:
Success! The configuration is valid.It catches typos in argument names (An argument named "contenu" is not expected here. Did you mean "content"?) and forgotten braces (Error: Unclosed configuration block). It does not check that the result would be good architecture.
Terraform compares the desired state to what it knows of the infrastructure:
terraform planTerraform 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.message will be created
+ resource "local_file" "message" {
+ content = "Hello, this file was created with Terraform."
+ content_md5 = (known after apply)
+ filename = "./message.txt"
+ id = (known after apply)
…
}
Plan: 1 to add, 0 to change, 0 to destroy.The plan answers three questions, and the last line sums them up in three numbers:
to add)to change)to destroy)Each attribute line bears the symbol of the action. (known after apply) signals a value Terraform cannot know before creating the object: here the file's ID and content checksums.
After review:
terraform applyTerraform redisplays the plan then asks for your approval:
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
local_file.message: Creating...
local_file.message: Creation complete after 0s [id=ff6b93dc92e7e1d2ba4f9dad3cc16e03ac649b45]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.Only yes in full is accepted; y, Y or an empty line give Apply cancelled. and nothing is touched. Terraform then calls the providers in the order dictated by dependencies, and updates the state. The last line repeats the three numbers from the plan: Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
If you rerun terraform plan without changing anything, the estimate is empty:
No changes. Your infrastructure matches the configuration.That is the idempotence from lesson 01, seen in the terminal.
terraform destroydestroy is an apply whose plan contains only -. The question asked is more insistent (Do you really want to destroy all resources? … There is no undo.), the expected answer is still yes, and the last line is:
Destroy complete! Resources: 1 destroyed.After a destroy, terraform state list displays nothing more. In this course, each project ends thus.
| Symbol | Action | Sentence in the plan | What it means for you |
|---|---|---|---|
+ | create | will be created | A new object; nothing existing is touched. |
~ | modify in place | will be updated in-place | The object remains, an attribute changes. |
-/+ | replace | must be replaced | The object is destroyed then recreated; its content and ID vanish. |
- | destroy | will be destroyed | The object vanishes. |
Terraform figures out how to reach the desired state, but the exact behavior depends on each provider. Some arguments can be modified in place; others force resource replacement. The local provider of Project 01 gives a clear example: changing the content line of a local_file does not produce a ~, but a replacement -/+. Here is the real plan, captured after the text change:
# local_file.message must be replaced
-/+ resource "local_file" "message" {
~ content = "Hello, this file was created with Terraform." -> "Second version of the file created with Terraform." # forces replacement
~ content_md5 = "6daf774f0eb6d3da439c871afec7cf90" -> (known after apply)
~ id = "ff6b93dc92e7e1d2ba4f9dad3cc16e03ac649b45" -> (known after apply)
# (3 unchanged attributes hidden)
}
Plan: 1 to add, 0 to change, 1 to destroy.Read the three clues: must be replaced in the title, # forces replacement at the end of the content line, and 1 to add, 0 to change, 1 to destroy in the summary. For a text file, the nuance is inconsequential. For a database, -/+ means loss of data: that is why the plan must be read, even when the code change looks small.
The key difference between
~and-/+. The~beforecontentsays the value changes. The-/+beforeresourcesays how Terraform will do it: destroy, then recreate. When a~line bears# forces replacement, it is this line that triggered the-/+of the whole block.
resource "type" "name" block has an address, type.name, that you find in the plan and in state.init once per folder (Terraform has been successfully initialized!), then fmt, validate (Success! The configuration is valid.), plan (Plan: 1 to add, 0 to change, 0 to destroy.), apply with yes (Apply complete! Resources: 1 added, 0 changed, 0 destroyed.), and destroy at end of session (Destroy complete! Resources: 1 destroyed.).+ creates, ~ modifies in place, -/+ destroys then recreates, - destroys. The summary Plan: N to add, N to change, N to destroy. counts them.~ or a -/+; the local_file of Project 01 is replaced as soon as content changes.terraform init be executed, and what files does it create?content, which three clues say it is a replacement and not an in-place modification?y instead of yes?