Automatic configuration deployment with cdist
In my job as „the IT guy“ at a company in Ettlingen I am also responsible for the company network and the configuration and maintenance of the server infrastructure. In the past I did all this by hand: I configured every service directly on the server. Every configuration change I did with a text editor directly on the server.
While with this approach one is able to react very fast on problems, on the other hand it is error-prone. You can't test changes, you don't have a documentation. And if you have a documentation, it regularly lacks of the latest changes.
If you ever have to migrate to another hardware, you have to copy the right configuration files to the right places and change the right things to adopt it to the new environment. You also have to install the right software dependencies of the software running on the server. In an emergency case a really bad starting situation.
When I recently replaced some old boxes by newer ones to avoid
hardware failure, I took the chance and decided to invest some time to
automate configuration of the new boxes. The world of automated
configuration deployment is rather small. There are just a few
solutions, the most often mentioned are
Puppet, Chef as
well as CFEngine. For my purposes (a rather
small company network) the three are too big, too complex. I was
looking for something less „magical,“ something following the KISS
principle. After a lot of research some friends of mine suggested
cdist
. cdist
has been written by
Nico Schottelius, a FOSS
hacker and system administrator living in Zurich. It is free software
(licensed as GLPv3). cdist
configuration deployment consists
basically of executing shell scripts which copy or modify files and
install packages. Pretty easy, for example this file adds the
PostgreSQL repository for Debian to the sources.list
:
__block sources-list \
--state present \
--file /etc/apt/sources.list \
--text - <<EOF
deb http://apt.postgresql.org/pub/repos/apt/ wheezy-pgdg main
EOF
The __block
call is a function defined by cdist
, it calls this kind
of construct
types.
Everything you do is calling these types to manipulate, copy, link and
move files.
A cdist
configurations entry point is the
manifest/init
file. In this entry point file you specify what to do, an easy example:
__package apache2 --state present
This would install the apache2
webserver packet on the target host.
Configuration deployment by service types
One of the requirements I had was the ability to quickly move a service to another machine, for example because of a hardware failure. So my first approach was to define service functions and define via environment variable the type of service I'd like to deploy. This way I could for example call
SERVICES="web mail" cdist config -c ./conf/ 10.0.0.2
and deploy the webserver configuration and its dependencies as well as
the mailserver configuration and its dependencies to the node
10.0.0.2
. But this way I've got the problem again that it is not
documented when a service moves to another host, and it is error-prone
as well: what if I type a service name wrong or add a service name
which doesn't belong to this host?
Configuration deployment by host definitions
So I went to a different type of configuration deployment: host definitions. I define in an external file hosts and specify (among some other things) the services to deploy:
SERVICES_10002="web mail"
When I move a service to another host I change this file and make a
new git commit; this way every change is documented via git
history. It also is less error-prone, since I don't have to specify
the services again and again on the command line - I'm sure one day I
would forget a service or add a service to the wrong box. And due to
this structure my manifest/init
file is pretty easy:
for file in conf/manifest/*.sh; do
. $file
done
This part reads every .sh
file containing the modules for the
services.
__file /etc/cdist-configured
__cdistmarker
HOSTCONFIG="$(getmap "SERVICES_" "$__target_host")"
HOSTNAME="$(getmap "HOSTNAME_" "$__target_host")"
Since Bash below v4 doesn't support dictionaries, we have to do a
little trick to get the configuration for the host. We call a
self-defined function getmap
, this function basically we constructs
a string of the form <first arg><second arg>
, gets the value of the
variable with that name and returns it to the caller. Thus we read the
modules to activate and the hostname we should set.
MY_OS="$(cat "$__global/explorer/os")"
if [ "$MY_OS" = "freebsd" ]; then
base_freebsd
else
base_linux
fi
This snippet calls a function which does the basic configuration of the system (hostname, time zone, etc, pp) depending on the target OS.
fun="base_$HOSTNAME"
fn_exists "$fun" && eval "$fun"
This snippet checks if a host-specific configuration function exists, for example to install the RAID monitoring utilities on the right host. If it exists, the function gets called.
for feature in $HOSTCONFIG; do
if [ fn_exists "$feature" ];
eval "$feature"
else
echo "$feature does not exist!"
exit 1
fi
done
This snippet loops to each service defined by the host configuration and calls the function configuring the service. That's it, now we have a host configuration based configuration deployment. Of course, the functions which deploy the actual services still are missing ;-)
Deploying a service configuration
I moved the configuration of a specific service to a function. This
way I get a basic modularization; for example the function webserver
installs all software requirements and the configuration for the
software on the target host. I will try to explain some things with
the configuration function for the web server:
webserver() {
__user ssh_data_in \
--state present \
--create-home
__user git \
--state present \
--create-home
This snippet ensures that the users ssh_data_in
and git
exist,
with created home directories.
require="__user/ssh_data_in" __directory /home/ssh_data_in/.ssh/ \
--owner ssh_data_in \
--group ssh_data_in \
--mode 0700
require="__directory/home/ssh_data_in/.ssh/" __file /home/ssh_data_in/.ssh/authorized_keys \
--state present \
--owner ssh_data_in \
--group ssh_data_in \
--source - <<EOF
ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIB+bxdCSP8zo04Xmf+ItBlJtgXWhbBP69HetzSj/IH18ugnKrflylDL0SaCAizjKJKZnjnkW0RNqKEHQzMsMSOs8bSfu9L2W5Qg3peJ4T4BZy3Q7cfYNOYkMM6vYl3pv2coJvDlnUc2/BY95NlVHYhW4YtHYPjBTwB3a4eaotl4RQ==
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDosf/bRU/avCgY47OgIY3thRM5cLejAicnghRrjeqzfCR8WGvkaJeKxGc9B+4O4DX3/kp3USOZNzD45Uk6s7LYPRTMWoY+CPgiyuC2fNYmON+lpsnl3ebmY0wyigOIH6HiKf3GZlsnp6E1efxbLlgNBIbyhp3MSs6LeNok+/3090LNdEOJvwOpEHYveJ/qCeHT2ipbzToZS5LGMJ192xqYik/4Ov8drg3WHhusYw1HJl/CM/jE3XPZScCaCGmvGvetsa4nJiy2PBn7Qnp3AYckHw3RyyGShmJb/IfeXI8ypKTqtzmVOZ4RnZk+OS0q/5/o/9pJBg6TB9OuJgBhah+V ckruse@vali.local
EOF
The feature I use in this block is a dependency. cdist
does not
execute the types/functions in the order I call them, but it may
generate some remote code and executes it later. With the special
viarable require
you can specify dependencies; in this case I want
to ensure that the directory /home/ssh_data_in/.ssh/
exists and
belongs to the user and group ssh_data_in
- but after the user has
been created. When the directory has been created we create a file
authorized_keys
containing two public keys.
__package libapache2-mod-xsendfile \
--state present
__package rsync \
--state present
__package syslog-ng \
--state present
__package build-essential \
--state present
__package ruby1.9.3 \
--state present
__package ruby-dev \
--state present
__package curl \
--state present
__package libcurl4-gnutls-dev \
--state present
__package libxml2-dev \
--state present
require="__package_update_index" __package libpq-dev \
--state present
require="__package_update_index" __package nodejs \
--state present
__package php-mail \
--state present
__package php-mail-mime \
--state present
require="__package/nodejs __package/curl" __install_npm
__package apache2 \
--state present
require="__package/apache2 __package/ruby1.9.3" __package libapache2-mod-passenger \
--state present
__package php5 \
--state present
require="__package/apache2 __package/php5" __package libapache2-mod-php5 \
--state present
require="__package/php5" __package phpldapadmin \
--state present
require="__package/php5" __package phppgadmin \
--state present
require="__package/php5" __package phpmyadmin \
--state present
__package pdftk --state present
__package git --state present
require="__package/ruby1.9.3" __package_rubygem bundler --state present
This snippet installs a lot of software packets we need for the
software running on the web server. __install_npm
is a self-written
function installing NPM via node.js.
require_for_restart="__package/apache2 __package/libapache2-mod-passenger __package/libapache2-mod-php5 __package/libapache2-mod-xsendfile"
for site in ./files/webserver/apache/sites/*; do
site_name="`basename $site`"
doc_root="`grep DocumentRoot $site | sed 's/DocumentRoot//' | tr -d ' \t'`"
require_for_restart="$require_for_restart __file/etc/apache2/sites-available/$site_name __directory/$doc_root"
require="__package/apache2" __file /etc/apache2/sites-available/$site_name \
--source $site \
--state present
# if it is a ruby thing we use my user as owner
if [[ $doc_root == *public* ]]; then
__directory $doc_root \
--state present \
--owner ckruse \
--group ckruse \
--parents
__directory ${doc_root%/}/.. \
--state present \
--owner ckruse \
--group ckruse \
--parents
else
__directory $doc_root \
--state present \
--owner www-data \
--group www-data \
--parents
__directory ${doc_root%/}/.. \
--state present \
--owner www-data \
--group www-data \
--parents
fi
if [ "$site_name" == "default" ]; then
require_for_restart="$require_for_restart __link/etc/apache2/sites-enabled/000-$site_name"
require="__file/etc/apache2/sites-available/$site_name" __link /etc/apache2/sites-enabled/000-$site_name \
--source ../sites-available/$site_name \
--type symbolic
else
require_for_restart="$require_for_restart __link/etc/apache2/sites-enabled/$site_name"
require="__file/etc/apache2/sites-available/$site_name" __link /etc/apache2/sites-enabled/$site_name \
--source ../sites-available/$site_name \
--type symbolic \
--state present
fi
done
require="__package/apache2" __file /etc/apache2/sites-available/default-ssl \
--state absent
require="__package/apache2" __link /etc/apache2/sites-enabled/default-ssl \
--source ../sites-available/default-ssl \
--type symbolic \
--state absent
require="__package/apache2" __link /etc/apache2/mods-enabled/ssl \
--source ../mods-available/ssl \
--type symbolic \
--state absent
require="__package/apache2" __link /etc/apache2/mods-enabled/expires.load \
--source ../mods-available/expires.load \
--type symbolic \
--state present
require="__package/apache2" __link /etc/apache2/mods-enabled/headers.load \
--source ../mods-available/headers.load \
--type symbolic \
--state present
require="__package/apache2" __link /etc/apache2/mods-enabled/xsendfile.load \
--source ../mods-available/xsendfile.load \
--type symbolic \
--state present
require="__package/apache2" __file /etc/apache2/mods-available/xsendfile.conf \
--state present \
--source <<EOF
XSendFile On
XSendFilePath /mnt/storage/noodles/
EOF
require="__file/etc/apache2/mods-available/xsendfile.conf" __link /etc/apache2/mods-enabled/xsendfile.conf \
--source ../mods-available/xsendfile.conf \
--type symbolic \
--state present
require_for_restart="$require_for_restart __file/etc/apache2/sites-available/default-ssl __link/etc/apache2/sites-enabled/default-ssl __link/etc/apache2/mods-enabled/ssl __link/etc/apache2/mods-enabled/xsendfile.conf"
for to_enable in passenger.load passenger.conf rewrite.load; do
require_for_restart="$require_for_restart __link/etc/apache2/mods-enabled/$to_enable"
require="__package/apache2" __link /etc/apache2/mods-enabled/$to_enable \
--source ../mods-available/$to_enable \
--type symbolic \
--state present
done
require_for_restart="$require_for_restart __block/apache2-charset"
require="__package/apache2" __block apache2-charset \
--state present \
--file /etc/apache2/conf.d/charset \
--text - <<EOF
AddDefaultCharset UTF-8
EOF
require_for_restart="$require_for_restart __remove/etc/apache2/conf.d/phpldapadmin"
require="__package/phpldapadmin" __remove /etc/apache2/conf.d/phpldapadmin \
--pattern 'Alias /phpldapadmin'
require_for_restart="$require_for_restart __remove/etc/apache2/conf.d/phppgadmin"
require="__package/phppgadmin" __remove /etc/apache2/conf.d/phppgadmin \
--pattern "Alias /phppgadmin"
require="$require_for_restart" __service apache2 --action restart
}
This block configures the Apache web server, enables some modules and installs some virtual hosts. It also ensures that some files don't exist. For example we use a NGINX for SSL termination so we don't want the SSL module of Apache to be enabled. When all files and packages have been installed, we restart the service. And bam, we have a working web server setup! Of course there is a little bit more in the real deployment function, I shortened it a bit to the essential things.
Conclusion
cdist
is a nice and simple tool. The complete company infrastructure
now gets configured using this approach. Yay!