Config
Software often depends on tunable parameters: resource caps, feature toggles, input paths, ports, and any number of application-specific properties. We could embed such information in source code, but then we would have to rebuild and redeploy our code every time we changed the config. The point of config is to tune a program’s behavior without rebuilding or redeploying.
Sometimes, you’ll see a set of constants in source code that the developer clearly considers to be, in some sense, configurable. They may even be defined in a “config” namespace:
mod config { | |
pub const DATABASE_NAME: &str = "ourdb"; | |
pub const DATABASE_PORT: u16 = 5432; | |
} | |
fn main() { | |
let db = database::connect(config::DATABASE_NAME, config::DATABASE_PORT); | |
// ... | |
} |
Assigning meaningful names to hard-coded values, though laudable, doesn’t actually make them run-time configurable. Alternatively, some programs read a separate config file, usually in a declarative language like JSON, YAML, XML, or TOML:
[database] | |
name = "ourdb" | |
port = 5432 |
Reading a file is slightly more complicated than hard-coding constants, because it requires deserialization and error-handling:
mod config { | |
use serde::Deserialize; | |
use std::{error, fs, path}; | |
#[derive(Deserialize, Debug)] | |
pub struct Database { | |
pub name: String, | |
pub port: u16, | |
} | |
#[derive(Deserialize, Debug)] | |
pub struct Config { | |
pub database: Database, | |
// ... | |
} | |
pub fn load<P>(path: P) -> Result<Config, Box<dyn error::Error>> | |
where | |
P: AsRef<path::Path>, | |
{ | |
Ok(toml::from_str(&fs::read_to_string(path)?)?) | |
} | |
} | |
fn main() { | |
let config = match config::load("config.toml") { | |
Ok(config) => config, | |
Err(err) => { | |
eprintln!("error: can't read config: {}", err); | |
std::process::exit(1); | |
} | |
}; | |
let db = database::connect(&config.database.name, config.database.port); | |
// ... | |
} |
Are “config files” really config? It depends: If you can edit the config without redeploying the code it configures, then yes, config files are good enough. Programs often have to be bounced (stopped and restarted) before they’ll pick up any config changes, but a common enhancement is to have them reread config in response to a Unix signal like SIGHUP.
If you cannot deploy the config without also redeploying the code, then no, even so-called config files are not really config. Whether they are written in a Turing-complete language like C++ or a declarative format like YAML is irrelevant: The files are effectively source code, not configuration.
A third alternative is to store config in a shared service like etcd or Zookeeper. This is the most flexible option, especially if a configurator UI is available so that non-developers can easily update parameter values. It’s even more complicated than using config files, though, because it introduces another moving part to the system; and of course, such services need their own config. Who configures the configurators?
Configuration, by definition, should be tunable independently from the code it configures. Otherwise, it might as well be part of that code in the first place. Separately tunable config lets SREs change ports and security keys without redeploying. It lets domain experts (like quants or campaign managers) tweak parameters in real time. Perhaps most satisfyingly, it frees developers from being nagged every time someone wants to change something that ought to be independent of the code base.