The 60 Chef questions interviewers actually ask, with direct answers, runnable recipes, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
60 questions with answersKey Takeaways
Chef is a configuration management and automation tool that treats infrastructure as code. You write recipes in a Ruby-based domain-specific language (DSL) that declare the state a server should be in: which packages are installed, which services run, which files exist. The chef-client agent then converges the node to that state and re-runs safely because resources are idempotent. According to the official Chef documentation, the core building blocks are resources, recipes, cookbooks, and the run-list that tells a node what to apply. In interviews, Chef questions probe how these pieces fit together and how a run actually executes, not command trivia. This page collects the 60 questions that come up most, each with a direct answer and a runnable snippet. If you're still building fundamentals, the official Chef docs are the canonical path; the first DevOps round is increasingly a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.
Watch: What Is Chef in DevOps? Chef Training Video
Video: What Is Chef in DevOps? Chef Training Video (Simplilearn, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your Chef certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
Chef is a configuration management tool that treats infrastructure as code. You write recipes in a Ruby-based DSL that declare the state a server should be in, and Chef converges each node to that state automatically.
It solves configuration drift and manual server setup. Instead of SSHing into machines and running commands by hand, you version-control the desired state and apply it consistently across ten or ten thousand nodes.
Key point: A one-line definition plus the drift problem it solves beats a feature list. Interviewers open with this to hear how you organize an answer.
Watch a deeper explanation
Video: Chef Tutorial for Beginners (Simplilearn, YouTube)
A resource is the smallest unit: a declaration of one piece of desired state, like a package that should be installed or a service that should run. A recipe is an ordered collection of resources. A cookbook packages recipes together with templates, files, attributes, and metadata.
Think of it as nesting: resources make recipes, recipes make cookbooks, cookbooks get applied to nodes. Knowing this hierarchy cold is the base every other Chef question builds on.
# a recipe: an ordered list of resources
package 'nginx' do
action :install
end
service 'nginx' do
action [:enable, :start]
end| Unit | What it is | Example |
|---|---|---|
| Resource | One piece of desired state | package 'nginx' |
| Recipe | Ordered collection of resources | install and start nginx |
| Cookbook | Package of recipes plus assets | the whole webserver cookbook |
Key point: The follow-up is often 'give me an example of a resource'. Have package or service ready.
Idempotency means applying the same recipe repeatedly produces the same end state, and does nothing extra after the first successful run. A resource checks the node's current state and acts only when there's a gap.
It matters because chef-client runs on a schedule, often every 30 minutes. Without idempotency, every run would reinstall packages and rewrite files, causing churn and outages. Idempotency is what makes recurring convergence safe.
Key point: This is the single most-asked Chef concept. If you can explain why re-running a recipe is safe, you sound like you've actually used it.
The classic model has three parts. The Chef Workstation is where you author and test cookbooks. The Chef Infra Server stores cookbooks, node data, and policy centrally. Nodes are the managed machines, each running a chef-client agent that pulls its run-list and converges.
It's a pull model: nodes reach out to the server on a schedule, download what they need, and apply it locally. That's the difference from Ansible's push model.
How a Chef change reaches a node (pull model)
This is a pull model: nodes fetch and apply. That's the contrast with Ansible's push model.
A run-list is the ordered set of recipes and roles that chef-client applies to a node, and it defines exactly what configuration that node gets. Order matters here: resources run in the sequence the run-list and the recipes inside it define them, top to bottom.
You set it per node or through roles. When chef-client runs, it reads the run-list, expands any roles into their recipes, and builds the resource collection from that order.
# a node's run-list, applied top to bottom
recipe[base::default]
role[webserver]
recipe[app::deploy]knife is the command-line tool that connects your workstation to the Chef Infra Server. You use it to upload cookbooks, bootstrap new nodes, manage roles, environments, and data bags, and query node data.
It's your main day-to-day interface with the server. Almost every workstation-to-server operation goes through a knife subcommand.
knife cookbook upload webserver
knife node list
knife bootstrap 10.0.0.5 -N web-01 --run-list 'recipe[webserver]'Watch a deeper explanation
Video: What Is Chef in DevOps? Chef Training Video (Simplilearn, YouTube)
chef-client is the agent that runs on a node and syncs with the Chef Infra Server: it pulls cookbooks and node data, then converges. chef-solo runs cookbooks locally with no server at all, reading everything from disk.
chef-solo is now largely replaced by chef-client's local mode (--local-mode), which gives you serverless runs with the modern client. Both suit testing and small setups without central infrastructure.
| chef-client | chef-solo / local mode | |
|---|---|---|
| Needs a server | Yes (normal mode) | No |
| Node data source | Chef Infra Server | Local files |
| Typical use | Production fleets | Testing, small or air-gapped setups |
Attributes are key-value data that describe a node: things like the port a service listens on or a package version. Recipes read attributes to stay generic, so the same cookbook configures dev and prod differently by data, not by code.
They come from several sources (defaults in cookbooks, roles, environments, and ohai's automatic system facts) and merge by a defined precedence. You reference them through the node object.
# attributes/default.rb
default['nginx']['port'] = 8080
# recipe reads it
template '/etc/nginx/nginx.conf' do
variables(port: node['nginx']['port'])
endohai is a tool that profiles a node at the start of every chef-client run. It collects system data: operating system, network interfaces, memory, CPU, hostname, and more, then exposes it as automatic attributes.
Recipes use ohai data to make decisions, like picking a package name that differs across Linux distributions. You can run ohai standalone to see everything it detects.
# ohai gives you node['platform_family']
package 'httpd' if node['platform_family'] == 'rhel'
package 'apache2' if node['platform_family'] == 'debian'Use the package resource. You The package and declare the action; :install is the default. Chef picks the right package manager for the platform (apt, yum, and so on) automatically.
Because it's idempotent, the resource installs only if the package isn't already present at the requested version. Re-running does nothing.
package 'git' do
action :install
end
# pin a version
package 'nginx' do
version '1.24.0'
action :install
endUse the service resource. Actions like :enable (start on boot), :start, :restart, and :stop declare the state you want. Chef detects the init system (systemd, sysvinit) and does the right thing.
A common pattern is :enable plus :start so the service both survives reboots and runs now.
service 'nginx' do
supports restart: true, status: true
action [:enable, :start]
endThe template resource renders an ERB template file with variables and writes it to the node. It's how you generate config files whose contents depend on attributes or node data instead of shipping a static file.
Templates live in the cookbook's templates directory and end in .erb. You pass values through the variables property, and they become available inside the template.
template '/etc/app/config.yml' do
source 'config.yml.erb'
variables(port: node['app']['port'], env: node.chef_environment)
notifies :restart, 'service[app]'
endfile manages content you define inline or leave as-is (permissions, ownership, or deletion). cookbook_file copies a static file straight from the cookbook to the node with no processing. template renders an ERB file with variables before writing it.
The rule of thumb: static file, use cookbook_file; dynamic content that depends on data, use template; managing an existing path's attributes, use file.
| Resource | Content source | Processing |
|---|---|---|
| file | Inline or none | None, manages the path itself |
| cookbook_file | Static file in the cookbook | Copied verbatim |
| template | ERB file in the cookbook | Rendered with variables |
metadata.rb declares a cookbook's identity and dependencies: its name, version, maintainer, supported platforms, and the other cookbooks it depends on. The Chef Infra Server reads it to resolve what a node needs.
Version constraints here drive dependency resolution, so keeping them accurate is what stops a node from pulling a broken or incompatible cookbook.
name 'webserver'
version '2.1.0'
maintainer 'Platform Team'
depends 'nginx', '~> 12.0'
supports 'ubuntu'Every cookbook can have a default.rb recipe, and it's the one Chef applies when you add the cookbook to a run-list by name only, recipe[webserver]. It's the entry point most cookbooks expose, often including other recipes in the right order.
To run a different recipe, name it explicitly: recipe[webserver::ssl]. The double-colon separates the cookbook from the recipe file inside it.
recipe[webserver] # runs webserver/recipes/default.rb
recipe[webserver::ssl] # runs webserver/recipes/ssl.rbA role groups a run-list and attributes under a name that describes a server's function, like webserver or database. Assign the role to many nodes and they all share that configuration, so you edit one role instead of every node.
Roles are convenient but blunt: they can't be versioned the way cookbooks can. Many teams have moved to policyfiles for stricter, versioned control, which is worth mentioning.
# a node's run-list can reference a role
role[webserver]
# the role expands into its own run-list and attributesEnvironments model the stages of your infrastructure, like development, staging, and production. Each can pin cookbook versions and set environment-specific attributes, so prod stays on tested versions while dev gets the latest.
A node belongs to exactly one environment. Version constraints in the environment are what keep an untested cookbook from reaching production nodes.
They wire resources together so one change triggers action in another. notifies sends a signal from the resource that changed; subscribes listens from the resource that wants to react. The classic case: a config template notifies a service to restart, but only if the template actually changed.
By default the action is delayed to the end of the run, which batches restarts. Add :immediately to act right away.
template '/etc/nginx/nginx.conf' do
source 'nginx.conf.erb'
notifies :restart, 'service[nginx]', :delayed
end
service 'nginx' do
action [:enable, :start]
endKey point: The 'only if it changed' detail is the point. Volunteering that shows you understand why notifications avoid needless restarts.
Guards are conditions that decide whether a resource runs. only_if runs the resource when the condition is true; not_if runs it when the condition is false. You pass a shell command or a Ruby block.
They're a common way to make custom actions idempotent: wrap an execute resource in a guard so the command only fires when it actually needs to.
execute 'create_db' do
command 'createdb myapp'
not_if 'psql -lqt | cut -d| -f1 | grep -qw myapp'
endexecute runs a raw shell command on the node. It's the escape hatch for one-off actions that no built-in resource covers, like reloading a kernel setting or running a vendor installer script. You give it a command string and, ideally, a guard.
Avoid leaning on it because raw commands aren't idempotent by default; they run every converge unless you add a guard. Prefer a purpose-built resource (package, service, file) when one exists, and reach for execute only when nothing else fits, always with a guard.
execute 'reload_sysctl' do
command 'sysctl -p'
action :nothing
subscribes :run, 'template[/etc/sysctl.conf]', :immediately
endChef Workstation is the toolkit you install on your local machine to develop and test cookbooks. It bundles knife, chef-run, Test Kitchen, InSpec, cookstyle, and the chef command into one package.
It replaced the older ChefDK. If an interviewer mentions ChefDK, noting that Chef Workstation is the current toolchain shows you're up to date.
A data bag is a global, JSON-structured store of information kept on the Chef Infra Server, indexed for search. Use it for data that isn't tied to one node: user lists, application credentials, shared settings.
Sensitive data belongs in an encrypted data bag, which encrypts item contents so they're not readable in plain text on the server or in transit.
# read a data bag item in a recipe
users = data_bag('admins')
users.each do |name|
admin = data_bag_item('admins', name)
user admin['id']
endConverging is the act of bringing a node into its desired state during a chef-client run. Chef compiles the recipes into an ordered resource collection, then executes each resource in turn, changing only the things that don't already match what the recipe declares.
A successful converge leaves the node matching its run-list. Because resources are idempotent, a second converge on an unchanged node reports zero updated resources.
For candidates with working experience: the run lifecycle, testing, attribute precedence, and the questions that separate users from understanders.
A run has a compile phase and a converge phase. In compile, Chef evaluates every recipe top to bottom as Ruby and builds the resource collection: an ordered list of resources. Nothing changes on the node yet. In converge, Chef walks that collection and executes each resource's action, applying changes.
This split explains a classic gotcha: plain Ruby code in a recipe runs during compile, before any resource acts, so it can't see state that a resource sets up later. Understanding compile-versus-converge timing is a strong intermediate signal.
How a chef-client run executes
Ruby that isn't inside a resource runs during compile, before any resource converges. That timing trips people up.
Key point: Drawing the compile-then-converge split, and naming what runs when, is what pushes this from a memorized answer to real understanding.
Watch a deeper explanation
Video: Chef Resources, Recipes, and Cookbooks (Study9, YouTube)
Attributes come from multiple sources (cookbook defaults, roles, environments, and node overrides) at precedence levels: default, force_default, normal, override, force_override, and automatic (ohai). Higher levels win when the same key is set in more than one place.
ohai's automatic attributes sit at the top and can't be overridden in recipes, which is deliberate: you shouldn't fake the node's real memory or platform. Knowing the order, and that automatic wins, is the intermediate-level detail.
| Precedence (low to high) | Typical source |
|---|---|
| default | Cookbook attributes, roles, environments |
| normal | Node object, set_unless persists across runs |
| override | Roles, environments, recipes |
| automatic | ohai, highest, cannot be overridden |
Test Kitchen provisions a throwaway instance (a VM or container), converges your cookbook on it, and runs verifiers to confirm the result, then destroys it. It's how you test a cookbook against a real OS before it touches production.
The workflow is create, converge, verify, destroy, driven by a .kitchen.yml that lists platforms and suites. Verifiers are usually InSpec controls checking that packages, services, and files ended up correct.
# kitchen.yml
driver:
name: dokken
provisioner:
name: chef_zero
platforms:
- name: ubuntu-22.04
suites:
- name: default
run_list:
- recipe[webserver::default]
verifier:
inspec_tests:
- test/integration/defaultInSpec is a testing and compliance framework that describes the state a system should be in as human-readable controls, then checks a machine against them. In the Chef workflow it's the verifier Test Kitchen runs after a converge.
Beyond cookbook testing, InSpec runs as an ongoing compliance scanner: you write controls for security baselines (CIS benchmarks, for example) and audit fleets against them.
describe package('nginx') do
it { should be_installed }
end
describe service('nginx') do
it { should be_enabled }
it { should be_running }
end
describe port(80) do
it { should be_listening }
endChefSpec is a unit-testing framework for cookbooks built on RSpec. It runs a recipe through the compile phase in memory, without converging a real node, and lets you assert that the expected resources were declared with the expected properties.
It's fast because nothing is actually installed; it simulates the run. Use ChefSpec for quick logic checks (did the recipe choose the right package for this platform) and Test Kitchen with InSpec for the real end-to-end verification.
describe 'webserver::default' do
platform 'ubuntu'
it 'installs nginx' do
expect(chef_run).to install_package('nginx')
end
endA custom resource lets you define your own resource type with properties and actions, so recipes can call it like a built-in. You declare the interface (properties) and the implementation (action blocks that use existing resources).
Write one when you repeat the same cluster of resources with small variations across recipes. It packages that pattern behind a clean, reusable, idempotent interface, which is far better than copy-pasting or overusing execute.
# resources/site.rb
property :domain, String, name_property: true
property :port, Integer, default: 80
action :create do
template "/etc/nginx/sites/#{new_resource.domain}" do
source 'site.conf.erb'
variables(domain: new_resource.domain, port: new_resource.port)
end
endKey point: Knowing custom resources replaced the old definitions and LWRPs, and being able to sketch one, marks you as current, not stuck on old Chef.
Because recipe code outside resources runs at compile time, a value that only exists after a resource acts isn't available when the resource is declared. Wrapping it in lazy {} defers evaluation to converge time, when the value exists.
The common case: a property that depends on a file created earlier in the same run. Without lazy, Chef reads it during compile and gets nothing; with lazy, it reads it during converge.
file '/tmp/report.txt' do
content lazy { File.read('/tmp/generated_at_converge.txt') }
endRaw execute commands run on every converge unless guarded, so they aren't idempotent, and they hide intent: reviewers can't see what The node should be in. That's the opposite of what configuration management is for.
The fix is to replace each execute with the purpose-built resource: package for installs, service for daemons, template or file for config, user and group for accounts. Where a command genuinely has no resource, keep execute but add only_if or not_if so it fires only when needed.
Dependencies are declared in metadata.rb with version constraints. The Chef Infra Server (or a policyfile) resolves the full graph so a node gets a compatible set. Berkshelf was the classic dependency manager; policyfiles now handle this with a lock file.
Version constraints matter: ~> 3.0 allows 3.x but not 4.0, which protects you from a breaking upstream change reaching nodes unreviewed.
A policyfile pins a node's entire configuration (its run-list and the exact version of every cookbook in the dependency graph) into a single lock file. You build it once, and every node using that policy gets identical, reproducible cookbook versions.
Teams adopted policyfiles because roles and environments couldn't lock the full dependency tree, so two nodes with the same role could still converge different cookbook versions. Policyfiles close that gap and make promotion between stages explicit.
| Roles + environments | Policyfiles | |
|---|---|---|
| Locks full dependency graph | No | Yes |
| Reproducible across nodes | Not guaranteed | Guaranteed by lock file |
| Promotion between stages | Version pins per environment | Explicit policy groups |
Berkshelf is a dependency manager for cookbooks. It reads a Berksfile, resolves the dependency graph, downloads the needed cookbooks (from the Supermarket or Git), and uploads them together, so you don't fetch each dependency by hand.
It solved the same problem policyfiles now address. Many older codebases still use it, so recognizing a Berksfile and explaining its role is useful, while noting policyfiles are the newer approach.
Supermarket is the public repository of community cookbooks. Instead of writing everything yourself, you pull a maintained cookbook (for nginx, mysql, or users) and either use it directly or wrap it.
The mature pattern is a wrapper cookbook: depend on the community cookbook, then override its attributes and add your own recipes in your wrapper, so you get upstream maintenance without forking.
A wrapper cookbook depends on another cookbook (usually a community one) and customizes it by setting attributes and calling its recipes with include_recipe, rather than modifying the original. Your changes live in your cookbook; the upstream stays pristine.
This keeps you on the upstream's update path. When the community cookbook releases a fix, you bump the dependency instead of re-applying a fork's patches.
# your wrapper recipe
node.default['nginx']['worker_processes'] = 4
include_recipe 'nginx::default'
# add your own resources on top
template '/etc/nginx/conf.d/app.conf' do
source 'app.conf.erb'
endinclude_recipe pulls another recipe's resources into the current run at the point it's called, as if written inline. It's how one recipe composes others, and how wrapper cookbooks call the recipe they wrap.
Chef guards against duplicates: including the same recipe twice runs it only once. That makes composition safe without tracking what's already been included.
Chef Vault encrypts a data bag item so that only specific nodes and users can decrypt it, using each node's public key. A plain encrypted data bag uses one shared secret that every node needs, which is hard to rotate and leaks widely.
With Chef Vault, you grant access per node, and rotating or revoking a node's access re-encrypts against the updated key list. It's the better answer when an interviewer pushes on how you'd manage secrets at scale.
Environments separate stages (dev, staging, prod) and pin versions per stage. Roles group a run-list and attributes by server function. Policyfiles pin a node's whole configuration into one versioned, reproducible lock.
In modern Chef, many teams use policyfiles instead of roles-plus-environments because policyfiles lock the full dependency graph and make promotion explicit. Knowing that both models exist, and why teams moved, is the answer the question needs.
chef-client typically runs on a schedule, commonly every 30 minutes, so nodes self-heal from drift. You configure it as a cron job, a systemd timer, or the built-in daemon interval, often set up during bootstrap.
Because runs are recurring and idempotent, an unchanged node converges to zero updated resources each time, and any manual drift gets corrected at the next run.
Chef stops at the failed resource. Everything before it in the resource collection already ran and stays applied; everything after it doesn't run. So the node can be left in a partial state, which is why order and idempotency matter.
The next converge re-runs the whole recipe. Idempotent resources skip what's already correct and retry the part that failed, so a fixed underlying issue usually resolves on the following run without manual cleanup.
Key point: The 'partial state, then re-converge' answer shows you understand the run model. Interviewers use this to see if you've debugged a real failure.
Search queries the Chef Infra Server's index for nodes, roles, or data bag items matching a query. Recipes use it to discover other nodes dynamically, like finding every app server so a load balancer config can list them.
It couples nodes through the server rather than hardcoding IPs. The trade-off to mention: search results depend on other nodes having reported in, so a fresh environment can return incomplete data.
app_servers = search(:node, 'role:app AND chef_environment:production')
template '/etc/haproxy/haproxy.cfg' do
source 'haproxy.cfg.erb'
variables(backends: app_servers)
endcookstyle is a linting tool for cookbooks, built on RuboCop with a set of Chef-specific rules layered on top. It flags style issues, deprecated patterns, and common mistakes, and it can autocorrect many of them automatically so you don't fix each one by hand.
It slots into CI so bad patterns get caught before review. Running it, along with ChefSpec and Test Kitchen, is the standard cookbook quality pipeline.
cookstyle . # lint the current cookbook
cookstyle -a . # autocorrect what it safely canadvanced rounds probe internals, design judgment, and production scars. Expect every answer here to draw a follow-up.
Match the tool to the job. Terraform provisions infrastructure (it creates the servers and cloud resources). Chef, Puppet, and Ansible configure what runs on those servers. So Terraform often pairs with one of the config tools rather than competing with them.
Among the config tools, Chef fits teams that want configuration as real code with full Ruby and accept an agent. Ansible fits teams wanting agentless push and simple YAML for quick onboarding. Puppet fits large fleets wanting declarative state enforcement with its own DSL. The honest coverage names team skills and existing tooling as deciding factors, not just features.
| Tool | Job | Model | Config language |
|---|---|---|---|
| Terraform | Provision infrastructure | Agentless, declarative | HCL |
| Chef | Configure servers | Agent, pull | Ruby DSL |
| Ansible | Configure servers | Agentless, push | YAML |
| Puppet | Configure servers | Agent, pull | Puppet DSL |
Key point: Leading with 'Terraform provisions, Chef configures' separates candidates who understand the categories from those who list features.
Watch a deeper explanation
Video: Chef vs Puppet vs Ansible vs Saltstack (Simplilearn, YouTube)
Say a recipe creates a config file with a template resource, then, in plain Ruby below it, reads that file to compute a value. The read happens during compile, before the template resource converges, so the file doesn't exist yet and the read fails or returns stale content.
The fix is either to defer the read with lazy so it happens at converge, or to restructure so the dependent logic lives inside a resource rather than bare Ruby. Being able to name this failure and both fixes is a production signal.
template '/etc/app.conf' do
source 'app.conf.erb'
end
# BUG: runs at compile, before the template exists
# value = File.read('/etc/app.conf')
# FIX: defer to converge
file '/tmp/copy.conf' do
content lazy { File.read('/etc/app.conf') }
endModern custom resources replaced both the old definitions and the light-weight resource/provider (LWRP) pattern. Definitions were just macros with no true resource semantics (no notifications, no why-run), and LWRPs had awkward separate resource and provider files.
Custom resources unify the model: one file, real properties, actions, notification support, and why-run compatibility. If a codebase still has definitions or LWRPs, migrating them is a common modernization task, and recognizing that dates your Chef knowledge correctly.
Built-in resources are idempotent for you. For anything custom (an execute block, a script, a shell-out) you make it idempotent yourself: check current state first and act only on a gap. Use guards (only_if, not_if), or inside a custom resource compare current_resource to new_resource before changing anything.
The senior habit is to describe idempotency as a property you design for, not a checkbox: every action should be safe to run on the thousandth converge, changing nothing if the state already matches.
action :create do
return if ::File.exist?('/opt/app/installed.flag')
# ... do the install ...
file '/opt/app/installed.flag' do
content 'done'
end
endEncrypted data bags work but share one secret across all nodes, which is painful to rotate. Chef Vault improves that by encrypting per node's public key, so access is granted and revoked per node. At larger scale, teams push secrets out of Chef entirely into an external manager (HashiCorp Vault, AWS Secrets Manager) and fetch them at converge.
The design point to state: secrets should never sit in cookbooks or version control, access should be revocable per node, and rotation shouldn't require touching every recipe. External managers meet all three, which is why they win at scale.
The server is a dependency for normal chef-client runs, so its availability matters. Options include a clustered or frontend-plus-backend server topology, load-balanced frontends over a replicated backend datastore, and regular backups of the server data.
A resilience point worth raising: because runs are scheduled and idempotent, a brief server outage doesn't break already-converged nodes; they just can't fetch new policy until it returns. For stricter decoupling, policyfiles plus local-mode or chef-zero runs reduce the hard dependency on a live server.
Immutable infrastructure replaces servers instead of mutating them, which is the opposite of Chef's converge-in-place model. But Chef still fits at image-build time: you use a cookbook to bake a golden AMI or container image, then deploy that image immutably.
So the pattern shifts from 'converge running nodes on a schedule' to 'converge once during the build, then never mutate'. Being able to place Chef in that pipeline, rather than claiming it's obsolete, is the mature take.
Read the stacktrace first: Chef writes a detailed failure report naming the resource, the action, and the error. Re-run with chef-client -l debug or use why-run mode to see intended changes without applying them. Reproduce locally with Test Kitchen on the same platform, and check what changed: a new cookbook version, an attribute, an upstream package.
Then fix at the right layer and confirm with a clean converge plus InSpec. The technical sequence (read the report, reproduce, isolate the change, fix, verify) matters more than any single flag.
Key point: The methodology is; the debug flags are supporting evidence.
why-run (chef-client --why-run) does a dry run of a converge: it walks the resource collection and reports what would change without actually changing the node. It's Chef's preview mechanism, so you can see the intended effect of a run before applying it to production.
It's useful but imperfect: resources that depend on earlier changes can report inaccurately, because in a dry run those earlier changes never happened. Treat it as a guide, not a guarantee, and say so if asked.
Lint with cookstyle, unit-test recipe logic with ChefSpec (fast, no real nodes), then integration-test with Test Kitchen converging on real platforms and verifying with InSpec. On green, upload the cookbook or build the policyfile, promote through environments or policy groups, and only then reach production.
The layering is the point: cheap fast checks first (lint, unit), expensive real-node checks next (Test Kitchen), promotion gated on both. Naming why each layer exists indicates production experience.
Cookbook CI pipeline
Cheap checks run first so failures surface fast; the slow real-node tests only run once the basics pass.
Trace the precedence. The same key can be set at default, normal, override, and automatic levels from cookbooks, roles, environments, and node data, and the highest level wins. A common surprise is ohai's automatic attributes overriding a value you set at default.
Diagnose with node['key'] inspection during a run, or knife node show to see the merged result and where values originate. The fix is to set the attribute at the correct precedence level, not to fight the merge with force_override, which usually hides the real problem.
The pull model helps: nodes converge independently on their own schedule, so load spreads over time rather than hitting all at once. The strain points are the server's search index and API under many simultaneous check-ins, and thundering-herd effects when nodes sync their schedules.
Mitigations include splaying run times so nodes don't converge in lockstep, caching, scaling the server's frontends and datastore, and reducing heavy search queries in recipes. Naming splay and search load shows you've run Chef at scale, not just on a laptop.
Handlers run at defined points in a chef-client run to react to results. Report handlers run at the end of every run to send metrics or notifications; exception handlers run when a run fails, to alert or capture diagnostics. Start handlers run at the beginning.
They're how you wire Chef into observability: post converge results to a dashboard, page on failures, or log run data. Custom handlers are plain Ruby classes registered in the client config.
Chef Automate is the dashboard and analytics layer over Chef Infra and InSpec. It aggregates converge results, compliance scan data, and node status into one view, with visibility into which nodes converged, which failed, and which fail compliance controls.
It answers the operational questions the raw server can't: fleet-wide convergence health and compliance posture over time. it matters.
Resources converge in the order they're added to the resource collection, which follows recipe order and run-list order. So a package declared before a service runs first. This deterministic ordering is what lets you rely on setup happening before use.
The subtlety: notifications can inject actions out of that linear order (delayed notifications fire at the end, immediate ones interrupt). And compile-time Ruby runs before any resource. Naming these exceptions to the simple 'top to bottom' story is the senior detail.
Treat it as a migration, not a rewrite overnight. Inventory what the cookbooks actually do, decide the target (Ansible, Terraform plus something, or baked images), and move in slices: convert one cookbook or one service class at a time, running old and new in parallel behind tests until the new path is trusted.
The judgment interviewers probe: don't big-bang it, keep InSpec controls as the shared verification across both systems so you can prove parity, and migrate the highest-value or highest-churn cookbooks first. The point is a safe, verifiable transition, not tool loyalty.
Clarify first: how many nodes, which platforms, what's shared versus role-specific, how secrets and environments work. Then structure it: a base cookbook for common setup (users, hardening, monitoring agent) on every node, role-specific cookbooks (webserver, database), attributes for per-environment values, and policyfiles to lock versions per stage.
Secrets go to Chef Vault or an external manager. Web servers discover database servers with search or explicit attributes. Close on operations: scheduled converges with splay, InSpec compliance controls, and a CI pipeline gating promotion. Structuring the answer (clarify, cookbook layout, data and secrets, discovery, operations) is what the question scores.
# policyfile-style run-lists
# every node
run_list 'base::default'
# web nodes
run_list 'base::default', 'webserver::default'
# db nodes
run_list 'base::default', 'database::default'Chef wins when you want infrastructure defined as real code with the full expressiveness of Ruby, and you can accept running an agent on every node. It's declarative at the resource level but lets you drop into procedural Ruby when logic gets involved, which suits teams that treat configuration as software. Where it costs more is the learning curve (you write Ruby, not YAML) and the pull-based agent model, which needs a client on each node and, in the classic setup, a Chef Infra Server. Ansible trades that away for agentless push over SSH and simpler YAML. Puppet is the closest peer: agent-based, declarative, but with its own DSL rather than Ruby. Terraform sits in a different lane: it provisions infrastructure (creates the servers) while Chef configures what runs on them. Saying these trade-offs out loud is itself an interview signal.
| Tool | Language | Model | Best at |
|---|---|---|---|
| Chef | Ruby DSL | Agent, pull, declarative resources | Config as real code, complex logic |
| Ansible | YAML | Agentless, push over SSH | Simple setup, quick onboarding |
| Puppet | Puppet DSL | Agent, pull, declarative | Large fleets, strong state enforcement |
| Terraform | HCL | Agentless, declarative provisioning | Creating cloud infrastructure, not config |
Prepare in layers, and practice out loud. Most Chef rounds move from concept questions to writing or reading a recipe to a design or debugging discussion, so rehearse each stage rather than only reading answers.
The typical Chef interview flow
Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These Chef questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.
See how the AI Coding Interviewer works