Infrastructure as Code with OpenTofu (Terraform-Compatible) and Ansible
Exit

Lesson 1 of 10 · Fundamentals

Infrastructure as code: why, and with which tools

30 min Debian 12

Objective

Understand what "describing your infrastructure as code" means, what it changes in a team's daily work, and where the main tools fit: OpenTofu and Terraform, Ansible, Pulumi, CloudFormation. Then you will write your very first OpenTofu configuration.

Key points

  • Infrastructure as code (IaC) means describing networks, servers, firewalls, databases or DNS records in text files, versioned in Git, and then letting a tool create and maintain them.
  • A declarative approach describes the desired state ("three web servers"); the tool works out the actions itself. An imperative approach describes the steps ("create a server, then a second one…").
  • We distinguish provisioning (creating resources: OpenTofu, Terraform, CloudFormation, Pulumi) from configuration management (installing and tuning software inside machines: Ansible, Puppet, Chef).
  • OpenTofu is an open source fork of Terraform, created in 2023 after Terraform's license change; it keeps its language (HCL) and almost all of its commands.
  • A declarative tool is idempotent: run it ten times, and it only makes changes when there is a gap between the code and reality.

The problem: hand-made infrastructure

Imagine a web agency in Casablanca that hosts the websites of twenty clients. For five years, every server has been created by hand in the provider's console: one click for the machine, another for the firewall, a rule added "temporarily" one night during an outage.

The result:

  • nobody knows exactly how the main client's server is configured; the administrator who built it has left;
  • creating a test environment identical to production takes two days, and it is never truly identical;
  • after an incident (accidental deletion, hacking), rebuilding takes a week;
  • changes are not reviewed: a firewall rule open to the whole Internet goes unnoticed.

These are called "snowflake servers": each one is unique and impossible to reproduce.

The solution: describe everything in files

