Files
finistdev-configuration-as-…/index.html
Arnaud Prémel-Cabic 0e1f2d896b docs: slides 39/40 — note Ansible for on-demand one-off patching
Highlight Ansible's punctual/push ops (single patch on demand) alongside
config/deploy, contrasted with Puppet's continuous enforcement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:55:48 +02:00

1534 lines
90 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Configuration as Code — Puppet vs Ansible vs Terraform</title>
<!-- Reveal.js (vendored) -->
<link rel="stylesheet" href="vendor/reveal.js/dist/reset.css">
<link rel="stylesheet" href="vendor/reveal.js/dist/reveal.css">
<link rel="stylesheet" href="vendor/reveal.js/dist/white.css">
<!-- Highlight.js for code blocks -->
<link rel="stylesheet" href="vendor/reveal.js/plugin/highlight/github.css">
<style>
:root {
/* OVHcloud Design System palette (ovh/design-system) */
--ods-blue-500: #0050d7;
--ods-blue-800: #00185e;
--ods-text: #4d5592;
--ods-neutral-600: #666666;
/* Tool brand colors */
--puppet-color: #A06010;
--ansible-color: #CC0000;
--terraform-color: #7B42BC;
--strip-color: var(--ods-blue-500);
}
/* Blue accent strip — fixed outside Reveal.js, always visible */
.top-strip {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 6px;
background: var(--strip-color);
z-index: 50;
pointer-events: none;
}
.reveal .slides section {
text-align: left;
}
.reveal .slides section svg,
.reveal .slides section > img {
display: block;
margin-left: auto;
margin-right: auto;
}
.reveal h1, .reveal h2 { text-align: left; }
.reveal h1 { font-size: 1.8em; }
.reveal h2 { font-size: 1.05em; }
/* Content bullet lists sit a touch smaller than the heading */
.reveal .slides section ul { font-size: 0.9em; }
/* Section-divider titles (Terraform / Ansible / Puppet) stay large */
.reveal h2 .tf-col,
.reveal h2 .ansible-col,
.reveal h2 .puppet-col { font-size: 2em; }
.title-slide, .title-slide h1 { text-align: center !important; }
.title-slide .subtitle { color: var(--ods-text); }
.title-slide .meta { font-size: 0.6em; color: var(--ods-text); margin-top: 1em; }
.title-slide .meta a { color: var(--strip-color); }
.puppet-col { color: var(--puppet-color); }
.ansible-col { color: var(--ansible-color); }
.tf-col { color: var(--terraform-color); }
.filename {
font-family: monospace;
font-size: 0.65em;
color: var(--ods-text);
margin-bottom: 0.2em;
}
/* Code blocks: fill the slide without scrollbars */
.reveal pre {
width: 100%;
max-height: none;
overflow: visible;
margin: 0;
}
.reveal pre code {
font-size: 0.65em;
line-height: 1.6;
max-height: none;
overflow: visible;
padding: 1em;
}
.filename { margin-top: 0; margin-bottom: 0.1em; }
/* Tool logos — fixed like OVHcloud logo, toggled by JS per slide */
.tool-logo-global {
position: fixed;
top: 16px;
left: 16px;
width: 40px;
height: 40px;
opacity: 0.8;
z-index: 50;
pointer-events: none;
display: none;
}
/* Persistent logo — outside Reveal container, fixed to viewport */
.ovh-logo-global {
position: fixed;
bottom: 16px;
left: 16px;
height: 28px;
width: auto;
opacity: 0.7;
z-index: 50;
pointer-events: none;
}
</style>
</head>
<body>
<div class="top-strip"></div>
<img src="vendor/ovhcloud-logo.svg" alt="OVHcloud" class="ovh-logo-global">
<div class="reveal">
<div class="slides">
<!-- ─── SLIDE 1 : Title ─────────────────────────────────────────── -->
<section class="title-slide">
<h1>Configuration as Code</h1>
<p class="subtitle">
<img src="https://cdn.simpleicons.org/puppet/C17F00" alt="Puppet" style="height:0.9em; vertical-align:middle;"> Puppet ·
<img src="https://cdn.simpleicons.org/ansible" alt="Ansible" style="height:0.9em; vertical-align:middle;"> Ansible ·
<img src="https://cdn.simpleicons.org/terraform" alt="Terraform" style="height:0.9em; vertical-align:middle;"> Terraform
<br>— What's the difference and when to use what? —</p>
<p class="meta">FinistDevs · 2026</p>
<aside class="notes">
<ul>
<li>Welcome — quick intro to the topic: how we manage infrastructure as code.</li>
<li>Three tools compared: Puppet, Ansible, Terraform — what each is for, when to pick which.</li>
<li>Goal: not "which is best" but understanding their distinct roles.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 2 : Speaker intro ─────────────────────────────────── -->
<section>
<h2>Arnaud Prémel-Cabic</h2>
<p>Tech Lead @ OVHCloud</p>
<p><small style="color:var(--ods-text);">arnaud.premel-cabic@ovhcloud.com</small></p>
<aside class="notes">
<ul>
<li>Tech Lead at OVHcloud — work with these tools daily.</li>
<li>Keep it short, get to the content.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 3 ─────────────────────────────────────────────────── -->
<section>
<h2>"It works on my server."</h2>
<p><em>Why doesn't it work here when it works everywhere else?</em></p>
<aside class="notes">
<ul>
<li>The classic excuse — everyone's heard it (or said it).</li>
<li>Hook: the real problem isn't the code, it's the un-managed environment.</li>
<li>Sets up the whole talk: configuration we can't reproduce.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 4 ─────────────────────────────────────────────────── -->
<section>
<h2>You have a server. It works.</h2>
<p><em>Great.</em></p>
<aside class="notes">
<ul>
<li>One server, configured by hand — totally fine at this scale.</li>
<li>No tooling needed yet. The pain starts when you grow.</li>
</ul>
</aside>
<!-- SVG1 — single server, green check -->
<svg width="700" height="250" viewBox="0 0 700 250" style="margin-top:0.5em;" xmlns="http://www.w3.org/2000/svg">
<!-- Server body -->
<rect x="270" y="30" width="160" height="140" rx="12" ry="12" fill="#f0f2f8" stroke="#00185e" stroke-width="2.5"/>
<!-- Rack lines -->
<line x1="290" y1="65" x2="410" y2="65" stroke="#4d5592" stroke-width="1.5" stroke-linecap="round"/>
<line x1="290" y1="100" x2="410" y2="100" stroke="#4d5592" stroke-width="1.5" stroke-linecap="round"/>
<line x1="290" y1="135" x2="410" y2="135" stroke="#4d5592" stroke-width="1.5" stroke-linecap="round"/>
<!-- Drive bays / LEDs -->
<circle cx="298" cy="50" r="4" fill="#0050d7"/>
<circle cx="298" cy="85" r="4" fill="#0050d7"/>
<circle cx="298" cy="120" r="4" fill="#0050d7"/>
<!-- Power button -->
<circle cx="402" cy="155" r="5" fill="#00185e"/>
<!-- Server legs -->
<rect x="290" y="170" width="14" height="10" rx="2" fill="#666666"/>
<rect x="396" y="170" width="14" height="10" rx="2" fill="#666666"/>
<!-- Green checkmark circle -->
<circle cx="350" cy="215" r="22" fill="#2ecc71"/>
<polyline points="339,215 347,224 363,207" fill="none" stroke="#fff" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</section>
<!-- ─── SLIDE 5 ─────────────────────────────────────────────────── -->
<section>
<h2>You have 10 servers.</h2>
<ul>
<li>10 SSH sessions</li>
<li>10 manual edits</li>
<li>10 chances to make a mistake</li>
</ul>
<aside class="notes">
<ul>
<li>Manual edits don't scale — repetition breeds error.</li>
<li>Did you apply the change to all 10? Identically? You can't be sure.</li>
</ul>
</aside>
<!-- SVG2 — laptop + 10 servers fan-out -->
<svg width="720" height="260" viewBox="0 0 720 260" style="margin-top:0.5em;" xmlns="http://www.w3.org/2000/svg">
<!-- Laptop body -->
<rect x="20" y="60" width="120" height="80" rx="8" ry="8" fill="#f0f2f8" stroke="#00185e" stroke-width="2"/>
<!-- Screen inner -->
<rect x="30" y="68" width="100" height="56" rx="3" fill="#00185e"/>
<!-- Terminal prompt on screen -->
<text x="38" y="88" font-family="monospace" font-size="10" fill="#2ecc71">$</text>
<text x="48" y="88" font-family="monospace" font-size="9" fill="#aab4d5">ssh root@…</text>
<text x="38" y="103" font-family="monospace" font-size="10" fill="#2ecc71">$</text>
<text x="48" y="103" font-family="monospace" font-size="9" fill="#aab4d5">vim /etc/…</text>
<!-- Laptop base -->
<path d="M10,140 L150,140 L140,155 L20,155 Z" fill="#d8dce8" stroke="#00185e" stroke-width="1.5"/>
<!-- Keyboard hinge -->
<rect x="40" y="140" width="80" height="3" rx="1" fill="#666666"/>
<!-- 10 server icons on right, 2 columns of 5 -->
<!-- Dashed lines from laptop to each server -->
<!-- Column 1 (x=460) -->
<line x1="150" y1="100" x2="460" y2="18" stroke="#0050d7" stroke-width="1" stroke-dasharray="5,3" opacity="0.6"/>
<line x1="150" y1="100" x2="460" y2="66" stroke="#0050d7" stroke-width="1" stroke-dasharray="5,3" opacity="0.6"/>
<line x1="150" y1="100" x2="460" y2="114" stroke="#0050d7" stroke-width="1" stroke-dasharray="5,3" opacity="0.6"/>
<line x1="150" y1="100" x2="460" y2="162" stroke="#0050d7" stroke-width="1" stroke-dasharray="5,3" opacity="0.6"/>
<line x1="150" y1="100" x2="460" y2="210" stroke="#0050d7" stroke-width="1" stroke-dasharray="5,3" opacity="0.6"/>
<!-- Column 2 (x=590) -->
<line x1="150" y1="100" x2="590" y2="18" stroke="#0050d7" stroke-width="1" stroke-dasharray="5,3" opacity="0.6"/>
<line x1="150" y1="100" x2="590" y2="66" stroke="#0050d7" stroke-width="1" stroke-dasharray="5,3" opacity="0.6"/>
<line x1="150" y1="100" x2="590" y2="114" stroke="#0050d7" stroke-width="1" stroke-dasharray="5,3" opacity="0.6"/>
<line x1="150" y1="100" x2="590" y2="162" stroke="#0050d7" stroke-width="1" stroke-dasharray="5,3" opacity="0.6"/>
<line x1="150" y1="100" x2="590" y2="210" stroke="#0050d7" stroke-width="1" stroke-dasharray="5,3" opacity="0.6"/>
<!-- Arrow tips (small triangles) on server end -->
<polygon points="458,16 458,20 463,18" fill="#0050d7" opacity="0.7"/>
<polygon points="458,64 458,68 463,66" fill="#0050d7" opacity="0.7"/>
<polygon points="458,112 458,116 463,114" fill="#0050d7" opacity="0.7"/>
<polygon points="458,160 458,164 463,162" fill="#0050d7" opacity="0.7"/>
<polygon points="458,208 458,212 463,210" fill="#0050d7" opacity="0.7"/>
<polygon points="588,16 588,20 593,18" fill="#0050d7" opacity="0.7"/>
<polygon points="588,64 588,68 593,66" fill="#0050d7" opacity="0.7"/>
<polygon points="588,112 588,116 593,114" fill="#0050d7" opacity="0.7"/>
<polygon points="588,160 588,164 593,162" fill="#0050d7" opacity="0.7"/>
<polygon points="588,208 588,212 593,210" fill="#0050d7" opacity="0.7"/>
<!-- Column 1 servers -->
<rect x="465" y="5" width="100" height="26" rx="5" fill="#f0f2f8" stroke="#00185e" stroke-width="1.5"/>
<rect x="465" y="53" width="100" height="26" rx="5" fill="#f0f2f8" stroke="#00185e" stroke-width="1.5"/>
<rect x="465" y="101" width="100" height="26" rx="5" fill="#f0f2f8" stroke="#00185e" stroke-width="1.5"/>
<rect x="465" y="149" width="100" height="26" rx="5" fill="#f0f2f8" stroke="#00185e" stroke-width="1.5"/>
<rect x="465" y="197" width="100" height="26" rx="5" fill="#f0f2f8" stroke="#00185e" stroke-width="1.5"/>
<!-- Column 1 rack lines -->
<line x1="475" y1="18" x2="555" y2="18" stroke="#4d5592" stroke-width="1"/>
<line x1="475" y1="66" x2="555" y2="66" stroke="#4d5592" stroke-width="1"/>
<line x1="475" y1="114" x2="555" y2="114" stroke="#4d5592" stroke-width="1"/>
<line x1="475" y1="162" x2="555" y2="162" stroke="#4d5592" stroke-width="1"/>
<line x1="475" y1="210" x2="555" y2="210" stroke="#4d5592" stroke-width="1"/>
<!-- Column 1 LEDs -->
<circle cx="473" cy="12" r="2.5" fill="#0050d7"/>
<circle cx="473" cy="60" r="2.5" fill="#0050d7"/>
<circle cx="473" cy="108" r="2.5" fill="#0050d7"/>
<circle cx="473" cy="156" r="2.5" fill="#0050d7"/>
<circle cx="473" cy="204" r="2.5" fill="#0050d7"/>
<!-- Column 2 servers -->
<rect x="595" y="5" width="100" height="26" rx="5" fill="#f0f2f8" stroke="#00185e" stroke-width="1.5"/>
<rect x="595" y="53" width="100" height="26" rx="5" fill="#f0f2f8" stroke="#00185e" stroke-width="1.5"/>
<rect x="595" y="101" width="100" height="26" rx="5" fill="#f0f2f8" stroke="#00185e" stroke-width="1.5"/>
<rect x="595" y="149" width="100" height="26" rx="5" fill="#f0f2f8" stroke="#00185e" stroke-width="1.5"/>
<rect x="595" y="197" width="100" height="26" rx="5" fill="#f0f2f8" stroke="#00185e" stroke-width="1.5"/>
<!-- Column 2 rack lines -->
<line x1="605" y1="18" x2="685" y2="18" stroke="#4d5592" stroke-width="1"/>
<line x1="605" y1="66" x2="685" y2="66" stroke="#4d5592" stroke-width="1"/>
<line x1="605" y1="114" x2="685" y2="114" stroke="#4d5592" stroke-width="1"/>
<line x1="605" y1="162" x2="685" y2="162" stroke="#4d5592" stroke-width="1"/>
<line x1="605" y1="210" x2="685" y2="210" stroke="#4d5592" stroke-width="1"/>
<!-- Column 2 LEDs -->
<circle cx="603" cy="12" r="2.5" fill="#0050d7"/>
<circle cx="603" cy="60" r="2.5" fill="#0050d7"/>
<circle cx="603" cy="108" r="2.5" fill="#0050d7"/>
<circle cx="603" cy="156" r="2.5" fill="#0050d7"/>
<circle cx="603" cy="204" r="2.5" fill="#0050d7"/>
<!-- "ssh" labels floating near lines -->
<text x="260" y="65" font-family="monospace" font-size="9" fill="#0050d7" opacity="0.7" transform="rotate(-8,260,65)">ssh</text>
<text x="280" y="115" font-family="monospace" font-size="9" fill="#0050d7" opacity="0.7">ssh</text>
<text x="260" y="160" font-family="monospace" font-size="9" fill="#0050d7" opacity="0.7" transform="rotate(6,260,160)">ssh</text>
</svg>
</section>
<!-- ─── SLIDE 6 ─────────────────────────────────────────────────── -->
<section>
<h2>Now you have 100 servers.</h2>
<ul>
<li>Some 2 years old. Some a few months. Some brand new.</li>
<li>None of them are exactly alike.</li>
</ul>
<aside class="notes">
<ul>
<li>Different ages, different patch levels, manual tweaks over time.</li>
<li>Point at the snowflakes/warnings in the diagram — each box is subtly different.</li>
<li>This is the reality of a hand-managed fleet.</li>
</ul>
</aside>
<!-- SVG3 — 100-server chaos grid with drift indicators -->
<svg width="700" height="260" viewBox="0 0 700 260" style="margin-top:0.5em;" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Warning triangle symbol -->
<symbol id="warn" viewBox="0 0 16 16">
<polygon points="8,1 15,15 1,15" fill="#e00034" stroke="#fff" stroke-width="0.5"/>
<text x="8" y="13" text-anchor="middle" font-size="10" font-weight="bold" fill="#fff">!</text>
</symbol>
<!-- Snowflake symbol -->
<symbol id="snow" viewBox="0 0 16 16">
<text x="8" y="14" text-anchor="middle" font-size="14" fill="#0050d7"></text>
</symbol>
</defs>
<!-- Generate a 10×6 grid of servers (60 shown, suggests ~100) -->
<!-- Row 0 -->
<rect x="10" y="5" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="78" y="5" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="146" y="5" width="58" height="30" rx="4" fill="#fce8ec" stroke="#e00034" stroke-width="1.2"/>
<rect x="214" y="5" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="282" y="5" width="58" height="30" rx="4" fill="#e8edf8" stroke="#0050d7" stroke-width="1.2"/>
<rect x="350" y="5" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="418" y="5" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#666666" stroke-width="1.2"/>
<rect x="486" y="5" width="58" height="30" rx="4" fill="#fce8ec" stroke="#e00034" stroke-width="1.2"/>
<rect x="554" y="5" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="622" y="5" width="58" height="30" rx="4" fill="#e8edf8" stroke="#0050d7" stroke-width="1.2"/>
<!-- Row 1 -->
<rect x="10" y="45" width="58" height="30" rx="4" fill="#e8edf8" stroke="#0050d7" stroke-width="1.2"/>
<rect x="78" y="45" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="146" y="45" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="214" y="45" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#666666" stroke-width="1.2"/>
<rect x="282" y="45" width="58" height="30" rx="4" fill="#fce8ec" stroke="#e00034" stroke-width="1.2"/>
<rect x="350" y="45" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="418" y="45" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="486" y="45" width="58" height="30" rx="4" fill="#e8edf8" stroke="#0050d7" stroke-width="1.2"/>
<rect x="554" y="45" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="622" y="45" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#666666" stroke-width="1.2"/>
<!-- Row 2 -->
<rect x="10" y="85" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="78" y="85" width="58" height="30" rx="4" fill="#fce8ec" stroke="#e00034" stroke-width="1.2"/>
<rect x="146" y="85" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#666666" stroke-width="1.2"/>
<rect x="214" y="85" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="282" y="85" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="350" y="85" width="58" height="30" rx="4" fill="#e8edf8" stroke="#0050d7" stroke-width="1.2"/>
<rect x="418" y="85" width="58" height="30" rx="4" fill="#fce8ec" stroke="#e00034" stroke-width="1.2"/>
<rect x="486" y="85" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="554" y="85" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="622" y="85" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#666666" stroke-width="1.2"/>
<!-- Row 3 -->
<rect x="10" y="125" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#666666" stroke-width="1.2"/>
<rect x="78" y="125" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="146" y="125" width="58" height="30" rx="4" fill="#e8edf8" stroke="#0050d7" stroke-width="1.2"/>
<rect x="214" y="125" width="58" height="30" rx="4" fill="#fce8ec" stroke="#e00034" stroke-width="1.2"/>
<rect x="282" y="125" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="350" y="125" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="418" y="125" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="486" y="125" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#666666" stroke-width="1.2"/>
<rect x="554" y="125" width="58" height="30" rx="4" fill="#e8edf8" stroke="#0050d7" stroke-width="1.2"/>
<rect x="622" y="125" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<!-- Row 4 -->
<rect x="10" y="165" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="78" y="165" width="58" height="30" rx="4" fill="#e8edf8" stroke="#0050d7" stroke-width="1.2"/>
<rect x="146" y="165" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="214" y="165" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="282" y="165" width="58" height="30" rx="4" fill="#fce8ec" stroke="#e00034" stroke-width="1.2"/>
<rect x="350" y="165" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#666666" stroke-width="1.2"/>
<rect x="418" y="165" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="486" y="165" width="58" height="30" rx="4" fill="#fce8ec" stroke="#e00034" stroke-width="1.2"/>
<rect x="554" y="165" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="622" y="165" width="58" height="30" rx="4" fill="#e8edf8" stroke="#0050d7" stroke-width="1.2"/>
<!-- Row 5 (partial — fades into "...more") -->
<rect x="10" y="205" width="58" height="30" rx="4" fill="#e8edf8" stroke="#0050d7" stroke-width="1.2"/>
<rect x="78" y="205" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="146" y="205" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#666666" stroke-width="1.2"/>
<rect x="214" y="205" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="282" y="205" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<rect x="350" y="205" width="58" height="30" rx="4" fill="#fce8ec" stroke="#e00034" stroke-width="1.2"/>
<rect x="418" y="205" width="58" height="30" rx="4" fill="#f0f2f8" stroke="#00185e" stroke-width="1.2"/>
<!-- Ellipsis to suggest more -->
<text x="510" y="226" font-family="sans-serif" font-size="22" fill="#666666" letter-spacing="4">· · ·</text>
<!-- Rack lines on every server (subtle) -->
<g stroke="#4d5592" stroke-width="0.6" opacity="0.4">
<!-- Just add a mid-line to each server for texture -->
<line x1="18" y1="20" x2="60" y2="20"/> <line x1="86" y1="20" x2="128" y2="20"/>
<line x1="18" y1="60" x2="60" y2="60"/> <line x1="86" y1="60" x2="128" y2="60"/>
<line x1="18" y1="100" x2="60" y2="100"/> <line x1="86" y1="100" x2="128" y2="100"/>
<line x1="18" y1="140" x2="60" y2="140"/> <line x1="86" y1="140" x2="128" y2="140"/>
<line x1="18" y1="180" x2="60" y2="180"/> <line x1="86" y1="180" x2="128" y2="180"/>
<line x1="18" y1="220" x2="60" y2="220"/> <line x1="86" y1="220" x2="128" y2="220"/>
</g>
<!-- Warning triangles on red-tinted servers -->
<use href="#warn" x="50" y="7" width="14" height="14"/>
<use href="#warn" x="528" y="7" width="14" height="14"/>
<use href="#warn" x="324" y="47" width="14" height="14"/>
<use href="#warn" x="120" y="87" width="14" height="14"/>
<use href="#warn" x="460" y="87" width="14" height="14"/>
<use href="#warn" x="256" y="127" width="14" height="14"/>
<use href="#warn" x="324" y="167" width="14" height="14"/>
<use href="#warn" x="528" y="167" width="14" height="14"/>
<use href="#warn" x="392" y="207" width="14" height="14"/>
<!-- Snowflakes on blue-tinted servers (drift / unique configs) -->
<use href="#snow" x="324" y="6" width="14" height="14"/>
<use href="#snow" x="664" y="6" width="14" height="14"/>
<use href="#snow" x="52" y="46" width="14" height="14"/>
<use href="#snow" x="528" y="46" width="14" height="14"/>
<use href="#snow" x="392" y="86" width="14" height="14"/>
<use href="#snow" x="188" y="126" width="14" height="14"/>
<use href="#snow" x="596" y="126" width="14" height="14"/>
<use href="#snow" x="120" y="166" width="14" height="14"/>
<use href="#snow" x="664" y="166" width="14" height="14"/>
<use href="#snow" x="52" y="206" width="14" height="14"/>
<!-- Small status dots (mixed: green OK, amber, red) scattered -->
<circle cx="18" cy="12" r="2.5" fill="#2ecc71"/> <circle cx="86" cy="12" r="2.5" fill="#2ecc71"/>
<circle cx="154" cy="12" r="2.5" fill="#e00034"/> <circle cx="222" cy="12" r="2.5" fill="#2ecc71"/>
<circle cx="494" cy="12" r="2.5" fill="#e00034"/> <circle cx="562" cy="12" r="2.5" fill="#2ecc71"/>
<circle cx="18" cy="52" r="2.5" fill="#f39c12"/> <circle cx="86" cy="52" r="2.5" fill="#2ecc71"/>
<circle cx="154" cy="52" r="2.5" fill="#2ecc71"/> <circle cx="290" cy="52" r="2.5" fill="#e00034"/>
<circle cx="358" cy="52" r="2.5" fill="#2ecc71"/> <circle cx="426" cy="52" r="2.5" fill="#2ecc71"/>
<circle cx="18" cy="92" r="2.5" fill="#2ecc71"/> <circle cx="86" cy="92" r="2.5" fill="#e00034"/>
<circle cx="222" cy="92" r="2.5" fill="#2ecc71"/> <circle cx="290" cy="92" r="2.5" fill="#f39c12"/>
<circle cx="426" cy="92" r="2.5" fill="#e00034"/> <circle cx="494" cy="92" r="2.5" fill="#2ecc71"/>
<circle cx="18" cy="132" r="2.5" fill="#f39c12"/> <circle cx="86" cy="132" r="2.5" fill="#2ecc71"/>
<circle cx="222" cy="132" r="2.5" fill="#e00034"/> <circle cx="290" cy="132" r="2.5" fill="#2ecc71"/>
<circle cx="494" cy="132" r="2.5" fill="#f39c12"/>
<circle cx="18" cy="172" r="2.5" fill="#2ecc71"/> <circle cx="290" cy="172" r="2.5" fill="#e00034"/>
<circle cx="358" cy="172" r="2.5" fill="#f39c12"/> <circle cx="494" cy="172" r="2.5" fill="#e00034"/>
<circle cx="562" cy="172" r="2.5" fill="#2ecc71"/>
<circle cx="86" cy="212" r="2.5" fill="#2ecc71"/> <circle cx="222" cy="212" r="2.5" fill="#f39c12"/>
<circle cx="290" cy="212" r="2.5" fill="#2ecc71"/> <circle cx="358" cy="212" r="2.5" fill="#e00034"/>
</svg>
</section>
<!-- ─── SLIDE 7 ─────────────────────────────────────────────────── -->
<section>
<h2>Unique. Unreproducible. Undocumented.</h2>
<p><em>Welcome to configuration drift.</em></p>
<aside class="notes">
<ul>
<li>"Snowflake servers" — unique, fragile, impossible to recreate.</li>
<li>The "this is fine" meme — we've all normalized this chaos.</li>
<li>Lighten the mood, then pivot to the consequences.</li>
</ul>
</aside>
<img src="https://media.giphy.com/media/QMHoU66sBXqqLqYvGO/giphy.gif"
alt="This is fine"
style="height:220px; margin-top:0.5em; border-radius:6px;">
</section>
<!-- ─── SLIDE 8 ─────────────────────────────────────────────────── -->
<section>
<h2>Configuration drift is silent…</h2>
<ul>
<li>Can't reproduce the bug locally</li>
<li>Can't scale reliably</li>
<li>Can't onboard a new server without fear</li>
</ul>
<aside class="notes">
<ul>
<li>Define drift: state slowly diverging from intent, T0 → T2 in the diagram.</li>
<li>It's silent — no alarm goes off until prod breaks.</li>
<li>These four pains are why we need Configuration as Code.</li>
</ul>
</aside>
<svg width="750" height="220" style="margin-top:0.5em;" viewBox="0 0 750 220" xmlns="http://www.w3.org/2000/svg">
<line x1="60" y1="180" x2="700" y2="180" stroke="#666666" stroke-width="2.5"/>
<polygon points="700,174 720,180 700,186" fill="#666666"/>
<text x="140" y="210" text-anchor="middle" font-family="system-ui,sans-serif" font-size="14" fill="#4d5592" font-weight="600">T0</text>
<text x="390" y="210" text-anchor="middle" font-family="system-ui,sans-serif" font-size="14" fill="#4d5592" font-weight="600">T1</text>
<text x="630" y="210" text-anchor="middle" font-family="system-ui,sans-serif" font-size="14" fill="#4d5592" font-weight="600">T2</text>
<line x1="140" y1="175" x2="140" y2="185" stroke="#666666" stroke-width="2"/>
<line x1="390" y1="175" x2="390" y2="185" stroke="#666666" stroke-width="2"/>
<line x1="630" y1="175" x2="630" y2="185" stroke="#666666" stroke-width="2"/>
<!-- T0: identical servers -->
<rect x="90" y="70" width="28" height="36" rx="4" fill="#0050d7" opacity="0.9"/>
<rect x="122" y="70" width="28" height="36" rx="4" fill="#0050d7" opacity="0.9"/>
<rect x="154" y="70" width="28" height="36" rx="4" fill="#0050d7" opacity="0.9"/>
<rect x="186" y="70" width="28" height="36" rx="4" fill="#0050d7" opacity="0.9"/>
<text x="140" y="55" text-anchor="middle" font-family="system-ui,sans-serif" font-size="11" fill="#00a344" font-weight="600">ALL IDENTICAL</text>
<!-- T1: drifting -->
<rect x="340" y="70" width="28" height="36" rx="4" fill="#0050d7" opacity="0.9"/>
<rect x="372" y="68" width="28" height="36" rx="4" fill="#0050d7" opacity="0.7"/>
<rect x="404" y="72" width="28" height="36" rx="4" fill="#4d5592" opacity="0.85"/>
<rect x="436" y="70" width="28" height="36" rx="4" fill="#0050d7" opacity="0.9"/>
<text x="390" y="55" text-anchor="middle" font-family="system-ui,sans-serif" font-size="11" fill="#4d5592" font-weight="600">DRIFTING…</text>
<!-- T2: chaos -->
<rect x="580" y="74" width="28" height="36" rx="4" fill="#e00034" opacity="0.85"/>
<rect x="612" y="66" width="28" height="36" rx="4" fill="#4d5592" opacity="0.8"/>
<rect x="644" y="78" width="28" height="36" rx="4" fill="#00185e" opacity="0.9"/>
<rect x="676" y="70" width="28" height="36" rx="4" fill="#e00034" opacity="0.7"/>
<polygon points="594,68 600,58 606,68" fill="#e00034" stroke="white" stroke-width="1"/>
<text x="600" y="67" text-anchor="middle" font-family="system-ui,sans-serif" font-size="8" fill="white" font-weight="bold">!</text>
<polygon points="654,72 660,62 666,72" fill="#e00034" stroke="white" stroke-width="1"/>
<text x="660" y="71" text-anchor="middle" font-family="system-ui,sans-serif" font-size="8" fill="white" font-weight="bold">!</text>
<polygon points="684,64 690,54 696,64" fill="#e00034" stroke="white" stroke-width="1"/>
<text x="690" y="63" text-anchor="middle" font-family="system-ui,sans-serif" font-size="8" fill="white" font-weight="bold">!</text>
<text x="630" y="48" text-anchor="middle" font-family="system-ui,sans-serif" font-size="11" fill="#e00034" font-weight="700">CHAOS</text>
</svg>
</section>
<!-- ─── SLIDE 9 ─────────────────────────────────────────────────── -->
<section style="height:100%;">
<h2>What if your infrastructure was just… code?</h2>
<p><em>Your AI assistant can write it*. You still need to understand what it deploys.</em></p>
<svg width="750" height="220" style="margin-top:0.5em;" viewBox="0 0 750 220" xmlns="http://www.w3.org/2000/svg">
<!-- Code file icon -->
<rect x="80" y="35" width="120" height="150" rx="6" fill="none" stroke="#0050d7" stroke-width="2.5"/>
<path d="M165,35 L200,35 L200,70 L165,70 Z" fill="white" stroke="white" stroke-width="3"/>
<path d="M165,35 L200,70" stroke="#0050d7" stroke-width="2.5" fill="none"/>
<line x1="165" y1="35" x2="165" y2="70" stroke="#0050d7" stroke-width="2.5"/>
<line x1="165" y1="70" x2="200" y2="70" stroke="#0050d7" stroke-width="2.5"/>
<text x="140" y="108" text-anchor="middle" font-family="monospace" font-size="30" fill="#0050d7" font-weight="700">{ }</text>
<line x1="105" y1="128" x2="175" y2="128" stroke="#4d5592" stroke-width="2" opacity="0.4"/>
<line x1="112" y1="140" x2="168" y2="140" stroke="#4d5592" stroke-width="2" opacity="0.3"/>
<line x1="108" y1="152" x2="172" y2="152" stroke="#4d5592" stroke-width="2" opacity="0.4"/>
<!-- Arrow -->
<line x1="260" y1="110" x2="420" y2="110" stroke="#0050d7" stroke-width="4" stroke-linecap="round"/>
<polygon points="420,98 448,110 420,122" fill="#0050d7"/>
<text x="340" y="95" text-anchor="middle" font-family="system-ui,sans-serif" font-size="13" fill="#4d5592" font-weight="600">deploy</text>
<!-- Cloud with servers -->
<path d="M530,160 Q470,160 480,120 Q470,80 510,75 Q525,45 570,50 Q610,40 630,65 Q680,60 685,95 Q710,105 700,135 Q705,165 670,160 Z" fill="none" stroke="#0050d7" stroke-width="2.5" opacity="0.8"/>
<rect x="520" y="90" width="36" height="44" rx="5" fill="#0050d7" opacity="0.15" stroke="#0050d7" stroke-width="1.5"/>
<circle cx="548" cy="124" r="3" fill="#00a344"/>
<rect x="572" y="90" width="36" height="44" rx="5" fill="#0050d7" opacity="0.15" stroke="#0050d7" stroke-width="1.5"/>
<circle cx="600" cy="124" r="3" fill="#00a344"/>
<rect x="624" y="90" width="36" height="44" rx="5" fill="#0050d7" opacity="0.15" stroke="#0050d7" stroke-width="1.5"/>
<circle cx="652" cy="124" r="3" fill="#00a344"/>
</svg>
<small style="position:absolute; bottom:40px; right:0; color:var(--ods-neutral-600); font-size:0.45em;">*Like this presentation 🤖</small>
<aside class="notes">
<ul>
<li>The pitch: describe infra in files, deploy reproducibly.</li>
<li>Aside on AI: it can write the code, but you must understand what it deploys — own the result.</li>
<li>Fun fact: this deck itself was built with AI assistance.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 10 ────────────────────────────────────────────────── -->
<section>
<h2>Configuration as Code</h2>
<p>Machine-readable files. Version-controlled. Automated.</p>
<aside class="notes">
<ul>
<li>Three core benefits: reproducible, versionable, auditable.</li>
<li>Same input → same result. Git history = change log. Know who changed what & when.</li>
<li>This is the foundation all three tools share.</li>
</ul>
</aside>
<svg width="750" height="220" style="margin-top:0.5em;" viewBox="0 0 750 220" xmlns="http://www.w3.org/2000/svg">
<!-- Reproducible -->
<g transform="translate(125,80)">
<path d="M0,-32 A32,32 0 1,1 -22,22" fill="none" stroke="#0050d7" stroke-width="3" stroke-linecap="round"/>
<polygon points="-28,14 -22,26 -14,16" fill="#0050d7"/>
<path d="M0,32 A32,32 0 1,1 22,-22" fill="none" stroke="#0050d7" stroke-width="3" stroke-linecap="round"/>
<polygon points="28,-14 22,-26 14,-16" fill="#0050d7"/>
</g>
<text x="125" y="145" text-anchor="middle" font-family="system-ui,sans-serif" font-size="16" fill="#00185e" font-weight="700">Reproducible</text>
<text x="125" y="165" text-anchor="middle" font-family="system-ui,sans-serif" font-size="12" fill="#4d5592">Same input → same result</text>
<line x1="250" y1="40" x2="250" y2="180" stroke="#4d5592" stroke-width="0.5" opacity="0.3"/>
<!-- Versionable -->
<g transform="translate(375,80)">
<line x1="0" y1="-35" x2="0" y2="35" stroke="#0050d7" stroke-width="3" stroke-linecap="round"/>
<path d="M0,-5 Q20,-5 25,-25" fill="none" stroke="#0050d7" stroke-width="3" stroke-linecap="round"/>
<circle cx="0" cy="-30" r="5" fill="#0050d7"/>
<circle cx="0" cy="-5" r="5" fill="#0050d7"/>
<circle cx="0" cy="20" r="5" fill="#0050d7"/>
<circle cx="25" cy="-25" r="5" fill="#0050d7" opacity="0.7"/>
<path d="M25,-25 Q30,-10 0,20" fill="none" stroke="#0050d7" stroke-width="2.5" stroke-linecap="round" stroke-dasharray="4,3"/>
</g>
<text x="375" y="145" text-anchor="middle" font-family="system-ui,sans-serif" font-size="16" fill="#00185e" font-weight="700">Versionable</text>
<text x="375" y="165" text-anchor="middle" font-family="system-ui,sans-serif" font-size="12" fill="#4d5592">Track every change</text>
<line x1="500" y1="40" x2="500" y2="180" stroke="#4d5592" stroke-width="0.5" opacity="0.3"/>
<!-- Auditable -->
<g transform="translate(625,72)">
<circle cx="-8" cy="-8" r="22" fill="none" stroke="#0050d7" stroke-width="3"/>
<line x1="8" y1="8" x2="24" y2="24" stroke="#0050d7" stroke-width="4" stroke-linecap="round"/>
<polyline points="-18,-14 -14,-10 -6,-18" fill="none" stroke="#0050d7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="-2" y1="-14" x2="8" y2="-14" stroke="#0050d7" stroke-width="1.5" opacity="0.5"/>
<polyline points="-18,-2 -14,2 -6,-6" fill="none" stroke="#0050d7" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="-2" y1="-2" x2="8" y2="-2" stroke="#0050d7" stroke-width="1.5" opacity="0.5"/>
</g>
<text x="625" y="145" text-anchor="middle" font-family="system-ui,sans-serif" font-size="16" fill="#00185e" font-weight="700">Auditable</text>
<text x="625" y="165" text-anchor="middle" font-family="system-ui,sans-serif" font-size="12" fill="#4d5592">Who changed what &amp; when</text>
</svg>
</section>
<!-- ─── SLIDE 11 ────────────────────────────────────────────────── -->
<section style="text-align:center;">
<h2 style="text-align:center;">Meet the three musketeers of infrastructure.</h2>
<p style="font-size:1.2em; margin-top:1em;">
<img src="https://cdn.simpleicons.org/puppet/C17F00" alt="Puppet" style="height:0.9em; vertical-align:middle;"> <span class="puppet-col">Puppet</span> &nbsp;·&nbsp;
<img src="https://cdn.simpleicons.org/ansible" alt="Ansible" style="height:0.9em; vertical-align:middle;"> <span class="ansible-col">Ansible</span> &nbsp;·&nbsp;
<img src="https://cdn.simpleicons.org/terraform" alt="Terraform" style="height:0.9em; vertical-align:middle;"> <span class="tf-col">Terraform</span>
</p>
<p><em>Each solves a different problem.</em></p>
<aside class="notes">
<ul>
<li>Introduce the three: Puppet, Ansible, Terraform.</li>
<li>Tease the punchline early: they're complementary, not rivals.</li>
<li>We'll go through them in deploy order: provision → configure → enforce.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 12 : Terraform intro ──────────────────────────────── -->
<section class="s-tf">
<h2><span class="tf-col">Terraform</span></h2>
<p>Start here. Before you configure a server, you need to have one.</p>
<aside class="notes">
<ul>
<li>First layer: provisioning. You can't configure what doesn't exist.</li>
<li>Terraform's job is creating the infrastructure itself.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 13 : What is Terraform ────────────────────────────── -->
<section class="s-tf">
<h2>What is Terraform?</h2>
<ul>
<li><strong>Infrastructure as Code</strong> tool for provisioning cloud resources</li>
<li>Created by <strong>HashiCorp</strong> in 2014</li>
<li>Written in <strong>Go</strong></li>
<li>BUSL 1.1 license since 2023 (was MPL)</li>
</ul>
<aside class="notes">
<ul>
<li>The IaC standard for cloud provisioning.</li>
<li>HashiCorp, 2014, Go. Flag the 2023 license change — we'll come back to it (OpenTofu).</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 14 : Terraform concepts ───────────────────────────── -->
<section class="s-tf">
<h2>HCL: HashiCorp Configuration Language</h2>
<p>Declarative, human-readable — pure JSON works too.</p>
<aside class="notes">
<ul>
<li>Declarative: you describe the desired end state, not the steps.</li>
<li>HCL is the common language across all HashiCorp tools.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 15 : Terraform workflow ───────────────────────────── -->
<section class="s-tf">
<h2>The Terraform workflow</h2>
<ul>
<li><code>terraform plan</code> — preview what will change</li>
<li><strong>Review</strong> — validate the plan before proceeding</li>
<li><code>terraform apply</code> — create or update resources</li>
<li><code>terraform destroy</code> — tear everything down</li>
</ul>
<aside class="notes">
<ul>
<li>Key safety feature: plan lets you preview before anything changes.</li>
<li>Always review the plan — this is what makes Terraform safe in prod.</li>
<li>apply is idempotent; destroy cleanly removes everything it created.</li>
</ul>
</aside>
<svg width="900" height="150" style="margin-top:0.5em;" viewBox="0 0 900 150" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="wf-arrow" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="10" markerHeight="7" orient="auto-start-auto">
<path d="M0,0 L10,3.5 L0,7z" fill="#7B42BC"/>
</marker>
</defs>
<!-- Box 1: HCL Code -->
<rect x="10" y="15" width="140" height="100" rx="12" fill="none" stroke="#7B42BC" stroke-width="2.2"/>
<g transform="translate(50,28)">
<path d="M0,4 L0,30 Q0,33 3,33 L27,33 Q30,33 30,30 L30,10 L20,0 L3,0 Q0,0 0,4z" fill="none" stroke="#7B42BC" stroke-width="1.6"/>
<path d="M20,0 L20,7 Q20,10 23,10 L30,10" fill="none" stroke="#7B42BC" stroke-width="1.4"/>
</g>
<text x="80" y="92" text-anchor="middle" font-family="system-ui,sans-serif" font-size="14" font-weight="600" fill="#00185e">HCL Code</text>
<!-- Arrow 1→2 -->
<line x1="158" y1="65" x2="195" y2="65" stroke="#7B42BC" stroke-width="2" marker-end="url(#wf-arrow)"/>
<!-- Box 2: plan -->
<rect x="203" y="15" width="140" height="100" rx="12" fill="none" stroke="#7B42BC" stroke-width="2.2"/>
<g transform="translate(248,30)">
<ellipse cx="15" cy="15" rx="18" ry="12" fill="none" stroke="#7B42BC" stroke-width="1.6"/>
<circle cx="15" cy="15" r="6" fill="none" stroke="#7B42BC" stroke-width="1.6"/>
<circle cx="15" cy="15" r="2.5" fill="#7B42BC"/>
</g>
<text x="273" y="92" text-anchor="middle" font-family="monospace" font-size="14" font-weight="600" fill="#00185e">plan</text>
<!-- Arrow 2→3 -->
<line x1="351" y1="65" x2="388" y2="65" stroke="#7B42BC" stroke-width="2" marker-end="url(#wf-arrow)"/>
<!-- Box 3: review (new) -->
<rect x="396" y="15" width="140" height="100" rx="12" fill="#f6f0ff" stroke="#7B42BC" stroke-width="2.2" stroke-dasharray="6,3"/>
<g transform="translate(446,28)">
<polyline points="3,18 10,25 25,8" fill="none" stroke="#7B42BC" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<text x="466" y="92" text-anchor="middle" font-family="system-ui,sans-serif" font-size="14" font-weight="600" fill="#00185e">review</text>
<!-- Arrow 3→4 -->
<line x1="544" y1="65" x2="581" y2="65" stroke="#7B42BC" stroke-width="2" marker-end="url(#wf-arrow)"/>
<!-- Box 4: apply -->
<rect x="589" y="15" width="140" height="100" rx="12" fill="none" stroke="#7B42BC" stroke-width="2.2"/>
<g transform="translate(634,30)">
<polygon points="5,2 30,15 5,28" fill="none" stroke="#7B42BC" stroke-width="1.8" stroke-linejoin="round"/>
</g>
<text x="659" y="92" text-anchor="middle" font-family="monospace" font-size="14" font-weight="600" fill="#00185e">apply</text>
<!-- Arrow 4→5 -->
<line x1="737" y1="65" x2="774" y2="65" stroke="#7B42BC" stroke-width="2" marker-end="url(#wf-arrow)"/>
<!-- Box 5: Resources -->
<rect x="782" y="15" width="108" height="100" rx="12" fill="none" stroke="#7B42BC" stroke-width="2.2"/>
<g transform="translate(810,28)">
<path d="M12,28 Q0,28 0,20 Q0,14 6,12 Q6,4 15,2 Q24,0 28,6 Q29,5 32,5 Q38,5 38,11 Q44,12 44,18 Q44,24 38,25 Q38,28 34,28z" fill="none" stroke="#7B42BC" stroke-width="1.6"/>
</g>
<text x="836" y="92" text-anchor="middle" font-family="system-ui,sans-serif" font-size="13" font-weight="600" fill="#00185e">Resources</text>
</svg>
</section>
<!-- ─── SLIDE 16 : Terraform state ──────────────────────────────── -->
<section class="s-tf">
<h2>Terraform remembers what it built.</h2>
<ul>
<li>The <code>.tfstate</code> file maps code to real-world resources</li>
<li>Store it <strong>remotely</strong> — never commit it to Git</li>
<li>May contain sensitive values: credentials, tokens, secrets</li>
</ul>
<p><em>Handle with care.</em></p>
<aside class="notes">
<ul>
<li>State is how Terraform knows what it already built — maps code to real resources.</li>
<li>Two big gotchas: store it remotely (team access + locking), never commit it (secrets inside).</li>
<li>Lost or corrupt state = Terraform loses track of reality.</li>
</ul>
</aside>
<svg width="750" height="220" style="margin-top:0.5em;" viewBox="0 0 750 220" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="st-arrow" viewBox="0 0 8 6" refX="8" refY="3" markerWidth="8" markerHeight="6" orient="auto-start-auto">
<path d="M0,0 L8,3 L0,6z" fill="#7B42BC"/>
</marker>
</defs>
<!-- Left: HCL files -->
<g transform="translate(30,18)">
<rect width="80" height="48" rx="6" fill="#f6f0ff" stroke="#7B42BC" stroke-width="1.5"/>
<text x="40" y="44" text-anchor="middle" font-family="monospace" font-size="9" fill="#4d5592">main.tf</text>
</g>
<g transform="translate(30,85)">
<rect width="80" height="48" rx="6" fill="#f6f0ff" stroke="#7B42BC" stroke-width="1.5"/>
<text x="40" y="44" text-anchor="middle" font-family="monospace" font-size="9" fill="#4d5592">network.tf</text>
</g>
<g transform="translate(30,152)">
<rect width="80" height="48" rx="6" fill="#f6f0ff" stroke="#7B42BC" stroke-width="1.5"/>
<text x="40" y="44" text-anchor="middle" font-family="monospace" font-size="9" fill="#4d5592">dns.tf</text>
</g>
<text x="70" y="215" text-anchor="middle" font-family="system-ui,sans-serif" font-size="11" fill="#666666">Configuration</text>
<!-- Lines to center -->
<line x1="115" y1="42" x2="280" y2="88" stroke="#7B42BC" stroke-width="1.4" stroke-dasharray="4,3" marker-end="url(#st-arrow)"/>
<line x1="115" y1="109" x2="280" y2="108" stroke="#7B42BC" stroke-width="1.4" stroke-dasharray="4,3" marker-end="url(#st-arrow)"/>
<line x1="115" y1="176" x2="280" y2="128" stroke="#7B42BC" stroke-width="1.4" stroke-dasharray="4,3" marker-end="url(#st-arrow)"/>
<!-- Center: .tfstate cylinder -->
<g transform="translate(285,42)">
<rect y="18" width="180" height="100" fill="#f6f0ff" stroke="#7B42BC" stroke-width="2"/>
<ellipse cx="90" cy="18" rx="90" ry="18" fill="#f6f0ff" stroke="#7B42BC" stroke-width="2"/>
<path d="M0,118 Q0,136 90,136 Q180,136 180,118" fill="#f6f0ff" stroke="#7B42BC" stroke-width="2"/>
<rect x="1" y="100" width="178" height="19" fill="#f6f0ff" stroke="none"/>
<line x1="0" y1="18" x2="0" y2="118" stroke="#7B42BC" stroke-width="2"/>
<line x1="180" y1="18" x2="180" y2="118" stroke="#7B42BC" stroke-width="2"/>
<text x="90" y="78" text-anchor="middle" font-family="monospace" font-size="20" font-weight="700" fill="#00185e">.tfstate</text>
<text x="90" y="98" text-anchor="middle" font-family="system-ui,sans-serif" font-size="11" fill="#4d5592">state mapping</text>
<g transform="translate(72,-8)">
<rect x="8" y="12" width="20" height="15" rx="3" fill="#FFD54F" stroke="#E65100" stroke-width="1.3"/>
<path d="M12,12 L12,7 Q12,1 18,1 Q24,1 24,7 L24,12" fill="none" stroke="#E65100" stroke-width="1.5"/>
<circle cx="18" cy="19" r="2" fill="#E65100"/>
</g>
</g>
<text x="375" y="215" text-anchor="middle" font-family="system-ui,sans-serif" font-size="11" fill="#666666">Source of Truth</text>
<!-- Lines to right -->
<line x1="470" y1="88" x2="595" y2="42" stroke="#00a344" stroke-width="1.4" stroke-dasharray="4,3" marker-end="url(#st-arrow)"/>
<line x1="470" y1="108" x2="595" y2="109" stroke="#00a344" stroke-width="1.4" stroke-dasharray="4,3" marker-end="url(#st-arrow)"/>
<line x1="470" y1="128" x2="595" y2="176" stroke="#00a344" stroke-width="1.4" stroke-dasharray="4,3" marker-end="url(#st-arrow)"/>
<!-- Right: cloud resources -->
<g transform="translate(600,18)">
<rect width="80" height="48" rx="6" fill="#e8f5e9" stroke="#00a344" stroke-width="1.5"/>
<text x="40" y="42" text-anchor="middle" font-family="system-ui,sans-serif" font-size="10" font-weight="600" fill="#00185e">VM</text>
</g>
<g transform="translate(600,85)">
<rect width="80" height="48" rx="6" fill="#e8f5e9" stroke="#00a344" stroke-width="1.5"/>
<text x="40" y="42" text-anchor="middle" font-family="system-ui,sans-serif" font-size="10" font-weight="600" fill="#00185e">VNet</text>
</g>
<g transform="translate(600,152)">
<rect width="80" height="48" rx="6" fill="#e8f5e9" stroke="#00a344" stroke-width="1.5"/>
<text x="40" y="42" text-anchor="middle" font-family="system-ui,sans-serif" font-size="10" font-weight="600" fill="#00185e">DNS Zone</text>
</g>
<text x="640" y="215" text-anchor="middle" font-family="system-ui,sans-serif" font-size="11" fill="#666666">Real Resources</text>
</svg>
</section>
<!-- ─── SLIDE 17 : Terraform providers ──────────────────────────── -->
<section class="s-tf">
<h2>One tool. Every API.</h2>
<ul>
<li><strong>1000+ providers</strong> — OVHcloud, Scaleway, Clever Cloud, AWS, Cloudflare, Kubernetes…</li>
<li>Not just cloud — DNS, monitoring, CI/CD, anything with an API</li>
</ul>
<p><em>If it has an API, there's a Terraform provider for it.</em></p>
<aside class="notes">
<ul>
<li>The real power: one workflow for everything, not just one cloud.</li>
<li>Examples beyond cloud: DNS records, GitHub repos, monitoring dashboards.</li>
<li>Providers are the plugin ecosystem that makes Terraform universal.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 18 : Terraform code ───────────────────────────────── -->
<section class="s-tf">
<p class="filename"># main.tf</p>
<pre><code class="language-hcl" data-trim>
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
}
</code></pre>
<aside class="notes">
<ul>
<li>Real OVHcloud example: spin up an instance, then point a DNS record at it.</li>
<li>Note the implicit dependency — the DNS record references the instance's IP, so Terraform orders them automatically.</li>
<li>Two different providers (OpenStack + OVH) working together in one file.</li>
<li>Version syntax: <code>~&gt;</code> is the pessimistic constraint operator. <code>~&gt; 2.0</code> allows any 2.x (&gt;= 2.0, &lt; 3.0) — patch and minor updates, but never a breaking major bump. <code>~&gt; 2.13.0</code> would pin tighter, allowing only 2.13.x.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 19 : Terraform CLI ────────────────────────────────── -->
<section class="s-tf">
<p class="filename">$ terminal</p>
<pre><code class="language-bash" data-trim data-noescape>
$ 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.
</code></pre>
<aside class="notes">
<ul>
<li>Walk the lifecycle: init (download providers) → plan → apply.</li>
<li>"2 added, 0 changed, 0 destroyed" — exactly the diff you reviewed, nothing more.</li>
<li>Re-running apply with no changes does nothing — that's idempotence.</li>
<li>Each <code>...</code> on the slide hides real output — call it out so nobody thinks Terraform is this terse.</li>
<li>The <code>...</code> after <code>plan</code>: a full per-resource <code>+</code>/<code>-</code> diff of every attribute Terraform will set — this is what you actually review.</li>
<li>The <code>...</code> after <code>apply</code>: Terraform replays that same plan, then pauses for an interactive "yes" prompt — skip it with <code>-auto-approve</code> in CI.</li>
<li>I also trimmed init: real output adds backend init, "Installed … (signed by …)" lines, and the <code>.terraform.lock.hcl</code> being written.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 20 : OpenTofu ─────────────────────────────────────── -->
<section class="s-tf">
<h2>HashiCorp changed Terraform's license.</h2>
<ul>
<li><strong>BUSL 1.1</strong> instead of MPL — no longer truly open-source</li>
<li>The community responded: <strong>OpenTofu</strong>, now a CNCF project</li>
</ul>
<p><em>Drop-in for migration. Diverging features. Community-driven.</em></p>
<aside class="notes">
<ul>
<li>2023: HashiCorp moved to BUSL 1.1 — restricts commercial competitors, not truly open-source anymore.</li>
<li>Community forked it: OpenTofu, accepted into the Linux Foundation in 2023, a CNCF project since 2025.</li>
<li>Practical takeaway: drop-in compatible, swap the binary. Worth knowing for licensing-sensitive orgs.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 21 : Terraform platforms ──────────────────────────── -->
<section class="s-tf">
<h2>Terraform at scale needs a platform.</h2>
<ul>
<li><strong>Terraform Enterprise / HCP Terraform</strong> — remote state, RBAC, audit logs</li>
<li><strong>Spacelift</strong> — GitOps-first CI/CD for Terraform and OpenTofu</li>
<li><strong>Atlantis</strong> — open-source, plan &amp; apply from pull requests</li>
<li><strong>env0, Scalr</strong> — SaaS with policy &amp; cost management</li>
</ul>
<aside class="notes">
<ul>
<li>Running Terraform from a laptop doesn't scale to a team.</li>
<li>You need: shared/locked state, RBAC, audit, plan-on-PR, policy & cost guardrails.</li>
<li>Range from SaaS (HCP, Spacelift, env0, Scalr) to self-hosted open-source (Atlantis).</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 22 : Ansible intro ────────────────────────────────── -->
<section class="s-ansible">
<h2><span class="ansible-col">Ansible</span></h2>
<p>Your servers are provisioned. Now make them do something.</p>
<aside class="notes">
<ul>
<li>Second layer: configuration. The VMs exist — now install and set things up.</li>
<li>Transition: Terraform built the box, Ansible makes it useful.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 23 : What is Ansible ──────────────────────────────── -->
<section class="s-ansible">
<h2>What is Ansible?</h2>
<ul>
<li><strong>Agentless automation</strong> tool for configuration and orchestration</li>
<li>Created by <strong>Michael DeHaan</strong> in 2012</li>
<li>Acquired by <strong>Red Hat</strong> in 2015</li>
<li>Written in <strong>Python</strong> — GPLv3 license</li>
</ul>
<aside class="notes">
<ul>
<li>Key differentiator: agentless — nothing to install on targets.</li>
<li>Red Hat backed, Python, genuinely open-source (GPLv3) — contrast with Terraform's BUSL.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 24 : Ansible concepts ─────────────────────────────── -->
<section class="s-ansible">
<h2>Push-based. Runs over SSH.</h2>
<ul>
<li>YAML playbooks run tasks in order, across any number of hosts</li>
<li>Nothing to install on target servers — just <strong>Python + SSH</strong></li>
<li>Idempotent modules — same playbook runs safely again and again</li>
</ul>
<aside class="notes">
<ul>
<li>Push model: control node connects out over SSH and runs tasks — point at the diagram.</li>
<li>"Agentless" = just needs Python + SSH on the target, no daemon.</li>
<li>Idempotent modules: re-running is safe, only changes what's needed.</li>
</ul>
</aside>
<svg width="750" height="250" viewBox="0 0 750 250" xmlns="http://www.w3.org/2000/svg" style="margin-top:0.5em;">
<defs>
<marker id="a-push" viewBox="0 0 10 7" refX="9" refY="3.5" markerWidth="9" markerHeight="7" orient="auto">
<polygon points="0 0,10 3.5,0 7" fill="#CC0000"/>
</marker>
</defs>
<!-- Laptop -->
<rect x="50" y="68" width="100" height="65" rx="6" fill="#2b2b3a"/>
<rect x="56" y="73" width="88" height="48" rx="3" fill="#14142a"/>
<text x="100" y="103" text-anchor="middle" fill="#5cf" font-family="monospace" font-size="13">&gt;_</text>
<rect x="38" y="135" width="124" height="9" rx="4" fill="#444"/>
<text x="100" y="164" text-anchor="middle" fill="#4d5592" font-size="13" font-weight="bold" font-family="sans-serif">Control Node</text>
<!-- PUSH badge -->
<rect x="310" y="10" width="80" height="28" rx="14" fill="#CC0000"/>
<text x="350" y="30" text-anchor="middle" fill="#fff" font-size="14" font-weight="bold" font-family="sans-serif">PUSH</text>
<!-- Arrows -->
<line x1="170" y1="100" x2="548" y2="30" stroke="#CC0000" stroke-width="2" marker-end="url(#a-push)"/>
<line x1="170" y1="100" x2="548" y2="72" stroke="#CC0000" stroke-width="2" marker-end="url(#a-push)"/>
<line x1="170" y1="100" x2="548" y2="117" stroke="#CC0000" stroke-width="2" marker-end="url(#a-push)"/>
<line x1="170" y1="100" x2="548" y2="162" stroke="#CC0000" stroke-width="2" marker-end="url(#a-push)"/>
<line x1="170" y1="100" x2="548" y2="207" stroke="#CC0000" stroke-width="2" marker-end="url(#a-push)"/>
<!-- SSH label -->
<rect x="310" y="46" width="42" height="18" rx="3" fill="#fff" fill-opacity="0.85"/>
<text x="331" y="59" text-anchor="middle" fill="#CC0000" font-size="11" font-weight="bold" font-family="monospace">SSH</text>
<!-- Servers -->
<g fill="#f2f2f8" stroke="#aaa">
<g transform="translate(555,16)"><rect width="120" height="28" rx="4"/><circle cx="105" cy="19" r="3" fill="#5cf"/></g>
<g transform="translate(555,58)"><rect width="120" height="28" rx="4"/><circle cx="105" cy="19" r="3" fill="#5cf"/></g>
<g transform="translate(555,103)"><rect width="120" height="28" rx="4"/><circle cx="105" cy="19" r="3" fill="#5cf"/></g>
<g transform="translate(555,148)"><rect width="120" height="28" rx="4"/><circle cx="105" cy="19" r="3" fill="#5cf"/></g>
<g transform="translate(555,193)"><rect width="120" height="28" rx="4"/><circle cx="105" cy="19" r="3" fill="#5cf"/></g>
</g>
<text x="615" y="245" text-anchor="middle" fill="#4d5592" font-size="12" font-family="sans-serif">Managed Hosts</text>
<text x="615" y="12" text-anchor="middle" fill="#999" font-size="10" font-style="italic" font-family="sans-serif">no agent required</text>
</svg>
</section>
<!-- ─── SLIDE 25 : Ansible code ─────────────────────────────────── -->
<section class="s-ansible">
<p class="filename"># playbook/webserver.yml</p>
<pre><code class="language-yaml" data-trim>
- 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
</code></pre>
<aside class="notes">
<ul>
<li>Plain YAML — tasks run top to bottom against the "webservers" group.</li>
<li>become: true = run as root (sudo).</li>
<li>Handlers are the neat bit: restart nginx only if the config actually changed.</li>
<li>The <code>.j2</code> in <code>nginx.conf.j2</code> = Jinja2, Ansible's templating engine. The template module renders it on the control node — <code>{{ variables }}</code>, <code>{% if %}</code>/<code>{% for %}</code> logic, filters like <code>{{ value | default(...) }}</code> — then ships the result to <code>dest</code>.</li>
<li>That's how one template serves many hosts: same file, per-host values from inventory/group_vars.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 26 : Ansible inventory ────────────────────────────── -->
<section class="s-ansible">
<h2>Who runs where? The inventory.</h2>
<ul>
<li><strong>Static</strong> — a hand-written INI/YAML file of hosts &amp; groups. Simple, versioned, ideal for stable fleets.</li>
<li><strong>Dynamic</strong> — a plugin builds the host list at runtime. Best source here: <strong>the Terraform state we just wrote</strong> — Ansible configures exactly what Terraform provisioned.</li>
</ul>
<div style="display:flex; gap:1.5em; margin-top:0.3em; text-align:left;">
<div style="flex:1;">
<p class="filename"># inventory.yml — static</p>
<pre><code class="language-yaml" data-trim>
webservers:
hosts:
web-01:
ansible_host: 10.0.0.11
web-02:
ansible_host: 10.0.0.12
</code></pre>
</div>
<div style="flex:1;">
<p class="filename"># terraform.yml — dynamic (from TF state)</p>
<pre><code class="language-yaml" data-trim>
plugin: cloud.terraform.terraform_state
backend_type: local
backend_config:
path: ../terraform/terraform.tfstate
</code></pre>
</div>
</div>
<aside class="notes">
<ul>
<li>The <code>-i</code> flag on the next slide points here — Ansible needs to know which hosts to target.</li>
<li><strong>Static</strong>: explicit list checked into Git. Predictable, but you maintain it by hand.</li>
<li><strong>This is the Terraform → Ansible handoff</strong>: Terraform creates the instance and records it in <code>.tfstate</code> (remember slide 16); the <code>cloud.terraform.terraform_state</code> 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.</li>
<li>Other dynamic sources exist too — cloud plugins like <code>openstack.cloud.openstack</code> or AWS/OVHcloud query the provider API directly. <code>keyed_groups</code> build groups from tags/metadata.</li>
<li>Provision with Terraform, then immediately configure with Ansible against the freshly-created hosts — that's the combined workflow we land on at the end.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 27 : Ansible CLI ──────────────────────────────────── -->
<section class="s-ansible">
<p class="filename">$ terminal</p>
<pre><code class="language-bash" data-trim data-noescape>
$ 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
</code></pre>
<aside class="notes">
<ul>
<li>"ok" vs "changed" — ok means already in desired state, changed means it acted. That's idempotence visible in the output.</li>
<li>The handler only fired because the config task reported "changed".</li>
<li>Run it again and everything would be "ok", changed=0.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 28 : Ansible operations ───────────────────────────── -->
<section class="s-ansible">
<h2>Not just configuration. Operations.</h2>
<ul>
<li>Patch 200 servers tonight</li>
<li>Roll out a kernel upgrade with a canary strategy</li>
<li>Run a compliance audit across your whole fleet</li>
</ul>
<p><em>The go-to tool for one-off tasks and recurring operations.</em></p>
<aside class="notes">
<ul>
<li>Ansible isn't only "set up a server once" — it shines for ad-hoc operations.</li>
<li>Imperative/orchestration angle: patching, rolling upgrades, canary, audits across the fleet.</li>
<li>This is where it differs most from Puppet's "always converge" model.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 29 : Ansible Galaxy ───────────────────────────────── -->
<section class="s-ansible">
<h2>The community does the heavy lifting.</h2>
<ul>
<li><strong>Ansible Galaxy</strong> — 10,000+ ready-made roles and collections</li>
<li>Don't write a playbook to install Docker from scratch — someone already did</li>
</ul>
<pre><code class="language-bash" data-trim>
$ ansible-galaxy install &lt;namespace&gt;.&lt;role&gt;
</code></pre>
<aside class="notes">
<ul>
<li>Galaxy = the package registry for reusable roles/collections.</li>
<li>Don't reinvent common setups — pull a battle-tested role (geerlingguy is the famous example).</li>
<li>Huge productivity multiplier — but it's a supply chain: roles run with privilege on your hosts.</li>
<li>Prefer trusted sources: Red Hat <strong>Certified</strong> collections and verified publishers (Automation Hub), or well-known community authors like geerlingguy. Be wary of unmaintained, low-download, single-author roles.</li>
<li>Pin versions in <code>requirements.yml</code> and skim the code before importing — treat it like any other dependency.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 30 : Ansible platforms ────────────────────────────── -->
<section class="s-ansible">
<h2>Ansible at scale: open-source vs enterprise.</h2>
<ul>
<li><strong>AWX</strong> — open-source web UI, API, and scheduler</li>
<li><strong>Ansible Automation Platform</strong> (Red Hat) — enterprise AWX with support</li>
<li><strong>Semaphore</strong> — lightweight open-source alternative</li>
</ul>
<p><em>Core engine remains GPLv3 — truly open-source.</em></p>
<aside class="notes">
<ul>
<li>At scale you want a UI/scheduler/RBAC layer on top of the CLI.</li>
<li>AWX (free) → Ansible Automation Platform (Red Hat, supported) is the main path; Semaphore is a lighter option.</li>
<li>Reassure: the core stays GPLv3 — no license rug-pull like Terraform.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 31 : Puppet intro ─────────────────────────────────── -->
<section class="s-puppet">
<h2><span class="puppet-col">Puppet</span></h2>
<p>Your servers are configured. Now keep them that way.</p>
<aside class="notes">
<ul>
<li>Third layer: enforcement. Configured once isn't enough — drift creeps back.</li>
<li>Puppet's job: keep state correct continuously, forever.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 32 : What is Puppet ───────────────────────────────── -->
<section class="s-puppet">
<h2>What is Puppet?</h2>
<ul>
<li><strong>Configuration management</strong> tool for enforcing system state</li>
<li>Created by <strong>Luke Kanies</strong> in 2005</li>
<li>Puppet Inc. acquired by <strong>Perforce</strong> in 2022</li>
<li>Written in <strong>Ruby</strong> and <strong>Clojure</strong></li>
</ul>
<aside class="notes">
<ul>
<li>The oldest of the three (2005) — pioneered config management.</li>
<li>Now owned by Perforce. We'll touch on what that means for the community later.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 33 : Puppet concepts ──────────────────────────────── -->
<section class="s-puppet">
<h2>Pull, not push. Agents, not SSH.</h2>
<ul>
<li>Every 30 minutes, each <code>puppet-agent</code> polls the Puppet Server</li>
<li>Compiles a catalog and enforces it locally</li>
</ul>
<p><em>Drift is corrected automatically.</em></p>
<aside class="notes">
<ul>
<li>Opposite model to Ansible: pull, not push — agents reach out to the server.</li>
<li>Every ~30 min the agent fetches a catalog and converges the node — point at the diagram.</li>
<li>This is the key idea: enforcement runs on a loop, not just at deploy.</li>
</ul>
</aside>
<svg width="750" height="250" viewBox="0 0 750 250" xmlns="http://www.w3.org/2000/svg" style="margin-top:0.5em;">
<defs>
<marker id="a-pull" viewBox="0 0 10 7" refX="9" refY="3.5" markerWidth="9" markerHeight="7" orient="auto">
<polygon points="0 0,10 3.5,0 7" fill="#A06010"/>
</marker>
</defs>
<!-- Puppet Server -->
<rect x="290" y="28" width="170" height="60" rx="8" fill="#fdf4e8" stroke="#A06010" stroke-width="2"/>
<rect x="300" y="40" width="36" height="36" rx="4" fill="#f5e6d0" stroke="#A06010"/>
<line x1="306" y1="50" x2="324" y2="50" stroke="#c08030"/><line x1="306" y1="57" x2="324" y2="57" stroke="#c08030"/><line x1="306" y1="64" x2="324" y2="64" stroke="#c08030"/>
<rect x="346" y="42" width="24" height="30" rx="2" fill="#fff" stroke="#A06010"/>
<text x="380" y="53" fill="#A06010" font-size="9" font-family="sans-serif">catalog</text>
<text x="375" y="20" text-anchor="middle" fill="#4d5592" font-size="13" font-weight="bold" font-family="sans-serif">Puppet Server</text>
<!-- Clock -->
<circle cx="540" cy="40" r="18" fill="none" stroke="#A06010" stroke-width="1.5"/>
<line x1="540" y1="40" x2="540" y2="28" stroke="#A06010" stroke-width="1.5"/>
<line x1="540" y1="40" x2="550" y2="40" stroke="#A06010" stroke-width="1.5"/>
<text x="540" y="72" text-anchor="middle" fill="#A06010" font-size="10" font-family="sans-serif">every 30 min</text>
<!-- Pull arrows -->
<line x1="95" y1="160" x2="330" y2="92" stroke="#A06010" stroke-width="1.8" stroke-dasharray="6,4" marker-end="url(#a-pull)"/>
<line x1="225" y1="160" x2="350" y2="92" stroke="#A06010" stroke-width="1.8" stroke-dasharray="6,4" marker-end="url(#a-pull)"/>
<line x1="375" y1="160" x2="375" y2="92" stroke="#A06010" stroke-width="1.8" stroke-dasharray="6,4" marker-end="url(#a-pull)"/>
<line x1="525" y1="160" x2="400" y2="92" stroke="#A06010" stroke-width="1.8" stroke-dasharray="6,4" marker-end="url(#a-pull)"/>
<line x1="655" y1="160" x2="420" y2="92" stroke="#A06010" stroke-width="1.8" stroke-dasharray="6,4" marker-end="url(#a-pull)"/>
<rect x="420" y="112" width="38" height="17" rx="3" fill="#fff" fill-opacity="0.9"/>
<text x="439" y="124" text-anchor="middle" fill="#A06010" font-size="11" font-weight="bold" font-style="italic" font-family="sans-serif">pull</text>
<!-- Agent nodes -->
<g transform="translate(50,160)"><rect width="90" height="34" rx="4" fill="#f2f2f8" stroke="#aaa"/><circle cx="76" cy="22" r="3" fill="#5cf"/><rect x="26" y="36" width="38" height="14" rx="3" fill="#A06010"/><text x="45" y="47" text-anchor="middle" fill="#fff" font-size="8" font-family="sans-serif">agent</text></g>
<g transform="translate(180,160)"><rect width="90" height="34" rx="4" fill="#f2f2f8" stroke="#aaa"/><circle cx="76" cy="22" r="3" fill="#5cf"/><rect x="26" y="36" width="38" height="14" rx="3" fill="#A06010"/><text x="45" y="47" text-anchor="middle" fill="#fff" font-size="8" font-family="sans-serif">agent</text></g>
<g transform="translate(330,160)"><rect width="90" height="34" rx="4" fill="#f2f2f8" stroke="#aaa"/><circle cx="76" cy="22" r="3" fill="#5cf"/><rect x="26" y="36" width="38" height="14" rx="3" fill="#A06010"/><text x="45" y="47" text-anchor="middle" fill="#fff" font-size="8" font-family="sans-serif">agent</text></g>
<g transform="translate(480,160)"><rect width="90" height="34" rx="4" fill="#f2f2f8" stroke="#aaa"/><circle cx="76" cy="22" r="3" fill="#5cf"/><rect x="26" y="36" width="38" height="14" rx="3" fill="#A06010"/><text x="45" y="47" text-anchor="middle" fill="#fff" font-size="8" font-family="sans-serif">agent</text></g>
<g transform="translate(610,160)"><rect width="90" height="34" rx="4" fill="#f2f2f8" stroke="#aaa"/><circle cx="76" cy="22" r="3" fill="#5cf"/><rect x="26" y="36" width="38" height="14" rx="3" fill="#A06010"/><text x="45" y="47" text-anchor="middle" fill="#fff" font-size="8" font-family="sans-serif">agent</text></g>
<text x="375" y="240" text-anchor="middle" fill="#4d5592" font-size="12" font-family="sans-serif">Managed Nodes</text>
</svg>
</section>
<!-- ─── SLIDE 34 : Puppet code ──────────────────────────────────── -->
<section class="s-puppet">
<p class="filename"># manifests/webserver.pp</p>
<pre><code class="language-puppet" data-trim>
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,
}
}
</code></pre>
<aside class="notes">
<ul>
<li>Same nginx example as Ansible — compare the styles side by side.</li>
<li>Pure declarative: describe resources (package, file, service) and desired state, not steps.</li>
<li>notify chains the dependency: config change → restart service.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 35 : Puppet CLI ───────────────────────────────────── -->
<section class="s-puppet">
<p class="filename">$ terminal</p>
<pre><code class="language-bash" data-trim data-noescape>
$ 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
</code></pre>
<aside class="notes">
<ul>
<li>Agent run: fetch catalog → compare to actual → apply only the diffs.</li>
<li>This normally runs automatically every 30 min; -t is a manual trigger for demo.</li>
<li>Next run with no drift would report no changes.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 36 : Puppet drift detection ───────────────────────── -->
<section class="s-puppet">
<h2>Someone SSH'd in and changed something.</h2>
<ul>
<li>Puppet noticed. Puppet fixed it.</li>
<li>Continuous compliance — not just at deploy time. <strong>Every. 30. Minutes.</strong></li>
<li>No manual remediation</li>
</ul>
<aside class="notes">
<ul>
<li>The killer feature: self-healing. Someone hand-edits a file → Puppet reverts it next run.</li>
<li>Walk the loop in the diagram: drift detected → agent applies → compliant → repeat.</li>
<li>This is what "continuous compliance" means in practice.</li>
</ul>
</aside>
<svg width="750" height="200" viewBox="0 0 750 200" xmlns="http://www.w3.org/2000/svg" style="margin-top:0.5em;">
<defs>
<marker id="a-cycle" viewBox="0 0 10 7" refX="9" refY="3.5" markerWidth="9" markerHeight="7" orient="auto">
<polygon points="0 0,10 3.5,0 7" fill="#4d5592"/>
</marker>
</defs>
<!-- 1: Drift detected -->
<rect x="30" y="50" width="190" height="80" rx="12" fill="#fde8ec" stroke="#e00034" stroke-width="1.5"/>
<polygon points="70,68 80,88 60,88" fill="none" stroke="#e00034" stroke-width="2" stroke-linejoin="round"/>
<text x="70" y="84" text-anchor="middle" fill="#e00034" font-size="11" font-weight="bold" font-family="sans-serif">!</text>
<text x="135" y="84" text-anchor="middle" fill="#e00034" font-size="13" font-weight="bold" font-family="sans-serif">Drift</text>
<text x="135" y="100" text-anchor="middle" fill="#e00034" font-size="13" font-weight="bold" font-family="sans-serif">detected</text>
<circle cx="42" cy="56" r="10" fill="#e00034"/><text x="42" y="60" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold" font-family="sans-serif">1</text>
<!-- 2: Agent applies catalog -->
<rect x="520" y="50" width="200" height="80" rx="12" fill="#fdf4e8" stroke="#A06010" stroke-width="1.5"/>
<circle cx="565" cy="85" r="13" fill="none" stroke="#A06010" stroke-width="2"/>
<circle cx="565" cy="85" r="5" fill="#A06010"/>
<text x="632" y="84" text-anchor="middle" fill="#A06010" font-size="13" font-weight="bold" font-family="sans-serif">Agent</text>
<text x="632" y="100" text-anchor="middle" fill="#A06010" font-size="13" font-weight="bold" font-family="sans-serif">applies catalog</text>
<circle cx="532" cy="56" r="10" fill="#A06010"/><text x="532" y="60" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold" font-family="sans-serif">2</text>
<!-- 3: Compliant -->
<rect x="275" y="140" width="200" height="50" rx="12" fill="#e6f7ee" stroke="#00a344" stroke-width="1.5"/>
<polyline points="310,164 318,174 332,156" fill="none" stroke="#00a344" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<text x="395" y="171" text-anchor="middle" fill="#00a344" font-size="15" font-weight="bold" font-family="sans-serif">Compliant</text>
<circle cx="287" cy="146" r="10" fill="#00a344"/><text x="287" y="150" text-anchor="middle" fill="#fff" font-size="10" font-weight="bold" font-family="sans-serif">3</text>
<!-- Curved arrows -->
<path d="M 220,72 C 330,20 420,20 518,72" fill="none" stroke="#4d5592" stroke-width="2" marker-end="url(#a-cycle)"/>
<path d="M 600,132 C 580,155 530,168 477,165" fill="none" stroke="#4d5592" stroke-width="2" marker-end="url(#a-cycle)"/>
<path d="M 275,165 C 210,168 140,155 120,132" fill="none" stroke="#4d5592" stroke-width="2" marker-end="url(#a-cycle)"/>
<text x="375" y="22" text-anchor="middle" fill="#4d5592" font-size="10" font-family="sans-serif">continuous enforcement loop</text>
</svg>
</section>
<!-- ─── SLIDE 37 : Puppet platforms ─────────────────────────────── -->
<section class="s-puppet">
<h2>Puppet: large fleets, zero drift.</h2>
<ul>
<li>Continuous compliance, auditability, and guaranteed state — at scale</li>
<li>Best suited for enterprises with hundreds or thousands of long-lived servers</li>
<li>Fewer SaaS options than Terraform or Ansible</li>
</ul>
<p><em>Puppet Enterprise and Foreman are self-hosted. No managed cloud offering.</em></p>
<aside class="notes">
<ul>
<li>Sweet spot: large fleets of long-lived servers where drift control matters.</li>
<li>Trade-off vs the others: fewer SaaS options, more setup — it's self-hosted.</li>
<li>Honest framing: overkill for a handful of ephemeral cloud VMs.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 38 : Puppet community ─────────────────────────────── -->
<section class="s-puppet">
<h2>The ecosystem outlives the company.</h2>
<ul>
<li><strong>Vox Pupuli</strong> — 100+ open-source Puppet modules, community-maintained</li>
<li><strong>OpenVox</strong> — an emerging open-source fork of the Puppet core</li>
</ul>
<p><em>The community is strong, with or without Puppet Inc.</em></p>
<aside class="notes">
<ul>
<li>Addresses the "is Puppet dying?" worry after the Perforce acquisition.</li>
<li>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.</li>
<li>Vox Pupuli couldn't accept the Puppet Core Developer EULA — its restrictions block testing and redistribution of the community modules.</li>
<li>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.)</li>
<li>Reassurance: the open ecosystem outlives any single vendor.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 39 : They're complementary ────────────────────────── -->
<section>
<h2>They're not competing. They're complementary.</h2>
<p>Each solves a different layer of the same problem.</p>
<aside class="notes">
<ul>
<li>The payoff slide — the "vs" framing was a trap. They stack.</li>
<li>Terraform provisions → Ansible configures → Puppet enforces. Three layers.</li>
<li>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.</li>
<li>"Which should I use?" → depends which layer of the problem you have.</li>
</ul>
</aside>
<svg width="800" height="280" viewBox="0 0 800 280" xmlns="http://www.w3.org/2000/svg" style="margin-top:0.5em;">
<defs>
<marker id="a-down" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="9" markerHeight="9" orient="auto">
<polygon points="0 0,10 5,0 10" fill="#4d5592"/>
</marker>
</defs>
<!-- Terraform layer -->
<rect x="120" y="10" width="560" height="64" rx="10" fill="#7B42BC" fill-opacity="0.12" stroke="#7B42BC" stroke-width="2"/>
<text x="220" y="38" fill="#7B42BC" font-size="18" font-weight="bold" font-family="sans-serif">Terraform</text>
<text x="338" y="38" fill="#5a2e8c" font-size="16" font-family="sans-serif">— Provision</text>
<text x="220" y="56" fill="#8855cc" font-size="11" font-style="italic" font-family="sans-serif">VMs, networks, cloud resources</text>
<!-- Arrow between Terraform and Ansible -->
<line x1="400" y1="78" x2="400" y2="100" stroke="#4d5592" stroke-width="2.5" marker-end="url(#a-down)"/>
<!-- Ansible layer -->
<rect x="120" y="104" width="560" height="64" rx="10" fill="#CC0000" fill-opacity="0.1" stroke="#CC0000" stroke-width="2"/>
<text x="220" y="132" fill="#CC0000" font-size="18" font-weight="bold" font-family="sans-serif">Ansible</text>
<text x="306" y="132" fill="#a00" font-size="16" font-family="sans-serif">— Configure</text>
<text x="220" y="150" fill="#cc3333" font-size="11" font-style="italic" font-family="sans-serif">packages, services, deploys, on-demand patching</text>
<!-- Arrow between Ansible and Puppet -->
<line x1="400" y1="172" x2="400" y2="194" stroke="#4d5592" stroke-width="2.5" marker-end="url(#a-down)"/>
<!-- Puppet layer -->
<rect x="120" y="198" width="560" height="64" rx="10" fill="#A06010" fill-opacity="0.1" stroke="#A06010" stroke-width="2"/>
<text x="220" y="228" fill="#A06010" font-size="18" font-weight="bold" font-family="sans-serif">Puppet</text>
<text x="298" y="228" fill="#8a5010" font-size="16" font-family="sans-serif">— Enforce</text>
<text x="220" y="246" fill="#b07020" font-size="11" font-style="italic" font-family="sans-serif">continuous compliance, drift correction</text>
</svg>
</section>
<!-- ─── SLIDE 40 : Real-world stack ─────────────────────────────── -->
<section>
<h2>A common production setup:</h2>
<ol>
<li><span class="tf-col">Terraform</span> provisions the VM</li>
<li><span class="ansible-col">Ansible</span> configures it, deploys the app, and pushes one-off patches</li>
<li><span class="puppet-col">Puppet</span> continuously enforces compliance</li>
</ol>
<aside class="notes">
<ul>
<li>Concrete recap of how they fit together end to end.</li>
<li>You don't have to use all three — but they layer cleanly when you do.</li>
<li>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.</li>
<li>Pick by your actual need: just provisioning? Terraform. Ad-hoc ops / one-off patch? Ansible. Drift control? Puppet.</li>
</ul>
</aside>
</section>
<!-- ─── SLIDE 41 : Closing ──────────────────────────────────────── -->
<section class="title-slide">
<h1>Questions?</h1>
<p class="subtitle">Thank you!</p>
<p class="meta" style="margin-top:2em;">
Arnaud Prémel-Cabic &nbsp;·&nbsp; arnaud.premel-cabic@ovhcloud.com<br>
Slides: <a href="https://ministicraft.pages.git.cloud.arnaud-pc.fr/finistdev-configuration-as-code/" target="_blank">ministicraft.pages.git.cloud.arnaud-pc.fr/finistdev-configuration-as-code/</a>
</p>
<p class="meta" style="margin-top:1.5em; font-size:0.45em; color:var(--ods-neutral-600);">🤖 Made with Claude &amp; GitHub Copilot</p>
<aside class="notes">
<ul>
<li>Thank the audience, point to the slides URL.</li>
<li>Open the floor for questions.</li>
<li>Backup topics if quiet: Kubernetes operators, Pulumi, secrets management.</li>
</ul>
</aside>
</section>
</div>
</div>
<script src="vendor/reveal.js/dist/reveal.js"></script>
<script src="vendor/reveal.js/plugin/notes/notes.js"></script>
<script src="vendor/reveal.js/plugin/highlight/highlight.js"></script>
<script>
const toolLogos = {
's-tf': 'https://cdn.simpleicons.org/terraform',
's-ansible': 'https://cdn.simpleicons.org/ansible',
's-puppet': 'https://cdn.simpleicons.org/puppet/C17F00'
};
const altNames = {'s-tf': 'Terraform', 's-ansible': 'Ansible', 's-puppet': 'Puppet'};
// Create fixed logo elements outside Reveal (like OVHcloud logo)
const logoElements = {};
Object.entries(toolLogos).forEach(([cls, src]) => {
const img = document.createElement('img');
img.src = src;
img.alt = altNames[cls];
img.className = 'tool-logo-global';
document.body.appendChild(img);
logoElements[cls] = img;
});
// Show/hide the correct tool logo based on current slide's class
function updateToolLogo() {
const slide = Reveal.getCurrentSlide();
Object.entries(logoElements).forEach(([cls, img]) => {
img.style.display = slide && slide.classList.contains(cls) ? 'block' : 'none';
});
}
Reveal.initialize({
width: 1280,
height: 720,
margin: 0.04,
minScale: 0.2,
maxScale: 2.0,
hash: true,
slideNumber: 'c/t',
transition: 'slide',
backgroundTransition: 'fade',
controls: true,
progress: true,
center: true,
plugins: [ RevealNotes, RevealHighlight ]
}).then(updateToolLogo);
Reveal.on('slidechanged', updateToolLogo);
</script>
</body>
</html>