Angular6 Runtime environment Variables
CODING ·
angular environment-variables
Out of the box, Angular provides a convenient method for setting environment variables for each our local dev environment and our production environment.
First we set our environment variables per environment:
environments/environment.ts
export const environment = {
production: false,
api: 'https://localhost:1234/api'
};
environments/environment.prod.ts
export const environment = {
production: true,
api: 'https://www.example.com/api'
};
And then when we build for Prod we simply need to tell webpack that we are building for prod and it performs a file substitution for us
../angular.json
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
},
}
ng build --prod
However, this solution doesn’t account for running in a CD (Continuous Deployment) pipeline. The problem lies in the fact that our build server will build the application only once for production, and then our CD tooling will move the build payload through our various environments: Dev, Stage, Test, Prod.
We could have an environment.*.ts file for each of my environments, then update my webpack config to substitute these files per environment.
BUT… We don’t really have the opportunity to rebuild the product for the intermediate environments. Besides even if we changed our process to handle this, this isn’t good practice anyway, as we should maintain a single payload through the entire process.
Put simply, what we are after is a way to configure the environment variables at runtime, not compile time. These days .NET Core handles this by retrieving the settings from appSettings.json (in the past web.config). Then we can then tell my deployment tooling (Octopus Deploy, VSTS, etc) to update this file each time the application is promoted through the pipeline.
To solve this problem in Angular, my first approach was to create an appSettings.json file, deploy it as an asset with my build, and have the existing environment.ts file(s) pull the values from here.
assets/environment.json
{
"api": "https://localhost:1234/api"
}
environments/environment.ts
declare function require(url: string);
var json = require('./environment.json');
export const environment = {
production: false,
api: json.api
};
Should this work, nothing else in my application needs to change
BUT This approach didn’t work, and I can only surmise that it is due to the fact that environment is exported as a const, and as such the values are evaluated at compile time (not runtime).
In search of an alternative method, I came across the following post from Juri Strumpflohner. A simple solution that solves my problems, but also allows me to take it a step further and fetch some/all of the variables from an external service.
The Solution
- Create the environment.json file
assets/environment.json
{
"api": "https://localhost:1234/api"
}
I place this file in the assets folder, to ensure it gets distributed with the application.
- [optional] Define a model of the config file
models/environment.ts
export class environment {
api: string;
}
- now config service that fetches the config
service/environment.services.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { appSettings } from '../models/appSettings';
@Injectable()
export class EnvironmentService {
private appConfig : appSettings;
constructor(private http: HttpClient) { }
loadAppConfig() {
return this.http.get('/assets/environment.json')
.toPromise()
.then(data => {
this.appConfig = data as appSettings;
});
}
getConfig() : appSettings {
return this.appConfig;
}
}
Note: We are using the HttpClient to fetch the settings from a file. We could also call an endpoint to fetch the environment variables too
Note: As explained by Juri, The loadAppConfig() method needs to return a promise, as opposed to an Observable, as the API consuming this service currently only works with Promises.
- Now for the magic: Configure the service to run as an app initialiser
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { EnvironmentService } from 'services/environment.service.ts';
@NgModule({
declarations: [
AppComponent
],
imports: [
HttpClientModule
],
providers: [
EnvironmentService,
{
provide: APP_INITIALIZER,
useFactory: (svc: EnvironmentService) => { return () => svc.loadAppConfig(); },
multi: true,
deps: [EnvironmentService]
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
Note: APP_INITIALIZER is an injection token that will instruct the application to run the factory method on application startup. In our case, this will result in calling our loadAppConfig() method
Note: ‘multi: true’ is required when providing APP_INITIALIZER. ‘multi: true’ tells the Dependency Injection container, that there can be more than one instance of the provided service (and to provide them all)
- Update all references from environment to pull the settings from config.service
- Finally, update my release definitions to perform the variable substitution for each environment.