By treating roles and profiles as puppet modules, we can use r10k and librarian-puppet to manage the deployment of our puppet code into our puppet environements.

I shall assume that puppet is configured to use to use directory environments and that the environment path is $confdir/environments (ie. the default location). I also assume that both r10k and librarian-puppet are installed and in the path.

You should also understand and embrace the role-profile-module pattern, first described by Craig Dunn and subsequently by Adrian Thebo and Gary Larizza. Quoting Gary:

  • Roles abstract profiles
  • Profiles abstract component modules
  • Hiera abstracts configuration data
  • Component modules abstract resources
  • Resources abstract the underlying OS implementation 

I find the following points useful to clarify the purpose of each of the layers in this model:

  • Roles, profiles, and component modules can all be implemented as puppet modules
  • Each node is assigned exactly one role (either in site.pp or, preferably, using some external node classifier)
  • Each role includes one or more profiles
  • Each profile loads configuration data and feeds it into the component modules – this is where your business logic should go
  • Each component module should be generic and contain no site-specific data. You should be able to publish all your component modules on PuppetForge without leaking any secrets.

We can further extend this model to include environments. An environment can be thought of as a group of roles and can also be implemented as a puppet module.

So, how do we set this up?

At the top-level, we put a Puppetfile in the puppet config dir containing a list of our environments. This will look something like this:

#!/usr/bin/env ruby
#^syntax detection

forge 'https://forgeapi.puppetlabs.com'

mod 'yo61-env_production',
  :git => 'git@github.com:yo61/puppet-demo_env_production.git'

mod 'yo61-env_staging',
  :git => 'git@github.com:yo61/puppet-demo_env_staging.git'

Each environment is defined as a puppet module. Any valid Puppetfile syntax may be used to specifiy the module location, including alternate branches or specific version tags. 

Each of the environment modules should contain all the usual things you would put in a puppet environment, eg. a manifests/site.pp, etc. as well as a Puppetfile containing a list of all the roles to be deployed to this environment. The Puppetfile for a simple environment would look something like this:

#!/usr/bin/env ruby
#^syntax detection

forge "https://forgeapi.puppetlabs.com"

# list all the roles that are included in this environment
mod 'yo61-role_default',
  :git => 'git@github.com:yo61/puppet-demo-roles-profiles.git',
  :path => 'modules/role_default'

mod 'yo61-role_foo',
  :git => 'git@github.com:yo61/puppet-demo-roles-profiles.git',
  :path => 'modules/role_foo'

mod 'yo61-role_bar',
  :git => 'git@github.com:yo61/puppet-demo-roles-profiles.git',
  :path => 'modules/role_bar'

Like the top-level Puppetfile used to defined environments, each role is defined as a puppet module.

Each of the role modules will contain a simple class that loads the profiles used by the role, and a Puppetfile containing a list of all profiles used by the role. The Puppetfile for a simple role would look something like this:

#!/usr/bin/env ruby
#^syntax detection

forge "https://forgeapi.puppetlabs.com"

# list all the profiles that are included in this role
mod 'yo61-profile_common',
  :git => 'git@github.com:yo61/puppet-demo-roles-profiles.git',
  :path => 'modules/profile_common'

Each of the profile modules will contain all the puppet code required to define the business logic, load configuration data, etc. and a Puppetfile containing a list of all the component modules used by the profile. The Puppetfile for a simple profile would look something like this:

#!/usr/bin/env ruby
#^syntax detection

forge "https://forgeapi.puppetlabs.com"

# include all the modules used by this profile
mod 'puppetlabs-stdlib',
mod 'stahnma-epel'
mod 'puppetlabs-ntp'

Again, any valid Puppetfile syntax may be used.

We've now defined all our environments, roles, and profiles and we're ready to deploy each environment.

First, we run r10k to deploy each of the environment modules into the environment dir (/etc/puppet/environments):

# switch to the location of the top-level Puppetfile
cd /etc/puppet
PUPPETFILE_DIR=/etc/puppet/environments r10k puppetfile install

This will create a directory in /etc/puppet/environments for each of the environments defined in the top-level Puppetfile.

Next, we change into each of the newly-created environment directories and run librarian-puppet to install all the roles required by that environment.

cd /etc/puppet/environments/production
LIBRARIAN_PUPPET_PATH=modules librarian-puppet install --no-use-v1-api --strip-dot-git

The best bit is that librarian-puppet supports recursive module dependency resolution so this one command installs not only the roles, but also all the profiles, and component modules required by each of the roles.

My next article will present a script that runs r10k and librarian-puppet as described in this article, and also updates puppet environments atomically

 

