Infrastructure as code using AWS Cloudformation and Chef : Chef

By Parikshit Agnihotry

Jul 25, 2017 . 4 min read

This is third post in a 3 part series.

  1. Introduction: High level set up
  2. Cloudformation: Setting up Cloudformation
  3. Chef: Automating using Chef

This post discusses how to use chef-solo to provision instances at a basic level. Once you have automated instance spin up using cloudformation, you could add in chef to configure the softwares, execute scripts, install code, configurations and start your software. We will be setting up chef-solo which does not need a chef server to run.

Install Chef

To install chef, we used the script: https://www.chef.io/chef/install.sh which downloads and installs chef. This script in version as optional argument, else installs the latest version. In production, it is recommended you specify the version you would like to be installed.

usage: install.sh [-P project] [-c release_channel] [-v version] [-f filename | -d download_dir]

solo.rb

This file specifies config parameters for chef.

Sample solo.rb –

    cookbook_path [
      '/var/chef/cookbooks'
    ]
    environment 'example'
    environment_path '/var/chef/environments'
    file_backup_path '/var/chef/backup'
    file_cache_path '/var/chef/cache'

Roles

A chef role can be defined in json (or Ruby DSL). By default, chef looks for roles in /var/chef/roles, you can override that in solo.rb as role_path.

    
    {
      "name": "base_node_mongo_nginx",
      "description": "This role sets up web server with node and mysql",
      "chef_type": "role",
      "MONGO":{
        "URL":"127.0.0.1",
        "PORT":"27017",
        "DBNAME":"testdb_staging"
      },
      "run_list": [
        "recipe[init_config]",
        "recipe[install_node]", //install nodejs and npm rpm
        "recipe[install_mongo]", //install mongo rpm
        "recipe[install_nginx]", //install nginx and copy right config
      ]
    }

This is one of our sample roles, this has variables declared in “MONGO” object, these can be set up in the role and referenced in the recipes specified in the run_list.

This allows developers to write one recipe and configure it accordingly for different roles. For instance – staging, production, development can use same recipe and different roles.

A role can also specify another role in run_list. For example:

    
    "run_list": [
      "role[base_node_mongo_nginx]",
      "recipe[install_app]" //install application, run npm install, and start server
    ]
    

This would first execute the role base and then recipe finalize. This can be very useful in certain use cases. For instance, we have one base_node role that installs node js and configures it on centos 6.5, we could use that as a starter role and “inherit” a role from it only doing application specific set up there.

For variables, you can define default values in the base role and override them in the inherited role.

Cookbooks & Recipes

Chef recipes are ruby files that are a component of cookbooks. There are a lot of details to Cookbooks, but to keep things simple we will discuss writing simple recipes with some resources. A much more detailed documentation can be found here:

template vs cookbook_file

Initially, when you are setting up your chef recipe you might end up just using cookbook_file which simply copies the file over to a location, but as you start parameterizing contents of the file with variables you will need to replace cookbook_file with template. For example, you might want 2 nginx configurations on different virtual hosts. A very common use case. Here, instead of having separate file, you can make one config and customize the contents with variables.

role:
    {
      "name": "staging",
      "description": "This role sets up nginx config",
      "chef_type": "role",
      "ENV":"staging",
      "SERVER":{
        "DOCUMENT_ROOT":"/var/www/public",
        "SERVER_NAME":"vhost.domain.com"
      },
      "run_list": [
        "recipe[install_nginx]"
      ]
    }
    
recipe – install_nginx:
    template "/etc/nginx/conf.d/nginx.#{node['ENV']}.conf" do
      source "nginx.template.conf"
      mode 0755
      variables({
        :DOCUMENT_ROOT => node['SERVER']['DOCUMENT_ROOT'],
        :SERVER_NAME => node['SERVER']['SERVER_NAME'],
      })
      notifies :restart, 'service[nginx]', :immediately
    end
nginx.template.conf:
    server {
    
      listen 80;
      listen [::]:80;
    
      server_name <%= @SERVER_NAME %>;
    
      location / {
        root <%= @DOCUMENT_ROOT %>;
      }
    }
    

This is a template resource that does following:

  1. takes nginx.template.conf
  2. replaces variables DOCUMENT_ROOT, SERVER_NAME and SERVER_PORT
  3. creates the file to /etc/nginx/conf.d/nginx.staging.conf
  4. sets the mode to 755
  5. restarts service nginx immediately after doing all the above.

Folder set up

Here is a sample folder set up, keeps the recipe and files/templates associated with it together

    
    
    chef-solo
    --cookbooks -> folder that contains all the recip[e folders
    --↳---install_nginx -> folder that contains all the files for a recipe
    -------↳--recipes -> folder for recipe
    ----------↳--default.rb -> the recipe with resource blocks
    -------↳--templates -> folder that contains all templates
    ----------↳--nginx.template.conf -> template
    -------↳--metadata.rb -> name and description

Executing Chef

Using chef-solo, you can pass in the solo.rb and the role json to execute it.

chef-solo -c /var/chef/solo.rb -j /var/chef/roles/production.json

Official chef documentation: