Super Slick Agile Puppet for Devops

Super Slick Agile Puppet for Devops

With a superb buzzword laden title like that, then I reckon massive traffic boost is inevitable.

Puppet is my favourite Configuration Management tool. This is not a post to try and persuade anyone not to use Ansible, Chef or any other. What I want to do is show I build Puppet based infrastuctures in such away that it meets all the basic tenets of DevOps/Agile/buzzword-of-the-month.

What to we need:

All the config is stored in Git, with Jenkins watching it.

Puppet tends to fall apart pretty quickly if you do not have DNS in place. You can start using host files, but that will get old quickly. Ideally, the first thing you will do with Puppet is install a DNS server managed by Puppet. Maybe that will be the next post.

Puppet

Starting with a base Centos 6 install, the installation is very easy:

yum -y install https://yum.puppetlabs.com/puppetlabs-release-el-6.noarch.rpm
yum -y install puppet puppet-server rubygem-activerecord

Our environments need a place to go, so create that:

mkdir /etc/puppet/environments
chgrp puppet /etc/puppet/environments
chmod 2775 /etc/puppet/environments

The configuration will look like:

[main]
    logdir = /var/log/puppet
    rundir = /var/run/puppet
    ssldir = $vardir/ssl
    trusted_node_data = true
    pluginsync = true

[agent]
    classfile = $vardir/classes.txt
    localconfig = $vardir/localconfig
    report = true
    environment = production
    ca_server = puppet.chriscowley.lan
    server = puppet.chriscowley.lan

[master]
    environmentpath = $confdir/environments
    # Passenger
    ssl_client_header        = SSL_CLIENT_S_DN
    ssl_client_verify_header = SSL_CLIENT_VERIFY

Do not use the Puppetmaster service. It uses Webrick, which is bad. Any more than 5 agents and it will start slowing down. Puppet is a RoR app, so stick it behind Apache/Passenger. We installed the puppet-server package for a simple reason: when you start it the first time, it will create your SSL certificates automatically. After that initial start you can stop it and forget it ever existed. So just run:

service puppetmaster start
service puppetmaster stop

Unfortunately, you will need to put SELinux into Permissive mode temporarily. Once you have fired it up you can build a local policy and re-enable it.

yum install httpd httpd-devel mod_ssl ruby-devel rubygems gcc gcc-c++ curl-devel openssl-devel zlib-devel
gem install rack passenger
passenger-install-apache2-module

Next you need to configure Apache to serve up the RoR app.

mkdir -p /usr/share/puppet/rack/puppetmasterd
mkdir /usr/share/puppet/rack/puppetmasterd/public /usr/share/puppet/rack/puppetmasterd/tmp
cp /usr/share/puppet/ext/rack/config.ru /usr/share/puppet/rack/puppetmasterd/
chown puppet:puppet /usr/share/puppet/rack/puppetmasterd/config.ru
https://gist.githubusercontent.com/chriscowley/00e75ee021ce314fab1e/raw/c87abc38182eafc6d22a80d13078ac044fdde49f/puppetmaster.conf | sed 's/puppet-server.example.com/puppet.yourlan.lan/g'

You will need to modify the sed command in the last line to match your environment.

You may also need to change the Passenger paths to match what the output of passenger-install-apache2-module told you. It is up to date as of the time of writing.

Hiera

Your config file (/etc/puppet/hiera.yaml) will already be created, mine looks like this:

---
:backends:
  - yaml
:hierarchy:
  - defaults
  - "nodes/%{clientcert}"
  - "virtual/%{::virtual}"
  - "%{environment}"
  - "%{::osfamily}"
  - global

:yaml:
  :datadir: "/etc/puppet/environments/%{::environment}/hieradata"

There is also an /etc/hiera.yaml which Puppet does not use. change this to a symbolic link to avoid confusion.

ln -svf /etc/puppet/hiera.yaml /etc/hiera.yaml

If you were to test it now, you will see a few errors:

Info: Retrieving pluginfacts
Error: /File[/var/lib/puppet/facts.d]: Could not evaluate: Could not retrieve information from environment production source(s) puppet://puppet/pluginfacts
Info: Retrieving plugin
Error: /File[/var/lib/puppet/lib]: Could not evaluate: Could not retrieve information from environment production source(s) puppet://puppet/plugins

Don\'t worry about that for now, the important thing is that the agent connects to the master. If that happens the master does return an HTTP error, then you are good.

R10k

This is the tool I use to manage my modules. It can pull them off the Forge, or from wherever you tell it too. Most often that will be Github, or an internal Git repo if that\'s what you use.

You need to install it from Ruby Gems, then there is a little configuration to do.

:

gem install r10k
mkdir /var/cache/r10k
chgrp puppet /var/cache/r10k
chmod 2775 /var/cache/r10k

The file /etc/r10k.yaml should contain:

# location for cached repos
:cachedir: '/var/cache/r10k'

# git repositories containing environments
:sources:
  :base:
    remote: '/srv/puppet.git'
    basedir: '/etc/puppet/environments'

# purge non-existing environments found here
:purgedirs:
  - '/etc/puppet/environments'

Git

The core of your this process is the ubiquitous Git.

yum install git

You need a Git repo to store everything, and also launch a deploy script when you push to it. To start with we\'ll put it on the Puppet server. In the future I would put this on a dedicated machine, have Jenkins run tests, then run the deploy script on success.

However, it is not a standard repository, so you cannot just run git init. It needs:

mkdir -pv /srv/puppet.git
git init --bare --shared=group /srv/puppet.git
chgrp -R puppet /srv/puppet.git
cd /srv/puppet.git
git symbolic-ref HEAD refs/heads/production

Continuing to work as root is not acceptable, so create user (if you do not already have one).

