# Configuration as Code

Puppet · Ansible · Terraform
— What's the difference and when to use what? —

FinistDevs · 2026

- Welcome — quick intro to the topic: how we manage infrastructure as code.
- Three tools compared: Puppet, Ansible, Terraform — what each is for, when to pick which.
- Goal: not "which is best" but understanding their distinct roles.

## Arnaud Prémel-Cabic

Tech Lead @ OVHCloud

arnaud.premel-cabic@ovhcloud.com

- Tech Lead at OVHcloud — work with these tools daily.
- Keep it short, get to the content.

## "It works on my server."

Why doesn't it work here when it works everywhere else?

- The classic excuse — everyone's heard it (or said it).
- Hook: the real problem isn't the code, it's the un-managed environment.
- Sets up the whole talk: configuration we can't reproduce.

## You have a server. It works.

Great.

- One server, configured by hand — totally fine at this scale.
- No tooling needed yet. The pain starts when you grow.

## You have 10 servers.

- 10 SSH sessions
- 10 manual edits
- 10 chances to make a mistake

- Manual edits don't scale — repetition breeds error.
- Did you apply the change to all 10? Identically? You can't be sure.

## Now you have 100 servers.

- Some 2 years old. Some a few months. Some brand new.
- None of them are exactly alike.

- Different ages, different patch levels, manual tweaks over time.
- Point at the snowflakes/warnings in the diagram — each box is subtly different.
- This is the reality of a hand-managed fleet.

## Unique. Unreproducible. Undocumented.

Welcome to configuration drift.

- "Snowflake servers" — unique, fragile, impossible to recreate.
- The "this is fine" meme — we've all normalized this chaos.
- Lighten the mood, then pivot to the consequences.

## Configuration drift is silent…

- Can't reproduce the bug locally
- Can't scale reliably
- Can't onboard a new server without fear

- Define drift: state slowly diverging from intent, T0 → T2 in the diagram.
- It's silent — no alarm goes off until prod breaks.
- These four pains are why we need Configuration as Code.

## What if your infrastructure was just… code?

Your AI assistant can write it*. You still need to understand what it deploys.

*Like this presentation 🤖

- The pitch: describe infra in files, deploy reproducibly.
- Aside on AI: it can write the code, but you must understand what it deploys — own the result.
- Fun fact: this deck itself was built with AI assistance.

## Configuration as Code

Machine-readable files. Version-controlled. Automated.

- Three core benefits: reproducible, versionable, auditable.
- Same input → same result. Git history = change log. Know who changed what & when.
- This is the foundation all three tools share.

## Meet the three musketeers of infrastructure.

Puppet · Ansible · Terraform

Each solves a different problem.

- Introduce the three: Puppet, Ansible, Terraform.
- Tease the punchline early: they're complementary, not rivals.
- We'll go through them in deploy order: provision → configure → enforce.

## Terraform

Start here. Before you configure a server, you need to have one.

- First layer: provisioning. You can't configure what doesn't exist.
- Terraform's job is creating the infrastructure itself.

## What is Terraform?

- Infrastructure as Code tool for provisioning cloud resources
- Created by HashiCorp in 2014
- Written in Go
- BUSL 1.1 license since 2023 (was MPL)

- The IaC standard for cloud provisioning.
- HashiCorp, 2014, Go. Flag the 2023 license change — we'll come back to it (OpenTofu).

## HCL: HashiCorp Configuration Language

Declarative, human-readable — pure JSON works too.

- Declarative: you describe the desired end state, not the steps.
- HCL is the common language across all HashiCorp tools.

## The Terraform workflow

- `terraform plan` — preview what will change
- Review — validate the plan before proceeding
- `terraform apply` — create or update resources
- `terraform destroy` — tear everything down

- Key safety feature: plan lets you preview before anything changes.
- Always review the plan — this is what makes Terraform safe in prod.
- apply is idempotent; destroy cleanly removes everything it created.

## Terraform remembers what it built.

