Skip to content

Instantly share code, notes, and snippets.

@danielsdeleo
Last active August 15, 2016 23:27
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save danielsdeleo/9112b6c7932452bdb7e8 to your computer and use it in GitHub Desktop.
Save danielsdeleo/9112b6c7932452bdb7e8 to your computer and use it in GitHub Desktop.
Policyfile Guided Tour

If you watched the ChefConf keynote, attended last years' community summits, or follow our open source mailing lists, you've probably heard about Policyfiles.

If you haven't, here's the deal: Policies are a new feature of Chef that combine the very best parts of Roles, Environments, and client-side dependency resolvers like Berkshelf into a single easy to use workflow. Policies are built by defining a Policyfile, which looks similar to a Chef Role combined with a Berksfile. When a Policy is ready for upload, a workstation command included with the ChefDK compiles the Policyfile into a Policyfile.lock file. This locked Policy, along with all of the cookbooks it references, are treated as a single unit by the Chef tooling. The bundle of Policyfile.lock and cookbooks are uploaded to the server simultaneously. They are also promoted simultaneously through the deployment lifecycle, from dev to QA to production

Policies make your chef-client runs completely repeatable, because cookbooks referenced in a Policy are identified by a unique hash based on their contents. This means that once the lock file + cookbook bundle has been generated, the code underlying it will never change.

For more information, see the Policyfile README in the ChefDK repo.

Previously we've recommended that you only use Policyfiles in specialized testing environments because using Policyfiles in compatibility mode along side existing infrastructure could cause unexpected behavior. With Chef Server 12.1 and ChefDK 0.7, Policyfile data is now stored via specialized APIs, making it safe (and a lot easier) to use Policyfiles in your existing Chef Server setup.

To help you get familiar with the workflows Policyfiles make possible, we'll walk through deploying a simple demo application using Policyfiles to manage our dependencies across the application's lifecycle.

The code here is based on a demo my colleague created for the London Chef meetup. It deploys the "Awesome Appliance Repair" Python application.

Getting Started With Local Development and Testing

To follow along with this example, you'll need:

Initialize Shell

If you have previously installed ChefDK or Test Kitchen on your machine using rubygems, you might have older versions of these tools in your PATH. If that's the case, you can use chef shell-init to setup your environment variables, like so (I use zsh, be sure to update the command for your shell):

eval "$(chef shell-init zsh)"
which kitchen
# => /opt/chefdk/bin/kitchen

Generate:

For this example, we'll structure our code as if we were developing our infrastructure code alongside the application, in the same source repo.

Note that Policyfiles don't require you to manage your code this way, you can use individual git repos per cookbook or a monolithic repo if you prefer. Just follow along for now :)

We use chef generate to create the required files and directories for us:

mkdir aar
cd aar
chef generate app .
# Policyfiles will be the default someday, 'till then:
chef generate policyfile

Commit

We'll commit our work now so we can roll back to a fairly blank slate if we make a mistake later:

git add .
git commit -m 'initial policyfile demo commit'

Edit Policyfile:

We describe how we want Chef to compose our cookbooks to configure a machine to run our application by editing the Policyfile.rb. This file defines a few things:

  • name: This describes the kind of machine we are creating. We name this "aar", which is our abbreviation for "Awesome Appliance Repair."
  • default_source: The place where we get shared cookbooks. The default source is :community, which is the Chef Supermarket site. You can also use an internal supermarket instance or a monolithic Chef Repo.
  • run_list: The list of recipes, in order, that you want Chef to evaluate to configure your system. When using Policyfiles, you set the run_list in the Policyfile instead of on the node.
  • cookbook: A Policyfile.rb can have multiple cookbook statements; these can configure specific cookbooks to be loaded from alternative sources or set additional version constraints on them.
  • Default and override attributes: these define attributes at the 'role' precedence level. We'll look at these later.

Edit the Policyfile.rb as follows:

# Policyfile.rb - Describe how you want Chef to build your system.
#
# For more information on the Policyfile feature, visit
# https://github.com/opscode/chef-dk/blob/master/POLICYFILE_README.md

# A name that describes what the system you're building with Chef does.
name "aar"

# Where to find external cookbooks:
default_source :community

# run_list: chef-client will run these recipes in the order specified.
run_list "aar::default"

# Specify a custom source for a single cookbook:
 cookbook "aar", path: "cookbooks/aar"

Chef Install

With our basic Policyfile.rb, we run chef install to fetch dependencies and generate a Policyfile.lock.json. We haven't specified any dependencies yet, so we don't need to fetch anything, but we will need the lockfile to be generated before we can proceed to the next step.

chef install

Let's take a peek at the Policyfile.lock.json we just created. We'll go over each part individually:

Revision ID

  "revision_id": "5f750bf464100b487cd7c276c5d532341b79fbeb5e8accd29538ae972896992b",

Each time we create or update the lock, chef will automatically generate a revision_id based on the content. These values are used to automatically version your policies, so that you can apply different revsions of a policy to different set of servers. We'll see this in action a little later.

Name and Run List

  "name": "aar",
  "run_list": [
    "recipe[aar::default]"
  ],

