As an experiment, for the past few weeks I’ve been working on replacing bb tasks with plain clj
. In the course of doing that I thought I’d also experiment with redoing the config system. I haven’t made a final decision about putting these changes into Biff, but I’m feeling good about them so far.
For the tasks, deps.edn
would look like this:
...
:aliases
{:dev {:extra-deps {com.biffweb/build {...}}
:extra-paths ["dev"]
:jvm-opts ["-XX:-OmitStackTraceInFastThrow"
"-XX:+CrashOnOutOfMemoryError"
"-Dbiff.env.file=env/dev.env"]
:main-opts ["-m" "build"]}
:prod {:jvm-opts ["-XX:-OmitStackTraceInFastThrow"
"-XX:+CrashOnOutOfMemoryError"
"-Dbiff.env.file=env/prod.env"]
:main-opts ["-m" "com.example"]}}}
Instead of bb foo
, you’d run clj -Mdev foo
. All the commands would behave the same as they do now. The default prod setup would have you run clj -Mprod
to start the app. Or something. I might fiddle with the specifics. I’m doing compilation automatically, and task startup time is actually looking fine. So I think this’ll work out pretty well–then the only prereq to using Biff is having clj
installed; no need to install bb
if you haven’t already.
As for config: this part I’m still pondering a fair bit. But at least as a proof-of-concept, I have it set up so you can set a :biff.config/spec
key in your system map that describes how config should be merged in from the environment. This is what I have for the example app currently:
(def spec
{:biff.middleware/cookie-secret {:secret true}
:biff.xtdb.jdbc/jdbcUrl {:secret true}
:biff/jwt-secret {:secret true}
:postmark/api-key {:secret true}
:recaptcha/secret-key {:secret true}
:biff.beholder/enabled {:default false, :coerce parse-boolean}
:biff.middleware/secure {:default true, :coerce parse-boolean}
:biff.xtdb/dir {:default "storage/xtdb"}
:biff.xtdb/topology {:default :standalone, :coerce keyword}
:biff/base-url {:default "http://localhost:8080"}
:biff/host {:default "localhost", :var-name "HOST"}
:biff/port {:default 8080, :coerce parse-long, :var-name "PORT"}
:postmark/from {}
:recaptcha/site-key {}
:biff.system-properties/user.timezone {:default "UTC"}
:biff.system-properties/clojure.tools.logging.factory {:default "clojure.tools.logging.impl/slf4j-factory"}})
...
(def initial-system
{:biff.config/spec config/spec
...})
And just finished writing up a docstring for a use-env-config
component which would replace the current use-config
and use-secrets
components:
"Merges configuration from the environment (or a file) into the system map.
Also sets system properties.
`spec` is a map where each key-value pair corresponds to an environment
variable. For example:
{:biff.middleware/cookie-secret {:secret true}
:biff/port {:default 8080, :coerce parse-long, :var-name \"PORT\"}
:biff.system-properties/user.timezone {:default \"UTC\"}
...}
Each key will be added to the system map. The value for each key will be read
from the environment by converting the key to an environment variable name.
Periods, slashes, and hyphens are converted to underscores, and letters are
converted to uppercase. For example:
:biff.middleware/cookie-secret -> BIFF_MIDDLEWARE_COOKIE_SECRET
Config value parsing can be modified by setting option keys in the values of
`spec`. The following options are recognized:
- :secret
When true, the config value will converted to a zero-argument function that
returns the config value (`value` -> `(fn [] value)`). This way, secrets
won't be exposed if you log the system map.
- :default
If the config value is nil or empty, it will be replaced with the value of
:default.
- :coerce
A one-argument function that will be used to parse the config value, if it
is set and non-empty.
- :var-name
If set, the config value will be read from this environment variable
instead of inferring an environment variable name as described above.
Environment variables can also be read from a file by setting the
biff.env.file system property, for example:
$ cat env/dev.env
HOST=0.0.0.0
...
$ clj -J-Dbiff.env.file=env/dev.env ...
If a config value is set in both the given file and in the environment, the
value from the environment will take precedence.
Any config values that are set to a key with a namespace of
`biff.system-properties` will be added to the system properties. For example:
:biff.system-properties/user.timezone {:default \"UTC\"}
;; Equivalent to the following, unless you override the default value:
(System/setProperty \"user.timezone\" \"UTC\")"
When you start a new project, you’ll have env/prod.env
and env/dev.env
files generated for you, which will look like this (with some of the values filled in):
## env/prod.env
BIFF_BASE_URL=https://example.com
BIFF_JWT_SECRET=
BIFF_MIDDLEWARE_COOKIE_SECRET=
POSTMARK_API_KEY=
POSTMARK_FROM=
RECAPTCHA_SECRET_KEY=
RECAPTCHA_SITE_KEY=
## env/dev.env
# Application
BIFF_BEHOLDER_ENABLED=true
BIFF_JWT_SECRET=
BIFF_MIDDLEWARE_COOKIE_SECRET=
BIFF_MIDDLEWARE_SECURE=false
HOST=0.0.0.0
RECAPTCHA_SECRET_KEY=
RECAPTCHA_SITE_KEY=
# Tasks
MAIN_NS=com.example
GENERATE_ASSETS_FN=com.example/generate-assets!
SOFT_DEPLOY_FN=com.example/on-save
SERVER=example.com
If you want to deploy with, say, an uberjar (there’ll be a clj -Mdev uberjar
task by the way), for config you just have to make sure the right env variables are set.
(That is I think the main advantage of going to env variables for config: easier to work with various PaaS things etc. that expect you to pass in config through the environment. )