- The `.tfstate` file maps code to real-world resources
- Store it remotely — never commit it to Git
- May contain sensitive values: credentials, tokens, secrets

Handle with care.

- State is how Terraform knows what it already built — maps code to real resources.
- Two big gotchas: store it remotely (team access + locking), never commit it (secrets inside).
- Lost or corrupt state = Terraform loses track of reality.

## One tool. Every API.

- 1000+ providers — OVHcloud, Scaleway, Clever Cloud, AWS, Cloudflare, Kubernetes…
- Not just cloud — DNS, monitoring, CI/CD, anything with an API

If it has an API, there's a Terraform provider for it.

- The real power: one workflow for everything, not just one cloud.
- Examples beyond cloud: DNS records, GitHub repos, monitoring dashboards.
- Providers are the plugin ecosystem that makes Terraform universal.

# main.tf

    terraform {
      required_providers {
        openstack = { source = "terraform-provider-openstack/openstack", version = "~> 3.0" }
        ovh       = { source = "ovh/ovh", version = "~> 2.0" }
      }
    }

    resource "openstack_compute_instance_v2" "web" {
      name        = "finistdevs-web"
      image_name  = "Debian 13"
      flavor_name = "b3-8"
      network { name = "Ext-Net" }
    }

    resource "ovh_domain_zone_record" "web" {
      zone      = "example.com"
      subdomain = "finistdevs"
      fieldtype = "A"
      target    = openstack_compute_instance_v2.web.access_ip_v4
    }

- Real OVHcloud example: spin up an instance, then point a DNS record at it.
- Note the implicit dependency — the DNS record references the instance's IP, so Terraform orders them automatically.
- Two different providers (OpenStack + OVH) working together in one file.
- Version syntax: `~>` is the pessimistic constraint operator. `~> 2.0` allows any 2.x (>= 2.0, < 3.0) — patch and minor updates, but never a breaking major bump. `~> 2.13.0` would pin tighter, allowing only 2.13.x.

$ terminal

    $ terraform init
    Initializing provider plugins...
    - Finding terraform-provider-openstack/openstack versions matching "~> 3.0"...
    - Finding ovh/ovh versions matching "~> 2.0"...
    - Installing terraform-provider-openstack/openstack v3.4.0...
    - Installing ovh/ovh v2.13.1...
    Terraform has been successfully initialized!

    $ terraform plan
    ...
    Plan: 2 to add, 0 to change, 0 to destroy.

    $ terraform apply
    ...
    openstack_compute_instance_v2.web: Creating...
    openstack_compute_instance_v2.web: Creation complete after 45s [id=abc-123]
    ovh_domain_zone_record.web: Creating...
    ovh_domain_zone_record.web: Creation complete after 3s [id=456]

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

- Walk the lifecycle: init (download providers) → plan → apply.
- "2 added, 0 changed, 0 destroyed" — exactly the diff you reviewed, nothing more.
- Re-running apply with no changes does nothing — that's idempotence.
- Each `...` on the slide hides real output — call it out so nobody thinks Terraform is this terse.
- The `...` after `plan`: a full per-resource `+`/`-` diff of every attribute Terraform will set — this is what you actually review.
- The `...` after `apply`: Terraform replays that same plan, then pauses for an interactive "yes" prompt — skip it with `-auto-approve` in CI.
- I also trimmed init: real output adds backend init, "Installed … (signed by …)" lines, and the `.terraform.lock.hcl` being written.

## HashiCorp changed Terraform's license.

- BUSL 1.1 instead of MPL — no longer truly open-source
- The community responded: OpenTofu, now a CNCF project

Drop-in for migration. Diverging features. Community-driven.

- 2023: HashiCorp moved to BUSL 1.1 — restricts commercial competitors, not truly open-source anymore.
- Community forked it: OpenTofu, accepted into the Linux Foundation in 2023, a CNCF project since 2025.
- Practical takeaway: drop-in compatible, swap the binary. Worth knowing for licensing-sensitive orgs.

