How To: Using Environment Variables in the Browser with Angular and Azure App Services, Part 2

Why and how to separate configuration from code using Angular and Azure App Services, Part 2

Sean Fisher

7 minute read

This is part two of a two-part series. For the first part see How To: Using Environment Variables in the Browser with Angular and Azure App Services, Part 1

In this two-part series I’ll talk about:

Part 1:

  • Why to separate your configuration and code
  • How to do it “in general”
  • How to do it with an Azure App Service
  • Some typical solutions for managing environment variables in the browser

Part 2:

  • Using environment variables with an Angular app served from an Azure App Service.

In How To: Using Environment Variables in the Browser with Angular and Azure App Services, Part 1 I talked about why its good to separate your configuration and code, how environment variables are usually the smartest answer, and how challenging it is to accomplish this with front-end code. I gave three examples of what people typically do in order to accomplish this.

In this part we’ll focus on one particular way (the best way, I think), and dive into a specific implementation using Angular 1 being served from an Azure App Service. These concepts will be similar for other Javascript front-ends, but I’ve chosen to use Angular 1 for this example.

Let’s take a look at how our solution will work.

  1. Before the Angular app bootstraps, it will make a request to the server for the environment variables using a GET /configs.json
  2. The server will pull the environment variables from the Azure app settings and then format it into a nice JSON object
  3. The client app will heavily cache the configs to avoid making the round-trip multiple times

Simple! You’ll notice that we’ll have to add some sort of processing logic on the App Service, which means hosting a server. Luckily, the App Services all run IIS, and we can configure IIS to run a little Node.js server only on a particular path.

Server-side

Let’s put the client aside and focus on how this works on the server.

Configuring the App Service

First we need to configure IIS to run our little Node.js server on the correct path. We’re actually going to fool the browser a little bit. The browser will ask for GET /configs.json but we are going to redirect it to GET /currentConfigs.js, which is where we’ll be running our Node.js server. Why do the redirect? Our IIS config needs to run a .js file for the server, and besides, we have other plans for that configs.json path (you’ll see).

We can configure these rules with a Web.config file. Drop this into the root of your web site:

<?xml version="1.0">
<configuration>
    <system.webServer>
        <rewrite>
            <rules>
                <clear />
                <rule name="Get dynamic configs in server environment">
                    <match url="configs.json" ignoreCase="true"/>
                    <action type="Redirect" url="currentConfigs.js" redirectType="Permanent" appendQueryString="true" />
                </rule>
            </rules>
        </rewrite>
        <handlers>
            <add name="iisnode" path="currentConfigs.js" verb="*" modules="iisnode" />
        </handlers>
    </system.webServer>
</configuration>

OK, XML is nasty, but here are the important parts:

  1. Add a rewrite rule to permanently redirect from /configs.json to currentConfigs.js
  2. Tell IIS to use the iisnode handler to handle any requests to currentConfigs.js. You guessed it, that means that iisnode will execute our little js file and we’re in business.

When you deploy your site (with this in the root) to an Azure App Service, it will automatically pick up these settings, and if you make a request to http://myazuresite.azurewebsites.net/configs.json it should redirect you over to currentConfigs.js (which doesn’t exist yet).

But what about localhost?

When you’re on your localhost you’re probably not running your Angular website through IIS. No problem - that’s why the Angular is going to ask for GET /configs.json and not go straight to currentConfigs.js. We’re actually going to add a configs.json file with all of your config values for localhost. This is a slight violation of maintaining your config separate from your code. We’re maintaining our developer configuration in our code, which is then overwritten any time we deploy anywhere. An acceptable trade-off, and if you wanted to fully replicate your IIS environment on your local machine, you’re welcome to do that and skip this step, although we’re going to use configs.json in the next step as well.

Here’s configs.json. Put in it all of the environment variables that your app will need:

{
    "myApiUrl": "https://localhost:3000",
    "myAppInsightsInstrumentationKey": "f209edf5-de97-4443-8607-c3d70d0573ee",
    // whatever you want
}

Now when you’re running locally, the Angular app will grab this file and not use the Node.js server.