The lock includes the name and run list we specified previously. The run list is normalized to the least ambiguous form.

Cookbook Locks

  "cookbook_locks": {
    "aar": {
      "version": "0.1.0",
      "identifier": "cff2d37260c04b21053ad30b68aa20e674e52e6c",
      "dotted_decimal_identifier": "58532310150070347.9294424438433962.36174175743596",
      "source": "cookbooks/aar",
      "cache_key": null,
      "scm_info": {
        "scm": "git",
        "remote": null,
        "revision": "3455fb415d56f9a7cabbb76f2063942a6547b2eb",
        "working_tree_clean": true,
        "published": false,
        "synchronized_remote_branches": [

        ]
      },
      "source_options": {
        "path": "cookbooks/aar"
      }
    }
  },

For each cookbook we use, there is a corresponding entry in the cookbook_locks section. The exact data collected about each cookbook is dependent on the cookbook's source. In this case, we have a cookbook sourced from the local disk which happens to be in a git repo. In the event we need to debug this cookbook later, ChefDK has collected information about the cookbook's git revision. If we'd setup a remote, git would tell us the cookbook's git URL and whether we'd pushed this commit to a branch on the remote.

Attributes

  "default_attributes": {

  },
  "override_attributes": {

  },

Policyfiles have attributes that replace role attributes. We'll see these a little later.