## Terraform at scale needs a platform.

- Terraform Enterprise / HCP Terraform — remote state, RBAC, audit logs
- Spacelift — GitOps-first CI/CD for Terraform and OpenTofu
- Atlantis — open-source, plan & apply from pull requests
- env0, Scalr — SaaS with policy & cost management

- Running Terraform from a laptop doesn't scale to a team.
- You need: shared/locked state, RBAC, audit, plan-on-PR, policy & cost guardrails.
- Range from SaaS (HCP, Spacelift, env0, Scalr) to self-hosted open-source (Atlantis).

## Ansible

Your servers are provisioned. Now make them do something.

- Second layer: configuration. The VMs exist — now install and set things up.
- Transition: Terraform built the box, Ansible makes it useful.

## What is Ansible?

- Agentless automation tool for configuration and orchestration
- Created by Michael DeHaan in 2012
- Acquired by Red Hat in 2015
- Written in Python — GPLv3 license

- Key differentiator: agentless — nothing to install on targets.
- Red Hat backed, Python, genuinely open-source (GPLv3) — contrast with Terraform's BUSL.

## Push-based. Runs over SSH.

- YAML playbooks run tasks in order, across any number of hosts
- Nothing to install on target servers — just Python + SSH
- Idempotent modules — same playbook runs safely again and again

- Push model: control node connects out over SSH and runs tasks — point at the diagram.
- "Agentless" = just needs Python + SSH on the target, no daemon.
- Idempotent modules: re-running is safe, only changes what's needed.

# playbook/webserver.yml

    - name: Configure web server
      hosts: webservers
      become: true

      tasks:
        - name: Install nginx
          ansible.builtin.package:
            name: nginx
            state: present

        - name: Deploy nginx config
          ansible.builtin.template:
            src: nginx.conf.j2
            dest: /etc/nginx/nginx.conf
          notify: Restart nginx

      handlers:
        - name: Restart nginx
          ansible.builtin.service:
            name: nginx
            state: restarted

- Plain YAML — tasks run top to bottom against the "webservers" group.
- become: true = run as root (sudo).
- Handlers are the neat bit: restart nginx only if the config actually changed.
- The `.j2` in `nginx.conf.j2` = Jinja2, Ansible's templating engine. The template module renders it on the control node — `{{ variables }}`, `{% if %}`/`{% for %}` logic, filters like `{{ value | default(...) }}` — then ships the result to `dest`.
- That's how one template serves many hosts: same file, per-host values from inventory/group_vars.

## Who runs where? The inventory.

- Static — a hand-written INI/YAML file of hosts & groups. Simple, versioned, ideal for stable fleets.
- Dynamic — a plugin builds the host list at runtime. Best source here: the Terraform state we just wrote — Ansible configures exactly what Terraform provisioned.

# inventory.yml — static

    webservers:
      hosts:
        web-01:
          ansible_host: 10.0.0.11
        web-02:
          ansible_host: 10.0.0.12

# terraform.yml — dynamic (from TF state)

    plugin: cloud.terraform.terraform_state
    backend_type: local
    backend_config:
      path: ../terraform/terraform.tfstate

- The `-i` flag on the next slide points here — Ansible needs to know which hosts to target.
- Static: explicit list checked into Git. Predictable, but you maintain it by hand.
- This is the Terraform → Ansible handoff: Terraform creates the instance and records it in `.tfstate` (remember slide 16); the `cloud.terraform.terraform_state` plugin reads that same state and turns each resource into an Ansible host. One source of truth — no second host list to keep in sync.
- Other dynamic sources exist too — cloud plugins like `openstack.cloud.openstack` or AWS/OVHcloud query the provider API directly. `keyed_groups` build groups from tags/metadata.
- Provision with Terraform, then immediately configure with Ansible against the freshly-created hosts — that's the combined workflow we land on at the end.

