Every project has a story…

...every story has a bit of magic in it…

…where is magic, there are Fairies...

…and where are Fairies, there is...

"MAGIC DUST"

our story about configurable frontend apps

... how we've done it with a little help of magic from The Fairies

by Frontend Wolves: Rafał Brzoska & Łukasz "Max" Kokoszka

PROLOGUE

One day..

fairies had a discussion…

how to help humans developers deliver

configuration magic dust to their apps depending on their different needs and expectations

Hmmm…. We have “environments functionality”

During the discussion Silvermist proposed to go the standard Angular way:

And it’s already there!

specific environment.ts

ng build

artefact (dist folder)

deployment

running app

runtime

deployment

code/build

Is this the only way?

Is it always fine?

They all agreed that this is a well-know standard, which is broadly used….

Lets think this through for a moment…

Mhmmm...

Only Thinkerbell had doubts...

Girls but this is done on the build time

 

The configuration magic dust gets burned into the output artefact… So you cannot manipulate this easily on any later stage.

Maybe… Oh Maybe we could find a better way!

Thinkerbell!... oh no...

 

...here we go again!

 

CHAPTER  1

Direct Dust Injection

But it was not Tinkerbell who opened the pandora box…

 

It was Fawn...

 

Maybe we shouldn’t drift so far away from this solution?

 

 Let’s go the same way, but instead of using

multiple environment files,

and making the decision on build time...

 

...let’s either provide the configuration magic dust for all possible runtime locations in“production” env file

Oh.. Fawn.. Wait wait...

 

single environment.ts

ng build

artefact (dist folder)

deployment

running app

runtime

deployment

code/build

dev

stage

prod

browser url

dev

stage

prod

dev

stage

prod

By a single config file you mean something like this?

 
export const environment = {
  'dev.example.com': {
    apiPrefix: '/api/v1/',
    authServiceLocation: '/auth/'
  },
  'stage.example.com': {
    apiPrefix: '/backend/api/v1/',
    authServiceLocation: '/backend/auth/'
  },
  'example.com': {
    apiPrefix: '/api/',
    authServiceLocation: 'http://auth.example.com'
  }
};

... with loading via something like this?

 
import { environment } from '../../environments/environment';

export function configInitializer(configService) {
  return () => {
    return new Promise((resolve, reject) => {

      const hostname = window.location.hostname;

      if (environment.hasOwnProperty(hostname)) {
        configService.inject(environment[ hostname ]);
        resolve();
        return;
      }

      reject();
      return;

    });
  };
}

...or simply provide multiple files with configuration magic dust
and then just use it
the same way!

 

ng build

artefact (dist folder)

deployment

running app

runtime

deployment

code/build

dev.json

stage.json

prod.json

browser url

dev

stage

prod

stage

dynamic import

dev

prod

normal app code

*.chunk.js

*.chunk.js

For multiple files you think about something similar?

 
// app/configs/dev_example_com.config.json
{
  "apiPrefix": "/api/v1/",
  "authServiceLocation": "/auth/"
}


// app/configs/stage_example_com.config.json
{
  "apiPrefix": "/api/v2/",
  "authServiceLocation": "/auth/"
}


// app/configs/example_com.config.json
{
  "apiPrefix": "/api/",
  "authServiceLocation": "https://auth.example.com/"
}

And loading done ie. via webpack dynamic imports?

 
import { ConfigService } from '../services/config.service';

export function configInitializer(configService){
  return () => {
    return new Promise((resolve, reject) => {

      const hostname = window.location.hostname.replace(/\./gi, '_');
      const importPromise = import(
        /* webpackMode: "lazy" */`../../configurations/${ hostname }.config.json`
      );

      importPromise
        .then((configuration) => {
          configService.inject(configuration);
          resolve();
        })
        .catch(reject);

    });
  };
}

Yes, exactly!

 

They hoped that this will close the topic… as this looks legit ;)

 

But Tinkerbell was far from being convinced and satisfied…

 

Girls! Do we really should burn all the configurations magic dust into our artefacts?