# reseau.tf — an extract of what you will write in this course
resource "aws_security_group" "web" {
  name = "web-client-atlas"

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

What you gain:

Before (by hand) After (IaC)
Documentation is wrong or missing The code is the documentation
A change = an invisible click A change = a commit reviewed by a colleague
Test does not look like production The same code creates test and production
Rebuilding takes days tofu apply recreates everything in minutes
"Who opened this port?" git log answers, with the date and the reason

IaC applies software development practices to infrastructure: version control, code review, testing, continuous integration.

Declarative or imperative

Let's compare two ways of asking for three web servers.

Imperative approach, a script:

# creer-serveurs.sh — each run CREATES three more servers
for i in 1 2 3; do
  fournisseur-cli server create --name "web-$i" --type petit --image debian-12
done

If you run this script twice, you have six servers. If it fails on the second one, you have to clean up by hand. To go from three to five servers, you have to write another script.

Declarative approach, OpenTofu:

resource "fournisseur_server" "web" {
  count = 3
  name  = "web-${count.index + 1}"
  type  = "petit"
  image = "debian-12"
}

You describe the result. On the first run, OpenTofu creates three servers. On the second, it does nothing: reality already matches the code. If you write count = 5, it creates two. If you write count = 2, it deletes one. The tool calculates the difference.

To do this, OpenTofu keeps a state: the list of what it has created, with the real identifiers at the provider. You will study it in detail in lesson 2.1.

The tool landscape

Tool Category Language Targets License
OpenTofu declarative provisioning HCL any provider with an API (AWS, Azure, GCP, OVHcloud, Scaleway, Proxmox, Kubernetes, DNS…) MPL 2.0 (open source), Linux Foundation
Terraform declarative provisioning HCL the same BUSL 1.1 since version 1.6 (2023)
Ansible configuration management, orchestration YAML (playbooks) systems (Linux, Windows, network), over SSH, agentless GPL v3
Pulumi declarative provisioning Python, TypeScript, Go, C#… any provider Apache 2.0 (engine)
CloudFormation declarative provisioning YAML or JSON AWS only AWS service
Bicep / ARM declarative provisioning Bicep, JSON Azure only Microsoft service
Puppet, Chef configuration management their own DSLs systems, with an agent various
cloud-init first-boot configuration YAML a new virtual machine Apache 2.0 / GPL v3

A few guidelines for choosing:

  • Several providers, or a local provider (OVHcloud, a Moroccan hosting company, Proxmox): OpenTofu or Terraform, thanks to their thousands of providers.
  • Everything on AWS and a team that wants to stay in that ecosystem: CloudFormation is possible, but you will be locked in to AWS.
  • A team of developers who prefer a real programming language (loops, unit tests): Pulumi.
  • Configuring what is inside the machines (packages, files, services): Ansible. It is very often combined with OpenTofu (lesson 3.1): OpenTofu creates the machines, Ansible configures them.

OpenTofu is not a configuration management tool: it creates and deletes resources, but it does not install nginx inside a machine (it can only pass a startup script, cloud-init for example).

OpenTofu and Terraform: a little history

  • 2014: HashiCorp releases Terraform under the open source MPL 2.0 license. It becomes the reference tool for multi-cloud IaC.
  • August 2023: HashiCorp moves its products, including Terraform from version 1.6, to the Business Source License (BUSL 1.1). The code remains readable, but some uses that compete with HashiCorp are no longer allowed. Companies and contributors then start a fork of the MPL code, first called OpenTF.
  • September 2023: the project joins the Linux Foundation under the name OpenTofu.
  • January 2024: release of OpenTofu 1.6, the first stable version, compatible with Terraform 1.5/1.6.
  • Since then: the two projects evolve separately. OpenTofu adds its own features (state encryption, enabled in lifecycle, .tofu files…), Terraform adds its own. HashiCorp was also acquired by IBM (deal completed in 2025).

In practice, for this course:

OpenTofu Terraform
Command tofu init, tofu plan, tofu apply terraform init, terraform plan, terraform apply
Language HCL, .tf files (and .tofu) HCL, .tf files
Settings block terraform { … } (same name, for compatibility) terraform { … }
Public registry registry.opentofu.org registry.terraform.io
Providers the same (hashicorp/aws, hashicorp/local…) the same

What you learn here therefore also applies to an existing Terraform project. Moving a recent Terraform project to OpenTofu usually means backing up the state, installing tofu, then running tofu init and a tofu plan that must show no changes.

The OpenTofu workflow

   write (*.tf)   ──▶  tofu init  ──▶  tofu plan  ──▶  review  ──▶  tofu apply
        ▲              downloads       calculates the   (you,       applies the
        │              the providers   differences      a colleague) changes
        └──────────────── git commit, push, merge request ◀───────────────┘
  1. Write: you describe the resources in .tf files.
  2. Plan: tofu plan compares the code, the state and reality, then shows what would be created (+), modified (~) or destroyed (-).
  3. Apply: tofu apply executes the plan, after your confirmation.

The next lesson covers these commands in detail.

Your first file

The terraform_data resource type is provided by OpenTofu itself, with no download at all: it creates nothing at a provider, it simply keeps a value in the state. It is perfect for learning.

# /root/iac/premier/main.tf
resource "terraform_data" "bonjour" {
  input = "Bonjour depuis Rabat"
}
# /root/iac/premier/outputs.tf
output "message" {
  value = terraform_data.bonjour.output
}
cd /root/iac/premier
tofu init          # no provider to download here
tofu apply         # answer "yes"
# Outputs:
# message = "Bonjour depuis Rabat"

And what does not go into Git:

# .gitignore (in this file, a comment takes up the whole line)
# downloaded providers, recreated by tofu init
.terraform/
# the state can contain secrets: it goes into a remote backend
*.tfstate
# its backups (terraform.tfstate.backup)
*.tfstate.*
crash.log
# saved plans
*.tfplan

The .terraform.lock.hcl file, on the other hand, must be committed: it pins the exact provider versions for the whole team.

Common pitfalls

  • Mixing clicks and code: a change made by hand in the console will be reverted (or cause an error) on the next tofu apply. Once you use IaC, everything goes through the code.
  • Versioning the state (terraform.tfstate) in Git: it can contain passwords in clear text, and two people overwrite each other's changes.
  • Believing that OpenTofu configures the software inside machines: for that, use Ansible or cloud-init.
  • Going ahead without a plan: an apply without reading the plan can destroy a database because an argument changed.

Lab

The lab container includes OpenTofu but has no network access: you write files, and the grader reads them back. The terraform_data resource requires no download: you can even try tofu init then tofu apply -auto-approve in the folder to see the output.

  1. a first terraform_data resource;
  2. an output that displays its value;
  3. the terraform block that requires a minimum version, with well-formatted files (tofu fmt);
  4. the .gitignore of an OpenTofu project.

To write a file without an editor: cat > main.tf <<'EOF' … EOF.

Quiz

  1. What is the difference between a declarative approach and an imperative approach?
  2. You change count = 3 to count = 5 and run OpenTofu again. What happens?
  3. OpenTofu or Ansible: which one creates the virtual machine, and which one installs nginx in it?
  4. Why does OpenTofu exist, and what does it keep from Terraform?
  5. Should you commit terraform.tfstate to Git? And .terraform.lock.hcl?
Answers
  1. Declarative: you describe the desired final state and the tool works out the actions. Imperative: you write the sequence of actions to run.
  2. OpenTofu compares the code with the state, sees that two instances are missing and creates two, without touching the three existing ones.
  3. OpenTofu creates the machine (provisioning); Ansible installs and configures nginx in it (configuration management).
  4. It was created in 2023, under the Linux Foundation, after Terraform moved to the BUSL license; it keeps the HCL language, the commands (with tofu instead of terraform) and the providers.
  5. No for the state: it can contain secrets and must be shared through a remote backend with locking. Yes for .terraform.lock.hcl, which pins the provider versions for the whole team.

Reference sources: OpenTofu documentation (MPL 2.0). Original MasterLinux text.

Practical exercises

To do on Debian 12

The online terminal is coming soon. Meanwhile, do these tasks on your own Linux machine, a virtual machine or WSL.

  1. Write /root/iac/premier/main.tf: a resource of type terraform_data named bonjour, whose input argument is "Bonjour depuis Rabat"
    Show a hintresource "<type>" "<name>" { argument = value } — on three lines, with the argument indented by two spaces.
    Show the solution
    printf '%s\n' 'resource "terraform_data" "bonjour" {' '  input = "Bonjour depuis Rabat"' '}' > /root/iac/premier/main.tf

    Check : f=/root/iac/premier/main.tf; grep -Eq '^resource +"terraform_data" +"bonjour" *[{] *$' $f && grep -Eq '^ +input *= *"Bonjour depuis Rabat" *$' $f && grep -Eq "^[}] *$" $f

  2. Write /root/iac/premier/outputs.tf: an output named message whose value is the output attribute of the terraform_data.bonjour resource
    Show a hintoutput "message" { value = <type>.<name>.<attribute> }
    Show the solution
    printf '%s\n' 'output "message" {' '  value = terraform_data.bonjour.output' '}' > /root/iac/premier/outputs.tf

    Check : f=/root/iac/premier/outputs.tf; grep -Eq '^output +"message" *[{] *$' $f && grep -Eq '^ +value *= *terraform_data\.bonjour\.output *$' $f

  3. Write /root/iac/premier/versions.tf: a terraform block that requires an OpenTofu version greater than or equal to 1.6.0 (required_version). Your three files must be well formatted: tofu fmt -check /root/iac/premier must report nothing
    Show a hintThe block is indeed called terraform, even with OpenTofu. Two spaces of indentation, one space on each side of the equals sign.
    Show the solution
    printf '%s\n' 'terraform {' '  required_version = ">= 1.6.0"' '}' > /root/iac/premier/versions.tf

    Check : f=/root/iac/premier/versions.tf; grep -Eq "^terraform *[{] *$" $f && grep -Eq '^ +required_version *= *">= *1\.6(\.0)?" *$' $f && { ! command -v tofu >/dev/null 2>&1 || tofu fmt -check /root/iac/premier >/dev/null 2>&1; }

  4. Write /root/iac/premier/.gitignore so that the .terraform/ folder and the state files (*.tfstate and *.tfstate.*) are never versioned, but WITHOUT ignoring the lock file .terraform.lock.hcl
    Show a hintOne line per pattern. The .terraform.lock.hcl file, on the other hand, is committed to Git.
    Show the solution
    printf '%s\n' '.terraform/' '*.tfstate' '*.tfstate.*' 'crash.log' '*.tfplan' > /root/iac/premier/.gitignore

    Check : f=/root/iac/premier/.gitignore; grep -Eq "^(\*\*/)?\.terraform/?$" $f && grep -Eq "^\*\.tfstate$" $f && grep -Eq "^\*\.tfstate\.\*$" $f && ! grep -q "lock" $f

Official references consulted: OpenTofu — Getting started — https://opentofu.org/docs/intro/ ; OpenTofu — OpenTofu vs. Chef, Puppet, etc. — https://opentofu.org/docs/intro/vs/chef-puppet/ ; OpenTofu — OpenTofu vs. CloudFormation, Heat, etc. — https://opentofu.org/docs/intro/vs/cloudformation/ ; OpenTofu — Migrating to OpenTofu — https://opentofu.org/docs/intro/migration/ ; OpenTofu — Use cases — https://opentofu.org/docs/intro/use-cases/. Original MasterLinux text.