Puppetize a Build server

The Puppet series so far has really focused on VM builds and just started to touch on software packaging. We need an appropriate place to do this work, and what better way to set that than via Puppet itself? Today, we’ll create some roles and profiles for a build server, which could be permanent and share amongst developers, spun up as needed for the team, or spun up per developer.

Build Profile and Role

The last few examples we have done with FPM were on our “production” servers. That’s less than ideal for a few reasons. You wouldn’t want to mess up the publicly available service while packaging, whether by overwriting a file, exhausting resources, or the brief outage when services restart. It is not a good idea to add compilers and development libraries to any server unnecessarily as it increases the attack surface (additional security risks, additional packages to patch, additional items for auditors to flag, etc). You also probably do not want your build servers in the same environment as your production servers (unless, as is the case in these examples, your “production” environment is your lab – so just pretend it’s a different environment). Let’s assume that we do not have a good reason to violate these best practices, so our goal is set up a dedicated build server. It will require all the software we have been using so far, and we will throw on a local user. If you have LDAP or another directory service in your lab, I would suggest using it, but this is a good example as sometimes the build network is restricted.

We have two profiles to create, then. The first is the local users. We’ll call this class ::profile::build_users, in case we create another grouping of users later. The second profile is for our build software, and we will call it ::profile::build. Here are the two class files, located at profile/manifests/users/build.pp and profile/manifests/build.pp, respectively.

class profile::users::build {
  # crypted form of 'password'
  $adminpassword = '$6$TV63aMpF$7ZUXhaJKJSCIjJCDftQJnAtymSuYElpkCPTsBirG9nxQr1ZtM6apF6U3sJYlzLssqziv0rqX.MXWnleSvKBR0.'