We're using puppet + puppetdb in an EC2 environment where nodes come and go quite regularly. We have a custom autosign script that uses ec2 security info to validate the nodes before allowing the autosigning. This is all good, but it can leave a lot of "dead" nodes in puppet, eg. if a bunch of nodes are created by an autoscale policy and then terminated.

To get rid of these zombie nodes from puppet/puppetdb we can just use:

puppet node deactivate <certname1> <certname2> ... <certnameN>

We can query puppetdb to get a list of nodes that have not sent puppet reports for, say, 24 hours. The puppetdb query we need is something like this:

'query=["<", "report-timestamp", "$cutoff_date"]'

where $cutoff_date is a date in ISO8601 format, eg. 2015-03-05T13:39:45+0000

We can use date to generate the cutoff date with something like this:

$cutoff_date=$(date -d '-1 day' -Isec)

We then plug this into the query string and send it with curl as follows:

curl --silent -G 'http://localhost:8080/v4/nodes' \
  --data-urlencode "query=[\"<\", \"report-timestamp\", \"$(date -d '-1 day' -Isec)\"]"

Finally, we filter through jq to get a list of certnames:

curl --silent -G 'http://localhost:8080/v4/nodes' \
  --data-urlencode "query=[\"<\", \"report-timestamp\", \"$(date -d '-1 day' -Isec)\"]" \
  | jq '.[].certname'

We can then pass the list of nodes to the "puppet node deactivate" command.

One of my very early frustrations with puppet was that it allows variables to be used when they were undefined. Primarily this bit me by not catching typos in variable names which were often very hard to track down. I was very pleased when Puppetlabs introduced a strict_variables mode which throws an error if a manifest attempts to use an undefined variable.

I recently need to check for the existence of a fact. Without strict_variables, this is straight-forward:

if $::some_fact {
  # do stuff here
}

If the fact "some_fact" exists, the variable is a non-empty string and evaluates as true in boolean context. If the fact doesn't exist, the variable is an empty string which evaluates as false in boolean context.

But, with strict_variables enforced, this throws an error:

Error: Undefined variable "::some_fact"; Undefined variable "some_fact" at line ...

The solution is to use the getvar function from stdlib:

if getvar('::some_fact') {
  # do stuff here
}

This works exactly the same as in the previous example, but doesn't throw an error if "some_fact" doesn't exist.

The team at bitly has written an http reverse proxy that provides authentication using Google's OAuth2 API. They write about it in a blog post.

The proxy is written in Go but builds to a single, statically-linked executable, ie. there are no complex run-time dependencies, which is great.

I've built an RPM for EL7 which also includes a sample systemd unit file, and sample configuration file. Both source and binary RPMs are available in my yum repo.

Additionally, I've create a puppet module that installs the RPM, creates a systemd service, and sets up an nginx front end to the proxy service. The module is available from the Puppetforge, and also on github.

I'd be interested in any feedback/comments/bug reports/pull requests.

I recently had need to install uwsgi on EL7 (CentOS 7, actually, but RHEL 7 will be the same).

I ended up rebuilding the uwsgi SRPM from Fedora 21 which was relatively straight-forward but it required a few tweaks to the .spec file. I also had to build a chain of dependencies: mongodb, perl-Cora, libecb, perl-EV, libev, zeromq, perl-BDB, perl-AnyEvent-BDB, perl-AnyEvent-AIO.

All packages (including SRPMs) are in my repo: http://repo.yo61.net/el/7/

I'm setting up a new puppet master running under passenger on CentOS 7 using packages from the puppetlabs and foreman repos. I used a fork of Stephen Johnson's puppet module to set everything up (with puppet apply). All went swimmingly, except I would see this error in the logs the first time the puppet master app loaded (ie. the first time it got a request):

[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] *** Phusion Passenger: no passenger_native_support.so found for the current Ruby interpreter. Compiling one (set PASSENGER_COMPILE_NATIVE_SUPPORT_BINARY=0 to disable)...
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] # mkdir -p /usr/share/gems/gems/passenger-4.0.18/lib/phusion_passenger/locations.ini/buildout/ruby/ruby-2.0.0-x86_64-linux
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] Not a valid directory. Trying a different one...
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] -------------------------------
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] # mkdir -p /var/lib/puppet/.passenger/native_support/4.0.18/ruby-2.0.0-x86_64-linux
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] # cd /var/lib/puppet/.passenger/native_support/4.0.18/ruby-2.0.0-x86_64-linux
[ 2014-11-07 23:22:13.2600 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] # /usr/bin/ruby '/usr/share/gems/gems/passenger-4.0.18/ruby_extension_source/extconf.rb'
[ 2014-11-07 23:22:13.3048 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] /usr/bin/ruby: No such file or directory -- /usr/share/gems/gems/passenger-4.0.18/ruby_extension_source/extconf.rb (LoadError)
[ 2014-11-07 23:22:13.3156 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] Compilation failed.
[ 2014-11-07 23:22:13.3156 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] -------------------------------
[ 2014-11-07 23:22:13.3157 2603/7f1a0660e700 Pool2/Spawner.h:159 ]: [App 2643 stderr] Ruby native_support extension not loaded. Continuing without native_support.

