The information contained in this post was gleaned while working on a project for my current employer New Context Services. By the way, we’re hiring, and it’s a fantastic place to work: https://www.newcontext.com/careers/

If you’ve used Packer you already know it’s a great tool for creating VM images rapidly. Recently I had the need to run chef in audit mode on the image I was creating. It is really easy to use Chef Solo in packer; there is a built-in provisioner for that. The only problem is, Chef Solo does not support audit mode. That requires Chef client. Great! There’s a Chef Client provisioner too.

It’s really simple; the example is as so:

{
  "type": "chef-client",
  "server_url": "https://mychefserver.com/"
}

But that, as they say, is where my trouble started. By way of the server_url above, you may have inferred the Chef Client provisioner assumes you have a Chef server set up and that it is ready to be contacted by the image one is creating. In this case, running in a pipline without a Chef server, that assumption was false. Thus, the first work around. Packer allows you to specify the execute command for the Chef client. In our case, that looked like this:

"execute_command": "{{if .Sudo}}sudo {{end}}chef-client -z --audit-mode enabled --no-color -c {{.ConfigPath}} -j {{.JsonPath}}"

thus our server_url

{
"server_url": "http://localhost:8889"
}

The next problem to solve was one of cookbooks. Since Packer assumed an existing Chef server, it also assumed that would be the source of the cookbooks which were going to be used. Using the Chef Zero provisioner, all the cookbooks in your run list, and dependencies, would be pulled in and placed in the correct locations in the image. Not so with the Chef Client provisioner. We will have to get all our cookbooks into a directory, and place that directory in our image.

In our packer wrapper script, we have

SCRIPT_FOLDER_RELATIVE=$(dirname "$0")

if [[ -z "$COOKBOOK_PATH" ]]; then
  COOKBOOK_PATH="${SCRIPT_FOLDER_RELATIVE}/../../our_cookbook_checked_out_from_git"
fi

berks vendor --berksfile ${COOKBOOK_PATH}/Berksfile ${SCRIPT_FOLDER_RELATIVE}/cookbooks

Since we are now uploading this file, we must tell Packer where to put it, as well as tell Chef where to find it. We have these variables

{
  "script_folder_relative": "{{env `SCRIPT_FOLDER_RELATIVE`}}",
  "vendored_cookbooks": "{{env `SCRIPT_FOLDER_RELATIVE`}}/cookbooks/",
}

And then under provisioners, we tell Packer to create the directory, and to upload the cookbooks to it.

{
  "type": "shell",
  "inline": ["mkdir -p /tmp/packer-chef-client/cookbooks"]
},
{
  "type": "file",
  "source": "{{user `vendored_cookbooks`}}",
  "destination": "/tmp/packer-chef-client/cookbooks/"
}

We also need to tell Chef where to find the cookbooks. This is done via a custom client.rb template (see below). This is placed via a directive in the Chef Client provisioner block.

{
"config_template": "{{ user `script_folder_relative` }}/client.rb.tpl"
}

This template is in the same directory as the Packer wrapper and template JSON, and looks like this;

log_level        :info
log_location     STDOUT
chef_server_url  "{{.ServerUrl}}"
client_key       "{{.ClientKey}}"
validation_client_name "{{.ValidationClientName}}"
node_name "{{.NodeName}}"
cookbook_path "/tmp/packer-chef-client/cookbooks"

This nets us a successful Chef run. However. There is one more catch. At the end of the Chef run, since packer assumes we are using a real Chef server, it tries to invoke knife to delete the node from the Chef server. This fails, since Chef Zero has already shut down, and thus fails the entire Packer run. So, we have one more trick up our sleave. Packer allows you to specify the path to the knife command as well. In Packer template, that looks like this:

{
  "knife_command": "/bin/true"
}

Appendix (Example files):

See above for client.rb.template

Packer wrapper

#!/bin/bash
set -ea
SCRIPT_FOLDER_RELATIVE=$(dirname "$0")

if [[ -z "$COOKBOOK_PATH" ]]; then
  COOKBOOK_PATH="${SCRIPT_FOLDER_RELATIVE}/../../our_cookbook_checked_out_from_git"
fi

berks vendor --berksfile ${COOKBOOK_PATH}/Berksfile ${SCRIPT_FOLDER_RELATIVE}/cookbooks

packer build -var "build_host=$HOSTNAME" ${SCRIPT_FOLDER_RELATIVE}/our_packer_template.json

our_packer_template.json

This includes an install command to restrict it to Chef 12

{
  "variables": {
    "script_folder_relative": "{{env `SCRIPT_FOLDER_RELATIVE`}}",
    "vendored_cookbooks": "{{env `SCRIPT_FOLDER_RELATIVE`}}/cookbooks/"
  },
  "builders": [
    {
      "type": "amazon-ebs",
      "region": "us-west-2",
      "source_ami": "ami-835b4efa",
      "instance_type": "t2.micro",
      "ssh_username": "ubuntu",
      "ami_name": "our-ami-{{timestamp}}"
    }
  ],
  "provisioners": [
    {
      "type": "shell",
      "inline": ["mkdir -p /tmp/packer-chef-client/cookbooks"]
    },
    {
      "type": "file",
      "source": "{{user `vendored_cookbooks`}}",
      "destination": "/tmp/packer-chef-client/cookbooks/"
    },
    {
      "type": "chef-client",
      "server_url": "http://localhost:8889",
      "config_template": "{{ user `script_folder_relative` }}/client.rb.tpl",
      "run_list": [
        "recipe[our_cookbook_checked_out_from_git]"
      ],
      "install_command": "curl -L https://omnitruck.chef.io/install.sh | {{if .Sudo}}sudo{{end}} bash -s -- -v 12",
      "execute_command": "{{if .Sudo}}sudo {{end}}chef-client -z --audit-mode enabled --no-color -c {{.ConfigPath}} -j {{.JsonPath}}",
      "knife_command": "/bin/true"
    }
  ]
}

Comments

comments powered by Disqus