I have an Angular 7 application where the app.module.ts
files looks like the following. Note that in this example, there are 2 modules with each having 1 key whose value needs to be externalized. By externalized
, I mean the values should be acquired from the environment at runtime.
@NgModule({
declarations: [ ... ],
imports: [
SomeModule.forRoot({ apiKey1: "needs to be externalized" }),
AnotherModule.forRoot({ apiKey2: "needs to also be externalized" })
],
providers: [ ... ],
bootstrap: [AppComponent]
})
export class AppModule { }
What I do is build this application (e.g. ng build
and then containerize it using Docker). At deployment time, the DevOps
person wants to run the docker container as follows.
docker run -e API_KEY_1='somekey' -e API_KEY_2='anotherkey' -p 80:80 my-container:production
Note that API_KEY_1
should map to apiKey1
and API_KEY_2
should map to apiKey2
.
Is there any disciplined way of externalizing values for an Angular application?
I thought about writing a helper script to do string substitution against the file, but I think this approach is not very disciplined (as the transpiled Angular app files are obfuscated and minified). The script would run at container startup, read the environment variables (key and value), and then look at the files to replace the old values with the ones from the environment.
Eventually, the Angular app will be orchestrated with Kubernetes. I'm wondering if there's anything there that might help or influence how to externalize the values in a best practice way.
Any help is appreciated.
You could use a substitution in custom entry-point.
FROM nginx
RUN apt-get update && apt-get -y install gettext-base nginx-extras
ADD docker-entrypoint.sh /
ADD settings.json.template /
COPY dist /usr/share/nginx/html
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
With a docker-entrypoint.sh
like this:
#!/bin/bash
envsubst < "settings.json.template" > "settings.json"
cp settings.json /usr/share/nginx/html/assets/
# Launch nginx
exec "$@"
And a settings.json.template
:
{
"apiKey2": "$API_KEY_2",
"apiKey1": "$API_KEY_1"
}
Then on your source you add a file settings-loader.ts
export const settingsLoader = new Promise<any>((resolve, reject) => {
const xmlhttp = new XMLHttpRequest();
const method = 'GET';
const url = './assets/settings.json';
xmlhttp.open(method, url, true);
xmlhttp.onload = function() {
if (xmlhttp.status === 200) {
const _environment = JSON.parse(xmlhttp.responseText);
resolve(_environment);
} else {
resolve();
}
};
xmlhttp.send();
});
And on your main.ts
:
import {enableProdMode} from '@angular/core';
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {AppModule} from './app/app.module';
import {environment} from './environments/environment';
import {settingsLoader} from 'settings-loader';
settingsLoader.then((settings) => {
if (settings != null) {
environment.settings = Object.assign(environment.settings, settings);
}
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule)
.catch(err => console.log(err));
});
Then you should have access in your code with
import {environment} from '../environments/environment';
console.log(environment.settings.apiKey1);