Skip to content

Terraform layout

Melvin PETIT edited this page Aug 27, 2026 · 1 revision

Terraform layout

Three stacks, three states, three lifetimes.

terraform/azure/        the machine that hosts the cloud
terraform/openstack/    what lives inside the cloud
terraform/kubernetes/   the k3s experiment

Why not a single stack

The reflex is to put everything in one folder. Splitting them was decided on one question: what happens if this is destroyed by mistake?

Stack Rebuild cost Changes
azure an hour, DevStack has to be reinstalled rarely
openstack thirty seconds constantly
kubernetes a few minutes it is an experiment

A single state would mean one wrong terraform destroy wipes the host along with the instance you meant to remove. Separate states keep the blast radius proportional to what you are working on.

The second reason is that they do not even talk to the same API: azure calls Azure, the other two call your own OpenStack.

What becomes a variable

Three tests, in order:

Does it differ between two people running this? Subscription, resource group, region, SSH key path. Those are variables with a default only when a wrong value is harmless.

Is it a security decision? allowed_ssh_cidr has no default. A default would be a silent choice made for the user, and the tempting default is 0.0.0.0/0. Terraform refusing to run is the correct behaviour here.

Would a change force a rebuild? VM size, image, flavor. Worth exposing, so adapting to a different subscription is a value change and not a code edit.

Everything else stays hardcoded. A variable nobody will ever change is noise.

Data sources, not resources

data "azurerm_resource_group" "lab" {
  name = var.resource_group_name
}

The resource group already exists and belongs to the school. data reads it, resource would claim ownership, and a terraform destroy would then try to delete a group shared with the whole class.

The rule: describe with resource only what you are willing to destroy.

Outputs are a contract

Outputs are not decoration, they are how the next tool gets its input:

  • public_ip and private_ip feed the Ansible inventory
  • floating_ip feeds the SSH command at the end of the demo
  • instance_name exists so the demo can name the instance instead of guessing

Whenever a value has to be copied by hand from one step to the next, that value should have been an output.

Dependencies are implicit

resource "openstack_networking_floatingip_associate_v2" "vm" {
  floating_ip = openstack_networking_floatingip_v2.vm.address
  port_id     = data.openstack_networking_port_v2.vm.id
}

Referencing another resource is what builds the graph. depends_on is a last resort, for the rare case where the link is real but invisible to Terraform.

The same idea powers the k3s node: the floating IP is created first because its address is referenced inside the cloud-init that configures the API server certificate.

How to arrive at this yourself

  1. List what already exists and what you are creating. The first group is data, the second is resource
  2. Group by lifetime. Things destroyed together belong in the same state
  3. Look for the hand copied values. Each one is a missing output
  4. Look for the dangerous defaults. Each one is a variable that should have none
  5. Read the provider documentation for the resource, not a blog post. Providers change: v3 of the OpenStack provider dropped openstack_compute_floatingip_associate_v2, and only the registry said so

Clone this wiki locally