@thinkmill/devops-env-vars
Advanced tools
Comparing version 2.0.0 to 3.0.0
40
index.js
@@ -9,23 +9,2 @@ 'use strict'; | ||
// Configure the VPC's in different AWS regions | ||
const AWS_VPCS = [ | ||
// Thinkmill Sydney (ap-southeast-2) | ||
{ cidr: '10.117.0.0/16', env: 'live' }, | ||
{ cidr: '10.118.0.0/16', env: 'staging' }, | ||
{ cidr: '10.119.0.0/16', env: 'testing' }, | ||
// Also.. 10.97.0.0/16 Blueshyft XIT | ||
// Thinkmill Ireland (eu-west-1) | ||
{ cidr: '10.130.0.0/16', env: 'live' }, | ||
{ cidr: '10.131.0.0/16', env: 'staging' }, | ||
{ cidr: '10.132.0.0/16', env: 'testing' }, | ||
// blueshyft Sydney (ap-southeast-2) | ||
{ cidr: '10.20.0.0/16', env: 'live' }, | ||
{ cidr: '10.21.0.0/16', env: 'staging' }, | ||
{ cidr: '10.22.0.0/16', env: 'testing' }, | ||
// Also.. 10.30.0.0/16 XIT | ||
]; | ||
// The different environments we support | ||
@@ -54,5 +33,9 @@ const SUPPORTED_ENVS = ['live', 'staging', 'testing', 'development']; | ||
/* | ||
Figures out which APP_ENV to use, based on the value supplied, the supported envs and the servers IP address | ||
// Figures out which APP_ENV to use, based on the value supplied, the supported envs and the servers IP address | ||
function determineAppEnv (_processAppEnv) { | ||
networksArray is expected in the format.. | ||
[{ cidr: '72.67.5.0/16', env: 'live' }, { cidr: '72.68.5.0/16', env: 'staging' }, { cidr: '72.69.5.0/16', env: 'testing' }] | ||
*/ | ||
function determineAppEnv (_processAppEnv, networksArray = []) { | ||
const debug = debugLib('@thinkmill/devops-env-vars:determineAppEnv'); | ||
@@ -71,9 +54,9 @@ | ||
const serverIp = getServerIp(); | ||
const possibleVpcs = AWS_VPCS.filter(vpc => new Netmask(vpc.cidr).contains(serverIp) && SUPPORTED_ENVS.includes(vpc.env)); | ||
if (possibleVpcs.length > 1) throw new Error(`Server IP matches > 1 potential VPC: ${possibleVpcs.map(vpc => (`${vpc.env} ${vpc.cidr}`)).join('; ')}`); | ||
const candidateNetworks = networksArray.filter(network => new Netmask(network.cidr).contains(serverIp) && SUPPORTED_ENVS.includes(network.env)); | ||
if (candidateNetworks.length > 1) throw new Error(`Server IP matches > 1 potential network: ${candidateNetworks.map(network => (`${network.env} ${network.cidr}`)).join('; ')}`); | ||
let envRtn = 'development'; | ||
if (possibleVpcs.length === 1) { | ||
debug(`APP_ENV determined from server IP as ${chalk.cyan(possibleVpcs[0].env)} (${chalk.green(serverIp)} is within ${chalk.green(possibleVpcs[0].cidr)})`); | ||
envRtn = possibleVpcs[0].env; | ||
if (candidateNetworks.length === 1) { | ||
debug(`APP_ENV determined from server IP as ${chalk.cyan(candidateNetworks[0].env)} (${chalk.green(serverIp)} is within ${chalk.green(candidateNetworks[0].cidr)})`); | ||
envRtn = candidateNetworks[0].env; | ||
} | ||
@@ -174,3 +157,2 @@ | ||
supportedEnvs: SUPPORTED_ENVS, | ||
awsVpcs: AWS_VPCS, | ||
}; |
{ | ||
"name": "@thinkmill/devops-env-vars", | ||
"version": "2.0.0", | ||
"version": "3.0.0", | ||
"description": "Helper functions that encapsulate our treatment of environment vars for KeystoneJS apps", | ||
"main": "index.js", | ||
"license": "MIT", | ||
"scripts": { | ||
@@ -25,4 +26,5 @@ "release-patch": "npm version patch && git push && git push --tags", | ||
"devDependencies": { | ||
"eslint": "^3.2.2" | ||
"eslint": "^3.10.0", | ||
"eslint-config-keystone": "^3.0.0" | ||
} | ||
} |
197
README.md
@@ -10,12 +10,5 @@ Devops: Environment Variables | ||
The code block below demonstrates how this library should be used in a modern KeystoneJS app. | ||
It's based on the [`config.js` in the `admyt-platform` codebase](https://github.com/Thinkmill/admyt-platform/blob/develop/config.js). | ||
The code block below demonstrates how this library can be used in a fictional KeystoneJS app, the `chumble-platform`. | ||
```javascript | ||
'use strict'; | ||
// This doesn't actually stop the config from being loaded onto clients, it's just a warning for developers | ||
if (typeof window !== 'undefined') throw new Error(`You definitely shouldn't require ./config on the client`); | ||
```js | ||
const envLib = require('@thinkmill/devops-env-vars'); | ||
@@ -26,4 +19,10 @@ const path = require('path'); | ||
// This doesn't actually stop the config from being loaded onto clients, it's just a warning for developers | ||
if (typeof window !== 'undefined') throw new Error(`You definitely shouldn't require ./config on the client`); | ||
// Determine the current APP_ENV | ||
const APP_ENV = envLib.determineAppEnv(process.env.APP_ENV); | ||
const APP_ENV = envLib.determineAppEnv( | ||
process.env.APP_ENV, | ||
[{ cidr: '72.67.5.0/16', env: 'live' }, { cidr: '72.68.5.0/16', env: 'staging' }, { cidr: '72.69.5.0/16', env: 'testing' }], | ||
); | ||
@@ -43,12 +42,12 @@ // Convert the APP_ENV to some handy flags | ||
// If not supplied, Keystone will default to localhost (ie. in dev) | ||
MONGO_URI: { required: flags.IN_PRODUCTION, default: 'mongodb://localhost/admyt-platform' }, | ||
MONGO_URI: { required: flags.IN_PRODUCTION, default: 'mongodb://localhost/chumble-platform' }, | ||
// Used to encrypt user cookies; not important in dev | ||
JWT_TOKEN_SECRET: { required: flags.IN_PRODUCTION, default: 'gottalovejwts' }, | ||
JWT_TOKEN_SECRET: { required: flags.IN_PRODUCTION, default: 'dev-secret-goes-here' }, | ||
// When not live, allow to be defaulted to a test key | ||
MANDRILL_API_KEY: { required: flags.IN_PRODUCTION, default: 'testkeygoeshere' }, | ||
MANDRILL_API_KEY: { required: flags.IN_PRODUCTION, default: 'test-key-goes-here' }, | ||
// Cloudinary creds; used by Types.CloudinaryImage | ||
CLOUDINARY_URL: { required: flags.IN_PRODUCTION, default: 'cloudinary://862989489411169:Wp74nFvzkSPGkQHgtCBH7wN4Yik@thinkmill' }, | ||
CLOUDINARY_URL: { required: flags.IN_PRODUCTION, default: 'cloudinary://012345678902345:9FDRoGKGpYZVASNDwyTdJRKOIku@thinkmill' }, | ||
@@ -69,8 +68,2 @@ // S3 credentials; used by Types.S3File | ||
// For the eCentric payment gateway | ||
ECENTRIC_MERCHANT_ID: { required: flags.IN_PRODUCTION }, | ||
// Recreate the entire DB | ||
RESET_DATABASE: { required: false, default: false, type: Boolean }, | ||
// What port should the webserver bind to | ||
@@ -81,13 +74,30 @@ PORT: { required: flags.IN_PRODUCTION, default: 3000, type: Number }, | ||
// Set any other static or derived vars (that don't need to be overridden by .env or process vars) | ||
config.OTHER_IMPORTANT_VARS = 'blah blah' | ||
config.FORCE_SSL = flags.IN_PRODUCTION; | ||
// Support details | ||
config.FROM_EMAIL = 'support@chumble.com.au'; | ||
config.FROM_NAME = 'Chumble Support'; | ||
config.SUPPORT_PHONE_NUMBER = '1800 422 554'; | ||
// .. | ||
// Where should we address the plumbus API | ||
config.PLUMBUS_API_URL = ({ | ||
live: 'https://api.plumbus.net.au', | ||
staging: 'https://api-staging.plumbus.net.au', | ||
testing: 'https://api-testing.plumbus.net.au', | ||
development: 'http://localhost:7634', // Use a local stub server in dev | ||
})[APP_ENV]; | ||
// Lock and export the config vars | ||
// Are we disabling developer authentication to developer endpoints? | ||
config.ALLOW_UNAUTHENTICATED_ACCESS_TO_DEVELOPER_ENDPOINTS = IN_DEVELOPMENT; | ||
// Can calls to the /ploobis/create endpoint specify their own fleeb? | ||
config.ALLOW_FLEEB_TO_BE_SPECIFIED_ON_CREATE = true; | ||
// Can ploobis be reset even after email generation has commenced | ||
config.ALLOW_PLOOBIS_RESET_AFTER_EMAIL_GENERATION = !IN_LIVE; | ||
// Freeze and export the config vars | ||
module.exports = Object.freeze(config); | ||
``` | ||
Lets step though the code above in detail. | ||
Lets step though the code above in detail.. | ||
@@ -99,7 +109,6 @@ | ||
Since some of the config variables are also often needed client side, there's a temptation to simply require `config.js` there too. | ||
This is a terrible, terrible idea; it usually exposes security-sensitive values to the end user. | ||
The `config.js` file should simply never leave the server. | ||
This is a terrible idea for, hopefully, obvious reasons; it almost certainly exposes security-sensitive values to the end user. | ||
We put this warning in place as a last ditch effort to prevent accidental inclusion. | ||
```javascript | ||
```js | ||
// This doesn't actually stop the config from being loaded onto clients, it's just a warning for developers | ||
@@ -113,12 +122,13 @@ if (typeof window !== 'undefined') throw new Error(`You definitely shouldn't require ./config on the client`); | ||
First, we call `determineAppEnv()`, which determines the current `APP_ENV` by inspecting the servers IP address the `APP_ENV` value supplied by `process.env` (if present): | ||
We call `determineAppEnv()` to determines the current `APP_ENV`. | ||
```javascript | ||
```js | ||
// Determine the current APP_ENV | ||
const APP_ENV = envLib.determineAppEnv(process.env.APP_ENV); | ||
const APP_ENV = envLib.determineAppEnv( | ||
process.env.APP_ENV, | ||
[{ cidr: '72.67.5.0/16', env: 'live' }, { cidr: '72.68.5.0/16', env: 'staging' }, { cidr: '72.69.5.0/16', env: 'testing' }], | ||
); | ||
``` | ||
It inspects the servers IP address the `APP_ENV` value supplied by `process.env` (if present). | ||
This determination is based on the IP address ranges we use for VPCs in our deployed regions, | ||
(documented in the Thinkmill Wiki)[https://github.com/Thinkmill/wiki/blob/master/infrastructure/ip-addresses.md]. | ||
The valid `APP_ENV` are: | ||
@@ -141,5 +151,3 @@ | ||
This may not hold for all apps, especially older apps created before our `APP_ENV` usage was codified. | ||
`envLib.buildAppFlags(APP_ENV)` | ||
@@ -150,3 +158,3 @@ -------------------------------------------------------------------------------- | ||
```javascript | ||
```js | ||
// Convert the APP_ENV to some handy flags | ||
@@ -163,3 +171,3 @@ const flags = envLib.buildAppFlags(APP_ENV); | ||
```javascript | ||
```js | ||
console.log(flags); | ||
@@ -173,5 +181,5 @@ // { IN_LIVE: false, IN_STAGING: true, IN_TESTING: false, IN_DEVELOPMENT: false, IN_PRODUCTION: true } | ||
Next, standard practice is to seek out a `.env` file in the directory above the application root, named for the current `APP_ENV`: | ||
Standard practice is to seek out a `.env` file in the directory above the application root, named for the current `APP_ENV`: | ||
```javascript | ||
```js | ||
// Attempt to read the local .env file for this APP_ENV | ||
@@ -181,5 +189,6 @@ if (!flags.IN_DEVELOPMENT) dotenv.config({ path: path.resolve(`../${APP_ENV}.env`) }); | ||
This file should contain any credentials, settings, etc. that are required for the environment but too sensitive to store in the codebase. | ||
Mandrill API keys, merchant account credentials, live Mongo connection URIs, etc. might be required for a live system but generally aren't needed in development. | ||
As such, the code above skips this step when `IN_DEVELOPMENT` is true. | ||
This file should contain any variables required for the environment but security sensitive, so not store in the repo. | ||
Eg. Mandrill API keys, merchant account credentials, Mongo DB URIs, etc. | ||
Often these can be defaulted in development environments. | ||
The code above skips this step when `IN_DEVELOPMENT` is true. | ||
@@ -189,3 +198,5 @@ If the `.env` file isn't found a warning will be printed to `stderr` but the app will continue to load. | ||
**The `dotenv` package loads these variables directly into the `process.env` scope.** | ||
**IMPORTANT:** | ||
The `dotenv` package loads these variables directly into the `process.env` scope. | ||
This is the default behaviour of `dotenv` and actually pretty useful if you have variables used by packages that don't accept values any other way. | ||
@@ -200,3 +211,3 @@ In it's standard usage, no other part of this process alters the `process.env` scope; we mostly work out of the `config` object, created next. | ||
```javascript | ||
```js | ||
// Extract the vars defined from process.env and apply validation and defaults | ||
@@ -220,5 +231,7 @@ const config = envLib.mergeConfig(APP_ENV, flags, process.env, { | ||
If supplied, the value given by the environment will be interpreted as this type. | ||
If an appropriate value can't be unambiguously determined (ie. a value of "coffee" suppled for a `Boolean` value) an error will be thrown. | ||
If an appropriate value can't be unambiguously determined (eg. a value of "coffee" suppled for a `Boolean` value) an error will be thrown. | ||
As noted above, **the `mergeConfig()` function does not modify the `process.env` scope**. | ||
**IMPORTANT:** | ||
As noted above, the `mergeConfig()` function does not modify the `process.env` scope. | ||
Variables that are defaulted based on the validation rules supplied will only exist in the object returned by `mergeConfig()`. | ||
@@ -234,57 +247,18 @@ | ||
They're usually either constants or values that are derived from the other environment variables. | ||
Some examples, adapted from various codebases, are included below. | ||
### Static Values | ||
### Examples | ||
Values that are constant for now but may change in future. Eg.. | ||
Support contact details: | ||
SodaKING product pricing: | ||
```javascript | ||
config.CANISTER_EXCHANGE_PRICE_PER_UNIT_IN_CENTS = 1895; | ||
config.CANISTER_SELL_PRICE_PER_UNIT_IN_CENTS = 4495; | ||
```js | ||
// Support details | ||
config.FROM_EMAIL = 'support@chumble.com.au'; | ||
config.FROM_NAME = 'Chumble Support'; | ||
config.SUPPORT_PHONE_NUMBER = '1800 422 554'; | ||
``` | ||
Blueshyft support contact details: | ||
An the URL of an external system based on the current `APP_ENV`: | ||
```javascript | ||
config.FROM_EMAIL = 'support@blueshyft.com.au'; | ||
config.FROM_NAME = 'Blueshyft Support'; | ||
config.SUPPORT_PHONE_NUMBER = '1800 817 483'; | ||
``` | ||
### Addressing External Systems | ||
Many (all?) Thinkmill apps rely on external systems that differ between environments (`APP_ENV`). | ||
This is especially true in for blueshyft, where requests often require the cooperation of shared | ||
internal services (such as the core, transaction engine, etc) and external services (such as remote partner APIs). | ||
Since both these approaches add values directly to the config object (without using `mergeConfig()`), | ||
values set in this way can't be overridden/set without code changes. | ||
#### blueshyft Apps | ||
For the blueshyft network of apps, the | ||
[`@thinkmill/blueshyft-network` package](https://www.npmjs.com/package/@thinkmill/blueshyft-network) | ||
was developed to centralise the addressing of apps across environments. | ||
Usage of the package looks like this: | ||
```javascript | ||
const network = require('@thinkmill/blueshyft-network'); | ||
// Pull in any vars we want for the network config (for this APP_ENV) and merge them into our config | ||
config = Object.assign(config, network.getVars(APP_ENV, [ | ||
'CORE_API_URL', | ||
'PCA_TRANSACTIONS_API_URL', | ||
'TLS_ECOSYSTEM', | ||
])); | ||
``` | ||
See the [package docs](https://www.npmjs.com/package/@thinkmill/blueshyft-network) for details. | ||
#### Non-blueshyft Apps | ||
In non-blueshyft systems, an implementation pattern has evolved to define a set of values while maintaining readability. | ||
```javascript | ||
```js | ||
// Where should we address the plumbus API | ||
config.PLUMBUS_API_URL = ({ | ||
@@ -294,21 +268,17 @@ live: 'https://api.plumbus.net.au', | ||
testing: 'https://api-testing.plumbus.net.au', | ||
development: 'http://localhost:7634', // Plumbus stub server | ||
development: 'http://localhost:7634', // Use a local stub server in dev | ||
})[APP_ENV]; | ||
``` | ||
It can be useful to control specific functionality with feature flags: | ||
### Feature Flags | ||
It's often useful to control specific code branches with individual flags. | ||
These examples taken from the `blueshyft-transactions-api` codebase: | ||
```javascript | ||
```js | ||
// Are we disabling developer authentication to developer endpoints? | ||
config.ALLOW_UNAUTHENTICATED_ACCESS_TO_DEVELOPER_ENDPOINTS = IN_DEVELOPMENT; | ||
// Can calls to the /sweeps/create end point specify the sweepday used or do we exclusively rely on getNextSweepday() | ||
config.ALLOW_SWEEPDAY_TO_BE_SPECIFIED_ON_CREATE = true; | ||
// Can calls to the /ploobis/create endpoint specify their own fleeb? | ||
config.ALLOW_FLEEB_TO_BE_SPECIFIED_ON_CREATE = true; | ||
// Can sweeps be 'reset' after email generation has started | ||
config.ALLOW_RESET_AFTER_EMAIL_GENERATION = !IN_LIVE; | ||
// Can ploobis be reset even after email generation has commenced | ||
config.ALLOW_PLOOBIS_RESET_AFTER_EMAIL_GENERATION = !IN_LIVE; | ||
``` | ||
@@ -320,10 +290,9 @@ | ||
The final lines in our example export the `config` object we've created for use by the app after | ||
[freezing](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) it. | ||
This prevents any other part of the application from accidenally making changes to this object. | ||
In this example we [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) | ||
the config object before exporting it for use in our app. | ||
This goes some way towards preventing other parts of the application from unintentionally setting config values. | ||
```javascript | ||
// Lock and export the config vars | ||
```js | ||
// Freeze and export the config vars | ||
module.exports = Object.freeze(config); | ||
``` | ||
45871
6
2
121
281