$ terminal

    $ ansible-playbook -i inventory playbook/webserver.yml

    PLAY [Configure web server] ***************************************************

    TASK [Gathering Facts] ********************************************************
    ok: [finistdevs-web]

    TASK [Install nginx] **********************************************************
    changed: [finistdevs-web]

    TASK [Deploy nginx config] ****************************************************
    changed: [finistdevs-web]

    RUNNING HANDLER [Restart nginx] ***********************************************
    changed: [finistdevs-web]

    PLAY RECAP ********************************************************************
    finistdevs-web : ok=4  changed=3  unreachable=0  failed=0  skipped=0

- "ok" vs "changed" — ok means already in desired state, changed means it acted. That's idempotence visible in the output.
- The handler only fired because the config task reported "changed".
- Run it again and everything would be "ok", changed=0.

## Not just configuration. Operations.

- Patch 200 servers tonight
- Roll out a kernel upgrade with a canary strategy
- Run a compliance audit across your whole fleet

The go-to tool for one-off tasks and recurring operations.

- Ansible isn't only "set up a server once" — it shines for ad-hoc operations.
- Imperative/orchestration angle: patching, rolling upgrades, canary, audits across the fleet.
- This is where it differs most from Puppet's "always converge" model.

## The community does the heavy lifting.

- Ansible Galaxy — 10,000+ ready-made roles and collections
- Don't write a playbook to install Docker from scratch — someone already did

    $ ansible-galaxy install <namespace>.<role>

- Galaxy = the package registry for reusable roles/collections.
- Don't reinvent common setups — pull a battle-tested role (geerlingguy is the famous example).
- Huge productivity multiplier — but it's a supply chain: roles run with privilege on your hosts.
- Prefer trusted sources: Red Hat Certified collections and verified publishers (Automation Hub), or well-known community authors like geerlingguy. Be wary of unmaintained, low-download, single-author roles.
- Pin versions in `requirements.yml` and skim the code before importing — treat it like any other dependency.

## Ansible at scale: open-source vs enterprise.

- AWX — open-source web UI, API, and scheduler
- Ansible Automation Platform (Red Hat) — enterprise AWX with support
- Semaphore — lightweight open-source alternative

Core engine remains GPLv3 — truly open-source.

- At scale you want a UI/scheduler/RBAC layer on top of the CLI.
- AWX (free) → Ansible Automation Platform (Red Hat, supported) is the main path; Semaphore is a lighter option.
- Reassure: the core stays GPLv3 — no license rug-pull like Terraform.

## Puppet

Your servers are configured. Now keep them that way.

- Third layer: enforcement. Configured once isn't enough — drift creeps back.
- Puppet's job: keep state correct continuously, forever.

## What is Puppet?

- Configuration management tool for enforcing system state
- Created by Luke Kanies in 2005
- Puppet Inc. acquired by Perforce in 2022
- Written in Ruby and Clojure

- The oldest of the three (2005) — pioneered config management.
- Now owned by Perforce. We'll touch on what that means for the community later.

## Pull, not push. Agents, not SSH.

- Every 30 minutes, each `puppet-agent` polls the Puppet Server
- Compiles a catalog and enforces it locally

Drift is corrected automatically.

- Opposite model to Ansible: pull, not push — agents reach out to the server.
- Every ~30 min the agent fetches a catalog and converges the node — point at the diagram.
- This is the key idea: enforcement runs on a loop, not just at deploy.

# manifests/webserver.pp

    class webserver {
      package { 'nginx':
        ensure => installed,
      }

      file { '/etc/nginx/nginx.conf':
        ensure  => file,
        content => template('webserver/nginx.conf.erb'),
        notify  => Service['nginx'],
      }

      service { 'nginx':
        ensure => running,
        enable => true,
      }
    }

- Same nginx example as Ansible — compare the styles side by side.
- Pure declarative: describe resources (package, file, service) and desired state, not steps.
- notify chains the dependency: config change → restart service.

