Build Lifecycle¶
From the code that you write until the code that runs into the client’s browser there is many stages of compilation, packing, deployment, etc. It’s important to understand these steps and how the data can flow through them.
Because we want a deployment system that automatically provides the domain name and the SSL certificate and copies the database and many other goodies that happen under the hood, we need to follow a given set of rules.
Important stages¶
There is no official list of stages for an app and this list is not exhaustive, however those are the important parts of the life of our apps through the lens if what is available to you and what you can do with it.
Coding¶
When you code, it’s usually happening on your machine. It’s easy because you have full control over it and no constraints. This means that it’s very easy to start writing code that won’t work once onto the servers.
A few attention points and techniques:
Make sure to have configured properly the Django storages to use S3 for media storage. Don’t use local media storage.
Use the Goku endpoint (or any other kind of tunnel like ngrok) in order to be able to test your app through HTTPS. If you have content policy or CORS errors, it’s the time to work out a solution. If the tunnel is too slow, at least test once through it.
Make sure that all the files that your code requires can be committed and are committed (see the Version Control section for more details on what can be committed)
Don’t use environment variables outside safe locations (more on that later). In practice, it means don’t use
process.env
in Vue code.
The code that you write must work in such a way that there is no need to change (or create) a single file in your repository to make it work. No matter the configuration, the context or the server this code must be able to run as-is on all the target platforms.
Building¶
The build time is when you transform your code into something that can be executed by the target platform. This includes:
Transpiling JS/CSS code
Creating minified bundles of code for the client
Regrouping static assets into one directory
Compressing static assets
What you’re building is simply another form of your code. If you build your code twice it should give exactly the same output. Hence, you can understand that at the time of build there is no access to environment variables. You simply cannot depend on environment variables to build your code, just like your code is not generated from environment variables.
Note
There is a slight exception to this in Django’s world. Indeed, manage.py
will
not accept to start of SECRET_KEY
and DATABASE_URL
are not set. Fortunately
you don’t need to have the right values of those variables for collectstatic
to work properly. This is an example out of a Dockerfile
:
env DATABASE_URL='postgresql://dum:dum@localhost/dummy' SECRET_KEY='dummy' python manage.py collectstatic --no-input --clear
As you can see, the values here are dummy and don’t need to be anything “real” as they will be used by the build process.
On the other hand, please make sure that you don’t create in your code a build-time dependency to environment variables.
The output of this build step will be stored somewhere before going to the next
phase. For example if you’re building a Felix project then the build artefacts
are sent to a Git repo under with-madrid/deploy
while if you’re building for
Kubernetes it will be sent to a private Docker repository.
Deploying¶
The deployment phase can vary a lot depending on what you’re doing, but basically it will take the build artifacts from the build phase and copy them to the server while also registering the service in proper locations and starting the whole thing.
This, again, it depends on if you’re using Felix or Kubernetes or something else, but it’s the same underlying idea every time.
Note
On Felix deploys, the build and deploy phase are distinct but happen indeed automatically one after the other. It’s easy to recognize: the weird bash script is the build phase and when it moves to Ansible it’s the deploy phase.
Run time¶
When the code starts running on the server, that’s when it finally gets access to environment variables. Hooray!
But wait. Didn’t way say that we can’t use environment variables from Vue code? Ain’t we fucked already?
Fortunately, no! There is easy solutions to the most common problems that we face.
Note
Not all existing projects follow these guidelines, for many legacy reasons. There is no point trying to reproduce past mistakes, please follow the patterns below instead.
Finding the API¶
Ok so you’ve got an API_URL
variable that gives you the base path to the API.
How do you get this to the client?
Let’s say that your front-end is https://front
and your API is https://api
.
Suppose that you want to create an item on the API by doing a
POST https://api/api/item
. What do you need to do that?
First you need to know the URL of the API, aka the content of
API_URL
. But but we said no environment variables.Second you need to avoid getting your ass busted by CORS, because a POST from
https://front
tohttps://api
ain’t gonna fly.
The easiest solution to this is to use the magic of proxies 🪄
Here we’re going to explore a Nuxt-specific solution. Typically, in your
nuxt.config.js
file you can have something like that:
module.exports = {
plugins: ["~/plugins/axios"],
modules: [["@nuxtjs/axios", { proxy: true }]],
proxy: {
"/api/": process.env.API_URL,
},
};
Note
We’re putting the axios
plugin in this demo to outline the fact that there
is a proxy
option that you need to set to true. However it’s not strictly
required to use the axios
module to make the proxy work (but save yourself
some time and use the axios
module).
Note
The proxy
section of this configuration makes use of environment variables.
This is one of the few authorized places to use them.
By doing so, we’re solving both problems at the same time:
From your code you make a
POST /api/items
Which results in a
POST https://front/api/items
Which is proxied by the server and generates a
POST https://api/api/items
No CORS issue, no environment variables at build time. Of course there many options to the proxy module that you can modulate to fit your needs but this is the basic idea.
Using GTM¶
Project managers are always eager to add a lot of JavaScript into our pages and this generally starts with GTM. In a nutshell, GTM (or other tag managers like Matomo Tag Manager) are systems which allow to add tracking pixels and other analytical tools into the page without changing the code of the page itself. It’s basically some JS code that will load as soon as the page is displayed and will do as instructed by the project managers from their management interface.
To sum up, it’s a good practice to include a tag manager into the project because it will increase the flexibility of the app to include different tracking tools without making changes to the code.
Another item is that the code must be integrated exactly following Google’s instructions (which are kind of not nice) because this helps proving the ownership of the website on other Google tools. If the guidelines from Google are not followed exactly, this breaks the authentication mechanism and it can become annoying.
Django¶
From a purely Django app, using something like GTM (or any of the
marketing/tracking tools that we use) is usually fairly easy. You just have to
include the code into the base.html
of your project’s template and then inject
the GTM_ID
into it.
To do so:
Create a
GTM_ID
environment variableRead this variable from
settings.py
, something likeGTM_ID = os.getenv('GTM_ID')
Create a template context processor that will inject this value in all template renders
Note
It is much simpler to use a context processor than to manually inject the value
each time you call render()
to create a custom template tag.
Wagtail¶
In some cases, users are going to expect more configurability over what they can put in the HTML. In those cases, instead of reading the values from environment variables it is best to let some option to the user to configure this and store it into database.
You can for example create the raw_html_head
and raw_html_body
fields which
you let the admin change from the Promote tab of the root page and then in the
template you fetch those values from DB.
Nuxt¶
That’s not the simplest thing to do. There is an example in the TFS project so I’ll make links from here.
Create a plugin that will manage the JS you want to include. By example the GTM plugin from TFS declares the
dataLayer
variable and sends an event to it every time there is a new page displayed (because GTM doesn’t understand HTML5 routing)List that plugin into
nuxt.config.js
Hook up to
vue-renderer:ssr:templateParams
to inject the parameters you needModify/create
app.html
to include the snippets provided by GTM and inject there the value received thanks to thevue-renderer:ssr:templateParams
hook
Note
Maybe you noticed that we could have done the same thing using the head
section of nuxt.config.js
but for some reason we’re doing this complicated
maneuver. It’s simple: the head
is evaluated at build time and cannot take
environment variables into account. Sad but true.
Note
You’ll also realize that there is a @nuxt-gtm
module which we don’t use. The
reasons are the same: it is simply too annoying to give it runtime config. The
TFS implementation is simple enough on top of complying with Google’s
requirements, we don’t need to use that module.
This is of course fairly artisanal and will have to be adapted to the specific needs of your project, but that can serve as a blueprint.
Getting a token to the front-end¶
Very, very rarely you’ll need to get a token or some kind of value from the Vue code. If you reach this point it’s probably that you’ve ignored other advices in this guide.
Nuxt provides you with a way to safely get bits of data available in Vue code. It’s the runtime config section of the configuration. As the name might suggest, this is configuration that is available at runtime.
Imagine that we have a website with 2 CSS styles that were made for two brands
of the same company that have the same website. Let’s say it’s brands
very-nice
and super-great
. As they ask you to host both websites separately,
you’ve created an environment variable BRAND=very-nice
or BRAND=super-great
depending on the instance.
In that case your nuxt configuration file would look like:
module.exports = {
publicRuntimeConfig: {
brand: process.env.BRAND || "very-nice",
},
};
Note
The publicRuntimeConfig
and privateRuntimeConfig
options are authorized
locations for environment variables, as they are specifically designed for that
purpose.
Doing so allows you to get this information from Nuxt code:
<template>
<h1 class="branded-title" :class="brandClass">{{ brandTitle }}</h1>
</template>
<style lang="less">
/**
* Very important brand title
*/
.branded-title {
font-size: 100px;
/**
* Very Nice's identity is blue bada dee bada doo
*/
&.-very-nice {
color: blue;
}
/**
* Super Great sees a red door and wants it painted blaaack
*/
&.-super-great {
color: red;
}
}
</style>
<script>
export default {
computed: {
/**
* Generates the brand's title based on the configured brand from
* runtime environment.
*
* @returns {string}
*/
brandTitle() {
return (
{
"very-nice": "Very Nice",
"super-great": "Super Great",
}[this.$config.brand] || "???"
);
},
/**
* Returns the CSS class modifier for this brand, see in the CSS
* style how this affects the title.
*
* @returns {string}
*/
brandClass() {
return `-${this.$config.brand}`;
},
},
};
</script>
What happens under the hood is that when you ask Nuxt to serve you the page it
will modify its internal __NUXT__
object (look for it in the HTML) to contain
this value. This way when the JS on the client side starts running, it can get
the value from there and use it.
If instead of this.$config.brand
you used process.env.BRAND
, the value would
have been replaced at build time and hardcoded as-is in the minified code. This
would go against the goal of being able to deploy the same code in different
environments.
Note
This can only work with Nuxt in universal
mode. But we use Nuxt specifically
for its universal
mode and its ability to do SSR, so all is fine. If you want
to use Nuxt in another mode then you need to seriously question your choices as
this guide won’t be able to help you.
If you look into TFS, there is also a Iubenda plugin integrated that makes use of this technique to start itself.
Server vs Client-side rendering¶
You’d think that once the code is running and your environment variables are injected through the proper channels you’d be out of your misery. Haha. Lol.
The core feature of Nuxt and the main reason why we use it instead of, let’s say, vue-cli, is its ability to do Server Side Rendering. Maybe let’s do a bit of history at this point:
Initially it was simple. The server generates HTML, sends HTML to the browser, the browser displays the page, that’s it.
Then it became pretty common to have JavaScript on the client side to alter details of the page (zoom on images, display details about a post, click on some stars, etc). This is the jQuery era.
Then those details became huge parts of the page, at which point the JS framework craze started. The idea was: render 100% of the content on the client side and communicate with the server through an API. If apps can do it why can’t web apps do it? This is the SPA era.
That was fantastic but presented one main big problem: Google doesn’t run SPAs (or at least did not at the time). Which means that your whole app was just a blank page for Google. This is when JS frameworks decided that instead of just sending an blank page and doing the rendering on the client they would do a first render on the server, send the HTML output and then keep on rendering on the client. This is the famous SSR that we’re talking about here.
In theory SSR is the best of both worlds and in practice it is indeed a lot of things that were complicated before. Using Vue components and splitting them by page, having the whole HTML code + just the required CSS to boost up the first paint, etc. These things are really important for performance and using Nuxt makes them super easy to get (as you don’t actually do anything to get them).
However this comes at a price, which is that you need to understand the limitations of SSR. Specifically, on the server there is no DOM, no canvas, no video, no audio, none of all that. You can render HTML but not attach events. Let’s see how to navigate in those waters.
DOM stuff¶
The good news about most of the DOM operations is that it’s Vue’s job to
manipulate the DOM. So all you have to do is to only rely on Vue APIs (either
through the template
part of your .vue
files either through the render()
function).
If you absolutely need to use native DOM functions (like findElementById()
,
addEventListener()
, etc) then think again because it is highly unlikely that
you actually need them. As a reminder, Vue covers
event binding,
changing the style or class of elements
and other input bindings.
If you still absolutely need to use them, you can create sections of the code that will not execute if you’re on the server:
if (process.client) {
console.log("this is only executed on the browser");
}
This is also true if you’re invoking a component that uses some kind of canvas or other things that Vue doesn’t manage. In those cases you can do:
<template>
<div class="some-component">
<h1>Hello</h1>
<client-only>
<component-that-uses-canvas />
</client-only>
</div>
</template>
Data loading¶
As explained in the introduction, one of the core features of Nuxt is that content can rendered on the server side so that Google is able to read the page (but also that the page displays faster).
This means that you cannot use created()
, mounted()
or any other Vue
hooks to make queries to the server. When the component comes alive it is
already too late to fetch data.
This is why you must trigger all data loading from the asyncData() hook that Nuxt provides.
All magic comes at a price, the asyncData()
hook doesn’t have access to this
because the component isn’t created yet but you still have access to the whole
context ($axios
, store
, etc.). What you return from asyncData()
will be
injected into the component’s data and additionally you have the opportunity to
cancel the request if you get errors from the server (by example a 404 if the
URL is wrong).
Note
It also applies in reverse: mounted()
won’t be called on the server. This
means that if you rely on some operations to happen in mounted()
in order to
transform the DOM before it is displayed it won’t work properly (although it
will look like it worked when you display the page, after a short blink).
The ultimate check to see that everything is loaded correctly is to open the
page, display its HTML code (not from the inspector but by doing CTRL + U
)
and look for all the information that you need to be there:
Menu entries
Footer entries
Meta tags (OG for Facebook, meta description,
<title>
, etc.)Page content
If those items are not present inside the HTML code that comes straight out of the server then you have a problem of data loading that you need to fix :)
12 factors¶
These rules are the 12 factors, largely invented for Heroku, but (mostly) relevant for us regardless of the infrastructure that we’re deploying into (Felix, Kubernetes, etc.).
Let’s see how we implement those factors:
Codebase — All the code shall be in a set of Git repositories. It is immutable, and we can deploy it identical to many locations. This is really a core concept here: the code cannot be changed between deployments.
Dependencies — Dependencies must be explicitly managed by the proper tool. In the case of Python it’s Pip, in the case of JS/CSS it’s NPM. In no case should dependencies be managed through submodules or copy/pasta. Doing so would result in version conflicts, security issues, technical dept and generally into confusion. More on that in the Dependencies Management section.
Config — If the code can’t be modified, the configuration has to come from somewhere. The somewhere is an old mechanism very easy to use with all deployment systems: environment variables. In local this manifests into a
.env
file at the root of your project (that you somehow read either through IDE/shell extensions either with some module in the app to go fetch the.env
’s content) and on the servers it’s up to the deployment system to inject the environment variables when needed.Backing services — Not all can be deployed as 12 factors. For example in the case of a DB this is completely impossible as this would contradict rule number 6 below. Those services must be managed differently and be considered as “attached” to your app. To keep up with the DB example, there will be a database instance created by the hosting provider and we’ll simply give to your app the reference to this database through environment variables.
Build, release, run — Build stages must be strictly separated. This is a by-product of previous rules and will be detailed further in this document.
Processes — If you build your app as a stateless process then you remove a lot of management problems associated to that process. This means that state-keeping between requests must go through appropriate channels: a database, a S3 bucket, a queue, etc. You cannot rely on disk files, shared memory or anything stored locally on the process’s host, as it might get deleted as soon as the request is over.
Port binding — The 12 factors manifesto says to expose everything through port binding. Well that was a bit complicated to keep up with this in Felix but everything is exposed via UNIX socket binding, which is basically the same thing. It’s the allegory of a pipe: there is a pipe that will receive requests and return responses to these requests. This is the only point of communication between the process and its environment.
Concurrency — One of the main reasons for rule 6 is that you need to be able to scale up your app easily. Being able to spin several process is key to that and thanks to this model you can easily go from 1 to 100 workers in a few seconds. The only bottleneck here is obviously the database but usually managed DB services have the ability to scale up the instance easily. Although this means that you need to write queries that can scale up (avoid locks and race conditions), which is an entire story on its own.
Disposability — Processes should start and stop quickly and gracefully. This way you can easily interchange processes if you want to change the underlying machines hosting the service by example.
Dev/prod parity — All environments must be as similar as possible to avoid any surprises, and in fact our deployment scripts for develop and production are exactly the same.
Logs — Logs are stored in a central location, indexed and can be searched. It’s considered as an events stream and not as a collection of files.
Admin processes — All that is related to the management of the DB or various administrative tasks must run as a one-off task. In Django’s world this is management commands. There is no
/_secret/do-cron-jobs
URLs that will trigger some cron jobs or other “background” tasks, because in terms of security that’s a bit borderline and because it would mean to use the web worker to accomplish those tasks. Instead, administrative processes should be spun on a different machine (at least in prod) and run independently of the web process.