This way we deliver a lot of useless magic dust to the users, which is also publicly available! 

 

CHAPTER  2

Dust Injection by Replacement

Imagine a situation, when we still have single deployable artefact, but inject magic dust configuration during deployment not during build!?

 

Then Iridessa stepped out of the line, probably inspired by the whole discussion and asked:

 

Ok ! So how do they deploy their frontend apps nowadays?

 

Again Silvermist - known also as the water fairy - was the first one to say something:

 

I’ve heard about some kind of dolphin or a fish…

 

No wait! Was it a whale?

 

Yhhhhhh…..

 

Yeah a whale! I think it has a whale in the logo and it is called docker!

 

It’s just like a bird egg! Everything inside to bring whatever is inside to life!

 

They contenerize their apps to be a self-sufficient,
encapsulated artefacts.

It is awesome!

 

Yes! This is some sort of magic as well! - said happily Tinkerbell.

 

And you can control this container behaviour

via environment variables.

 

Wow impressive - all the fairies nodded their heads.

 

So can we or can we not utilise this to support our goal!? - Rosetta asked

 

What if we could use those environment variables in our app?

 

 

For instance by leaving some kind of a magic dust configuration placeholders in index.html for later replacement and usage?

preprocess

artefact (dist folder)

running app

runtime

deployment

code/build

index.template.html

via placeholders

index.html

with env vars placeholders

ng build

artefact (docker image)

docker build

ENV_1

ENV_2

placeholders substitution

 on docker run

index.html with placeholders replaced

by deployment env specific values

<!--     src/index.template.html     --->
<html lang="en">
<head>
  <title>MagicDust</title>
  <script>
    Object.defineProperty(
      window,
      'MagicDust',
      {
        value: Object.freeze({
          apiPrefix: "<!-- @echo APP_API_PREFIX -->",
          authServiceLocation: "<!-- @echo APP_AUTH_SERVICE_LOCATION -->"
        })
      }
    );
  </script>
</head>
<body>
  <app-root></app-root>
</body>
</html>

...and then before application build do some magic to support local development  (ng serve) and all our target environments (docker)

 
// package.json

