When working with Chef's declarative resource model, there's a common misconception about how loops interact with resource evaluation. The core issue in the original code stems from how Chef compiles resources during the "compile phase" before executing them in the "converge phase".
The problem manifests because all bash "create_user"
resources end up with the same name ("create_user"), causing Chef to treat them as the same resource. During execution, Chef only sees the last evaluated version of this resource.
# This WON'T work properly - all resources have same name
node[:users].each do |user|
bash "create_user" do # ← Problem: identical resource names
# ...
end
end
For dynamic resource creation in loops, you must ensure each resource has a unique name. Here's the proper way to implement user creation in a loop:
node[:users].each do |user|
username = user['username']
bash "create_user_#{username}" do # ← Unique name per iteration
user "root"
code "useradd #{username}"
not_if "grep -q #{username} /etc/passwd"
end
end
The fixed version includes several important optimizations:
- Unique resource names using string interpolation
- Simplified grep check with
-q
flag - Removed unnecessary code blocks (using strings directly)
- More efficient passwd file checking
For production systems, consider using Chef's built-in user
resource instead of bash commands:
node[:users].each do |user|
user user['username'] do
action :create
system true
shell '/bin/bash'
# Additional user attributes...
end
end
To verify your recipe works as expected:
# Check convergence output
sudo chef-client -z recipe.rb
# Verify users were created
getent passwd | grep -E 'testA|testB|testC|testD|testE'
- Never reuse resource names in loops
- Avoid complex bash commands when native resources exist
- Remember Chef's two-phase execution (compile vs converge)
- Test not_if/only_if guards carefully
For larger deployments, consider using data bags instead of node attributes:
users = data_bag('users')
users.each do |id|
user = data_bag_item('users', id)
user user['username'] do
uid user['uid']
gid user['gid']
home "/home/#{user['username']}"
manage_home true
end
end
When working with Chef (particularly Chef Solo), many developers encounter issues when attempting to loop through arrays to create multiple resources. The key misunderstanding lies in how Chef evaluates Ruby code during the compile phase versus the converge phase.
Here's what typically doesn't work:
node[:users].each do |user|
bash "create_user" do
code "useradd #{user['username']}"
not_if "grep #{user['username']} /etc/passwd"
end
end
The fundamental issue is that all these blocks are creating resources with the same name ("create_user"), causing Chef to evaluate only the last iteration. Additionally, the lazy evaluation means the user variable isn't properly captured in each iteration.
The proper solution is to use Chef's resources
block to dynamically create uniquely named resources:
node[:users].each do |user|
username = user['username']
bash "create_user_#{username}" do
code "useradd #{username}"
not_if "grep #{username} /etc/passwd"
end
end
For real-world usage, you'd want to include more user attributes:
node['users'].each do |username, user_data|
user username do
comment user_data['comment'] || username
uid user_data['uid']
gid user_data['gid'] || username
home user_data['home'] || "/home/#{username}"
shell user_data['shell'] || '/bin/bash'
manage_home true
action :create
end
end
Instead of bash commands, we should use Chef's built-in user resource:
node['users'].each do |user|
user_account user['username'] do
comment user['full_name']
ssh_key user['ssh_key']
end
end
- Always use unique resource names in loops
- Verify attributes exist before accessing them
- Use Chef's native resources when available
- Test with
chef-client -z --why-run
first