When working with Chef's notification system, a common requirement is to create resources that should only execute when triggered by notifications, not during the normal convergence phase. This pattern is particularly useful for:
- Post-configuration cleanup tasks
- Service restarts that should be deferred
- Composite resource patterns where execution should be conditional
There are three primary methods to achieve notification-only execution in Chef:
1. Using the :nothing Action
The most straightforward approach is to declare the resource with :nothing
action:
execute 'conditional_task' do
command '/usr/bin/do_something'
action :nothing
end
template '/etc/config.file' do
source 'config.erb'
notifies :run, 'execute[conditional_task]', :delayed
end
2. Guard Clause Alternative
While Chef doesn't have a native only_if :notified
guard, you can simulate it using Ruby logic:
execute 'conditional_task' do
command '/usr/bin/do_something'
only_if { node.run_state.key?('some_notification_flag') }
action :run
end
ruby_block 'set_notification_flag' do
block do
node.run_state['some_notification_flag'] = true
end
action :nothing
notifies :run, 'execute[conditional_task]', :immediately
end
3. Custom Resource Pattern
For complex scenarios, consider creating a custom resource:
property :notified, [TrueClass, FalseClass], default: false
action :run do
return unless new_resource.notified
converge_by("Executing notified resource #{new_resource.name}") do
# Your implementation here
end
end
- Use
:delayed
notifications for batched execution at the end of the Chef run - For immediate execution, use
:immediately
notifications - Be cautious with
:before
notifications as they can create execution-order dependencies - Consider using
subscribes
instead ofnotifies
for more decoupled designs
Here's a complete example of managing a service that should only restart when config changes:
template '/etc/nginx/nginx.conf' do
source 'nginx.conf.erb'
notifies :restart, 'service[nginx]', :delayed
end
service 'nginx' do
action [:enable, :start]
supports restart: true, reload: true
# This service will only restart when notified
restart_command '/usr/sbin/nginx -t && systemctl restart nginx'
action :nothing
end
In Chef's declarative model, resources typically execute during the converge phase when they're declared. However, there are valid scenarios where you want resources to execute only when notified by other resources, not during their initial declaration.
The most straightforward approach is using the :immediately
notifier with lazy evaluation:
service 'my_service' do
action :nothing
end
template '/etc/config.conf' do
source 'config.erb'
notifies :restart, 'service[my_service]', :immediately
end
For more complex scenarios where you need to check notification status, you can implement a custom helper:
def notified?(resource_name)
run_context.subscribers.map(&:resource).any? do |r|
r.name == resource_name
end
end
execute 'conditional_command' do
command '/bin/do_something'
only_if { notified?('execute[conditional_command]') }
end
Problem: Resources still executing despite action :nothing
Fix: Ensure no other notifiers or subscriptions are triggering the resource unintentionally
# Wrong - will still execute during converge
execute 'problematic' do
command '/bin/foo'
action :nothing
end.run_action(:run)
# Right - truly notification-only
execute 'correct' do
command '/bin/foo'
action :nothing
end
Here's a complete example showing notification-only execution in a configuration workflow:
package 'nginx' do
action :install
end
service 'nginx' do
action [:enable, :start]
end
# This will only run when notified
execute 'reload_nginx' do
command 'nginx -s reload'
action :nothing
end
# First config file triggers reload
template '/etc/nginx/nginx.conf' do
source 'nginx.conf.erb'
notifies :run, 'execute[reload_nginx]', :delayed
end
# Second config file also triggers same reload
template '/etc/nginx/conf.d/app.conf' do
source 'app.conf.erb'
notifies :run, 'execute[reload_nginx]', :delayed
end
Verify your notification-only behavior with ChefSpec:
require 'chefspec'
describe 'my_cookbook::default' do
let(:chef_run) { ChefSpec::SoloRunner.new.converge(described_recipe) }
it 'should not execute the command by default' do
expect(chef_run).not_to run_execute('reload_nginx')
end
it 'should execute when notified' do
expect(chef_run.template('/etc/nginx/nginx.conf')).to \
notify('execute[reload_nginx]').to(:run).delayed
end
end