Adding a Node.js Server

OK so we need to now add the actual Node.js server that’s going to run on the app service. The point of this server is to return the environment variables, nothing more.

There’s a problem, though - we don’t actually want to return to the browser the complete list of environment variables on the server - that’s a bad idea. Sometimes sensitive app credentials are in the environment variables and we don’t want to expose them for all the world.

We’re going to use a whitelist (more secure than a blacklist). So we want to explicitly allow the environment variables that the Node.js server can return. Where do we have such a list? Oh, yes, that’s right, we just made one and put it in configs.json. So we’ll make our Node.js server only return the environment variables whose keys also exist in the configs.json.

Now what’s in this currentConfigs.js file, you ask?

'use strict';
var http = require('http');

// Creating a little node server
http.createServer(function (req, res) {
    const configFilePath = './configs.json';

    // These are Azure-specific. They'll prefix the app setting
    // and connection string environment variables
    // with these prefixes
    const connectionStringPrefixes = [
        'APPSETTING_',
        'SQLAZURECONNSTR_',
        'SQLCONNSTR_',
        'MYSQLCONNSTR_',
        'CUSTOMCONNSTR_'
    ];

    try {
        // Load default config file
        const defaultConfigs = require(configFilePath);
        const configs = {
            timestamp: new Date().toISOString()
        };

        // Look for each of the keys found in default config file in the environment variables and add them to returned json if they're present
        let keys = Object.keys(defaultConfigs);
        for (let i = keys.length; i--;) {
            // The keys could be prefixed by any one of the acceptable connection string prefixes
            for (let j = connectionStringPrefixes.length; j--;) {
                var newConfig = process.env[connectionStringPrefixes[j] + keys[i]];
                if (newConfig) {
                    configs[keys[i]] = newConfig;
                }
            }
        }

        // Prepare response
        const maxAgeInSeconds = 604800; // 604800 = 1 week in seconds
        const headers = {
            'Content-Type': 'application/json',
            'Cache-Control': 'public, max-age=' + maxAgeInSeconds,
            'Expires': new Date(Date.now() + (maxAgeInSeconds * 1000)).toUTCString()
        };
        res.writeHead(200, headers);
        res.end(JSON.stringify(configs));
    } catch (err) {
        res.writeHead(500);
        res.end(err.toString());
    }
}).listen(process.env.PORT);

You’ll also notice that we’re caching the response for 1 week (604800 seconds) so that the Angular app won’t have to ask for these config values that often. I would cache them for much shorter while you’re developing.

So now if you hit your /configs.json endpoint, you should be getting back either the configs.json file directly, if you’re running locally, or being redirected to currentConfigs.js if you’re running in Azure. The currentConfigs.js file should be returning a JSON object with all of your whitelisted environment variables.

Angular app

OK so now how do we configure the Angular app to request these environment configs before bootstrapping the app?

It’s pretty easy with an open-source module called angular-deferred-bootstrap. Once you’ve installed it (see instructions on the module page) and loaded it into your website (maybe on your index.html page), you run it like this:

var configUrl = "configs.json";
deferredBootstrapper.bootstrap({
    element: document.body,
    module: 'app',
    moduleResolves: [
        {
            module: 'config',
            resolve: {
                environmentConstants: ['$http', function ($http) {
                    return $http.get(configUrl);
                }]
            }
        }
    ]
});

And then you take out the ng-app directive in your HTML page. For you fancy people, this replaces angular.bootstrap().

There - what you’ve just done is caused your app to make a GET request to /configs.json. Once the response is received angular-deferred-bootstrap caches it and makes it available to your application:

angular.controller('MyController', ['environmentConstants', function(environmentConstants) {
    console.log('myApiUrl is: ' + environmentConstants.myApiUrl));
}]);

Nifty, eh?

Recap

Separating out your code and your config is best practice and comes with many benefits. However, it’s difficult to do on the front end. I’ve presented a way that lets your Javascript ask the Azure App Service for environment variables before the Angular application is even loaded.

How have you managed your config and code? Leave a response in the comments.

comments powered by Disqus