How do you deal with config files that need different settings based on various services that are running on a host and cooperate with other teams? It's a common question, and it came up on in #cfengine on irc.freenode.net recently.
The issue is that team A might be working on package A, which requires some environment variables set. But team B might be working on a totally different thing – and want to achieve the same thing. I hoped to give them a bit of 'library' code to take care of it, rather than have them touch a centralized environment-setting policy file.
Here, I look at three high level patterns I have seen.
- Competitive management
- Each bundle that needs a specific config manages it by itself, typically by using a parameterized bundle.
- Centralized management
- All config definition is in a single place, anyone needing something edits the global config.
- Cooperative management
- Each bundle that needs a specific config, publishes the config it needs, and another policy handles consolidating the configs and edits the config as necessary.
Let's take a look at an example of each of these patterns. I will start from an
example recently shared in #cfengine on irc.freenode.net that manages
/etc/environment
, /etc/sysstemd.conf
, and /etc/profile
. I'll first fix it,
so that it works, then show several different patterns that ac hive the smae
goal.
The original example
Hello everyone. I am trying to use set_line_based to set some "global" environment variables. I've created a bundle that I thought might make it easier. However, using this bundle more than once only applies the first 'invocation'. Code here: https://pastebin.com/mbU1Bb6i
Example usage:
|
|
Implementation:
|
|
The reason that only the first actuation of global_env
alters the file is
because the promise does not differ. Once a promise has been kept or repaired,
it is not actuated again within the same agent run. The promise can be made
unique by including the parameters which are different between actuation's in
the promise. Here we add a handle using both parameters, so that the promise is
unique across actuation's of the bundle for different values of name
and
value
.
|
|
I will combine them together into the same policy file and make some minor changes.
- I added handles using the parameters to make sure that the promises are unique across executions with different parameter values. For more on this, see the language concepts for bundles, it discusses how bundles are not functions.
- I added
bundle agent __main__
so that thevagrant_vm
bundle will be actuated when this policy file is run directly. - I removed the
unifi
methods promise because it was not provided in the original example. - I added reports that show the complete content of the files for easy demonstration.
- I include the stdlib for my executions, you don't see it, but know that it's in use.
- I guard the
systemd daemon-reload
with a check that the executing user is root because I prototype policy as my own user, inside org-mode using ob-cfengine3 (shameless plug). - I add an init bundle to reset as if running from a clean state with no pre-existing configuration.
- I added body contain
exec_owner()
because it isn't in the stdlib. - I added
create => "true"
to the files promises. - I prefixed the files promises with
/tmp
.
Competitive management
This is a common pattern. I think of this pattern as competitive because each service manages what it needs, multiple services may compete for control over the same resources like configuration files. Each service only worries about itself, this pattern allows for extra settings to exist in the configuration files which is beneficial in situations where control must be shared between multiple agents. This pattern results in files being edited multiple times within a single agent run which may be useful in ordering, but results in some overhead. Additionally, depending how the service restart on config change is managed, may lead to the same service re-starting multiple times in the same agent run.
Policy:
|
|
Output:
R: CFEngine 3.12.0 R: /tmp/etc/environment R: PAPERTRAIL_HOST=xxx.papertrailapp.com R: PAPERTRAIL_PORT=12345 R: APPOPTICS_USER=ops@whatever.com R: APPOPTICS_APIKEY=cafebabedeadbeef R: CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: CORE_JDBC_USER=vagrant R: SMS_ENABLED=false R: SMS_AWS_REGION=eu-west-1 R: /tmp/etc/systemd/system.conf R: DefaultEnvironment=PAPERTRAIL_HOST=xxx.papertrailapp.com R: DefaultEnvironment=PAPERTRAIL_PORT=12345 R: DefaultEnvironment=APPOPTICS_USER=ops@whatever.com R: DefaultEnvironment=APPOPTICS_APIKEY=cafebabedeadbeef R: DefaultEnvironment=CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: DefaultEnvironment=CORE_JDBC_USER=vagrant R: DefaultEnvironment=SMS_ENABLED=false R: DefaultEnvironment=SMS_AWS_REGION=eu-west-1 R: /tmp/etc/profile R: export PAPERTRAIL_HOST=xxx.papertrailapp.com R: export PAPERTRAIL_PORT=12345 R: export APPOPTICS_USER=ops@whatever.com R: export APPOPTICS_APIKEY=cafebabedeadbeef R: export CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: export CORE_JDBC_USER=vagrant R: export SMS_ENABLED=false R: export SMS_AWS_REGION=eu-west-1
A common evolution for this policy would be to condense the write operations by altering the parameter to pass a map of key value pairs.
Condense the write operations by changing the parameters
A quick change to the parameters allows multiple key values to be set at a single time, reducing IO.
Policy:
|
|
Output:
R: CFEngine 3.12.0 R: /tmp/etc/environment R: APPOPTICS_USER=ops@whatever.com R: PAPERTRAIL_PORT=12345 R: CORE_JDBC_USER=vagrant R: APPOPTICS_APIKEY=cafebabedeadbeef R: SMS_ENABLED=false R: PAPERTRAIL_HOST=xxx.papertrailapp.com R: SMS_AWS_REGION=eu-west-1 R: CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: /tmp/etc/systemd/system.conf R: DefaultEnvironment=APPOPTICS_USER=ops@whatever.com R: DefaultEnvironment=CORE_JDBC_USER=vagrant R: DefaultEnvironment=SMS_AWS_REGION=eu-west-1 R: DefaultEnvironment=SMS_ENABLED=false R: DefaultEnvironment=APPOPTICS_APIKEY=cafebabedeadbeef R: DefaultEnvironment=CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: DefaultEnvironment=PAPERTRAIL_HOST=xxx.papertrailapp.com R: DefaultEnvironment=PAPERTRAIL_PORT=12345 R: /tmp/etc/profile R: export SMS_ENABLED=false R: export CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: export APPOPTICS_USER=ops@whatever.com R: export CORE_JDBC_USER=vagrant R: export SMS_AWS_REGION=eu-west-1 R: export PAPERTRAIL_PORT=12345 R: export APPOPTICS_APIKEY=cafebabedeadbeef R: export PAPERTRAIL_HOST=xxx.papertrailapp.com
Centralized management
Let's take a look at what a centralized management pattern might look like. This is the simplest pattern, and many times how policies begin as a prototype, before evolving into another pattern.
We removed the vagrant_vm
bundle, and the uniqness from the promise handles.
Now, all the configuration data is right in the global_env
bundle. We use a
classic array as a generator to produce copies of the key values tailored for
each config file. An alternative to creating different variables using a classic
array generator we could copy set_line_based
and customize it to allow a
prefix to be specified.
Policy:
|
|
Output:
R: CFEngine 3.12.0 R: /tmp/etc/environment R: SMS_AWS_REGION=eu-east-2 R: PAPERTRAIL_HOST=xxx.trailpaperapp.com R: CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: SMS_ENABLED=false R: APPOPTICS_APIKEY=xxxxiiiiiiixxixxix R: APPOPTICS_USER=ops@whatever.com R: PAPERTRAIL_PORT=54321 R: CORE_JDBC_USER=vagrant R: /tmp/etc/systemd/system.conf R: DefaultEnvironment=APPOPTICS_USER=ops@whatever.com R: DefaultEnvironment=CORE_JDBC_USER=vagrant R: DefaultEnvironment=SMS_AWS_REGION=eu-east-2 R: DefaultEnvironment=SMS_ENABLED=false R: DefaultEnvironment=APPOPTICS_APIKEY=xxxxiiiiiiixxixxix R: DefaultEnvironment=CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: DefaultEnvironment=PAPERTRAIL_HOST=xxx.trailpaperapp.com R: DefaultEnvironment=PAPERTRAIL_PORT=54321 R: /tmp/etc/profile R: export SMS_ENABLED=false R: export CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: export APPOPTICS_USER=ops@whatever.com R: export CORE_JDBC_USER=vagrant R: export SMS_AWS_REGION=eu-east-2 R: export PAPERTRAIL_PORT=54321 R: export APPOPTICS_APIKEY=xxxxiiiiiiixxixxix R: export PAPERTRAIL_HOST=xxx.trailpaperapp.com
Common evolution's on this policy include moving variable definition into it's own bundle, separating the data into external files, and since all data is known at one time, full file management is an option.
Centralized management separate data bundle
Here I am sure to have the data bundle converge before the bundle that will use the data defined there. This separation enables more complex data convergence, delegation of control, and especially with more data can improve the readability of the policy.
Policy:
|
|
Output:
R: CFEngine 3.12.0 R: /tmp/etc/environment R: CORE_JDBC_USER=vagrant R: PAPERTRAIL_PORT=12345 R: CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: SMS_ENABLED=false R: SMS_AWS_REGION=eu-west-1 R: APPOPTICS_APIKEY=cafebabedeadbeef R: PAPERTRAIL_HOST=xxx.papertrailapp.com R: APPOPTICS_USER=ops@whatever.com R: /tmp/etc/systemd/system.conf R: DefaultEnvironment=APPOPTICS_USER=ops@whatever.com R: DefaultEnvironment=CORE_JDBC_USER=vagrant R: DefaultEnvironment=SMS_AWS_REGION=eu-west-1 R: DefaultEnvironment=SMS_ENABLED=false R: DefaultEnvironment=APPOPTICS_APIKEY=cafebabedeadbeef R: DefaultEnvironment=CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: DefaultEnvironment=PAPERTRAIL_HOST=xxx.papertrailapp.com R: DefaultEnvironment=PAPERTRAIL_PORT=12345 R: /tmp/etc/profile R: export SMS_ENABLED=false R: export CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: export APPOPTICS_USER=ops@whatever.com R: export CORE_JDBC_USER=vagrant R: export SMS_AWS_REGION=eu-west-1 R: export PAPERTRAIL_PORT=12345 R: export APPOPTICS_APIKEY=cafebabedeadbeef R: export PAPERTRAIL_HOST=xxx.papertrailapp.com
Centralized management separate data bundle with full file management
Here we remove the init to reset the environment and add full file content management by attaching edit_defaults empty to the files promises. This ensures no unknown content in the configuration file.
Policy:
|
|
Output:
R: CFEngine 3.12.0 R: /tmp/etc/environment R: PAPERTRAIL_HOST=xxx.trailpaperapp.com R: PAPERTRAIL_PORT=22222 R: APPOPTICS_USER=ops@cfengine.com R: APPOPTICS_APIKEY=toodledum R: CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: CORE_JDBC_USER=vagrant R: SMS_ENABLED=false R: SMS_AWS_REGION=eu-west-1 R: /tmp/etc/systemd/system.conf R: DefaultEnvironment=PAPERTRAIL_PORT=22222 R: DefaultEnvironment=APPOPTICS_APIKEY=toodledum R: DefaultEnvironment=SMS_ENABLED=false R: DefaultEnvironment=CORE_JDBC_USER=vagrant R: DefaultEnvironment=CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: DefaultEnvironment=APPOPTICS_USER=ops@cfengine.com R: DefaultEnvironment=PAPERTRAIL_HOST=xxx.trailpaperapp.com R: DefaultEnvironment=SMS_AWS_REGION=eu-west-1 R: /tmp/etc/profile R: export SMS_ENABLED=false R: export CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: export APPOPTICS_USER=ops@whatever.com R: export CORE_JDBC_USER=vagrant R: export SMS_AWS_REGION=eu-west-1 R: export PAPERTRAIL_PORT=12345 R: export APPOPTICS_APIKEY=cafebabedeadbeef R: export PAPERTRAIL_HOST=xxx.papertrailapp.com
Centralized management separate data bundle with external data files
In addition to the benefits derived from separating data bundles external data files can further enable delegation of control working much more easily with external systems, reduces the size of the policy and improve policy readability, and eases testing of policy. We can use the json, yaml or env file formats interchangeably. CSV is also possible, but different parsing would be required.
/tmp/env.env:
/tmp/env.json:
|
|
/tmp/env.yaml:
|
|
Policy:
|
|
Output:
R: CFEngine 3.12.0 R: /tmp/etc/environment R: PAPERTRAIL_HOST=xxx.papertrailapp.com R: PAPERTRAIL_PORT=11111 R: APPOPTICS_USER=ops@whatever.com R: APPOPTICS_APIKEY=cafebabedeadbeef R: CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: CORE_JDBC_USER=vagrant R: SMS_ENABLED=false R: SMS_AWS_REGION=eu-west-1 R: /tmp/etc/systemd/system.conf R: DefaultEnvironment=PAPERTRAIL_PORT=11111 R: DefaultEnvironment=APPOPTICS_APIKEY=cafebabedeadbeef R: DefaultEnvironment=SMS_ENABLED=false R: DefaultEnvironment=CORE_JDBC_USER=vagrant R: DefaultEnvironment=CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: DefaultEnvironment=APPOPTICS_USER=ops@whatever.com R: DefaultEnvironment=PAPERTRAIL_HOST=xxx.papertrailapp.com R: DefaultEnvironment=SMS_AWS_REGION=eu-west-1 R: /tmp/etc/profile R: export PAPERTRAIL_HOSTexport xxx.papertrailapp.com R: export SMS_AWS_REGIONexport eu-west-1 R: export PAPERTRAIL_PORTexport 11111 R: export APPOPTICS_USERexport ops@whatever.com R: export SMS_ENABLEDexport false R: export CORE_JDBC_USERexport vagrant R: export CORE_JDBC_URLexport jdbc:postgresql://localhost/xxx R: export APPOPTICS_APIKEYexport cafebabedeadbeef
Cooperative management
Here we allow each service to define the key values they need. They advertise or publish their data by tagging it. In this iteration, since we have full knowledge of the desired state at one time we switched the file promises to be templates so that the full content is managed. This helps to avoid unknown settings.
Policy:
|
|
Output:
R: /etc/environment R: PAPERTRAIL_HOST=xxx.papertrailapp.com R: PAPERTRAIL_PORT=11111 R: APPOPTICS_USER=ops@whatever.com R: APPOPTICS_APIKEY=cafebabedeadbeef R: CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: CORE_JDBC_USER=vagrant R: SMS_ENABLED=false R: SMS_AWS_REGION=eu-west-1 R: /etc/systemd/system.conf R: DefaultEnvironment=PAPERTRAIL_HOST=xxx.papertrailapp.com R: DefaultEnvironment=PAPERTRAIL_PORT=11111 R: DefaultEnvironment=APPOPTICS_USER=ops@whatever.com R: DefaultEnvironment=APPOPTICS_APIKEY=cafebabedeadbeef R: DefaultEnvironment=CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: DefaultEnvironment=CORE_JDBC_USER=vagrant R: DefaultEnvironment=SMS_ENABLED=false R: DefaultEnvironment=SMS_AWS_REGION=eu-west-1 R: /etc/profile R: export PAPERTRAIL_HOST=xxx.papertrailapp.com R: export PAPERTRAIL_PORT=11111 R: export APPOPTICS_USER=ops@whatever.com R: export APPOPTICS_APIKEY=cafebabedeadbeef R: export CORE_JDBC_URL=jdbc:postgresql://localhost/xxx R: export CORE_JDBC_USER=vagrant R: export SMS_ENABLED=false R: export SMS_AWS_REGION=eu-west-1
As with other patterns moving the data into external files would improve delegation of control and tighter integration with other systems.