A litmus test for whether an app has all config correctly factored out of the code is whether the codebase could be made open source at any moment, without compromising any credentials.
The twelve-factor app stores config in environment variables (often shortened to env vars or env). Env vars are easy to change between deploys without changing any code; unlike config files, there is little chance of them being checked into the code repo accidentally; and unlike custom config files, or other config mechanisms such as Java System Properties, they are a language- and OS-agnostic standard.
There are various methods available manage secrets and environment variables. Locally we will use our process manager configuration to handle our local environment variables and the test/staging/production environments will be managed by the platform you use to run your applications. For the majority of this workshop we'll be referencing configuration for the Now platform, but similar ideas exist whether you're deploying to kubernetes, a cloud provider, PAAS (like heroku) or gasp via virtual machines (using Chef, Puppet, Ansible, or otherwise).
Reader
is a lazy Product Type that enables the composition of computations that depend on a shared environment(e -> a)
. The left portion, thee
must be fixed to a type for all related computations. The right portiona
can vary in its type.
I'm sure this description is perfectly accessible for people well versed in category theory and Haskell data types, but for mere mortals like myself it requires further explanation.
The simplest way to understand Reader is that it provides a way to provide read-only data to any function (computation). So, for example, in our application where we have a specific base URL for calls to the football-data API, we may choose to pass this to our functions by wrapping them in a Reader rather than reaching into the process.env
global (which is impure).
const Reader = require('crocks/Reader');
const concat = require('crocks/pointfree/concat');
const { ask } = Reader;
// greet :: String -> Reader String String
const greet = greeting => Reader(name => `${greeting}, ${name}`);
// addFarewell :: String -> Reader String String
const addFarewell = farewell => str => ask(env => `${str}${farewell} ${env}`);
// flow :: Reader String String
const flow = greet('Hola')
.map(concat('...'))
.chain(addFarewell('See Ya'));
console.log(flow.runWith('Thomas'));
// => Hola, Thomas...See Ya Thomas
console.log(flow.runWith('Jenny'));
// => Hola, Jenny...See Ya Jenny
ReaderT
is a Monad Transformer that wraps a given Monad with aReader
. This allows the interface of a Reader that enables the composition of computations that depend on a shared environment(e -> a)
, but provides a way to abstract a means theReader
portion, when combiningReaderT
s of the same type. AllReaderT
s must provide the constructor of the target Monad that is being wrapped.
Ok, so in my opinion this is even harder to get into than before but is ultimately the most useful way of interacting with a Reader. What ReaderT is doing is embellishing the behaviour of the underlying Monad with the environment access features of a Reader.
It may not be immediately obvious when using this in simple examples such as those in this workshop, however, as your applications grow and the flows begin to have multiple steps (for example, multiple http requests) then having the ability to access the environment in any discrete step is very helpful indeed. This also allows us to split up the steps into discrete chunks to allow easier testing...
const ReaderT = require('crocks/Reader/ReaderT');
const Maybe = require('crocks/Maybe');
const safe = require('crocks/Maybe/safe');
const isNumber = require('crocks/predicates/isNumber');
const and = require('crocks/logic/and');
const MaybeReader = ReaderT(Maybe);
const { ask, liftFn } = MaybeReader;
const { Just, Nothing } = Maybe;
// add :: Number -> Number -> Number
const add = x => y => x + y;
// Typical Constructor
MaybeReader(safe(isNumber)).runWith(76);
//=> Just 76
MaybeReader(safe(isNumber)).runWith('76');
//=> Nothing
const add10ToEnv = ask(x => x + 10);
const add20ToEnv = ask(x => x + 20); // x will be set to the value provided in runWith
const flow = add10ToEnv.chain(x => add20ToEnv.map(y => x + y));
console.log(flow.runWith(1)); // Just(1 + 10) + Just(1 + 20) = Just 32
console.log(flow.runWith(10)); // Just(10 + 10) + Just(10 + 20) = Just 50
- Take the hard coded secrets and configuration out of your world cup microservice and retrieve them from the environment. Consider which elements of your application may need to differ between environments (this includes testing environments which run unit and integration tests)
- Introduce ReaderT into your application to provide configuration to your Async wrapped side-effects
Reader
documentationReaderT
documentation- Hashicorp Vault
- Consul
- Zookeeper
- Kubernetes Secrets
- Now Environment Variables and Secrets
Next - metrics