  ::profile::users::local_user { 'rnelson0':
    id       => 'rnelson0',
    state    => 'present',
    comment  => 'Rob Nelson',
    groups   => ['wheel'],
    password => $adminpassword,

  include '::sudo'
  ::sudo::conf { 'wheel':
    priority => 10,
    content  => '%wheel     ALL=(ALL)       ALL',

class profile::build {
  package {['ruby-devel', 'gcc', 'rpm-build']:
    ensure => present,
  package {'fpm':
    ensure   => present,
    provider => gem,
    require  => Package['ruby-devel'],

Hopefully, a lot of this looks familiar. For our users, we assign an encrypted string (you can use ‘crypt’ to get it or set a password for a user and steal it from /etc/shadow). We call a ::profile::users::user define called rnelson0 to create a user of the same name, add it to the existing group wheel (it will not create new groups, you would have to manage the group as well) and set the password (we’ll go over that define in a moment). We include the sudo class, which we can add to our Puppetfile as saz/sudo, then call a define that allows members of the group wheel unrestricted access to sudo. This is the puppetized version of our basic sudo access. The second manifest file should be VERY familiar and it just adds three packages from the CentOS repo followed by the ruby gem fpm. The require is in place because puppet may try and install the gem before the ruby-devel package, which won’t get very far.

We also need to create a definition at manifests/users/local_user.pp. Here’s what the file looks like:

# == Define: profile::users::local_user
# Definition for a local user
# === Parameters
# None
# === Variables
# None
# === Examples
#  ::profile::users::local_user { 'username':
#    id    => 'username',
#    state => 'present',
#    comment => 'Real Name",
#    groups  => ['group1', 'group2'],
#    password => 'encryptedstring',
#  }
# === Authors
# Rob Nelson <rnelson0@gmail.com>
# === Copyright
# Copyright 2014 Rob Nelson
define profile::users::local_user ( $state, $id, $comment, $groups, $password) {
    user { $id:
            ensure           => $state,
            shell            => '/bin/bash',
            home             => "/home/$id",
            comment          => $comment,
            managehome       => true,
            groups           => $groups,
            password_max_age => '90',

    case $::osfamily {
            RedHat: {$action = "/bin/sed -i -e 's/$id:!!:/$id:$password:/g' /etc/shadow; chage -d 0 $id"}
            Debian: {$action = "/bin/sed -i -e 's/$id:x:/$id:$password:/g' /etc/shadow; chage -d 0 $id"}

    exec { "$action":
            path    => '/usr/bin:/usr/sbin:/bin',
            onlyif  => "egrep -q  -e '$id:!!:' -e '$id:x:' /etc/shadow",
            require => User[$id]

This define creates a standard user resource with the specified values, then runs an exec. The exec checks for the default password values of a new user (either ‘!!’ or ‘x’, depending on OS) and if the value is found, replaces it with the encrypted password via sed and resets the account’s password age to 0. This is mostly similar to defining a user resource, but it also sets the initial password.

We can wrap this up with inside a new role, ::role::build, and generate YAML for the class in our hiera repo:

class role::build {
  include ::profile::base  # All roles should have the base profile
  include ::profile::users::build
  include ::profile::build


There’s one last step – our sudo config relies on the module saz/sudo module. Add it to your Puppetfile. Commit all the changes and redeploy your environments.

[rnelson0@puppet puppet-tutorial]$ git diff Puppetfile
diff --git a/Puppetfile b/Puppetfile
index c13c253..567d56d 100644
--- a/Puppetfile
+++ b/Puppetfile
@@ -9,6 +9,7 @@ mod "puppetlabs/stdlib"
 mod "saz/ssh"
 mod "yguenane/augeas"
 mod "yguenane/ygrpms"
+mod "saz/sudo"
 mod "saz/motd"
 mod "puppetlabs/puppetdb"
 mod "puppetlabs/postgresql"

Spin up a new node called build (or whatever would match the puppet_role of build, per your implementation). Let’s check the value of /etc/passwd quickly (my server is a repurposed existing VM, if you deploy from a template you’ll see much different results):

[root@build ~]# tail -5 /etc/passwd
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
memcached:x:495:489:Memcached daemon:/var/run/memcached:/sbin/nologin

Run the puppet agent, sign the cert, and run the puppet agent again. If you want, add –noop so you can see what it’s going to do first. After it complete, check /etc/passwd again and we’ll see that fpm is now available:

[root@build ~]# tail -5 /etc/passwd
memcached:x:495:489:Memcached daemon:/var/run/memcached:/sbin/nologin
rnelson0:x:501:501:Rob Nelson:/home/rnelson0:/bin/bash
[root@build ~]# fpm
Missing required -s flag. What package source did you want? {:level=>:warn}
Missing required -t flag. What package output did you want? {:level=>:warn}
No parameters given. You need to pass additional command arguments so that I know what you want to build packages from. For example, for '-s dir' you would pass a list of files and directories. For '-s gem' you would pass a one or more gems to package from. As a full example, this will make an rpm of the 'json' rubygem: `fpm -s gem -t rpm json` {:level=>:warn}
Fix the above problems, and you'll be rolling packages in no time! {:level=>:fatal}

If you try and log in, you will be asked to change your password:

login as: rnelson0
CentOS release 6.4 (Final)
Kernel \r on an \m
rnelson0@build's password:
You are required to change your password immediately (root enforced)
WARNING: Your password has expired.
You must change your password now and login again!
Changing password for user rnelson0.
Changing password for rnelson0.
(current) UNIX password:

We created one build server here, but if we wanted to create a number of them – one per development group, per region/datacenter, or even per developer – it wouldn’t take us much time at all to stand them up. It would be very easy to add multiple local users, once, to the ::profile::users::build class and have them replicated elsewhere, even without a directory service. Your users would still have to manage local passwords, but this might be acceptable with a low number of build servers.

At this point, it may occur to some: The server build is probably a better place to develop your puppet code than on the puppet master. This assumes, however, that you aren’t having to ssh to the master to run r10k anyway. Use whatever works in your environment.



Today, we created a role/profile/yaml set to define a build server and deployed one for the team. Next week, we’ll start automation of software development on the build server, iterating closer and closer to an automated build pipeline.


One thought on “Puppetize a Build server

  1. Pingback: Customizing bash and vim for better git and puppet use | rnelson0

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s