July 12, 2017

Using a puppet-control repo in Vagrant

Whether you use Puppet Enterprise or r10k, using a "control repo" with a branch for every environment is the way you want to set up Puppet these days. Finding a way to make this work well with Vagrant for local development was surprisingly difficult - most guides out there focus on a very simple puppet setup with no modules, or maybe assuming that puppet is installed on the host operating system. I wanted to write a bit about the things I discovered while experimenting trying to get a proper setup up and running.

This is not meant as an introduction to puppet or vagrant - you might want to read up on how to use these tools before starting this article, as I won't go into detail on how puppet or vagrant configuration works..

I'll assume you already have a puppet-control repository. If you don't, have a look at this template repo.

Modifications to the control repo

First of all, you probably want to add .gitignore rules in your control repo for node-specific hieradata for vagrant files, so that you can modify these files as much as you want. If you use the default hierarchy and make sure that all your vagrant hostnames end with .vagrant it would look like this:


Also make sure to add some sort of generic vagrant hiera file which applies to all vagrant machines. We set a provider custom fact which is set to "vagrant" for vagrant machines, and then load the hiera file providers/%{facts.provider}.yaml, but if you can think of another way of setting generic hiera data for vagrant machines, you can do it however you want.

How we'll run Puppet

By default, r10k creates one environments for every git branch. This is rather nice for deploying things remotely, but for developing locally, this means we'd have to commit and run a deploy command before any change we make becomes "public" to the Vagrant machines. This is too slow for us, so we will be using r10k sparingly - mostly just to install modules. We could actually use puppet- librarian instead and get module dependency management, but we'll stick with r10k to stay consistent with our production environment.

We'll create a "fake" environment called "vagrant", which all of our VMs will use (configured through puppet.conf). This environment will be a plain directory on the VM's filesystem, and we'll simply invoke puppet using puppet apply.

Creating the Vagrant repo

We'll create a new git repo which contains the Vagrant configuration:

  • A Vagrantfile
  • Provisioning scripts
  • Puppet configuration
  • r10k configuration

The control repo can exist inside of this vagrant repo (make sure to .gitignore it!) or outside. The important thing here is to share the correct directories in the Vagrantfile:

config.vm.share './control', '/etc/puppetlabs/code/environments/vagrant'
config.vm.share './puppet', '/etc/puppetlabs/puppet'
config.vm.share './r10k', '/etc/puppetlabs/r10k'

Configuration files

We do not need a lot of configuration to make this work. I'll refer to configuration file paths relative to the directory where your Vagrantfile is.

puppet/puppet.conf only needs to contain "environment = vagrant". You might want to add various configuration to stay consistent with your production environment, of course.

puppet/hiera.yaml does need to be present, but does not need any actual configuration. We need to put "version: 5" in there to prevent Puppet warnings.

r10k/r10k.yaml should contain "cachedir: /var/cache/r10k".


While Vagrant comes with a Puppet provisioner, it does not work that well with our workflow, so we just write a custom shell script that does the necessary things to get everything set up. Here's an example for CentOS/RHEL:

rhv=$(cat /etc/redhat-release | grep -Po '\d' | head -1)
rpm -Uvh https://yum.puppetlabs.com/puppetlabs-release-pc1-el-${rhv}.noarch.rpm
yum -y install puppet-agent
/opt/puppetlabs/puppet/bin/gem install r10k

Add it to our Vagrantfile:

config.vm.provision 'install_puppet', type: 'shell', path: 'install_puppet.sh'

Let's make sure it works by running this command:

$ vagrant up && vagrant ssh

Our first puppet run

We're almost ready to run puppet - only one thing is missing: Installing modules and their dependencies. We'll do this manually with r10k, inside the virtual machine:

$ cd /etc/puppetlabs/code/environments/vagrant
$ sudo /opt/puppetlabs/puppet/bin/r10k puppetfile install

You may also want to check for missing dependencies which need to be added to your puppetfile:

$ sudo /opt/puppetlabs/bin/puppet module list --tree

Once this is done, we can try executing a class:

$ sudo /opt/puppetlabs/bin/puppet apply -e "include profile::base"

At this point, you can start editing and testing your code changes in puppet- control.

May 8, 2016

Managing systemd units with Salt

In many cases, you will want to manage your own systemd service definitions. Here's how.

    - name: /etc/systemd/system/example.service
    - source: salt://example/systemd_unit.jinja
    - template: jinja
    - name: service.systemctl_reload
    - onchanges:
      - file: example_systemd_unit

    - name: example
    - watch:
      - module: example_systemd_unit

Let's walk through what this does. First we manage the systemd unit, which is just a file ending with .service in the correct directory. You may need to change the path to example.service based on your Linux distribution.

Second we have a module.run state calling service.systemctl_reload, but only when the service file changes. Systemd documentation will tell you that you need to run systemctl reload to apply changes made to service files, this is simply the Salt way of doing that.

Finally, we have a regular service.running. You just need to make sure the name of the service matches the name of your .service file, and also make sure that the every time the service definition changes and service.systemctl_reload gets called, the service also gets restarted. A watch is an implicit require, so we don't need to specify that the service state requires the service file to be present.

March 29, 2016

Dynamic includes in Salt

Writing Salt state files can be somewhat deceptive. They have a concept of includes, which allows you to split up state files and define dependencies, which can give you reduced duplication, a cleaner top.sls and a way to run state files individually without dropping all your requirements. However, unlike Python and other programming languages, the includes don't need (it's not even considered best practice) to be defined at the top of the file. Realizing this opens some opportunities.

For example, consider a state file uwsgi/apps.sls that sets up various uWSGI applications:

  - uwsgi.install

{% for name, app in pillar.get('uwsgi_apps', {}).items() %}
/etc/uwsgi/{{ name }}.ini:
    - source: salt://uwsgi/files/uwsgi.ini.jinja
    - template: jinja
    - context: { app: {{ app | json }} }
{% endfor %}

Obviously missing from this example is how to get the source code for the uWSGI applications, and setting up a systemd/supervisord service that keeps the app running. Ignore that.

uWSGI apps can be of many types: Ruby, Python (both v2 and v3), Perl, you name it. How do we deal with this? We could just include all the plugin types at the top of the SLS:

  - uwsgi.install
  - uwsgi.plugins.psgi # perl
  - uwsgi.plugins.python2
  - uwsgi.plugins.python3
  - uwsgi.plugins.rack # ruby

{% for name, app in pillar.get('uwsgi_apps', {}).items() %}

But it'd be nicer if we could include the plugins dynamically, based on whether any apps use them:

{% set plugins = [] %}
{% for name, app in pillar.get('uwsgi_apps', {}).items() %}
  {% for plugin in app.get('plugins', []) if plugin not in plugins %}
      {% do plugins.append(plugin) %}
  {% endfor %}
/etc/uwsgi/{{ name }}.ini:
    - source: salt://uwsgi/files/uwsgi.ini.jinja
    - template: jinja
    - context: { app: {{ app | json }} }
{% endfor %}

  - uwsgi.install
{% for plugin in plugins %}
  - uwsgi.plugin.{{ plugin }}
{% endfor %}