Build once deploy to multiple environments π
The twelve-factor app link
If we take a look at the description of such an app, it says:
In the modern era, software is commonly delivered as a service: called web apps, or software-as-a-service. The twelve-factor app is a methodology for building software-as-a-service apps that:
- Use declarative formats for setup automation, to minimize time and cost for new developers joining the project;
- Have a clean contract with the underlying operating system, offering maximum portability between execution environments;
- Are suitable for deployment on modern cloud platforms, obviating the need for servers and systems administration;
- Minimize divergence between development and production, enabling continuous deployment for maximum agility;
- And can scale up without significant changes to tooling, architecture, or development practices. The twelve-factor methodology can be applied to apps written in any programming language, and which use any combination of backing services (database, queue, memory cache, etc).
A twelve-factor app has 12 factors as the name implies, for this post we're scoping into the fifth topic Build, release, run. If we take a look at the description of this stage, it says:
A codebase is transformed into a (non-development) deploy through three stages:
- The build stage is a transform which converts a code repo into an executable bundle known as a build. Using a version of the code at a commit specified by the deployment process, the build stage fetches vendors dependencies and compiles binaries and assets.
- The release stage takes the build produced by the build stage and combines it with the deployβs current config. The resulting release contains both the build and the config and is ready for immediate execution in the execution environment.
- The run stage (also known as βruntimeβ) runs the app in the execution environment, by launching some set of the appβs processes against a selected release. Code becomes a build, which is combined with config to create a release.
The twelve-factor app uses strict separation between the build, release, and run stages. For example, it is impossible to make changes to the code at runtime, since there is no way to propagate those changes back to the build stage.
This blog post was updated in context of standalone components, if you're looking for that see Multiple releases using the same but configurable Angular Standalone Application build
environment.ts link
I believe this file is miss-used in most of the cases, maybe even the most miss-used file in an Angular application. In my opinion, Angular should be more explicit about the use of this file. Sadly, we weren't an exception and we didn't know it until some time ago. Our environment files had some config variables inside of them that varied between environments, e.g. the API endpoint or a token to use a 3rd party service. This caused us to create a separate environment
file for each environment dev, test, staging, production1, and production2. By storing our config variables in the environment
files it forced us to build the application for each environment when we needed to deploy a new version of our application. This was time-consuming and it was also error-prone. Short said, it is considered a bad practice.
As the start of our quest we extracted all of the environment specific variables into a config file, which is just a simple JSON file. This left us with 2 environment
files to define if it's a production build or not. These will be familiar as these are also the default files provided by Angular.
While we develop the application we're using the environment.ts
file, when we create a build on our CI server we use the environment.prod.ts
file. The Angular CLI picks up the correct file via the angular.json
file.
Deploying the application link
Now how should we deploy to an environment with the correct config variables? To do this, we will have to override the config file during the deployment. Depending on the infrastructure and tools that you're using, you could either replace the config file with the correct one, or you could define the config variables during your deploy step and override each config variable with the set variable.
We now have one build and multiple deploys, but we still have to load in the config file in our application. This is the part where we struggled for a bit.
Using the config file link
We thought we weren't the first ones on this quest so we went online in order to solve our problem. All of the solutions we encountered were using the APP_INITIALIZER
provider to postpone the bootstrap process of the Angular components until the config file was loaded.
format_quoteFor more info and a complete example of an implementation I highly recommend Juri Strumpflohner's blog Compile-time vs. Runtime configuration of your Angular App
At first this seemed like a good solution and after implementing it, it did what it supposed to do and it worked. We were happy.
Until at a specific moment where we needed to have access to our config from outside components and services, we had other configuration objects depending on the config file at the start up. We needed to create a configuration object to load a module, an example is that we needed to load ApplicationInsightsModule
at startup. We tried to chain multiple APP_INITIALIZER
providers in the hope we didn't have to start over. Unfortunately, we discovered that the config wasn't loaded at the time when we needed it. We tried multiple ways to get around it without changing our solution with the APP_INITIALIZER
provider, but without any success.
So we went back to the drawing board and asked for some help in the AngularInDepth group where Joost Koehoorn and Lars Gyrup Brink Nielsen offered some great guidance. With some help we discovered that platformBrowserDynamic
also accepts providers.
platformBrowserDynamic link
We needed to have the config variables loaded in before the application booted up, in order to do this we took the bootstrapping process a step further. We had to load the config file first, before everything else, before the application was loaded in. Angular boots up the application module inside the main.ts
file. In order to solve our problem we had to postpone this process until we loaded our config variables. Because platformBrowserDynamic
accepts providers, we saw an opportunity to define the config here.
The code below loads the config file via the fetch API
. This is a Promise, so we can know for sure that the config file will be loaded before we bootstrap the application module.
We store the retrieved config in the APP_CONFIG
(type-safe) token, which is defined in our application. The token consists of all of the configuration variables needed in the application. See the Angular documentation for more info about the configuration token.
In our code base, this looks like this.
Using APP_CONFIG link
Now we're a step further, we have loaded the config before we bootstrapped the application but we still have to make use of it. As the last hurdle in our quest, we had to create the ApplicationInsightsModule
's config. Because we know the config file is loaded when the AppModule
gets loaded, we can now access the config file and provide the Application Insights config. It's also fine to re-use the APP_CONFIG
config, but to make the config more scoped and maintainable for the modules, we're chopping up the large config into smaller chunks.
Wrapping up link
We now are fully compliant to the fifth factor of a twelve-factor app, being Build, release, run. This means we can now build our application one time and deploy it to every environment we have. During the deployment to an environment we have to deploy the correct version of the configuration file.
As bonus on having this kind of setup makes it very easy to create and use feature toggles.
TLDR:
- don't use the
environment.ts
file for environment specific config variables, create a custom configuration file (as JSON) - most of the time using the
APP_INITIALIZER
approach will be a solution to this problem - using the
platformBrowserDynamic
approach solves this problem in a different way (and is also easier to comprehend when you're not familiar with the bootstrap process?)
Incoming links
- Multiple releases using the same, but configurable, Angular Standalone Application build
- Configuring Azure Application Insights in an Angular application
Outgoing links
Feel free to update this blog post on GitHub, thanks in advance!
Join My Newsletter (WIP)
Join my weekly newsletter to receive my latest blog posts and bits, directly in your inbox.
Support me
I appreciate it if you would support me if have you enjoyed this post and found it useful, thank you in advance.