Quote of the Day

more Quotes

Categories

Get notified of new posts

Buy me coffee

  • Home>
  • Azure>

Using azure devops File Transform to deploy a same angular build to multiple environments.

Published September 30, 2019 in Angular , Azure , Devops - 4 Comments

For an angular application, at build time, if you pass in a configuration name, then the webpack build tool replaces the content of the environment.ts file with the content of the environment. For instance, if you run the command ng build --prod, angular replaces the content of environment.ts with that of the environment.prod.ts. Similarly, if you run ng build --staging, angular replaces environment.ts with environment.staging.ts, provided the file exists. This approach works but it requires building multiple times for different environments. In this post, I show you the approach I learn from my coworker to build one time and deploy to multiple environments, using angular APP_INITIALIZER and the File Transform task in azure devops.

You can find complete source code for this post on my github.

What I had tried

A few months ago, I wrote the post about using the Replace Tokens extension to replace variables in an angular application. I used the extension in a build pipeline to replace the tokens in the environment.ts file to display the build number from azure devops in the angular app. However, because the webpack build tool bundles the environment.ts together with other files and minifies the result file, I could not use the extension to replace the tokens in a release pipeline.

Another option I considered was fetching the configurations from the backend as part of initializing the angular app. This technique works but it requires making a call to the server and so can have an impact on the load time.

Overview

The idea is to load configurations from a json file, which resides under the assets directory such that webpack does not bundle it when building with the --prod configuration, then use the File Transform task to replace the values in the file at release time.

Below I describe the high level steps of an example implementation.

  • Put the configurations in a json file, and place it under the assets directory.
  • Load the configs in a service.
  • Use APP_INITIALIZER to ensure the configs are loaded before using the service.
  • Inject the service in places where you need to reference the configurations.
  • Set up azure release pipeline to replace the values in the json file with environment based variables in azure devops.

In the next sections, I describe the steps in more details.

Put the default configurations in a json file.

Suppose you have a configs.json file with the following content:

{ "ApiEndpoint": "https://localhost:44339",
  "ConfigsLoadedFrom": "Local" 
}

You would place this file under {baseDir}/src/assets directory where baseDir refers to the base directory of your angular app.

Load the configs in a service.

In the snippets below, I load the file using HttpClient. It’s a good practice to put the codes in a dedicated service.

import { Injectable } from '@angular/core';
import { HttpClient, HttpBackend } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})

export class ConfigsLoaderService {

   private httpClient: HttpClient;
   private configs: Configs;

   constructor(handler: HttpBackend) {
    this.httpClient = new HttpClient(handler); 
  }

  get ApiUrl() {
    return this.configs.ApiUrl; 
  }

  get ConfigsLoadedFrom() {
    return this.configs.ConfigsLoadedFrom;
  } 

  public async loadConfigs() : Promise<any> {
    return this.httpClient.get('assets/configs.json').pipe(settings => settings)
      .toPromise()
      .then(settings => {
        this.configs = settings as Configs; 
      });
  }
}

export interface Configs {
  ApiEndpoint: string 
}

In the above snippets, I use a separate HttpClient. In my project, I have an interceptor that intercepts http request to add an access token in the header. In this case, I don’t need or want the interceptor to add the access token. You can just inject and use the default instance of HttpClient.

Calling loadConfigs method using APP_INITIALIZER

It would be nice if we can just call the loadConfigs method from the constructor. However, one of the thing I have learned is that you cannot have asynchronous codes in your constructor. For more info, you can checkout this StackOverflow post. Another thing I have learned is APP_INITIALIZER waits for asynchronous calls to finish.

The below snippets show how to call and await the loadConfigs method using APP_INITIALIZER.

import { HttpClientModule } from '@angular/common/http';

@NgModule({
  // ...
  imports: [
  // ...
    HttpClientModule
  ],
  providers: [
    {
      provide: APP_INITIALIZER, 
      useFactory: appInitializerFactory, 
      deps: [ConfigsLoaderService],
      multi: true
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

export function appInitializerFactory(configsLoaderService: ConfigsLoaderService) {
  return () => configsLoaderService.loadConfigs(); 
}

That’s it in terms of the angular part. In the next section, I show how you can use the File Transform task in a release pipeline to replace the values in the json file with variables in azure devops.

Replace values in json file with environment based variables in azure devops.

In an azure devops release pipeline, add the File Transform task to run on an agent. Configure the settings as similar to the below screenshot. Obviously, you need to run this task before the actual deployment.

Using File Transform task to replace values in a json file.

Go to Variables, add the key/value pairs to match with the structure of the json file. If you have nested variables in your json configs, you can use JSONPath expressions to specify them in the name.

If you deploy to multiple environments, you can have multiple variables of the same name with different values for different environments and scope the variables for each environment. In the below screenshot, I scope the environment to Development, which is the stage name I set in the release pipeline. If I add another stage called Staging, I can add another set of variables and scope them to Staging, so that when I deploy to Staging, azure would use the variables for Staging, not Development. That way, I can use different values for different environments without having to build multiple times.

Scope variables for multiple environments in Azure Devops

References

File Transform task

How to Use the APP_INITIALIZER Token to Hook Into the Angular Bootstrap Process

APP_INITIALIZER – Tapping into Initialization Process in Angular

4 comments