Thursday, January 22, 2015

On Chef Recipes and the Two Phases of Convergence

In my Chef consulting, one error I've seen novices make is not keeping in mind when recipe code is evaluated.  Recipes are evaluated in two passes: the compile phase and the execute phase.  The compile phase prepares all the resources to be converged-- any ruby code that is not enclosed in a lazy block or ruby_block block will be evaluated at this time.  This includes variables for templates!  Anyway, the bad recipes follow this pattern:

1:  file '/tmp/my_file' do  
2:   content 'hello, world'  
3:  end  
4:    
5:  my_file_data = ::File.read('/tmp/my_file')  
6:    
7:  file '/tmp/other_file' do  
8:   content my_file_data  
9:  end  

A naive reading of this recipe would suggest that three things happen: a new file called "/tmp/my_file" will be created (if missing) containing the string "hello, world", the file will be read into the my_file_data variable, and another file "/tmp/other_file" will be created with the same content.

That's not what actually happens here.  In fact, this recipe won't converge at all unless /tmp/my_file has been previously created by something else.  Instead, you'll get an Errno::ENOENT.

The ::File.read() call is evaluated in the compile phase, but the file resources are created in the execute phase, which happens after the compile phase has completed! If you need to access something created in the execute phase, the accessing code also needs to evaluate in the execute phase, or you can push the file resources into the compile phase. Moving resource convergence into the compile phase isn't ideal, though, so I generally advise the former over the latter.

Here's one way to make this recipe converge:

1:  file '/tmp/my_file' do  
2:   content 'hello, world'  
3:  end  
4:    
5:  my_file_data = nil  
6:  ruby_block 'read_file' do   
7:   block { my_file_data = ::File.read('/tmp/my_file') }  
8:  end  
9:    
10:  file '/tmp/other_file' do  
11:   content lazy { my_file_data }  
12:  end  
Three things are changed here.

  1.  The my_file_data variable is declared in the compile phase to make it scoped appropriately for the /tmp/other_file file resource. 
  2.  The reading of that file is put into a ruby_block resource to make it happen during the execute phase.
  3. The content of the /tmp/other_file resource is enclosed in a lazy block to delay evaluation of the my_file_data variable until the execute phase.
Keep in mind when your code is being evaluated when you write your recipes.