12 factor configuration with Go's `flag` package

gmarik 3 min

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.

https://12factor.net/config

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

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

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

Cons

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.

References

Related Posts
Read More
Hosting static site with Hugo, Nginx, and Cloud Run
cdp-proxy: Chrome DevTools proxy and middleware for Go
Comments
read or add one↓