{
  "name": "magic-dust",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "node scripts/generateIndexHtml && ng serve",
    "build": "node scripts/generateIndexHtml --prod && ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
  
  ...

using a simple node script with tools like: 

preprocess* or sed

 

 

 

*https://github.com/jsoverson/preprocess

 
// scripts/generateIndexHtml.js

const args = require('yargs').argv;
const preprocess = require('preprocess').preprocessFileSync;

let context;

if (args.prod) { // when app gets build with --prod switch
  context = {
    APP_API_PREFIX: '${CLIENT_API_PREFIX}',
    APP_AUTH_SERVICE_LOCATION: '${APP_AUTH_SERVICE_LOCATION}'
  };
} else {
  context = { // when app gets build without --prod switch (local dev)
    APP_API_PREFIX: '/mocks/api/',
    APP_AUTH_SERVICE_LOCATION: 'http://localhost:4210'
  };
}

preprocess('src/index.template.html', 'src/index.html', context, { type: 'html' });

this way we could prepare our index.html, so it can be used by gettext's envsubst program, to directly inject OS envs as our magic dust configuration during deployment

 

 

 
# Dockerfile

FROM nginx:1.17.5-alpine

EXPOSE 80
EXPOSE 443

COPY dist /usr/share/nginx/www

WORKDIR /usr/share/nginx/www

# First substitute all env vars references in form of ${ENV_NAME}
CMD envsubst < index.html | sponge index.html && \

  # Then run server (nginx)
  nginx

so this:

 

 
<!--     src/index.template.html     --->
<html lang="en">
<head>
  <title>MagicDust</title>
  <script>
    Object.defineProperty(
      window,
      'MagicDust',
      {
        value: Object.freeze({
          apiPrefix: "<!-- @echo APP_API_PREFIX -->",
          authServiceLocation: "<!-- @echo APP_AUTH_SERVICE_LOCATION -->"
        })
      }
    );
  </script>
</head>
<body>
  <app-root></app-root>
</body>
</html>
<!--     src/index.template.html     --->
<html lang="en">
<head>
  <title>MagicDust</title>
  <script>
    Object.defineProperty(
      window,
      'MagicDust',
      {
        value: Object.freeze({
          apiPrefix: "/api/",
          authServiceLocation: "/auth/"
        })
      }
    );
  </script>
</head>
<body>
  <app-root></app-root>
</body>
</html>

and finally after deploy becomes this:

 
<!--     index.html in docker image    --->
<html lang="en">
<head>
  <title>MagicDust</title>
  <script>
    Object.defineProperty(
      window,
      'MagicDust',
      {
        value: Object.freeze({
          apiPrefix: "${APP_API_PREFIX}",
          authServiceLocation: "${APP_AUTH_SERVICE_LOCATION}"
        })
      }
    );
  </script>
</head>
<body>
  <app-root></app-root>
</body>
</html>

after preprocessing before build becomes this...

 

But ...Wait! I just reminded myself that they use a tool called

Kubernetes (k8s) to build huge ecosystems of those docker images!

 

 

 

CHAPTER  3

Discrete Dust Injection

Oh come on! - Vidia joined the discussion - I see where this is going!

 

 

 

Tinkerbell your curiosity will get us into trouble!

 

I have an option!

 

 

As far as I know k8s is capable of mounting files and "things" as files into containers! (in multiple namespaces)

 

There are those little things called Secrets and... and... ymhmmmm

 

 

ConfigMaps ?

 

 

Oh yes! Thank you

 

 

So maybe we could join concepts and make k8s load our magic dust configuration as a file depending on deployment namespace!?

 

 

This way in each namespace we could have separate configurations and just simply load them on bootstrap of our frontend app instead of this whole magic!

 

 

ng build

artefact (docker image)

deployment via k8s

running app

runtime

deployment

code/build

env (namespace) specific

config.json file has been mounted and can be loaded

normal app code

artefact (dist folder)

docker build

ConfigMap

When it comes to code, you mean such ConfigMap?

 
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-client-config-json
  namespace: prod
data:
  config.json: ewogICJoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgo=
  
  # ^ base64 encoded JSON:
  #   { 
  #      "apiPrefix": "/api/", 
  #      "authServiceLocation": "http://auth.example.com" 
  #   }

and such a Deployment?

 
apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: prod
  name: app-client
  labels:
    app: app-client
spec:
  replicas: 1
  template:
    metadata:
      labels:
        deployment: app-client
    spec:
      containers:
        - name: app-client-container
          image: app-client:latest
          volumeMounts:
            - mountPath: /usr/share/nginx/www/config.json
              name: app-client-config-volume
          ports:
            - containerPort: 80
      volumes:
        - name: app-client-config-volume
          configMap:
            name: app-client-config-json

used with APP_INITIALIZER like this?

 
import { HttpClient } from '@angular/common/http';
import { throwError } from 'rxjs';
import { tap, catchError } from 'rxjs/operators';
import { ConfigService } from '../services/config.service';

export function configInitializer(httpClient, configService): () => Promise<any> {
  return (): Promise<any> => {
    return new Promise((resolve, reject): void => {

      httpClient.get<any>('config.json').pipe(

        tap((config) => {
          configService.inject(config)
          resolve();
        }),

        catchError((err: any) => {
          reject();
          return throwError('magic dust not injected :(');
        })

      );
    });
  };
}

Again: Yes! You've got it!

 

But can we do this better? Especially when there is this "micro frontends" hype all around?

 

CHAPTER  4

The Fairies Concept

Girls I've got I splendid concept!

 

Tinkerbell shouted:

On the Spring Valley and our Queen name!

 

Let me call it our -  Fairies concept !!!

 

called nest.js and using it build quickly a dedicated webservice to serve the magic dust configurations for us depending on environment and it implicit configuration!

 

Let's take this, splendid, shiny, superb node js framework for building backend apps - so we won’t need any Java, .net dev to help us ;) 

 

It looks like Angular… So it awesome - if somebody haven’t noticed yet ;)

 