$ terminal

    $ puppet agent -t
    Info: Using environment 'production'
    Info: Retrieving pluginfacts
    Info: Caching catalog for finistdevs-web.example.com
    Info: Applying configuration version '1713052408'

    Notice: /Stage[main]/Webserver/Package[nginx]/ensure: created
    Notice: /Stage[main]/Webserver/File[/etc/nginx/nginx.conf]/content:
      --- /etc/nginx/nginx.conf
      +++ /tmp/puppet-file20260413
    Notice: /Stage[main]/Webserver/Service[nginx]/ensure: started

    Notice: Applied catalog in 12.34 seconds

- Agent run: fetch catalog → compare to actual → apply only the diffs.
- This normally runs automatically every 30 min; -t is a manual trigger for demo.
- Next run with no drift would report no changes.

## Someone SSH'd in and changed something.

- Puppet noticed. Puppet fixed it.
- Continuous compliance — not just at deploy time. Every. 30. Minutes.
- No manual remediation

- The killer feature: self-healing. Someone hand-edits a file → Puppet reverts it next run.
- Walk the loop in the diagram: drift detected → agent applies → compliant → repeat.
- This is what "continuous compliance" means in practice.

## Puppet: large fleets, zero drift.

- Continuous compliance, auditability, and guaranteed state — at scale
- Best suited for enterprises with hundreds or thousands of long-lived servers
- Fewer SaaS options than Terraform or Ansible

Puppet Enterprise and Foreman are self-hosted. No managed cloud offering.

- Sweet spot: large fleets of long-lived servers where drift control matters.
- Trade-off vs the others: fewer SaaS options, more setup — it's self-hosted.
- Honest framing: overkill for a handful of ephemeral cloud VMs.

## The ecosystem outlives the company.

- Vox Pupuli — 100+ open-source Puppet modules, community-maintained
- OpenVox — an emerging open-source fork of the Puppet core

The community is strong, with or without Puppet Inc.

- Addresses the "is Puppet dying?" worry after the Perforce acquisition.
- The Perforce shift: in Nov 2024 Perforce announced it would stop shipping public open-source Puppet binaries. From early 2025, official packages move to a private location — access needs a developer license (capped at 25 nodes) or a commercial license, and ongoing development moved to internal/private repos.
- Vox Pupuli couldn't accept the Puppet Core Developer EULA — its restrictions block testing and redistribution of the community modules.
- So they forked: OpenVox started as a community package mirror (Overlook InfraTech), and Vox Pupuli shipped the first release on Jan 21, 2025 — OpenVox 8.11 is functionally equivalent to Puppet 8.11, fully open, no EULA. (Same playbook as Terraform → OpenTofu.)
- Reassurance: the open ecosystem outlives any single vendor.

## They're not competing. They're complementary.

Each solves a different layer of the same problem.

- The payoff slide — the "vs" framing was a trap. They stack.
- Terraform provisions → Ansible configures → Puppet enforces. Three layers.
- Ansible also covers one-off ops — push a single patch across the fleet on demand (the push model). Puppet handles the continuous side; Ansible the punctual side.
- "Which should I use?" → depends which layer of the problem you have.

## A common production setup:

1. Terraform provisions the VM
2. Ansible configures it, deploys the app, and pushes one-off patches
3. Puppet continuously enforces compliance

- Concrete recap of how they fit together end to end.
- You don't have to use all three — but they layer cleanly when you do.
- Ansible's role isn't only first-time setup — it's also the tool for punctual ops, like pushing a single patch across the fleet on demand (slide on Operations). Puppet then keeps that state from drifting.
- Pick by your actual need: just provisioning? Terraform. Ad-hoc ops / one-off patch? Ansible. Drift control? Puppet.

# Questions?

Thank you!

Arnaud Prémel-Cabic · arnaud.premel-cabic@ovhcloud.com
Slides: ministicraft.pages.git.cloud.arnaud-pc.fr/finistdev-configuration-as-code/

🤖 Made with the help of Claude

- Thank the audience, point to the slides URL.
- Open the floor for questions.
- Backup topics if quiet: Kubernetes operators, Pulumi, secrets management.