The Rest

  "solution_dependencies": {

You can ignore the solution_dependencies section. It's used to keep track of dependencies in your cookbooks so ChefDK can check whether changes to your cookbooks are compatible with their dependencies without having to download the full cookbook list from supermarket every time.

Commit the lockfile

We'll want to compare it to an updated version later, to see what changed.

git add Policyfile.lock.json
git commit -a -m 'updated Policyfile and created lock'

Edit .kitchen.yml

ChefDK ships with a policyfile_zero provisioner for Test Kitchen that allows us to test our policies in local (or cloud) VMs. Note that currently Chef Zero doesn't fully support "native" Policyfile APIs, so instead it runs in compatibility mode. This isn't a problem on an isolated server like Chef's local mode, but it's something you might notice when debugging.

---
driver:
  name: vagrant
  network:
    - ["forwarded_port", {guest: 80, host: 8080}]

provisioner:
  name: policyfile_zero
  require_chef_omnibus: 12.3.0

platforms:
  - name: ubuntu-14.04

suites:
  - name: default
    attributes:

Run TK with Empty Cookbook

To verify our test rig works, we'll run kitchen with our empty cookbook:

kitchen converge

If you get an error like Message: Could not load the 'policyfile_zero' provisioner from the load path then you didn't run the chef shell-init step above. Run that and try again.

If that worked without a problem, you can throw that VM away:

kitchen destroy

Develop your Cookbook

This is where we'd normally run a TDD testing loop, but for the purpose of this walkthrough, we'll just import the aar cookbook fully formed:

cd cookbooks
curl -LO https://github.com/danielsdeleo/aar/releases/download/draft-1/aar-cookbook.tgz
tar zxvf aar-cookbook.tgz
rm aar-cookbook.tgz
cd ..

If you inspect cookbooks/aar/metadata.rb, you'll notice that our cookbook now has some dependencies, but we haven't yet downloaded them. We'll do that next.

Chef Update and Commit

Now that we've added some dependencies to our cookbook, we need to run chef update to fetch them.

chef update

This will recompute our dependencies and cache all the cookbooks we need for our Policy.

To see what's changed since our last commit, we could just run git diff, but chef diff gives us itemized output, listing added, removed, and changed cookbooks. Let's give it a go:

chef diff --head

Now that we're satisfied with our changes, we'll commit again.

git add .
git commit -m 'Update aar cookbook and deps'

Run Kitchen Again

We can run test kitchen again to see the result of our changes:

kitchen converge

Visit the Site

With the port forwarding, we can visit http://localhost:8080 and see the site. For more info on using the application, see the awesome appliance repair README on github.

Deploy

We can use the chef provision feature to create a "staging" and then a "production" node. We'll use these to see how we can deploy different revisions of a policy to different machines. Since chef provision is new and somewhat experimental, it's not yet integrated with chef generate, however, we can generate a cookbook like this (make sure it's named "provision", that name is special):

chef generate cookbook provision

Then overwrite the generated provision/recipes/default.rb with the following. NOTE: It's vital that you set the convergence_options as shown here. Nodes currently don't have any attributes to set Policyfile options; instead you must set policy_group and policy_name in the config file. The provisioning convergence options will take care of that for you automatically.

If you'd like to extend this example to use something other than Vagrant, you can learn more about Chef Provisioning on docs.chef.io.

context = ChefDK::ProvisioningData.context

# Set the port dynamically via the command line:
target_port = context.opts.port

with_driver 'vagrant:~/.vagrant.d/boxes' do

  options = {
    vagrant_options: {
      'vm.box' => 'opscode-ubuntu-14.04',
      'vm.network' => ":forwarded_port, guest: 80, host: #{target_port}"
    },
    convergence_options: context.convergence_options
  }


  machine context.node_name do
    machine_options(options)

    # This forces a chef run every time, which is sensible for `chef provision`
    # use cases.
    converge(true)
    action(context.action)
  end
end

Notice that we are making the forwarded port configurable via the command line. This lets us run multiple VMs on the same host without the forwarded ports colliding with each other.

We can sync the policy to the server and create our "staging" node with a single command:

chef provision staging --sync -n aar-staging-01 -o port=8000

That will sync our local policy lock to a policy group called 'staging' (creating that policy group in the process), then run an embedded Chef Client which creates a VM (via Chef Provisioning), configures it and converges it.

We can see the site running by visiting http://localhost:8000 (notice that's port 8000 this time).

Since it works in staging, we'll create a "production" node with the same policy:

chef provision production --sync -n aar-production-01 -o port=8888

Update the Attributes via Policyfile

Policyfiles allow us to set attributes. Since Policyfiles don't support roles, these attributes replace role attributes in the precedence hierarchy. In our Policyfile.rb, we set attributes using the same syntax we use in cookbooks. In this example, we'll change the version number that appears on the home page. Add the following line to your Policyfile.rb

default['aar']['version'] = "19.7.4"

To apply the changes to the Policyfile.lock.json, use chef update:

chef update --attributes

We can see the effect of our changes with chef diff:

chef diff --head

And we can see that our local policy differs from what we've deployed to our staging group:

chef diff staging

Now that we're satisfied with our changes, commit to git again:

git commit -a -m 'update aar version'

Deploy it to Staging:

In a normal TDD workflow, we'd run kitchen again to see our changes, but this time we'll just deploy it to staging by running chef provision again:

chef provision staging --sync -n aar-staging-01 -o port=8000

If we visit the site at http://localhost:8000 we see "Awesome Appliance v.19.7.4" right under the login dialog.

Let's suppose we're not ready to apply the change in production, but we want to run chef-client on our production machine. We can do this by using the --policy-name option instead of the --sync option:

chef provision production --policy-name aar -n aar-production-01 -o port=8888

Note that since we did not update the policy, nothing is updated. If we visit the web page at http://localhost:8888 we'll see that nothing has changed. Though we only changed the attributes, the same is true if we updated the cookbooks, since they're locked down by content in our policy.

Oh No! A Bug in Our Cookbook

To demonstrate how cookbook code is automatically versioned and sandboxed by policyfiles, let's introduce a "bug" into our cookbook. Add this line to the top of cookbooks/aar/recipes/default.rb:

raise "OH NO THIS IS A BUG"

Since this cookbook is local, chef will automatically pull in updates when we upload (no need to run chef update). Lets upload to staging and run chef-client again. This time, we'll use chef push to upload our changes without invoking provisioning (which is probably what you'd want to do in your normal workflow).

chef push staging

We can invoke provisioning without syncing the policy like so:

chef provision staging -p aar -n aar-staging-01 -o port=8000

This will cause an error like this in the chef run on the VM (which will cause another error in chef provision on your workstation):

================================================================================
Recipe Compile Error in /var/chef/cache/cookbooks/aar/recipes/default.rb
================================================================================

RuntimeError
------------
OH NO THIS IS A BUG

Cookbook Trace:
---------------
  /var/chef/cache/cookbooks/aar/recipes/default.rb:1:in `from_file'

But if we run Chef on our production node, everything is roses and sunshine:

chef provision production -p aar -n aar-production-01
# => Chef Client finished, 0/41 resources updated in 10.634577518 seconds

We can also confirm that the policies applied to each group are different with the show-policy subcommand:

chef show-policy aar

The output should be similar to:

aar
===

* production:  8312cd89c9
* staging:     eb0fedf311

Cleaning Up

To shut down the Vagrant VMs, you can use the -d option to chef provision to set the default action to destroy:

chef provision production --policy-name aar -n aar-production-01 -o port=8888 -d
chef provision staging -p aar -n aar-staging-01 -o port=8000 -d

Fin

Policyfiles give us a consistent and repeatable description of how we want Chef to configure our machines, with minimal hassle. Because versioning is built-in and automatic at both the cookbook and policy level, we can make changes to our infrastructure code safely and explicitly. While we still have work to do to fill out the feature set around Policyfiles, enough of it exists for you to get started today.

Currently, the most complete documentation is in the Policyfile README in the ChefDK repo, but we'll be moving documentation to docs.chef.io as we complete work on Policyfiles.

If you give it a try and find that any missing feature is a deal breaker for you, let us know and we'll do our best to make you successful.

Happy Cheffing!

@charlesjohnson
Copy link

Read the whole thing. A few notes:

  1. When will it be safe to get policyfile generation into the basic generators instead of as an add-in?
  2. This whole thing will work WAY better as the script to a youtube video or a webinar than it will as a document. Should we record it?
  3. Is the work scheduled to get "native" policyfile API support into Chef Zero?

@stephenlauck
Copy link

Just walked through this, would love to give it to customers as an intro to how Policyfiles work but the example needs to work on RHEL based systems. I got into the weeds trying to convert aar to work on centos and gave up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment