A Practical Guide for Runtime Configuration in Angular Applications
Why did we need runtime configuration?
Frontend developers face very common problems like production breaks due to API endpoint changes or typos, build given with specific feature missed, and managing specific builds for various environments (e.g QA, Prod). Typically all of the above entail building and deploying new code. But what if we could get configurations like API endpoints & feature toggles (you can read more about feature toggle here) on the fly?
How much easier would our lives be providing a hotfix for production issues without going for a new build, or having a single build of the codebase for all the environments?
At Zeotap we came up with an approach of reading configurations on the fly (on runtime). Being an Angular shop, this solution was created for an Angular framework, but the principles can be extended to any other popular framework.
What are runtime and build-time configurations?
In an Angular app, the standard command to create a build is ng build. And we can provide a configuration (e.g. environment — QA/Stage/Prod) as below:
ng build --configuration=prod
Here we are providing the config to the application (via CLI) when the build is generated. This is called build-time configuration.
In this case, since we are (with the help of Angular CLI) configuring the application at the time when the build is generated, this is called build-time configuration.
And in the case of runtime configuration, before the application renders we read the necessary configurations and store them at the application code.
Tools necessary and their setup steps
- Angular — https://angular.io/guide/setup-local
- Docker — https://docs.docker.com/get-docker/
Our Solution:
There are multiple ways to store the configuration and read it at the runtime. The strategy we thought of was to store it in a different repo so that every developer can go and add necessary configurations. The repo has configurations required for the main repo for all environments (QA, Stage, Prod).
We can access the features in our template using Pipes or Directives as below:
- Impure Pipes — We can use appFeature or appNotFeature to show a template based on whether the feature is enabled/disabled
- Structural Directives — We can use *appFeature or *appNotFeature for showing template
We will build a single docker image for QA/Staging/Production from Master branch. One docker image will be built, and the configs will be read at the runtime based on the environment we choose for deployment.
PS: We have used Kubernetes as a container-orchestration tool with which the config is read from the secondary repo by changing the volume inside the docker container of the main repo as shown in the image below.
Step by step guide on how to migrate from Build-time to Runtime:
I have created a Github repo and deployed it in Github Pages for User Listing App that has commits based on the steps to be followed to migrate your current application from build-time configuration to runtime. The important code snippets have an explanation that can help you better understand moving towards runtime config.
Below are the images of how our application looks when feature VIEW_USER_LISTING is true and false.
Initial app with Build-time
a. Once we have our basic angular application setup ready, we can start with the change in the environment file. Let’s add a userServiceUrl as a service endpoint and VIEW_USER_LISTING as a feature to decide whether to show the User Listing or not.
b. A user service file that reads API endpoint from the environment file for retrieving users
c. A user listing component that accesses VIEW_USER_LISTING feature from the environment file
d. A user listing HTML that uses viewUserListing variable to decide on whether to show the user listing table or not.
This is a common way of accessing a Service URL or a feature in Angular, since we access these variables directly from the environment file our code is tightly coupled with the Environment file or in other words Config. Is it good to have our code tightly coupled with the Config?.
You can access all the above code here.
Code changes for supporting Runtime config
a. With the Runtime configuration, our environment file will have info about neither Service URLs nor Features.
b. We will use a config.json file which will have Service URLs and Features.
c. A config service file that reads the data from config.json at runtime which will be provided by the config repo.
d. A user service file that reads the API endpoint using Config Service for retrieving users.
e. App module’s provider with APP_INITIALIZER. APP_INITIALIZER token is used when we want to have some configuration ready with us before the app is ready to render. And we have used a factory function called appLoader, this function is expected to return a promise, and once it returns the promise the app bootstrap process will continue.
f. App loader file which has the appLoader factory function.
g. A user listing component accessing the feature VIEW_USER_LISTING using Config Service.
h. A user listing HTML accessing the feature using Structural directive & Impure pipe
i. Docker run command using volume option to modify config in runtime, we run a separate container for the config repo so on main repo build we fetch the config.json based on the specific environment.
You can access all the above code here.
Summary:
Compile-time or Build-time configuration provided by Angular works really well for small scale application, but as the config list grows, the chances of facing issues at production due to some config mismatch become higher. Runtime configuration is therefore thebest way to read the config to build an application, independent of the environment.