Infrastructure as code
Assignment
"Veloatelier Matte wants to run its web shop on containers," Simone Wyss writes. "And I heard you spent another evening last week setting up VMs by hand. I want a platform you can rebuild from code at any time."
Learning goals
After this chapter you can:
- Tell declarative and imperative descriptions of infrastructure apart
- Explain desired state, actual state and the state file
- Describe the steps of plan and apply
- Explain drift and idempotence with an example from the lab
Click path and code
Section titled “Click path and code”In part 2 you created the network cc-net, the template ubuntu and the first VMs with CLI commands, one step after the other. That worked as long as you were alone and remembered the order. It gets hard when someone else has to build the same environment, when nobody knows after an error what already exists, or when you want to know six months later why a VM has 6 GB.
Infrastructure as code (IaC) describes infrastructure in files kept in Git. A tool reads the files and makes the infrastructure match them. The same files then serve as build instructions and as documentation. If you want to know why the VM has 6 GB, you find the line, and the Git history shows the commit with its reason.
Declarative and imperative
Section titled “Declarative and imperative”An imperative description says what to do and in which order. The commands of part 2 are imperative.
onevnet create cc-net.tplonetemplate create ubuntu.tplonetemplate instantiate ubuntu --name web-1Run them a second time and the first one fails because cc-net already exists, while the third one creates a second VM with the same name. The script itself does not check what is already there.
A declarative description says what the result should look like. The tool compares this desired state with the actual state and works out what it has to do. In the next chapter you describe the two VMs of the Kubernetes cluster this way.
resource "opennebula_virtual_machine" "k3s" { for_each = local.nodes name = each.key memory = 6144}If a VM is missing, OpenTofu creates it. If a VM has too little memory, OpenTofu changes it. If everything matches, OpenTofu does nothing.
| Imperative | Declarative | |
|---|---|---|
| Describes | the steps | the result |
| Second run | repeats the steps | changes only what differs |
| Order | set by a person | derived by the tool from dependencies |
| Examples | shell scripts, CLI commands | OpenTofu, Kubernetes manifests |
Ansible from chapter 2.2 sits in between. A playbook is a list of steps, but most of its modules are declarative, for example "this package is installed".
OpenTofu has to know which object in OpenNebula belongs to which block in the code. It keeps this mapping in its state, in the lab the file terraform.tfstate. It records, for example, that the block opennebula_virtual_machine.k3s["k3s-server"] is the VM with ID 11.
The state is sensitive. It holds every attribute of the objects, secrets included, such as the token the k3s nodes use to join the cluster. So it does not go into Git. If it gets lost, OpenTofu no longer knows its VMs and would create new ones on the next run. Teams keep the state in shared storage with a lock, so that two people cannot change things at the same time. Only you manage your lab, so the state stays a file on the front-end.
Plan and apply
Section titled “Plan and apply”Working with OpenTofu has two steps.
tofu planreads the code, the state and the actual state and shows what would change, without doing anything.+means create,~change,-delete and-/+replace.tofu applybuilds the same plan, asks for confirmation and carries it out.
The plan is where you catch mistakes. If it shows -/+ for a VM you only meant to rename, you stop. Teams show the plan in a pull request before anyone applies it.
Drift and idempotence
Section titled “Drift and idempotence”Drift is a difference between the actual state and the code, for example because someone changed a VM by hand in Sunstone. tofu plan makes drift visible and tofu apply reverts it.
In the lab, drift appeared that nobody had caused by hand. When OpenNebula starts a VM, it sets the machine type q35 itself. The code did not name one, so every later plan wanted to remove the value. Only after adding machine = "q35" to the code did tofu plan report "No changes". Values that a platform adds on its own belong in the code. Otherwise you learn to skim over plans with changes.
An operation is idempotent if running it a second time changes nothing. tofu apply without a change in the code is idempotent, the commands above are not. Only idempotence lets you run the same code as often as you like, whether in the lab, for a milestone or after an outage.
Exercise Click path versus code
Take your commands from chapters 2.3 and 2.4. In a table, write down for each step what happens when you run it a second time and how a declarative description would express the same step.
Solution
onevnet create and onetemplate create fail the second time because the name is taken. onetemplate instantiate creates another VM, since OpenNebula allows duplicate VM names. Declaratively, each step would be a block with the desired result, such as a network cc-net with an address range, a template ubuntu and exactly one VM web-1. A second run would find everything in place and do nothing.
Exercise Lost state
A colleague deletes terraform.tfstate by accident. The VMs keep running. What does the next tofu plan show, and what would a tofu apply lead to?
Solution
Without state, OpenTofu knows no objects and plans to create all VMs again. An apply would start two more VMs with the same fixed addresses, which are already in use, so it would fail or cause conflicts on the network. The right fix is to map the existing VMs back to the code with tofu import or to restore the state from a backup. That is why the state needs a backup but stays out of Git.
Text and figures are licensed under CC BY-SA 4.0 and the code under the MIT License. The sources are on GitHub.