running app 1

runtime

deployment

code/build

app 1

ng build

and

docker build

deployable artefact

deploy app

and publish configs

deployable artefact

running app 2

config webservice

app 2

load runtime set

load runtime set

code

configs

code

configs

code

configs

code

configs

if you wish overwrite configs from outside

Auth App

User App

As we can have multiple apps (micro frontends setup)

 

Auth App

Auth ConfigSet

Config prop

Config prop

User App

User ConfigSet

Config prop

Config prop

let’s allow our apps to define their unique - but broadly available - ConfigSets with config properties inside

 

Auth App

Auth ConfigSet

Config prop

Config prop

Global ConfigSet

User App

User ConfigSet

Config prop

Config prop

Config prop

Config prop

Additionaly let's have a single global ConfigSet

 

Auth App

Auth ConfigSet

Config prop

Config prop

Global ConfigSet

Runtime Set A

User App

User ConfigSet

Config prop

Config prop

Config prop

Config prop

Runtime Set A

Runtime Set B

Runtime Set B

... then allow each app to build a projection of what's needed as Runtime Sets based on individual ConfigSets 

 

On Angular side we create a simple

service capable of

delivering the configuration when asked

 
import { Injectable } from '@angular/core';

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

  private _cfg;
  private _runtimeSet: 'RuntimeSet_A';

  load() {
  	// do a HTTP REST call here to load RuntimeSet 
  }

  get(configSetName: string, configProperty: string): any {
    return this._cfg[ configSetName ] ? 
           this._cfg[ configSetName ][ configProperty ] : undefined;
  }
}

On the backend (nest.js) create

a simple Controller

 
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
import { StorageManager } from '../storage-manager.service';

@Controller('api')
export class ApiController {
  constructor(private readonly storageManager: StorageManager) {
  }

  // with a way to upload config sets
  @Post('/config-sets')
  async addConfigSet(@Body('id') id: string, @Body('data') data) {
    return await this.dbService.addOrModifyConfigSet(id, data);
  }
  
  // and a way to remove config sets
  @Delete('/config-sets/:id')
  removeConfigSet(@Param('id') id: string) {
    return this.storageManager.removeConfigSet(id);
  }
  
  ...
@Controller('api')
export class ApiController {

  ...

  // expose and endpoint which will create new RuntimeSets based on an
  // array of config-sets id
  @Post('/runtime-sets/:id')
  async setRuntimeSets(@Param('id') id, @Body() configSetsToUse) {
    return await this.storageManager.setRuntimeSetData(id, configSetsToUse);
  }

  // allow to get RuntimeSet by id
  @Get('/runtime-sets/:id')
  getRuntimeSetById(@Param('id') id) {
    return this.storageManager.getRuntimeSetById(id);
  }
  
}

As mentioned we can store our

magic dust  configurations in separate files -

as proposed earlier - but outside of our

Angular App but still reachable by docker

 

/project/

   .... typical Angular app structure

   - /configurations/

       - /dev.config.json

       - /stage.config.json

       - /prod.config.json

 

or deliver it  via k8s trick

 

Learn our docker image to publish given env-related configuration to our config webservice while it is brought to life

 
# Dockerfile

FROM nginx:1.17.5-alpine

EXPOSE 80
EXPOSE 443

COPY dist /usr/share/nginx/www
COPY configurations /configurations

// Publish file under CFG_FILE_TO_USE env
// to webservice under PUBLISH_CFG_TO env
RUN echo "curl --header 'Content-Type: application/json' \
	--request POST \
    --data '@./$CFG_FILE_TO_USE' $PUBLISH_CFG_TO" | sh

// Run you server
CMD nginx

Voila! We can request our magic dust configuration - it will be there...

 

always in latest version, always composed from what is actually needed

 

And? What do you think?

Do you feel inspired?

 

 

THE END

Thank you Fairies!

All Copyrights to The Fairies goes to Disney of course ;)

Configurable Frontend

By Rafał Brzoska

Configurable Frontend

  • 1,239