Cost-effective way to have your app conform with 12 factor methodology with Go’s stock flag
package.
Summary
Previously, before “cloud” was a thing, it was common to have configuration part of the source code, ie Rails’ config/database.yaml
.
These days, with immutable infrastucture, separation of configuration and code is preferred; quoting 12 factor:
III. Config
Store config in the environment
An app’s config is everything that is likely to vary between deploys (staging, production, developer environments, etc). This includes:
- Resource handles to the database, Memcached, and other backing services
- Credentials to external services such as Amazon S3 or Twitter
- Per-deploy values such as the canonical hostname for the deploy
Apps sometimes store config as constants in the code. This is a violation of twelve-factor, which requires strict separation of config from code. Config varies substantially across deploys, code does not.
This means that the app’s context sets the configuration which enables the app to run transparently as a serverless function, in a kubernetes pod, in a cloud run, in a docker swarm, or your laptop.
Problem
replaced `viper` with `flag` package and🤯.
— gmarik (@gmarik) August 27, 2019
How do you justify adding a dependency if stdlib provides same functionality even if some plumbing required? #golang pic.twitter.com/4fAoXVP7vU
Surprisingly often, in order to fulfill 12 Factor config requirements, people resort to packages with large API surface and as result large codebase and deep dependency graph.
Often times this is not necessary since the same functionality can be achieved with much less code and only using Go’s standard library packages.
Here’s an example of using flag
package to achieve equal result.
12 factor config with flag
package
- 2 ways to configure the app, through: 1) cli flags or 2) environment variables
- default values are configured from corresponding variables
- environment variables, if configured, set the
flag
's defaults usingLookupOr*
helpers - get full configuration with simple
getConfig
helper
package main
import (
"flag"
"fmt"
"os"
"strconv"
"log"
)
var (
// set by build process
Git_Revision string
Consul_URL string = "http://consul.local:8500"
Statsd_URL string
HTTP_ListenAddr string = ":8080"
HTTP_Timeout int = 16
)
func main() {
flag.StringVar(&Consul_URL, "consul-url", LookupEnvOrString("CONSUL_URL", Consul_URL), "service discovery url")
flag.StringVar(&Statsd_URL, "statsd-url", LookupEnvOrString("STATSD_URL", Statsd_URL), "statsd's host:port")
flag.StringVar(&HTTP_ListenAddr, "http-listen-addr", LookupEnvOrString("HTTP_LISTEN_ADDR", HTTP_ListenAddr), "http service listen address")
flag.IntVar(&HTTP_Timeout, "http-timeout", LookupEnvOrInt("HTTP_TIMEOUT", HTTP_Timeout), "http timeout requesting http services")
flag.Parse()
log.Printf("app.config %v\n", getConfig(flag.CommandLine))
log.Println("app.status=starting")
defer log.Println("app.status=shutdown")
log.Println("hello world")
}
func LookupEnvOrString(key string, defaultVal string) string {
if val, ok := os.LookupEnv(key); ok {
return val
}
return defaultVal
}
func LookupEnvOrInt(key string, defaultVal int) int {
if val, ok := os.LookupEnv(key); ok {
v, err := strconv.Atoi(val)
if err != nil {
log.Fatalf("LookupEnvOrInt[%s]: %v", key, err)
}
return v
}
return defaultVal
}
func getConfig(fs *flag.FlagSet) []string {
cfg := make([]string, 0, 10)
fs.VisitAll(func(f *flag.Flag) {
cfg = append(cfg, fmt.Sprintf("%s:%q", f.Name, f.Value.String()))
})
return cfg
}
see it in action on Playground
Conclusion
Pros
- no dependencies other than standard library
Cons
- a bit of plumbing code is required
- defaults to environment var’s value if latter is set
- env vars are manually named
- description may duplicate var’s comments
flag
package with combination with few helpers provides pragmatic way to configure your 12 factor-ready apps.
It’s not perfect but gets the job done.