This is the second part of my blog series on creating a fully functional Puppet stack with Docker. The first introductory post can be found here.

Packer

The description on the Packer website describes Packer as follows:

Packer is a tool for creating identical machine images for multiple platforms from a single source configuration.

Docker is one of the platforms that Packer supports. When building a Docker image it is generally recommended to use a special Dockerfile. Packer takes an alternative approach by running a container from a base image and performing a number of provisioning steps. This allows Packer to not require an extra (Docker specific) definition file. The downside to this approach is that you cannot build layers from each intermediate step in the provisioning process (although on the website it states that Hashicorp is working on this).

For my project there was also an additional reason to choose the Packer approach over a Dockerfile: I needed the container to have a hostname during provisioing. I've not yet been able to figure out how to set (or fake) a hostname during image creation when building from a Dockerfile.

Definitions

We need Packer definition files for the Puppetmaster, The Foreman and PuppetDB Docker images. I'll only describe the Foreman definition file in this blog post. The other definition files are very similar and can be found in the Github repository. The Packer definition file for The Foreman has the following contents:

{
  "builders": [
    {
      "type": "docker",
      "image": "centos:centos6",
      "export_path": "foreman.tar",
      "run_command": [
        "-h=foreman.localdomain",
        "-d",
        "-i",
        "-t",
        "{{.Image}}",
        "/bin/sh"
      ]
    }
  ],
  "provisioners": [
    {
      "type": "file",
      "source": "certs/",
      "destination": "/tmp/certs/"
    },
    {
      "type": "file",
      "source": "private_keys/",
      "destination": "/tmp/private_keys/"
    },
    {
      "type": "shell",
      "script": "scripts/foreman.sh"
    },
    {
      "type": "shell",
      "environment_vars": [
        "CERT=foreman.localdomain"
      ],
      "script": "scripts/copy_certs.sh"
    },
    {
      "type": "puppet-masterless",
      "manifest_file": "manifests/foreman.pp",
      "module_paths": [
        "modules"
      ],
      "prevent_sudo": true
    },
    {
      "type": "shell",
      "inline": "rm -rf /tmp/certs /tmp/private_keys"
    }
  ]
}

As you can see the definition consists of two main sections: builders and provisioners. Let's go through them step-by-step.

Builders

There is just a single entry in the builders array since we only need the Docker builder. If we wanted to create Virtualbox images as well it would just be a matter of adding a configuration for the virtualbox builder to the builders array.

The Docker builder takes a couple of configuration settings. First we specify that we want to use the Docker builder type. Next we specifiy which image the Docker builder should use to create the container. In this case we start from a clean CentOS 6 installation which is available as a verified image on the public Docker registry. The end-result of the Docker build is a compressed archive of the container state after all provisioning steps have completed. The export_path setting specifies the location for that archive. Finally, the run_command setting lists the parameters that should be passed to the Docker run command. It specifies that the container should:

  • Set the hostname to 'foreman.localdomain'
  • Run in detached mode
  • Keep STDIN open even when not attached to the container
  • Allocate a pseudo-tty to the container
  • Use the image specified in the definition to build the container from
  • Run a shell command in the container

Provisioners

The provisioners do the actual work to create the final image. Three types of provisioners are used:

File

This provisioner uploads files from the host system to the running container. In this case we upload directories containing certificates and private keys that have been generated by the Puppetmaster.

Certificates

The Puppetmaster, Foreman and PuppetDB communicate with each other over secure channels. We need to generate certificates that allow these services to communicate with each other. The Puppetmaster acts as a certificate authority and is capable of generating these certificates. There is a really good description on how it works in this blog post.

To acquire the certificates for PuppetDB and The Foreman we need the following steps:

1. Provision the Puppetmaster so that we can run a fully functional container from it.

packer build puppetmaster.json

2. Generate certificates for The Foreman and PuppetDB:

docker run --name="foreman_cert" -t iverberk/puppetmaster:packer puppet cert generate foreman.localdomain  
docker run --name="puppetdb_cert" -t iverberk/puppetmaster:packer puppet cert generate puppetdb.localdomain  

3. Copy the certificates (including the CA certificate) from the Puppetmaster container to the host

docker cp puppetdb_cert:/var/lib/puppet/ssl/certs/ca.pem certs/  
docker cp puppetdb_cert:/var/lib/puppet/ssl/certs/foreman.localdomain.pem certs/  
docker cp puppetdb_cert:/var/lib/puppet/ssl/certs/puppetdb.localdomain.pem certs/

docker cp puppetdb_cert:/var/lib/puppet/ssl/private_keys/foreman.localdomain.pem private_keys/  
docker cp puppetdb_cert:/var/lib/puppet/ssl/private_keys/puppetdb.localdomain.pem private_keys/  
Shell

This provisioner executes a script inside the container. The script can be a file that is copied from the host to the container or it can be specified inline, in which case Packer creates the script within the container. In this case we use two predefined script files. The first one configures the EPEL, Puppet and Foreman repositories and installs the Puppet agent:

#!/bin/bash

rpm --import https://yum.puppetlabs.com/RPM-GPG-KEY-puppetlabs && \  
rpm --import http://download.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-6 &&  \

rpm -ivh http://yum.puppetlabs.com/puppetlabs-release-el-6.noarch.rpm && \  
rpm -ivh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm  
yum -y install http://yum.theforeman.org/releases/1.6/el6/x86_64/foreman-release.rpm

yum -y install puppet-3.6.2-1.el6  

The next one copies the certificates that we uploaded with file provisioner to the correct locations. This script expects an environment variable to be passed. We inject this variable through the environment_vars setting:

#!/bin/bash -e

# Init SSL directory
puppet cert list

cp -f /tmp/certs/ca.pem /var/lib/puppet/ssl/certs/ca.pem  
cp -f /tmp/certs/$CERT.pem /var/lib/puppet/ssl/certs/$CERT.pem  
cp -f /tmp/private_keys/$CERT.pem /var/lib/puppet/ssl/private_keys/$CERT.pem  

In the final step we execute a small inline script that removes the temporary certificates from the container.

Masterless Puppet

This provider executes a self-contained Puppet run on the container. It expects all the manifests and modules to be present in the container. The manifest_file and modules_path settings specify which files need to be copied to the container so that the Puppet run succeeds. The prevent_sudo is set to true in order to prevent Packer from using the sudo command, which is not present in the base image. Besides, the whole provisioning is already done from a root context so it is superfluous to use sudo.

I've created three manifests that configure the resources on the Docker containers. The manifests depend on several community modules and one I created myself. They can all be installed by running the wonderful librarian-puppet against the included Puppetfile.

I've created a rather simple Puppet module that is specifically designed to install a Puppet infrastructure. It relies on the Puppetlabs PuppetDB module and Foreman installer to do most of the work but it enables full customizability of these applications


Let's have closer look at my Puppet infrastructure module in the next part of this blog series: Puppetmaster, PuppetDB and Foreman installation with Puppet