Learn how to define run lists and cookbooks declaratively with Policyfile.rb along with the Policyfile.lock.json lockfile for full reproducibility, as well as the use of policy groups for deterministic per-environment releases without mutating cookbooks on the server.

In episode 9 you learned about the Chef Infra Server from the administration side: installing the chef-server-core package, chef-server-ctl commands, creating users and organizations, SSL configuration, and backup plus high availability. The server is now ready to hold cookbooks, but there's a problem left unsolved: how do you guarantee that one tested combination of cookbooks can be released identically to many nodes?
The traditional approach with run lists and environments has weak points. Cookbook versions in an environment can change, cookbooks can be re-uploaded, and dependencies can drift because solvers differ over time. Policyfiles solve this problem with a mechanism similar to the lockfile used by package managers.
Episode 10 will cover the Policyfile.rb definition, the Policyfile.lock.json lockfile for full reproducibility, and policy groups that enable deterministic per-environment releases without changing cookbooks on the server.
In the classic approach, the run list and cookbook versions are determined in two separate places: the run list is attached to the node and versions are enforced by the environment. When a cookbook dependency is bumped, different solvers between the workstation and the server can produce different version combinations.
Policyfile changes this way of thinking. Now the complete infrastructure definition — which cookbooks, at what versions, and their dependencies — is captured in a single artifact called a policy. This combination is frozen in the lockfile, so every node using the same policy always runs exactly the same cookbook combination.
Here's a comparison of the two approaches:
| Aspect | Run List + Environment | Policyfile |
|---|---|---|
| Where the run list is defined | Attached to the node | Inside the policy |
| Cookbook versions | Environment, dynamically changing | Lockfile, frozen |
| Dependency solver | At run time on the server | Once when the policy is created |
| Per-environment releases | Mutates cookbooks on the server | Policy group, no mutation |
| Reproducibility | Prone to drift | Full and deterministic |
A Policyfile is a Ruby file that describes the policy name, run list, cookbooks used, and default attributes. Here's an example for web infrastructure:
name 'web_server_policy'
run_list 'web_server::default', 'monitoring::default'
default_source :supermarket
default_source :chef_repo, 'cookbooks/'
cookbook 'web_server', path: 'cookbooks/web_server'
cookbook 'monitoring', '= 1.2.3'
named_run_list 'minimal', 'web_server::default'name gives the policy a unique identity on the server.run_list defines the recipes to run, in a specific order.default_source :supermarket allows public cookbooks from the Supermarket.cookbook 'monitoring', '= 1.2.3' pins an exact version.named_run_list defines alternative run lists for special scenarios without changing the main policy.Tip
When installing cookbooks from a local path, make sure the relative path is calculated from where Policyfile.rb lives. Using path: 'cookbooks/web_server' is more predictable than using a different directory on each workstation.
Once Policyfile.rb is done, run chef install in the same directory:
chef installchef install reads Policyfile.rb, resolves all dependencies with the solver on the workstation, then writes Policyfile.lock.json. This file is the key to full reproducibility. Part of its contents looks like this:
{
"revision_id": "f7a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5",
"name": "web_server_policy",
"run_list": [
"recipe[web_server::default]",
"recipe[monitoring::default]"
],
"cookbook_locks": {
"web_server": {
"version": "0.2.0",
"identifier": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"dotted_decimal_identifier": "72456436728239420389.28230458704770911000.1"
}
}
}revision_id is a hash that captures the policy's overall identity. Two lockfiles with the same contents always produce the same revision_id, while any change, however small, changes it. cookbook_locks records each cookbook's version and unique identifier.
To place the policy on the server:
chef push development web_server_policy
chef push staging web_server_policy
chef push production web_server_policychef push <group> <policy> uploads the policy to the specified policy group. Chef archives all the cookbooks listed in the lockfile to the server along with the policy, so the server doesn't need to resolve dependencies again when nodes run.
Important
Never edit Policyfile.lock.json manually. This file is generated by chef install, and hand edits make the revision_id inconsistent. Re-run chef install every time Policyfile.rb changes, and commit both files to the repository together.
A policy group is a namespace that connects a policy to nodes. Examples are development, staging, and production. When you run chef push production web_server_policy, that version of the policy is bound to the production group, and all nodes using that group run the same policy.
The main advantage of policy groups is that releases no longer need to mutate cookbooks on the server. To bump an application version:
chef install to update the lockfile.staging group with chef push staging web_server_policy.production with chef push production web_server_policy.During step 2, production nodes still use the old policy revision because the production group hasn't been updated. No cookbook is changed on the server; only the policy-to-group binding changes.
To assign a node to a group, use the chef-client --policy-group subcommand when configuring the node, for example at bootstrap:
knife bootstrap 203.0.113.20 \
--ssh-user deploy \
--sudo \
--node-name web02 \
--policy-group production \
--policy-name web_server_policyNow the web02 node uses the web_server_policy from the production group. Chef-client no longer reads the node's run list or environment; it directly executes the policy bound to that group.
Policy groups don't have to match environment names. You can add extra groups for canary releases:
chef push canary web_server_policyNodes bootstrapped with --policy-group canary run the newest policy first. If the canary nodes look healthy, the release continues to the production group. This pattern enables staged rollouts without replacing node configuration one by one.
Warning
Chef-client refuses to run a policy if the node doesn't have a run list and environment consistent with its policy. When migrating nodes from the classic run list model to policies, re-bootstrap the node with the --policy-group and --policy-name options to avoid configuration conflicts.
In episode 10 you understood the Policyfile concept as a single artifact that captures the run list, cookbooks, and all their dependencies. You learned to define Policyfile.rb, freeze the version combination into Policyfile.lock.json with chef install, and upload and release policies to various groups with chef push. You also understand why policy groups enable deterministic per-environment releases without mutating cookbooks on the server.
Key takeaways:
In the next episode, episode 11, we'll test whether the infrastructure you've built truly complies with security policies: Chef InSpec. You'll learn to write compliance profiles with controls and describe blocks, run inspec exec to scan nodes, and integrate the results with Chef Automate. See you there.