3

I'm very open to learning if there's a better "best practices" way to do this, but I have some scripts that I run occasionally that edit a database, and so I need to pass the DB password for those scripts. I'm getting the password by calling a function that calls google cloud Secrets Manager, and I'm unable to add it to the process.env.

for example if I put this at the top of my script file:

process.env.DB_HOST='127.0.0.1';
process.env.DB_USER='michael';
process.env.DB_NAME='staging-db';
process.env.DB_PORT=1234;
process.env.DB_PASS= await accessSecret('projects/myproject-123/secrets/DB_PASS/versions/latest');

When the above runs I get the error
SyntaxError: await is only valid in async functions and the top level bodies of modules

But, if I move the process.env.DB_PASS setting inside my async main() function, then it has local scope to that main function. Other files called by functions in this script see process.env.DB_PASS as undefined (but do see values for any process.env variables set globally at the top of the file.

How do I pull in and set that secret without actually pasting the literal secret into the code?

To represent the problem of the scoping, here's a working-code recreation of that problem in action. This is the script file I'm running:

process.env.DB_HOST='127.0.0.1';
process.env.DB_USER='michael';
process.env.DB_NAME='staging-db';
process.env.DB_PORT=1234;
const db = require('../../src/database/process_pull_test');


const main = async () => {
  process.env.SCOPED_KEY = "helloimscoped"
  db.hello();
}

main().catch((e) => {console.error(e)});

Here is the process_pull_test file

console.log("SCOPED KEY", process.env.SCOPED_KEY);
const dbHost = process.env.DB_HOST;
const dbUser = process.env.DB_USER;
const dbName = process.env.DB_NAME;
const dbPort = process.env.DB_PORT;
const scopedKey = process.env.SCOPED_KEY;

async function hello() {
  console.log(dbHost);
  console.log(dbUser);
  console.log(dbName);
  console.log(dbPort);
  console.log(scopedKey);
  return console.log("Hello Secrets");
}


module.exports = {
  hello: hello
}

And, here is the output

SCOPED KEY undefined
127.0.0.1
michael
staging-db
1234
undefined
Hello Secrets
singmotor
  • 3,930
  • 12
  • 45
  • 79

2 Answers2

2

But, if I move the process.env.DB_PASS setting inside my async main() function then it has local scope to that main function. Other files called by functions in this script see process.env.DB_PASS as undefined (but do see values for any process.env variables set globally at the top of the file.

This is not correct, it will be set globally. Chances are simply that your other files execute before your main() runs.

Generally the solution is to simply make sure that after you set your environment variables in main(), you call all other logic. This means all your logic should be in functions.

Evert
  • 93,428
  • 18
  • 118
  • 189
  • I am certain that my main file is called first. The only other non-functional line other than those process.env sets and requires is `main().catch((e) => {console.error(e)});` I also tried moving all environment variable setting inside a function with `loadEnv().then(() =>{ main().catch((e) => {console.error(e)}); });` – singmotor Jan 12 '22 at 06:37
  • I've also added `console.log(process.env.DB_PASSWORD)` after setting it inside main, and confirmed that it is correctly pulled (it logs inside main before the external file/function is called). Then, logging inside the external file it's undefined – singmotor Jan 12 '22 at 06:41
  • 1
    Yes, the main file is loaded first, but required modules run code outside module functions at require time BEFORE you call your first function. – kevintechie Jan 12 '22 at 06:41
  • @kevintechie yet, what your describing makes no sense. If you don't believe me that `process` is always global, please share a _complete_ example as proof so we can analyze it together – Evert Jan 12 '22 at 06:46
  • I'm not disagreeing. I'm simply explaining why you are correct. OP is likely reading the process.env before it is set because the code that does so is executed at require time. – kevintechie Jan 12 '22 at 06:49
  • @kevintechie i meant to tag OP, not you! sorry for the confusion =) – Evert Jan 12 '22 at 06:51
  • @kevintechie I am including a file via require that uses those process.env values. I didn't realize that the file ran at require time. Let me trim down these two files to give the two of you a full-code example of the problem – singmotor Jan 12 '22 at 07:01
  • @singmotor Execution at require time one of the things that isn't covered by most Node tutorials and is often learned via painful experience. You're not the first one to be caught by this behavior. – kevintechie Jan 12 '22 at 07:06
  • @kevintechie I've added two example files to show the scoping issue. So is the solution here to 1. Run an async function to load/set process.env variables 2. Run a function with all of my imports 3. Run my main function? I can't do 2 though because my other functions use the imports in 2 and so then error out because they can't find those variables – singmotor Jan 12 '22 at 07:13
  • 1
    @singmotor great example, very helpful, and perfectly demonstrates the issue. You are assigning the constants _outside_ your `hello()` function. This code runs _before_ `main()`. Try moving the `const db*` statements inside the `hello()` function. – Evert Jan 12 '22 at 07:25
  • @Evert noted, but the process_pull_test isn't my code, and those variables are used in a bunch of different places/functions inside that file (not just that one hello() function. Is the only option to fix that other file to better fit best practices and remove those global variable assignments from process.env? – singmotor Jan 12 '22 at 07:29
  • 1
    @singmotor just don't rely on these file-level definitions, instead use functions. I assume part of the issue is that you `export const` these values in your real code. Better is to just export functions and return the data that all your other files need. – Evert Jan 12 '22 at 07:31
  • 1
    The alternative is that you use `let`, and instead of creating these variables immediately, you do it from some kind of `init()` function that you call from your `main()`. In this case _you_ need to guarantee that this function is actually used before anything else needs the values. The safest is to just use the function though. – Evert Jan 12 '22 at 07:32
  • @Evert I appreciate the patient answers, I think I'm close. When you say I "export const these values in your real code" which file are you referring to? The module.exports is the only exports in either file (and it actually exports 5 different functions. The difficulty with calling the init() is that there are almost 100 imports/requires from different functions in the file that all use various declared consts – singmotor Jan 12 '22 at 07:46
  • 1
    Without seeing all your code, it's had to say where specific consts / variables should go. Generally, configuation should be managed from a central location and accessed locally. Same with global consts, but they should not be in the global scope -- require them in and access locally. Consts that are only used locally can be left in the local file. – kevintechie Jan 12 '22 at 08:01
  • 1
    @singmotor it's only a problem for variables/constants that need to be initialized with a value that comes from an asynchronous source. And indeed, specific examples help so we can give specific solutions. The general solution is import functions not values. – Evert Jan 12 '22 at 16:23
1

Typically you use some sort of configuration tool such as dotenv or config to manage app settings and secrets. Be sure NOT to save the secret files in source control.

To get your db secret, you have to make an async call to Google and get the response before you attempt to read any variables that need that data.

I'll assume you're just running this from the command line.

First you'll need an async iife to execute your main:

(async () => {
    // you could put your main code here.
    await main();
})();

Next, add a require to your Google secrets manager. It can go anywhere, but you should make sure you have access to it where you only call it once. You might as well do it in your main file.

const gcsm = require('./secretManager');

Then call your Google secrets manager in main. (I'm making up the api.) and set your env. You will also need to await your call to db.hello() when you actually put async code in it. Technically, you don't need to do it with the code as written.

const main = async () => {
  const dbPassword = await gcsm.getSecret('some config location');
  process.env.DB_PASS = dbPassword;
  process.env.SCOPED_KEY = "helloimscoped"
  await db.hello();
}

In hello() in process_pull_test, read the env variable.

async function hello() {
  console.log(process.env.DB_PASS);
  console.log(dbHost);
  console.log(dbUser);
  console.log(dbName);
  console.log(dbPort);
  console.log(scopedKey);
  // note the following line doesn't return anything because
  // console.log returns undefined. You probably want to return a 
  // string value to test for now.
  return console.log("Hello Secrets");
}   

This should get you going.

Additional comments:

  • When you first log SCOPED_KEY in process_pull_test it isn't scoped it's just logged at require time and is still undefined.
  • When you log scopedKey in hello(), it's still undefined because you set it at require time at the top of process_pull_test.
  • When you set SCOPED_KEY in main(), it's too late, you've already saved it to scopedKey as undefined at require time. If you were to set scopedKey to process.env.SCOPED_KEY in hello() before you log it, you would see your updated value.
kevintechie
  • 1,441
  • 1
  • 13
  • 15
  • Yes, but the requirement here is that I don't save the secret locally. I need to pull it from Google Cloud Secrets – singmotor Jan 12 '22 at 06:38
  • You can still set process.env variables and they will be globally available. However, as evert mentioned you have to access them in function code that is called after they have been set. – kevintechie Jan 12 '22 at 06:46
  • 1
    @singmotor updated my answer based on your updates. – kevintechie Jan 12 '22 at 07:38
  • in your above example where are you importing `db` (since you call await db.hello()) – singmotor Jan 12 '22 at 07:56
  • I followed your code. You require db in your main file. – kevintechie Jan 12 '22 at 08:04
  • I just fixed a syntax error in my require of the gcsm module. I'm used to ES6 import now. – kevintechie Jan 12 '22 at 08:06
  • So you're importing db outside that first async block of code you put? If so, I'll follow your structure and give that a try, thanks you! – singmotor Jan 12 '22 at 08:08
  • 1
    Yes, generally you do all your imports / requires first. Then any setup you want to do at require time (only code that rely on static operations). Next, define your module functions and then export them (except don't export from main). – kevintechie Jan 12 '22 at 08:13