useradd <username>
usermod -G wheel,puppet <username>
visudo

Uncomment the line that reads:

%wheel        ALL=(ALL)       ALL

This gives your user full sudo privileges.

Deploy script

This is what does the magic stuff. It needs to be /srv/puppet.git/hooks/post-receive so that it runs when you push something to this repository.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash

umask 0002

while read oldrev newrev ref
do
    branch=$(echo $ref | cut -d/ -f3)
    echo
    echo "--> Deploying ${branch}..."
    echo
    r10k deploy environment $branch -p
    # sometimes r10k gets permissions wrong too
    find /etc/puppet/environments/$branch/modules -type d -exec chmod 2775 {} \; 2> /dev/null
    find /etc/puppet/environments/$branch/modules -type f -exec chmod 664 {} \; 2> /dev/null
done

Run chmod 0775 /srv/puppet.git/hooks/post-receive to make is executable and writable by anyone in the puppet group.

The first environment

Switch to your user

su - <username>

Clone the repository and create the necessary folder structure:

git clone /srv/puppet.git
cd puppet
mkdir -pv hieradata/nodes manifests site

Now you can create PuppetFile in the root of that repository. This is what tells R10k what modules to deploy.

# Puppet Forge
mod 'puppetlabs/ntp', '3.0.0-rc1'
mod 'puppetlabs/puppetdb', '3.0.1'
mod 'puppetlabs/stdlib', '3.2.1'
mod 'puppetlabs/concat', '1.0.0'
mod 'puppetlabs/inifile', '1.0.3'
mod 'puppetlabs/postgresql', '3.3.3'
mod 'puppetlabs/firewall', '1.0.2'
mod 'chriscowley/yumrepos', '0.0.2'

# Get a module from Github
#mod 'custom',
#  :git => 'https://github.com/chriscowley/puppet-pydio.git',
#  :ref => 'master'

A common error I make if I am not looking properly is to put the SSH URL from Github in there. This will not work unless you have added your SSH key on the Puppet server. Better just to put the HTTPS URL in there, there is need to write back to it after all.

Next you need to tell Puppet what agents should get what. To begin with, everything will get NTP, but only the Puppetmaster will get PuppetDB. To that end create hieradata/common.yaml with this:

---
classes:
  - ntp

ntp::servers:
  - 0.pool.ntp.org
  - 1.pool.ntp.org
  - 2.pool.ntp.org
  - 3.pool.ntp.org

Next create hieradata/nodes/$(hostname -s).yaml with:

---
classes:
  - puppetdb
  - puppetdb::master::config

Finally, you need to tell Puppet to get the data from Hiera. Create manifests.site.pp with

hiera_include('classes')

You should need nothing else.

Now you can push it to the master repository.

git add .
git commit -a -m "Initial commit"
git branch -m production
git push origin production

Testing

Of course, the whole point of all this is that we do as much testing as we can before any sort of deploy. We also want to keep our Git repository nice clean (especially if you push it to Github), so if we can avoid commits with stupid errors that would be great.

To perform your testing you need to replicate your production environment. From now on, I\'m going to assume that you are working on your own workstation.

Clone your repository:

git clone ssh://<username>@puppet.example.com/srv/puppet.git
cd puppet

To perform all the testing, RVM is your friend. This allows you to replicate the ruby environment on the master, have all the necessary gems installed in a contained environment and sets you up to integrate with Jenkins later. Install is with:

curl -sSL https://get.rvm.io | bash -s stable

Follow any instructions it gives your, then you can create your environment. This will be using a old version of ruby as we are running CentOS 6 on the master.

rvm install ruby-1.8.7
rvm use ruby-1.8.7
rvm gemset create puppet
rvm gemset use puppet
rvm --create use [email protected] --rvmrc

Create a Gemfile that contains:

source 'https://rubygems.org'

gem 'puppet-lint', '0.3.2'
gem 'puppet', '3.6.2'
gem 'kwalify', '0.7.2'

Now you can install the gems with bundle install.

The tests will be run by a pre-commit hook script, that looks something like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#!/bin/bash
# pre-commit git hook to check the validity of a puppet main manifest
#
# Prerequisites:
# gem install puppet-lint puppet
#
# Install:
# /path/to/repo/.git/hooks/pre-commit
#
# Authors:
# Chris Cowley <[email protected]>

echo "Checking style"
for file in `git diff --name-only --cached | grep -E '\.(pp)'`; do
  puppet-lint ${file}
  if [ $? -ne 0 ]; then
    style_bad=1
  else
    echo "Style looks good"
  fi
done

echo "Checking syntax"
for file in `git diff --name-only --cached | grep -E '\.(pp)'`; do
  puppet parser validate $file
  if [ $? -ne 0 ]; then
    syntax_bad=1
    echo "Syntax error in ${file}"
  else
    echo "Syntax looks good"
  fi
done

for file in `git diff --name-only --cached | grep -E '\.(yaml)'`; do
  echo "Checking YAML is valid"
  ruby -e "require 'yaml'; YAML.parse(File.open('$file'))"
  if [ $? -ne 0 ]; then
    yaml_bad=1
  else
    echo "YAML looks good"
  fi
done

if [ ${yaml_bad}  ];then
  exit 1
elif [ ${syntax_bad}  ]; then
  exit 1
elif [ ${style_bad}  ]; then
  exit 1
else
  exit 0
fi

This should set you up very nicely. Your environments are completely dynamic, you have a framework in place for testing.

For now the deployment is with a hook script, but that is not the ultimate goal. This Git repo needs to be on the Puppet master. You may well already have a Git server you want to use. TO this end, in a later post I will be add Jenkins into the mix. As you are running the tests in an RVM environment, it will be very easy to put into Jenkins. This can then perform the deployment.