Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users
Tabletop RPGs
Tabletop RPGs
Community Proposals
Community Proposals
tag:snake search within a tag
answers:0 unanswered questions
user:xxxx search by author id
score:0.5 posts with 0.5+ score
"snake oil" exact phrase
votes:4 posts with 4+ votes
created:<1w created < 1 week ago
post_type:xxxx type of post
Search help
Notifications
Mark all as read See all your notifications »
Q&A

Welcome to Software Development on Codidact!

Will you help us build our independent community of developers helping developers? We're small and trying to grow. We welcome questions about all aspects of software development, from design to code to QA and more. Got questions? Got answers? Got code you'd like someone to review? Please join us.

Post History

50%
+0 −0
Q&A How to inject environment configuration values when deploying an Angular application in Kubernetes or similar infrastructure?

Note: this is mainly based on Build your Angular App Once, Deploy Anywhere article. Read this article, especially the drawbacks section. The basic idea is to create and read a configuration file t...

posted 1y ago by Alexei‭

Answer
#1: Initial revision by user avatar Alexei‭ · 2022-12-23T13:57:12Z (over 1 year ago)
Note: this is mainly based on [Build your Angular App Once, Deploy Anywhere article](https://indepth.dev/posts/1338/build-your-angular-app-once-deploy-anywhere). Read this article, especially the [drawbacks section](https://indepth.dev/posts/1338/build-your-angular-app-once-deploy-anywhere#drawbacks-to-this-approach).

The basic idea is to create and read a configuration file that is kept as is when the Angular application is built (transpiled). I have made the following steps:

- add a configuration file

I have added app-config.json file in `src\config` and instructed the Angular builder to keep it as an "asset", by adding changing the assets part in angular.json file:

```json
"architect": {
"build": {
  "builder": "@angular-devkit/build-angular:browser",
  "options": {
	"outputPath": "dist/foo.spa",
	"index": "src/index.html",
	"main": "src/main.ts",
	"polyfills": "src/polyfills.ts",
	"tsConfig": "tsconfig.app.json",
	"inlineStyleLanguage": "scss",
	"assets": [
	  "src/favicon.ico",
	  "src/assets",
	  "src/config",
	  "src/config"
	],
```

- get rid of most of the content in the environment files 

I have removed all the content in environment.ts and environemnt.prod.ts, except for the `production: true/false` property which is still useful for some checks that rely on local vs. deployed application.

This will create some TS issues in all places that relied on properties from the `environment` object (to be tackled below).

- define a TS interface that mirrors the configuration. Example:

```ts
export interface AppConfig {
	production: false,

	apiUrl: "...",

	msalConfig: {
		applicationIdUri: "api://****",
			validateAuthority: true,
			redirectUri: "/",
			navigateToLoginRequestUrl: false
		}
	}	
}
```

- define a service to read the configuration from the configuration file. Notice that the function is async, since this is actually a HTTP call to read the SPA asset:

```ts
import { HttpClient, HttpXhrBackend } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { lastValueFrom, map } from "rxjs";
import { AppConfig } from "../models/app-config";

@Injectable({
  providedIn: 'root'
})
export class ConfigService {

  private configuration: AppConfig;

  constructor(
  ) { }

  async setConfig(): Promise<AppConfig> {
		// this is required because injecting HttpClient will lead to configuration not being available when MSAL is initialized
		// more info: https://github.com/AzureAD/microsoft-authentication-library-for-js/issues/1403#issuecomment-672777048
		const httpClient = new HttpClient(new HttpXhrBackend({ 
			build: () => new XMLHttpRequest() 
	}));

    const config = await lastValueFrom(httpClient.get<AppConfig>('./config/app-config.json'));
		return this.configuration = config;
  }

  get config(): AppConfig {
    return this.configuration;
  }
}
```

In my case, using MSAL made things more complicated and I could not simply inject `HttpClient` and I had to "new" it.

- all components or services that relied on environment should inject `ConfigService` and rely on `this.configService.config.{property}` instead.

- the configuration must be loaded at application startup (app initialization) and this can be done in `app.module.ts`:

```ts
const appInitializerFn = (configService: ConfigService) => {
  return () => {
    return configService.setConfig();
  };
};

ConfigService,
{
	provide: APP_INITIALIZER,
	useFactory: appInitializerFn,
	multi: true,
deps: [ConfigService]
},

// all services (or similar objects) must explicitly state the dependency on ConfigService because configuration reading is done async and the service might need the values sooner than the Promise is resolved.
{ provide: HTTP_INTERCEPTORS, useClass: BaseUrlInterceptor, multi: true, deps: [ConfigService]},
```

- some third-party services require to be adapted to the new configuration to properly work. In my case, MSAL (Azure AD Auth + Angular):

```ts
{ provide: HTTP_INTERCEPTORS, useClass: MsalInterceptor, multi: true },
{ provide: MSAL_INSTANCE, useFactory: msal.MSALInstanceFactory, deps: [ConfigService] },
{ provide: MSAL_GUARD_CONFIG, useFactory: msal.MSALGuardConfigFactory },
{ provide: MSAL_INTERCEPTOR_CONFIG, useFactory: msal.MSALInterceptorConfigFactory, deps: [ConfigService] },


export function MSALInstanceFactory(configService: ConfigService): IPublicClientApplication {

	const config = configService.config;

	return new PublicClientApplication({
		auth: {
			clientId: config.msalConfig.auth.clientId,
			authority: config.msalConfig.auth.authority,
			redirectUri: config.msalConfig.auth.redirectUri
		},
		cache: {
			cacheLocation: BrowserCacheLocation.LocalStorage,
			storeAuthStateInCookie: false
		},
		system: {
			loggerOptions: {
				loggerCallback,
				logLevel: LogLevel.Verbose,
				piiLoggingEnabled: false
			}
		}
	});
}

export function MSALInterceptorConfigFactory(configService: ConfigService): MsalInterceptorConfiguration {
	const config = configService.config;

	const protectedResourceMap = new Map<string, Array<string>>();
	protectedResourceMap.set("https://graph.microsoft.com/v1.0/me", ["user.read"]);
	protectedResourceMap.set(config.apiUrl, [config.msalConfig.applicationIdUri]);

	return {
		interactionType: InteractionType.Redirect,
		protectedResourceMap
	};
}

```

- testing when deployed as a Docker image

To check how it works during deployment, I have temporarily added a configuration prod copy and a COPY command in the Dockerfile:

```bash
COPY ./src/main/foo-spa/src/config/app-config.prod.json ./main/wwwroot/config/app-config.json
```