Export Chef Attributes for InSpec Usage

From Bonus Bits
Jump to: navigation, search

Purpose

This article gives the steps to export Chef node attributes to a JSON file as part of your cookbook. Then parse that node attributes JSON into an InSpec Profile. This is also using Test Kitchen to test. Meaning Inspec is ran by Test Kitchen


Environment

  • ChefDK 1.6.11
  • MacOS 10.13.5


Export Attributes

First, we need to add a little code to our cookbook to export all of the node attributes. The goal is to export the node hash of any or all the cookbooks we are using that have information that is important for conditioning or simply testing with Inspec tests.

Example 1

ruby_block "Save node attributes" do
  block do
    File.write("/tmp/.node_attributes.json", node.to_json)
  end
end

Example 2

file 'export node attributes' do
  path '/tmp/.node_attributes.json'
  backup false
  content(
    JSON.pretty_generate(node)
  )
  mode '0775'
  sensitive true
end

Example 3 (Selective)

Maybe you don't want all the cookbooks attributes. In this example, it cherry picks to cookbook attributes by selecting a top-level key in the node hash. Which generally is the name of the cookbook.

file 'export node attributes' do
  path '/tmp/.node_attributes.json'
  backup false
  content(
    JSON.pretty_generate(
      node.select { |k| %w(bonusbits_base nginx).include?(k) }
    )
  )
  mode '0775'
  sensitive true
end

Example 4

I found with Chef 13.x some values did not output to the select. Here is another way that seems to work better.

node_bonusbits_base = node['bonusbits_base'].to_hash
node_bonusbits_mediawiki_nginx = node['bonusbits_mediawiki_nginx'].to_hash

file 'node attributes to json' do
  path '/etc/chef/.chef-attributes.json'
  backup false
  content(
    Chef::JSONCompat.to_json_pretty(
    { 'bonusbits_base' => node_bonusbits_base, 'bonusbits_mediawiki_nginx' => node_bonusbits_mediawiki_nginx }
    )
  )
  mode '0775'
  sensitive true
end

Example 5

The above

Icon-Tip-Square-Green.png Beware that if you are storing any secrets in your cookbook node hash they will be in the resulting JSON. It is best practice to store secrets in the node_runstate[] hash and not the node hash.


Import to InSpec

Here is an easy example to import the JSON file using a helper method in an Inspec profile.

def node_attributes
  json('/tmp/.node_attributes.json').params
end

Gnome-sticky-notes-applet You can not use standard Ruby syntax i.e. JSON.parse(IO.read('/tmp/.node_attributes.json')). This is because that code will run on your local system and not the remote node you are testing and where the file should have been created by the cookbook.


Use Attributes

Now lets call our method to parse the JSON into a hash we want a value from. What I do is create other methods to call the node_attributes method that parse and give me a value or key/values back. Because if we call the node_attributes method from a test it will spam the test output will the entire node hash.

  1. Create a node_attributes.rb in the helpers folder
  2. Add our method to parse the .node_attributes.json file plus any methods you want to return key/values
    def node_attributes
      json('/tmp/.node_attributes.json').params
    end
    
    def node_aws_values
      node_attributes['bonusbits_base']['aws']
    end
    
    def node_env?(*env)
      my_env = node_attributes['bonusbits_base']['deployment_environment']
      env.include?(my_env)
    end
    
  3. In a control (test) be sure to set include relative path to the node_attributes methods file
    require_relative '../helpers/node_attributes'
    
  4. We can now call our parser methods to give us values for Inspec attributes (so they can be overroad if needed). for local variables or to answer conditions.

Example Control

require_relative '../helpers/node_attributes'

control 'bonusbits_base_cloudwatch_logs' do
  impact 1.0
  title 'Cloudwatch Logs'
  if (os[:family] == 'amazon') && (node_env?('prod'))
    describe package('awslogs') do
      it { should be_installed }
    end

    describe service('awslogs') do
      it { should be_installed }
      it { should be_enabled }
      it { should be_running }
    end
  end
end


Test Folder Structure Example

.
├── test
   └── inspec
       └── base
               ├── CHANGELOG.md
               ├── controls
               │   └── aws.rb
               │   ├── cloudwatch_logs.rb
               │   ├── epel.rb
               │   ├── node_info.rb
               │   ├── packages.rb
               │   ├── proxy.rb
               │   ├── selinux.rb
               │   ├── sudoers.rb
               │   └── yum_cron.rb
               ├── helpers
               │   └── node_attributes.rb
               └── inspec.yml


Sources