416 lines
12 KiB
Markdown
416 lines
12 KiB
Markdown
![]() |
---
|
||
|
date: 2014-06-25
|
||
|
title: Super Slick Agile Puppet for Devops
|
||
|
category: devops
|
||
|
featured_image: https://i.imgur.com/3SJXbMb.jpg
|
||
|
---
|
||
|
|
||
|
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.
|
||
|
|
||
|
<!-- more -->
|
||
|
What to we need:
|
||
|
|
||
|
- CentOS 6 - RHEL/CentOS is pretty much the defacto enterprise distro.
|
||
|
This will easily translate to Debian/Ubuntu or anything else.
|
||
|
- Puppet 3 - I like a traditional Master/Agent set up, if you prefer
|
||
|
master-less good for you. This is my blog, my rules.
|
||
|
- Git
|
||
|
- Dynamic Environments
|
||
|
- PuppetDB
|
||
|
- Hiera
|
||
|
- Jenkins
|
||
|
|
||
|
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](https://docs.puppetlabs.com/guides/passenger.html).
|
||
|
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](https://wiki.centos.org/HowTos/SELinux#head-faa96b3fdd922004cdb988c1989e56191c257c01)
|
||
|
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:
|
||
|
|
||
|
- To be **bare**
|
||
|
- To be **shared**
|
||
|
- Have the **master** branch renamed to **production**
|
||
|
|
||
|
<!-- -->
|
||
|
|
||
|
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.
|
||
|
|
||
|
#!/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](https://rvm.io/) 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 ruby-1.8.7-head@puppet --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:
|
||
|
|
||
|
#!/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 <chris@chriscowley.me.uk>
|
||
|
|
||
|
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.
|