You Don't Want Atomic Deploys

kevincox

Kevin Cox

Posted on August 24, 2021

You Don't Want Atomic Deploys

Among static site hosts atomic deploys are a very popular feature. Often mentioned in prime marketing real-estate on the homepage.

Advertising this as a feature is strange because atomic deploys are insufficient at best. At worst they can be considered an anti-feature.

What is Wrong with Atomic Deploys?

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.

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.

Case Study: Stable Filenames

Let’s have a trivial site. index.html which includes main.js, 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:

  1. User requests index.html and gets version 1.
  2. Atomic deploy occurs, version 2 is now active.
  3. User requests main.js and gets version 2.
  4. index.html tries to call the renamed function and crashes.

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.

Case Study: Versioned Filenames

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.

  1. User requests index.html and gets version 1.
  2. Atomic deploy occurs, version 2 is now active.
  3. User requests main-v1.js and gets a 404.
  4. index.html tries to call a function which failed to load and crashes.

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.

How to Deploy Static Sites

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.

Entires

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.)

In important requirement for Entries is that they must have a stable interface. 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.

In the above example index.html is an Entry.

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.

Assets

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.

In the above example main.js is an asset.

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.

Build

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:

I like to put all of my Assets into a subdirectory like /a/ (or /ipfs/). This makes it very easy to upload all of the Assets first and give them long cache headers (Cache-Control: max-age=31536000,public,immutable).

<!-- main.html -->
<script src=main-v2.js></script>
<button onclick="clicked()">Click Me</button>
// main-v2.js
function clicked() {
    alert("Hello!");
}

Deploy

The most important part of the deploy is that you must deploy Assets before Entries and ensure that all Assets from possibly-live deploys are still available. For a simple site like this the calculation could be as simple as the max-age 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.

The final deployment process looks like this:

  1. Upload all of the Assets to the site.
  2. Upload all of the Entires to the site.
  3. Remove any old Entires (if anything was removed or renamed).
  4. Clean up old Assets. (Optional)

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%.

Rollbacks look like this:

  1. Upload all of the Assets. (Only required if you have cleaned them)
  2. Upload all of the Entires.
  3. Remove any old Entries.

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.

💖 💪 🙅 🚩
kevincox
Kevin Cox

Posted on August 24, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related