{
    "componentChunkName": "component---src-templates-blog-post-js",
    "path": "/i-ran-terraform-destroy-on-my-own-production-on-purpose/",
    "result": {"data":{"site":{"siteMetadata":{"title":"catch { snail }","author":"Andrew"}},"markdownRemark":{"id":"f89c2c1c-50ba-58e8-8801-7a6acd7d5b20","excerpt":"In the k3s post I claimed Terraform is built on the same declarative idea as Kubernetes. This post is me paying for that sentence, because after a month of…","html":"<p>In the <a href=\"/so-about-that-docker-swarm-post-i-moved-to-k3s\">k3s post</a> I claimed Terraform is built on the same declarative idea as Kubernetes. This post is me paying for that sentence, because after a month of actually using Terraform I think it’s only half true — and the half that’s false is the interesting part.</p>\n<p>But first, the fun part, where I delete my server.</p>\n<h2 id=\"the-problem-my-vps-was-a-snowflake\" style=\"position:relative;\"><a href=\"#the-problem-my-vps-was-a-snowflake\" aria-label=\"the problem my vps was a snowflake permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The problem: my VPS was a snowflake</h2>\n<p>The Hetzner box running everything — this blog, the side projects, the <a href=\"/commit-message-driven-development\">commit-message bot</a> — was hand-built. SSH in, apt this, curl that, tweak, forget. If it died, “restore from backup” really meant “restore from whatever I remember about April.”</p>\n<p>So: everything into Terraform 0.13. The stack is three providers doing a small, honest job:</p>\n<div class=\"gatsby-highlight\" data-language=\"hcl\"><pre class=\"language-hcl\"><code class=\"language-hcl\"><span class=\"token keyword\">terraform</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token keyword\">required_providers</span> <span class=\"token punctuation\">{</span>\n    <span class=\"token property\">hcloud</span>     <span class=\"token punctuation\">=</span> <span class=\"token punctuation\">{</span> <span class=\"token property\">source</span> <span class=\"token punctuation\">=</span> <span class=\"token string\">\"hetznercloud/hcloud\"</span> <span class=\"token punctuation\">}</span>\n    <span class=\"token property\">cloudflare</span> <span class=\"token punctuation\">=</span> <span class=\"token punctuation\">{</span> <span class=\"token property\">source</span> <span class=\"token punctuation\">=</span> <span class=\"token string\">\"cloudflare/cloudflare\"</span> <span class=\"token punctuation\">}</span>\n  <span class=\"token punctuation\">}</span>\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">resource <span class=\"token type variable\">\"hcloud_server\"</span></span> <span class=\"token string\">\"main\"</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token property\">name</span>        <span class=\"token punctuation\">=</span> <span class=\"token string\">\"snail-01\"</span>\n  <span class=\"token property\">server_type</span> <span class=\"token punctuation\">=</span> <span class=\"token string\">\"cx21\"</span>\n  <span class=\"token property\">image</span>       <span class=\"token punctuation\">=</span> <span class=\"token string\">\"ubuntu-20.04\"</span>\n  <span class=\"token property\">location</span>    <span class=\"token punctuation\">=</span> <span class=\"token string\">\"nbg1\"</span>\n  <span class=\"token property\">user_data</span>   <span class=\"token punctuation\">=</span> file(<span class=\"token string\">\"cloud-init.yaml\"</span>)\n<span class=\"token punctuation\">}</span>\n\n<span class=\"token keyword\">resource <span class=\"token type variable\">\"cloudflare_record\"</span></span> <span class=\"token string\">\"blog\"</span> <span class=\"token punctuation\">{</span>\n  <span class=\"token property\">zone_id</span> <span class=\"token punctuation\">=</span> var.zone_id\n  <span class=\"token property\">name</span>    <span class=\"token punctuation\">=</span> <span class=\"token string\">\"ohsnail.com\"</span>\n  <span class=\"token property\">value</span>   <span class=\"token punctuation\">=</span> hcloud_server.main.ipv4_address\n  <span class=\"token property\">type</span>    <span class=\"token punctuation\">=</span> <span class=\"token string\">\"A\"</span>\n<span class=\"token punctuation\">}</span></code></pre></div>\n<p>The <code class=\"language-text\">cloud-init.yaml</code> is where the magic compounds: it installs k3s and applies my manifests from git. So Terraform builds the world, cloud-init boots the cluster, and the control loops from last post take it from there. Three layers of “make it so.”</p>\n<p>(The new <code class=\"language-text\">required_providers</code> block with <code class=\"language-text\">source</code> addresses is a 0.13 thing, by the way — third-party providers like hcloud finally live in the registry instead of “download this binary and put it in a folder like it’s 2003.“)</p>\n<p>Adopting the <em>existing</em> server meant a tour through <code class=\"language-text\">terraform import</code>, which I’d describe as filling out immigration paperwork for infrastructure that already lives here. Tedious, worth it, do it resource by resource and run <code class=\"language-text\">plan</code> obsessively until it shows no changes.</p>\n<h2 id=\"the-ritual\" style=\"position:relative;\"><a href=\"#the-ritual\" aria-label=\"the ritual permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The ritual</h2>\n<p>Here’s the thing though: a disaster recovery plan you’ve never executed is a religious belief. So on Friday night, with a beer for courage and a fresh state backup for cowardice:</p>\n<div class=\"gatsby-highlight\" data-language=\"bash\"><pre class=\"language-bash\"><code class=\"language-bash\">terraform destroy -auto-approve\n<span class=\"token comment\"># ...12 seconds of watching my entire digital life deallocate</span>\nterraform apply -auto-approve</code></pre></div>\n<p><strong>Four minutes and twenty seconds</strong> later, everything was back. Server, DNS, cluster, blog, TLS certs re-issued by cert-manager, the bot cheerfully suggesting I “fix flaky test.” The only data that needed restoring was two Docker volumes from a Restic backup — which is exactly the point: the ritual shows you, brutally, what is actually <em>state</em> versus what just <em>looks</em> important. Everything that came back automatically was never state at all. It was opinion, and opinions belong in git.</p>\n<p>My server is no longer a pet. It’s cattle with a herd size of one, and I can now hold the entire production environment in a <code class=\"language-text\">git clone</code>.</p>\n<p>I’m making this a quarterly ritual. Nothing keeps infrastructure honest like scheduled death.</p>\n<h2 id=\"the-part-where-i-correct-last-months-take\" style=\"position:relative;\"><a href=\"#the-part-where-i-correct-last-months-take\" aria-label=\"the part where i correct last months take permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>The part where I correct last month’s take</h2>\n<p>Now the confession. Kubernetes and Terraform are both “declarative,” but they reconcile in fundamentally different ways, and conflating them (as I did) hides the most important operational fact about Terraform:</p>\n<p><strong>Kubernetes reconciles continuously.</strong> Control loops compare desired vs. actual many times a minute, forever. Drift gets corrected while you sleep.</p>\n<p><strong>Terraform reconciles episodically.</strong> Nothing happens until a human runs <code class=\"language-text\">plan</code>. Between runs, Terraform isn’t watching anything — it’s not a loop, it’s a <em>snapshot ceremony</em>.</p>\n<p>Which leads to the mental model that actually made Terraform click for me: <strong>the state file is not a record of your infrastructure. It’s Terraform’s <em>beliefs</em> about your infrastructure.</strong> Reality can change behind its back — a colleague clicks something in the console, a provider auto-upgrades a field, you “quickly fix” a firewall rule at 2am — and the beliefs go stale. <code class=\"language-text\">terraform plan</code> is the moment beliefs are confronted with evidence, and the diff you’re reviewing is really three-way: what you declared, what Terraform believes, what’s actually out there.</p>\n<p>Sound familiar? A confident internal model drifting away from reality, generating authoritative statements about things that no longer exist — that’s the same failure mode I spent the spring documenting in <a href=\"/playing-with-gpt2\">GPT-2</a>, which happily invented APIs because nothing ever forced its beliefs to meet the world. Terraform ships the antidote as a first-class command. Half of engineering, I’m starting to suspect, is just deciding <em>when</em> your system’s beliefs get checked against reality: every few seconds (Kubernetes), every deliberate ceremony (Terraform), or never (my old snowflake server, and, well, GPT-2).</p>\n<p>Continuous reconciliation is strictly stronger <em>if</em> you can afford it. Terraform’s episodic model is the tradeoff you accept for managing things that are expensive, slow, or dangerous to change — you <em>want</em> a human reading the diff before a database gets replaced. The plan isn’t bureaucracy. The plan is the product.</p>\n<h2 id=\"honest-footnotes\" style=\"position:relative;\"><a href=\"#honest-footnotes\" aria-label=\"honest footnotes permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Honest footnotes</h2>\n<ul>\n<li><strong>The state file is sacred and radioactive.</strong> It contains everything, sometimes including secrets, and losing it means Terraform forgets your infrastructure exists. Mine lives in Terraform Cloud’s free remote state with locking. Never in git.</li>\n<li><code class=\"language-text\">destroy</code> will absolutely also destroy things you forgot depend on each other. Read the destroy plan slower than you’ve ever read anything.</li>\n<li>Some resources are lying about being cattle. Volumes, databases, anything holding user data — mark them <code class=\"language-text\">prevent_destroy</code> and treat them as pets with paperwork.</li>\n</ul>\n<h2 id=\"next\" style=\"position:relative;\"><a href=\"#next\" aria-label=\"next permalink\" class=\"anchor before\"><svg aria-hidden=\"true\" focusable=\"false\" height=\"16\" version=\"1.1\" viewBox=\"0 0 16 16\" width=\"16\"><path fill-rule=\"evenodd\" d=\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\"></path></svg></a>Next</h2>\n<p>The obvious itch: Terraform applies when I run it, but the manifests inside the cluster already live in git… so why am I the one running anything? The word for that itch is apparently “GitOps,” and it’s next quarter’s rabbit hole.</p>\n<p>Also my GPT-3 invite is <em>still</em> not here. OpenAI, I have a benchmark of exactly one silly task waiting.</p>","frontmatter":{"title":"I ran terraform destroy on my own production (on purpose)","date":"October 18, 2020","description":"Putting the whole stack in Terraform, then proving it by deleting the server — plus the difference between Terraform and Kubernetes everyone glosses over"}}},"pageContext":{"slug":"/i-ran-terraform-destroy-on-my-own-production-on-purpose/","previous":{"fields":{"slug":"/so-about-that-docker-swarm-post-i-moved-to-k3s/"},"frontmatter":{"title":"So about that Docker Swarm post: I moved to k3s"}},"next":{"fields":{"slug":"/the-scaling-ladder-a-production-setup-that-scales-by-not-scaling-yet/"},"frontmatter":{"title":"The scaling ladder: a production setup that scales by not scaling yet"}}}},
    "staticQueryHashes": ["3435731857","426816048","63159454"]}