Writing correct cookbooks: strict idempotency, declarative resource design, dependencies and versioning through metadata, modularity, and test-driven infrastructure with InSpec and Test Kitchen.

In episode 14 you automated node bootstrap in the cloud with Terraform and cloud-init. The more automation there is, the higher the demands on cookbook quality — a sloppy cookbook can wreck an environment in a single run. In episode 15 we discuss best practices: designing cookbooks that are idempotent, modular, and tested, plus test-driven infrastructure with InSpec and Test Kitchen.
The goals of this episode:
Idempotency is the ability to re-run without changing the final result. Resources like package, template, and service are idempotent by design — they only act when the state deviates from the desired state. What often causes trouble is execute: it always runs on every run unless we give it a guard.
execute "initialize app database" do
command "myapp-db init --force"
not_if "myapp-db status"
endImportant
A simple rule: every execute without a guard counts as a bug. Always provide a not_if or only_if that detects whether the work is already done.
package, not execute apt-get install.template, cookbook_file, or file — not scripts that write files by hand.| Goal | Correct resource | Wrong resource |
|---|---|---|
| Install a package | package | execute "apt-get install" |
| Write a file | template or file | execute "echo > file" |
| Run a service | service | execute "systemctl restart" |
All cookbook dependencies are declared in metadata.rb with version bounds. This lets the resolver (Berkshelf or Policyfile) compute the dependency graph deterministically.
name "my_app"
version "1.2.0"
depends "apt", ">= 6.0.0"
depends "chef-vault", ">= 3.0.0"Cookbooks are given semver versions in metadata.rb. Releases bump the version, are pinned in a lockfile such as Policyfile.lock.json, and are distributed through a private Supermarket.
Modularity means breaking responsibilities apart:
include_recipe "nginx::default"
node.override["nginx"]["worker_processes"] = 8The TDD principle: write the expectations first, then build the configuration to satisfy those expectations. In Chef, its counterparts are InSpec for assertions and Test Kitchen for running the cookbook in a temporary environment. Tests conventionally live in test/integration/ within the cookbook:
control "nginx_is_installed" do
describe package("nginx") do
it { should be_installed }
end
end
control "nginx_is_running" do
describe service("nginx") do
it { should be_running }
it { should be_enabled }
end
endThe development cycle with Kitchen:
kitchen converge — apply the cookbook to a VM or container.kitchen verify — run the tests; they should fail first.kitchen destroy — clean up the environment.Tip
Run kitchen test to trigger the whole create, converge, verify, destroy cycle in a single command — the perfect companion for a CI pipeline.
In this episode 15 you learned cookbook best practices: strict idempotency with guards, declarative resource design, dependencies and versioning through metadata, modularity by splitting cookbooks, and the test-driven infrastructure cycle with InSpec and Test Kitchen.
Key takeaways:
execute must be guarded.In the next episode, episode 16, we go beyond built-in resources: Custom Resources & Libraries — creating your own resources with the custom resource DSL, understanding LWRP and HWRP, and writing helpers and handlers that hook into the chef-client run cycle. See you in episode 16!