I double checked, and I do have the native libs installed – they're in the rubygem-passenger-native-libs rpm – the main library is in /usr/lib64/gems/ruby/passenger-4.0.18/native/passenger_native_support.so.

Digging in the passenger code, it tries to load the native libs by doing:

require 'native/passenger_native_support'

If I hacked this to:

require '/usr/lib64/gems/ruby/passenger-4.0.18/native/passenger_native_support'

then it loaded correctly.

It seems that /usr/lib64/gems/ruby/passenger-4.0.18 is not in the ruby load path.

Additional directories can be added to the ruby load path by setting an environment variable, RUBYLIB.

To set RUBYLIB for the apache process, I added the following line to /etc/sysconfig/httpd and restarted apache:

RUBYLIB=/usr/lib64/gems/ruby/passenger-4.0.18

The passenger native libraries now load correctly.

I was writing some basic RSpec tests for a puppet module this morning, methodically adding in fixtures and hiera data items to get the module to compile under the spec tests.

Then I hit this error:

Failures:

1) profile_puppet::master supported operating systems profile_puppet::master class without any parameters on redhat 6.4 should compile into a catalogue without dependency cycles
Failure/Error: it { should compile.with_all_deps }
NoMethodError:
undefined method `groups' for nil:NilClass
# ./spec/classes/init_spec.rb:36:in `block (5 levels) in '

Uh oh, that doesn't look good. I did what I always do in such circumstances and googled the error message: puppet NoMethodError: undefined method `groups' for nil:NilClass. The first hit was https://tickets.puppetlabs.com/browse/PUP-1547 which describes my situation completely (I am testing for RHEL 6.4 on OSX).

What's even better is that the ticket was updated 3 days ago with a pull request that fixes the issue. I applied the change locally, it worked perfectly, and I was able to complete my task.

Try doing that with proprietary software.

In his talk at Puppetconf 2013, James Fryman mentioned a blog post by James White which contains a list of guidelines for management which has come to be known as the jameswhite manifesto.

Here’s the same list but unconstrained by a fixed-width text box so you can actually read it. 🙂

Rules

On Infrastructure

  • There is one system, not a collection of systems.
  • The desired state of the system should be a known quantity.
  • The “known quantity” must be machine parseable.
  • The actual state of the system must self-correct to the desired state.
  • The only authoritative source for the actual state of the system is the system.
  • The entire system must be deployable using source media and text files.

On Buying Software

  • Keep the components in the infrastructure simple so it will be better understood.
  • All products must authenticate and authorize from external, configurable sources.
  • Use small tools that interoperate well, not one “do everything poorly” product.
  • Do not implement any product that no one in your organization has administered.
  • “Administered” does not mean saw it in a rigged demo, online or otherwise.
  • If you must deploy the product, hire someone who has implemented it before to do so.

On Automation

  • Do not author any code you would not buy.
  • Do not implement any product that does not provide an API.
  • The provided API must have all functionality that the application provides.
  • The provided API must be tailored to more than one language and platform.
  • Source code counts as an API, and may be restricted to one language or platform.
  • The API must include functional examples and not requre someone to be an expert on the product to use.
  • Do not use any product with configurations that are not machine parseable and machine writeable.
  • All data stored in the product must be machine readable and writeable by applications other than the product itself.
  • Writing hacks around the deficiencies in a product should be less work than writing the product’s functionality.

In general

  • Keep the disparity in your architecture to an absolute minimum.
  • Use Set Theory to accomplish this.
  • Do not improve manual processes if you can automate them instead.
  • Do not buy software that requires bare-metal.
  • Manual data transfers and datastores maintained manually are to be avoided.

I'm a big fan of provisioning tools, particularly puppet.

Sometimes I just want to quickly throw a clean install on a new machine that I can then use to provision other machines (and even to re-configure the puppetmaster).

So, I wrote a script to do just that. The only requirement is a minimal install of your favourite CentOS/Red Hat/Fedora OS and the script will do the rest.

It's available from github: https://github.com/robinbowes/puppet-server-bootstrap