简体   繁体   中英

Ensuring Azure keyvault secrets are loaded to config (node-config) at application startup

I have a NodeJS application that uses Node-Config ( https://www.npmjs.com/package/config ) to load application configurations. What I'm trying to do is to load secrets from Azure Keyvault to the config during startup, and ensure these are available before required (eg connecting to databases etc).

I have no problem connecting to and retrieving values from the Keyvault, but I am struggling with the non-blocking nature of JS. The application startup process is continuing before the config values have completed loaded (asynchronously) to the config.

  • One strategy could be to delay application launch to await the keyvault secrets loading How to await in the main during start up in node?
  • Another would be to not load them in Config but instead modify code where-ever secrets are used to load these asynchronously via promises

It seems like this will be a common problem, so I am hoping someone here can provide examples or a design pattern of the best way of ensuring remote keyvault secrets are loaded during startup.

Thanks in advance for suggestions.

Rod

I have now successfully resolved this question.

A key point to note is setting process.env['ALLOW_CONFIG_MUTATIONS']=true; Configs are immutable by default (they can't be changed after initial setting). Since async is going to resolve these later, it's critical that you adjust this setting. Otherwise you will see asynchronous configs obtaining correct values from the keystore, but when you check with config.get they will not have been set. This really should be added to the documentation at https://github.com/node-config/node-config/wiki/Asynchronous-Configurations

My solution: first, let's create a module for the Azure keystore client - azure-keyvault.mjs :

import { DefaultAzureCredential } from '@azure/identity';
import { SecretClient } from '@azure/keyvault-secrets';

// https://docs.microsoft.com/en-us/azure/developer/javascript/how-to/with-web-app/use-secret-environment-variables
if (
  !process.env.AZURE_TENANT_ID ||
  !process.env.AZURE_CLIENT_ID ||
  !process.env.AZURE_CLIENT_SECRET ||
  !process.env.KEY_VAULT_NAME
) {
  throw Error('azure-keyvault - required environment vars not configured');
}

const credential = new DefaultAzureCredential();

// Build the URL to reach your key vault
const url = `https://${process.env.KEY_VAULT_NAME}.vault.azure.net`;

// Create client to connect to service
const client = new SecretClient(url, credential);

export default client;

In the config (using @node-config) files:

process.env['ALLOW_CONFIG_MUTATIONS']=true;
const asyncConfig = require('config/async').asyncConfig;
const defer = require('config/defer').deferConfig;
const debug = require('debug')('app:config:default');
// example usage debug(`\`CASSANDRA_HOSTS\` environment variable is ${databaseHosts}`);

async function getSecret(secretName) {
  const client = await (await (import('../azure/azure-keyvault.mjs'))).default;
  const secret = await client.getSecret(secretName);
  // dev: debug(`Get Async config: ${secretName} : ${secret.value}`);
  return secret.value
}

module.exports = {
  //note: defer just calculates this config at the end of config generation 
  isProduction: defer(cfg => cfg.env === 'production'),

  database: {
    // use asyncConfig to obtain promise for secret
    username: asyncConfig(getSecret('DATABASE-USERNAME')), 
    password: asyncConfig(getSecret('DATABASE-PASSWORD')) 
  },
...
}

Finally modify application startup to resolve the async conferences BEFORE config.get is called

server.js

const { resolveAsyncConfigs } = require('config/async');
const config = require('config');
const P = require('bluebird');

...
function initServer() {
  return resolveAsyncConfigs(config).then(() => {
    // if you want to confirm the async configs have loaded
    // try outputting one of them to the console at this point
    console.log('db username: ' + config.get("database.username"));
    // now proceed with any operations that will require configs
    const client = require('./init/database.js');
    // continue with bootstrapping (whatever you code is)
    // in our case let's proceed once the db is ready
    return client.promiseToBeReady().then(function () {
      return new P.Promise(_pBootstrap);
    });
  });
}

I hope this helps others wishing to use config/async with remote keystores such as Azure. Comments or improvements on above welcome.

~ Rod

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM