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.

How to inject environment configuration values when deploying an Angular application in Kubernetes or similar infrastructure?

+3
−0

Context

I am currently migrating a Web application from on-prem infrastructure to K8s.

The legacy infrastructure relies on defining some tokens in the configuration files and these are replaced during the deployment as follows:

  • ASP.NET Core: appsettings.json tokens are replaced
  • Angular: replacements are done directly in the bundle js files

The issue

While the .NET Core application features a designated configuration file (appsettings.json) which can be found in the Docker container, the Angular relies on environment.{env}.ts files which have their content bundled in the main js file.

This prevents one important aspect of the deployment: build (the Docker image) once and deploy in any environment.

How to make an Angular application allow its configuration data to be changed after the production build is created? (at Docker container level, not at image level)

History
Why does this post require moderator attention?
You might want to add some details to your flag.
Why should this post be closed?

0 comment threads

2 answers

+2
−0

I'm not quite sure I see what the issue is. As far as I can tell, you could continue to do exactly what you're doing now, you'd just do the token replacement on the bundle.js when a container is provisioned. On the other hand, I can see why you might want to move away from this approach.

A more modern way to handle these kinds of concerns, especially in a elastic environment like Kubernetes is via service discovery/configurations services such as Consul or etcd. These may already be integrated if you are using a private cloud infrastructure. (I'll talk more about Consul, because I'm a bit more familiar with it and it's a bit more featureful.)

The idea is instead of deploying configuration files with a provisioning tool like Puppet/Chef/Ansible/Salt in a push-based manner, containers would access configuration from Consul/etcd in a pull-based manner.

This could be used in several ways.

The least compelling way would be to have a script run when the container starts that fetches the configuration details from Consul/etcd and then either does the token replacement a la your current solution or creates a config file a la the solution in your answer. By itself, this doesn't provide much more benefit over the provisioning approach. However, both Consul and etcd allow you to wait for a key to change value, so you could have this script additional wait for changes and then restart the container or recreate the config file without needing to manually reconfigure.

More compellingly, both Consul and etcd present their key-value stores via an HTTP API. So instead of making and serving a config file, you could just have the application directly talk to Consul/etcd. This has the benefit of needing on (re)configuration step and always having the latest configuration. It also allows you to control how often different parts of the config are checked. For example, you can pull an initial config when the SPA web page is first loaded, and then pull other parts more frequently even without a full page reload. And, again, you can also wait for changes and thus detect when the configuration has changed and force a page reload (or do something smarter) in that case. Practically speaking, it's likely the end-user's browser wouldn't have network access to the Consul/etcd server. This can be resolved by server configuration or by using a reverse proxy tool like Traefik. You would route a request to app.example.com/config/foo to consul.example.com/v1/kv/frontend/public/config/foo or whatever. Here consul.example.com is not accessible from the outside internet but is accessing from the Docker container serving app.example.com which is itself externally accessible.

As a bit of a tangent, one difference between etcd and Consul is that Consul acts as a DNS server. This is a simple but clever idea that has immense repercussions for configuration. A lot of configuration is specifying where other services are. Consul allows you to put something like app-db.consul as the database URL in various configuration files once and for all. Since app-db.consul is a valid and (internally) accessible URL, you can just use it as-is in existing tools. What server(s) app-db.consul refers to is automatically handled in real-time (with load balancing and health checks to kick out failed servers). Whether app-db.consul refers to a production or development server is handled by which Consul server you're talking to. By itself, this feature can often drastically reduce or even outright eliminate configuration as well as simplify deployment.

History
Why does this post require moderator attention?
You might want to add some details to your flag.

1 comment thread

Indeed, my solution is a poor man's solution to configuration management, and using a dedicated solu... (1 comment)
+0
−0

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 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:

"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:
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:
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:

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):
{ 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:

COPY ./src/main/foo-spa/src/config/app-config.prod.json ./main/wwwroot/config/app-config.json
History
Why does this post require moderator attention?
You might want to add some details to your flag.

0 comment threads

Sign up to answer this question »