How to Configure Chef Resources to Execute Only on Notifications


4 views

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 of notifies 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