You Don't Want Atomic Deploys
Published: 2021-08-24 22:10:19

---
title: You Don't Want Atomic Deploys
published: true
date: 2021-08-24 17:00:00 UTC
tags: 
canonical_url: https://kevincox.ca/2021/08/24/atomic-deploys/
---

<main><p>Among static site hosts atomic deploys are a <a href="https://www.netlify.com/">very</a> <a href="https://aws.amazon.com/amplify/hosting/">popular</a> <a href="https://render.com/docs/static-sites#instant-cache-invalidation">feature</a>. Often mentioned in prime marketing real-estate on the homepage.</p>
<p>Advertising this as a feature is strange because atomic deploys are insufficient at best. At worst they can be considered an anti-feature.</p>
<h2 id="what-is-wrong-with-atomic-deploys"><a href="https://kevincox.ca/2021/08/24/atomic-deploys/#what-is-wrong-with-atomic-deploys">What is Wrong with Atomic Deploys?</a></h2>
<p>The fundamental issue with atomic deploys is that the web isn’t atomic. Any deployment procedure that depends on an atomic deployment is broken. Not only because it unlikely that these providers provide a truly atomic deployment, (Are they stalling requests at the edge until all involved processes have confirmed the switchover? I doubt it.) but also because an atomic deployment isn’t sufficient or required for seamless deploys.</p>
<p>Deployments that depend on atomic version switches reliably break when a user requests the page before the deploy, then attempts to request referenced assets after the switch.</p>
<h3 id="case-study-stable-filenames"><a href="https://kevincox.ca/2021/08/24/atomic-deploys/#case-study-stable-filenames">Case Study: Stable Filenames</a></h3>
<p>Let’s have a trivial site. <code>index.html</code> which includes <code>main.js</code>, some handlers in the HTML reference functions in the JavaScript file. We will attempt to deploy a simple function rename. Now consider the following sequence of events:</p>
<ol>
<li>User requests <code>index.html</code> and gets version 1.</li>
<li>Atomic deploy occurs, version 2 is now active.</li>
<li>User requests <code>main.js</code> and gets version 2.</li>
<li>
<code>index.html</code> tries to call the renamed function and crashes.</li>
</ol>
<p>For a simple case like this the timing window is small, but add lazy-loading, caching or even a user with a bad connection and you will have situations where the user receives a broken site.</p>
<h3 id="case-study-versioned-filenames"><a href="https://kevincox.ca/2021/08/24/atomic-deploys/#case-study-versioned-filenames">Case Study: Versioned Filenames</a></h3>
<p>Let’s use the same site from the previous example. However this time the JavaScript file has a unique ID in the filename. This is commonly a hash of the contents, but for this example it will be the version number. This is a common practise to allow long caching of unchanged assets, but when the assets change a new URL will ensure that the new version is fetched.</p>
<ol>
<li>User requests <code>index.html</code> and gets version 1.</li>
<li>Atomic deploy occurs, version 2 is now active.</li>
<li>User requests <code>main-v1.js</code> and gets a 404.</li>
<li>
<code>index.html</code> tries to call a function which failed to load and crashes.</li>
</ol>
<p>Again, the site is broken. This case is possibly even worse because because compatibility between versions won’t help. Any time the JavaScript file changes (and gets a new URL) the request will fail since the old version is no longer available.</p>
<h2 id="how-to-deploy-static-sites"><a href="https://kevincox.ca/2021/08/24/atomic-deploys/#how-to-deploy-static-sites">How to Deploy Static Sites</a></h2>
<p>The correct way to deploy static sites with absolutely no errors is to split the site into two categories which I call Entries and Assets.</p>
<h3 id="entires"><a href="https://kevincox.ca/2021/08/24/atomic-deploys/#entires">Entires</a></h3>
<p>Entries, or “entrypoints” are places where your app is “entered”. The most common example is user-visible URLs. Those which should be stable over time so that they can be bookmarked. However this could also be a public JavaScript file that is used by third-parties. (For example if you have a widget that can be included in third-party sites.)</p>
<p>In important requirement for Entries is that they must have a <strong>stable interface</strong>. For an HTML page this is generally quite easy. The interface is “can be viewed in the browser”. However you also want to keep IDs and URL parameters stable so that links don’t break. For a JavaScript file then you should have API stability, you can release fixes but users shouldn’t care exactly what version they get.</p>
<p>In the above example <code>index.html</code> is an Entry.</p>
<p>There are some user-visible URLs that don’t need to be stable. For example a checkout flow could be considered an Asset if you want to pin the version used throughout the checkout. This makes it easier to refactor critical URL parameters as you don’t need to worry about version skew.</p>
<h3 id="assets"><a href="https://kevincox.ca/2021/08/24/atomic-deploys/#assets">Assets</a></h3>
<p>Everything else is an Asset. Assets should be given versioned filenames and are always referred to by this name. This removes the need for interface stability as every user gets exactly the version that they need.</p>
<p>In the above example <code>main.js</code> is an asset.</p>
<p>You can think of this as “version pinning”. When any Entry is loaded it “pins” all of the Assets by referencing them with versioned URLs. The user will continue using that version of your app until they load a new Entry.</p>
<h3 id="build"><a href="https://kevincox.ca/2021/08/24/atomic-deploys/#build">Build</a></h3>
<p>The main requirement of the build process is generating a unique path for each changed Asset and ensuring that that all Assets are referenced by this path. The result on our example site would look something like this:</p>
<p>I like to put all of my Assets into a subdirectory like <code>/a/</code> (or <a href="https://kevincox.ca/2020/10/30/state-of-ipfs-websites/#ipns-consistency"><code>/ipfs/</code></a>). This makes it very easy to upload all of the Assets first and give them long cache headers (<code>Cache-Control: max-age=31536000,public,immutable</code>).</p>
<div><pre><code><span>&lt;!-- main.html --&gt;</span>
<span>&lt;script </span><span>src=</span><span>main-v2.js</span><span>&gt;&lt;/script&gt;</span>
<span>&lt;button</span> <span>onclick=</span><span>"clicked()"</span><span>&gt;</span>Click Me<span>&lt;/button&gt;</span>
</code></pre></div>
<div><pre><code><span>// main-v2.js</span>
<span>function</span> <span>clicked</span><span>()</span> <span>{</span>
	<span>alert</span><span>(</span><span>"</span><span>Hello!</span><span>"</span><span>);</span>
<span>}</span>
</code></pre></div>
<h3 id="deploy"><a href="https://kevincox.ca/2021/08/24/atomic-deploys/#deploy">Deploy</a></h3>
<p>The most important part of the deploy is that you must deploy Assets before Entries and ensure that <strong>all</strong> Assets from possibly-live deploys are still available. For a simple site like this the calculation could be as simple as the <code>max-age</code> value of the HTML file plus some leeway for download time. For sites that lazy-load resources, Assets could potentially be alive forever! (Maybe a user has had that tab open for 10 years and finally decided to click the “Play” button.) But in general these old files are not too large and can be given a generous time window before cleaning.</p>
<p>The final deployment process looks like this:</p>
<ol>
<li>Upload all of the Assets to the site.</li>
<li>Upload all of the Entires to the site.</li>
<li>Remove any old Entires (if anything was removed or renamed).</li>
<li>Clean up old Assets. (Optional)</li>
</ol>
<p>Note that step 2 is where the new site goes “live”. This can be done atomically (including being done atomically with step 3) if you desire, but there is no real point. In fact it may be a good idea to start by serving the new Entries to a small portion of your users and evaluating your metrics before deploying to 100%.</p>
<p>Rollbacks look like this:</p>
<ol>
<li>Upload all of the Assets. (Only required if you have cleaned them)</li>
<li>Upload all of the Entires.</li>
<li>Remove any old Entries.</li>
</ol>
<p>It is identical to deploys except that step 1 is probably already done by the deploy. You can still do steps 2 and 3 together atomically if desired.</p>