Small-Scale Puppet

Introduction

In the days before configuration management systems became widespread, I used to manage my personal systems using bundle and a Git repository. This an update using Puppet for the configuration management instead of bundle. It's what I currently use to manage all of my personal systems.

The goals of this system include all the regular configuration management goals: ability to reproduce a system after a failure, making the same changes across multiple systems and keeping them consistent, and making changes using a programming language that ensures any variations from my template are repaired. In addition, I want to write changes locally in a fast editor, keep all configuration in Git, and push changes to remote systems, but I don't want to maintain a configuration management infrastructure, a TLS public-key infrastructure, or worry about security patches for a daemon.

This is a small-scale master-less Puppet setup that's designed for a small number of systems (less than ten). It supports systems that aren't continuously on-line, but requires manual action to push a new version of the configuration.

Puppet Layout

The private Git repository that holds my system configuration looks like a typical Puppet repository: top-level modules and nodes directories, with the normal Puppet module layout under the modules directory. The Puppet configuration is applied with puppet apply with nodes/ as its file argument. (See below for how this is automated.)

For this type of small configuration, I don't bother with a node classifier or any of the other complex machinery to map nodes to modules. I just have one file for each system in nodes, named after the system with .pp appended, and a defaults.pp with global defaults. Each file contains the node definition and is mostly include and class statements pulling in modules. (In a few cases, I'll put some very system-specific resources directly in the node, but I try to avoid that and put everything into modules.)

The rest looks like typical Puppet. You can find numerous resources on-line for how to set up your Puppet modules, and there are lots of strong, conflicting opinions about the best way to do this. Since I'm the only user of my configuration (and since it has private pieces and therefore isn't public), I just organize things via any idiosyncratic way that makes sense to me.

Pushing Changes

The interesting part of this setup is how to push out changes.

For my laptops, I just keep a local Git checkout of the repository in my home directory, update it as needed, and then run:

    puppet apply --modulepath "$(pwd)/modules:/usr/share/puppet/modules" \
        --show_diff "$(pwd)/nodes"

from the top level of the checkout. (I use some of the Puppet modules that are packaged for Debian, hence /usr/share/puppet/modules.) Add --noop before the "$(pwd)/nodes" argument to see changes before applying them.

The more interesting case is remote systems where I don't want to log in and update a checkout. For those, I create a Git remote named after the system:

    git remote add <system> ssh://root@<system>/root/puppet

and prior to the first push create the Git repository on the remote system as root:

    mkdir /root/puppet
    cd /root/puppet
    git init --shared
    git config receive.denyCurrentBranch ignore

and lock off the permissions on /root/puppet if desired. Then, I use the following shell script (called apply-puppet and kept in the root of the Puppet Git repository) to push changes to the system:

    #!/bin/sh
    set -e

    puppet-lint modules
    puppet-lint nodes

    case "$1" in
    host1|host2)
        hostname="$1"
        shift
        git push "$hostname"
        ssh -l root "$hostname" /root/bin/apply-puppet "$@"
        ;;
    *)
        puppet apply --modulepath "$(pwd)/modules:/usr/share/puppet/modules" \
           --show_diff "$@" "$(pwd)/nodes"
        ;;
    esac

This includes the command from above for the normal case of applying the template locally. The first case statement has all of the systems on which I want to update Puppet remotely and have Git remotes set up. (One could be a bit more creative here and dynamically read this list from the Git remotes.) Note that --noop will be passed along to Puppet, as will any other flags.

I run puppet-lint before every application of the template to make sure it stays clean at all times. You may want to take a different approach to linting.

Note that changes are made by ssh as root. This is a security decision that I make for my personal systems: I use Kerberos and ssh with GSS-API authentication, and a separate /root Kerberos principal that has direct access to root on my remote systems (via /root/.k5login). If you want to use sudo instead, you will have to add a small bit of additional complexity to store the checkout elsewhere and run the command under sudo. Similarly, on my local systems, I use su instead of sudo; if you want to use sudo, that's an obvious change to the puppet apply line.

/root/bin/apply-puppet is just this shell script:

    #!/bin/sh
    set -e
    PATH=/bin:/usr/bin:/sbin:/usr/sbin; export PATH
    cd /root/puppet
    git reset --hard
    puppet apply --modulepath /root/puppet/modules:/usr/share/puppet/modules \
        --show_diff "$@" /root/puppet/nodes

This is mostly the same as the non-remote case of the apply-puppet script in the repository, except that it adds git reset --hard to handle the push to a non-bare repository (and sanitizes the path and changes to the correct directory).

Last modified and spun 2017-11-05