Config Management
- Status: accepted
- Deciders: @dynamike, @pjdufour-truss
- Date: 2018-09-21
Context and Problem Statement
Our webserver (https://github.com/transcom/mymove/cmd/webserver
) currently supports config variables defined as flags or environment variables (via https://github.com/namsral/flag
), but our use of config throughout the application is not managed in a cohesive way. While the use of direnv and .envrc
provides some basic external validation of the environment variables, we have a need to bring config parsing and validation logic into the application itself. Beyond whether a config variable is set or not, we need to validate the values of these variables. For example:
- is a port in the range from
8000 - 8999
, - is the hostname valid,
- is the certificate valid,
- is the storage backend
filesystem
ors3
, - does s3 bucket name point to an existing bucket, and
- more.
The use of a more robust config framework with standard patterns will enable the seamless integration of new options and application contexts as we add new features to mymove
. Better management of config will enable the following:
- turn features on and off,
- debug startup errors,
- local docker server,
- per-branch test environments, and
- more.
Decision Drivers
- Maintained (new commits less than 6 months ago)
- Support environment variables, and command line flags.
- Supports integer, duration, and time variables.
- Supports complex config or JSON, e.g.,
map[string]string
. - Mark variables as required and implement sanity checks
Considered Options
- Built-in flag package
- Viper & pflag
- https://github.com/namsral/flag
- https://github.com/jessevdk/go-flags
Decision Outcome
Chosen option: "Viper & pflag". This option has the most community support and will give us continued flexibility as the code base grows over time.
Pros and Cons of the Options
Built-in flag package
Go ships with a built-in flag package that provides support for command line flags.
- Good, no additional dependencies.
- Good, maintained but shouldn't receive any improvements either.
- Good, supports bool, (u)int, (u)int64, (u)float64, time.Duration, and string.
- Bad, no support for JSON variables or complex config, e.g.,
map[string]string
. - Bad, cannot mark variables as required (only provide defaults)
- Bad, invalid flag values cause panic (making custom sanity checks impossible)
Viper & pflag
Viper and pflag are 2 packages that are used together to enable 12-factor applications in Go. VIPER is used by some of the most widely used Go programs, including Hugo
, go-swagger
, and jfrog-cli-go
.
- Good, viper and pflag each have over 50 contributors and are actively maintained.
- Good, viper and pflag are "owned" by a Steve Francia, a Google employee and the creator of Hugo.
- Good, supports aliases to enable non-breaking improvements.
- Good, supports bool, int, int64, float64, duration, string, map[string]string, []string, map[string][]string, and time.Time.
- Bad, no support for JSON variables.
- Good, can unmarshal flag values into structs.
- Bad, cannot mark flag as required (but can do defaults).
- Good, doesn't panic on bad values and can retrieve errors from pflag if needed.
- Good, supports json, toml, yaml, properties, and hcl config file formats.
Examples
Bind to config defined through command line flags (via pflag
) and environment variables.
flag := pflag.CommandLine
v := viper.New()
v.BindPFlags(flag) // bind to command line flags
// viper by default binds to upper case and
// supports a custom environment key replacer,
// but let's just use a typical one that replaces - with _
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
// AutomaticEnv turns on binding for all environment variables
v.AutomaticEnv()
Custom validation of config variable and error handling during program startup.
type InvalidConfigPort struct {
Name string
Value interface{}
Start int
End int
}
func (c *InvalidConfigPort) Error() string {
return "config variable " + c.Name + " has invalid value " + fmt.Sprintf("%#v", c.Value) + ", must be in range " + strconv.Itoa(c.Start) + " - " + strconv.Itoa(c.End)
}
...
func main() {
...
if v.IsSet("http-port-tls-none") {
if value := v.GetInt("http-port-tls-none"); value < 8000 || value > 8999 {
return &InvalidConfigPort{Name: "http-port-tls-none", Value: value, Start: 8000, End: 8999}
}
}
...
}
https://github.com/namsral/flag
flag is a drop-in replacement for Go's flag package that adds support for environment variables. Currently used by our webserver (https://github.com/transcom/mymove/cmd/webserver
).
- Bad, not maintained (the last code update was December 28, 2016).
- Good, supports bool, (u)int, (u)int64, (u)float64, time.Duration, and string (drop in replacement for built-in flag package)
- Bad, no support for JSON variables.
- Good, supports environment variables.
- Bad, only supports
name=value
andname value
config file formats - Bad, cannot mark variables as required.
- Bad, invalid values cause panic (making custom sanity checks impossible)
https://github.com/jessevdk/go-flags
go-flags enhances the functionality of the builtin flag
package with support for many useful features. You define your config as a single struct using fields and struct tags. Currently used by truss-aws-tools.
- Borderline, last updated on March 31, 2018.
- Good, supports a variety of integer, float, string, and maps, including
[]*string{}
- Bad, no support for JSON variables.
- Good, supports environment variables
- Bad, must unmarshal values into a single config struct. Creates some baseline structure and increases readability, but reduces flexibility for responsively handling multiple contexts.
- Good, can mark variables as required
- Bad, hard to make custom sanity checks, since the config is parsed into a struct all at once.