About Unified Mode
Availability
Unified Mode (unified_mode true
) is the default behavior starting in Chef Infra Client 18 (April 2022).
See the following table for Chef Infra Client versions where Unified Mode can be enabled in custom resources:
Chef Infra Client | Unified Mode |
---|---|
18.x (2022) | Default: unified_mode true |
17.x (2021) | Default: unified_mode false |
16.x (2020) | Default: unified_mode false |
15.3 and higher | Default: unified_mode false |
15.0-15.2 | Not available |
14.14-14.15 | Default: unified_mode false |
Lower than 14.14 | Not available |
Enable Unified Mode
Unified Mode is enabled by default starting in Chef Infra Client 18.
In Chef Infra Client 17 (April 2021) and some earlier versions, you can enable Unified Mode in custom resources by adding unified_mode true
. You can upgrade most custom resources to use Unified Mode without additional work other than testing and validation. See the following example:
# enable unified mode
unified_mode true
provides :myresource
actions :run do
[...]
end
Unified Mode isolation
If a Unified Mode resource calls a non-Unified Mode resource, the called resource isn’t executed in Unified Mode. Each resource maintains its own state whether it’s in Unified Mode or not. You don’t need to modify a custom resource that calls a Unified Mode resource since the calling context won’t affect the resource’s execution. Resources using Unified Mode may call resources not using Unified Mode and vice versa.
Benefits of Unified Mode
Single-pass execution
In Unified Mode, the Chef Infra Language executes from top to bottom, eliminating the compile and converge phases.
With the deferred execution of resources to converge time, the user has to understand many different details of the Ruby parser to understand what constructs relate to Chef Infra resources and what constructs are parts of the core Ruby language to determine when those expression are executed. All that complexity is removed in Unified Mode.
Elimination of lazy blocks
Several aspects of the Chef Infra Language still work but are no longer necessary in Unified Mode. Unified Mode eliminates the need for lazy blocks and the need to lazy Ruby code through a Ruby block.
Rescue blocks and other Ruby constructs work correctly
In Unified Mode, it’s now easy to write a rescue wrapper around a Chef Infra resource:
begin
execute "a command that fails" do
command "/bin/false"
end
rescue Mixlib::ShellOut::ShellCommandFailed => e
raise "failing because /bin/false returned a failed exit status"
end
Examples
Basic example
A simple motivating example is to have a resource that downloads a JSON message using the remote_file resource, parse the JSON using the ruby_block, and then render a value into a file or template resource.
Without Unified Mode, correctly writing this simple resource is complicated:
provides :downloader
action :doit do
remote_file "/tmp/users.json" do
source "https://jsonplaceholder.typicode.com/users"
end
array = nil
ruby_block "convert" do
block do
array = FFI_Yajl::Parser.parse(IO.read("/tmp/users.json"))
end
end
file "/tmp/phone" do
content lazy { array.last["phone"] }
end
end
Since the remote_file and file resources execute at converge time, the Ruby code to parse the JSON needs to be wrapped in a ruby_block
resource, the local variable then needs to be declared outside of that scope (requiring a deep knowledge of Ruby variable scoping rules), and then the content rendered into the file resource must be wrapped with lazy
since the Ruby parses all arguments of properties at compile time instead of converge time.
Unified Mode simplifies this resource:
unified_mode true
provides :downloader
action :doit do
remote_file "/tmp/users.json" do
source "https://jsonplaceholder.typicode.com/users"
end
array = FFI_Yajl::Parser.parse(IO.read("/tmp/users.json"))
file "/tmp/phone" do
content array.last["phone"]
end
end
Unified Mode eliminates the need for the ruby_block
resource, the lazy
evaluation, and the variable declaration, simplifying how the cookbook is authored.
Recovery and exception handling
Another advantage is in error recovery and the use of rescue.
unified_mode true
provides :redis_install
action :install do
version = "5.0.5"
# the downloading of this file acts as a guard for all the later
# resources -- but if the download is successful while the later
# resources fail for some transient issue, will won't redownload on
# the next run -- we lose our edge trigger.
#
remote_file "/tmp/redis-#{version}.tar.gz" do
source "http://download.redis.io/releases/redis-#{version}.tar.gz"
notifies :run, "execute[unzip_redis_archive]", :immediately
end
begin
execute "unzip_redis_archive" do
command "tar xzf redis-#{version}.tar.gz"
cwd "/tmp"
action :nothing
notifies :run, "execute[build_and_install_redis]", :immediately
end
execute "build_and_install_redis" do
command 'make && make install'
cwd "/tmp/redis-#{version}"
action :nothing
end
rescue Exception
file "/tmp/redis-#{version}.tar.gz" do
action :delete
end
raise
end
end
This simplified example shows how to trap exceptions from resources using normal Ruby syntax and to clean up the resource. Without Unified Mode, this syntax is impossible. Normally when the execute resources are parsed, they only create the objects in the resource_collection
to later be evaluated so that no exception is thrown while Ruby is parsing the action
block. Every action is delayed to the later converge phase. In Unified Mode, the resource runs when Ruby is done parsing its block, so exceptions happen in-line with Ruby parsing and the rescue clause now works as expected.
This is useful because the TAR extraction throws an exception (for example, the node could be out of disk space), which deletes the TAR file. The next time Chef Infra Client runs, the TAR file will be redownload. If the resource didn’t have file cleanup after an exception, the TAR file would remain on the client node even though the resource isn’t complete and the extraction didn’t happen, leaving the resource in a broken, indeterminate state.
Actions on Later Resources
Since Unified Mode executes your resource as it’s compiled, :immediate
notifications that execute later resources are handled differently than in the past.
:immediate
Notifications to Later Resources
Unified mode delays immediate notifications to later resources. In unified mode, the Chef Infra Client saves immediate notifications and executes them when the later resource is parsed. Immediate notifications to prior resources and delayed notifications behave the same as they did before unified mode.
The result of sequentially chaining immediate notifications is the same as before unified mode. Instead of immediately notifying results, the notifications fire in order as they’re parsed, which has the same outcome. If the parse order and the intended execution order are different, then the results may be different and are a reflection of the parse order.
The changes to sending immediate notification could result in subtle changes to behaviors in some resources, but it’s not a breaking change to common patterns of writing resources.
Chaining immediate notifications to later resources:
remote_file "#{Chef::Config[:file_cache_path]}/myservice.tgz" do
source "http://acme.com/myservice.tgz"
notifies :extract, "archive_file[myservice.tgz]", :immediately
end
archive_file "#{Chef::Config[:file_cache_path]}/myservice.tgz" do
destination '/srv/myservice'
notifies :start, "service[myservice]", :immediately
action :nothing
end
service "myservice" do
action :nothing
end
:before
Notifications to Later Resources
In unified mode, you must declare a resource before sending a before
notification to it.
Resources that subscribe to a before
notification to a later resource must be declared after the resource that triggers the notification.
This resource declares a before
notification to a later resource and will no longer work:
package "myservice" do
notifies :stop, "service[myservice]", :before
notifies :start, "service[myservice]", :immediately
end
service "myservice" do
action :nothing
end
Instead, declare the resource and then declare actions. For example:
service "myservice" do
action :nothing
end
package "myservice" do
notifies :stop, "service[myservice]", :before
notifies :start, "service[myservice]", :immediately
end
Out of Order Execution
Unified mode breaks custom resources that rely on the out-of-order execution of compile-time statements. Move any affected compile-time statements to the location in the code where they’re intended to execute.
Out-of-order execution is rare. Internally at Chef, none of our custom resources broke during our migration to unified mode. Instead, we discovered a few cases in which custom resource code was intended to run in order, but Chef Infra Client executed it out of order. In these cases, Unified Mode fixed errors instead of introducing bugs.
Notifications and accumulators
The accumulator pattern works unchanged. Notifications to the :root
run context still behave identically. Since the compile and converge phases of custom resources both fire in the converge time (typically) of the enclosing run_context
, the effect of eliminating the separate compile and converge phases of the custom resource has no visible effect from the outer context.
Troubleshooting Unified Mode
Unified mode changes the execution of a custom resource to run in one phase, in the order that the code is written, from the first line of the code to the last. Custom resources designed to use two phases may need modification. These fall into three general types:
- Resources with changes to internal sub-resources
- Resources with actions on later resources
- Resources that rely on the out-of-order execution
When designing a custom resource for unified mode:
- Declare a resource first and then declare actions on it
- Write resources in run-time order
Resources with changes to internal sub-resources
Some custom resources are designed to create and edit other sub-resources as part of the resource declaration. In unified mode, Chef Infra Client parses a resource code block that creates or edits a sub-resource and immediately tries to apply that change, even though the sub-resource doesn’t yet exist. This results in the execution of an incomplete resource.
For example, with Unified Mode enabled, this code from the dhcp cookbook is designed to create and edit a shared dhcp_subnet
resource, but it won’t work as expected:
# 'edit_resource' results in an incomplete subresource
sr = edit_resource(:dhcp_subnet, "#{new_resource.name}_sharedsubnet_#{subnet}") do
owner new_resource.owner
group new_resource.group
mode new_resource.mode
ip_version new_resource.ip_version
conf_dir new_resource.conf_dir
shared_network true
end
properties.each do |property, value|
sr.send(property, value)
end
To correct custom resources that change sub-resources during their declaration, you can:
- Apply properties in the code block (preferred)
- Run the resource explicitly (not preferred)
Apply properties in the code block
This pattern declares the sub-resource in one code block and then changes it in the next code block. This is the preferred pattern in Unified Mode because all resources execute in order at compile time.
dhcp_subnet "#{new_resource.name}_sharedsubnet_#{subnet}" do
owner new_resource.owner
group new_resource.group
mode new_resource.mode
ip_version new_resource.ip_version
conf_dir new_resource.conf_dir
shared_network true
properties.each do |property, value|
send(property, value)
end
end
Run the resource explicitly
Another solution is to continue saving the resource as a variable, declare action :nothing
within the codeblock, and then explicitly run the action in another code block.
The pattern of saving a resource as a variable and then forcing it to run at compile time with an explicit run_action
works as it has in the past, but it’s not a preferred pattern. Unified mode forces resource execution to compile time by default, which makes this pattern redundant.
sr = edit_resource(:dhcp_subnet, "#{new_resource.name}_sharedsubnet_#{subnet}") do
owner new_resource.owner
group new_resource.group
mode new_resource.mode
ip_version new_resource.ip_version
conf_dir new_resource.conf_dir
shared_network true
action :nothing
end
properties.each do |property, value|
sr.send(property, value)
end
# Run the action explicitly
sr.run_action(:create)