Chef Workshop

Outline

Let's use Chef to make a site that does stuff

Slides

Pre-Requisites

Vagrant

$ mkdir chef-workshop
$ cd chef-workshop
$ vagrant init chef/centos-6.6
A `Vagrantfile` has been placed in this directory...

Vagrant (cont.)

./Vagrantfile

Vagrant.configure(2) do |config|
  config.vm.box = "chef/centos-6.6"
  config.vm.network "forwarded_port", guest: 8080, host: 8080
end

Vagrant (cont.)

$ vagrant up
Bringing machine 'default' up with 'virtualbox'
provider...
$ vagrant ssh
Last login: Thu Jan 15 16:32:07 2015 from 10.0.2.2
[vagrant@localhost ~]$

Directory Skeleton

Install Knife Solo:

chef exec gem install knife-solo

Directory Skeleton (cont.)

Create the directory skeleton:

knife solo init .

Directory Skeleton (cont.)

./.chef
./.chef/knife.rb
./.gitignore
./Berksfile
./cookbooks
./data_bags
./environments
./nodes
./roles
./site-cookbooks

A Cookbook Appears

knife cookbook create animal-www -o site-cookbooks/

A Cookbook Appears (cont.)

./attributes
./definitions
./files/default
./libraries
./metadata.rb
./providers
./README.md
./recipes/default.rb
./resources
./templates/default

An Awesome Site

./www/index.html

<!DOCTYPE html>
<html>
<head><title>Animals R Fun</title></head>
<body>
  <p>Can I haz animals?</p>
  <ul><li>Aardvarks</li><li>Yeti Crab</li></ul>
</body>
</html>

An Awesome Site (cont.)

./site-cookbooks/animal-www/recipes/www.rb

link '/var/www' do
  to '/vagrant/www'
end

Running A Recipe

./Vagrantfile

Vagrant.configure(2) do |config|
  config.vm.box = "chef/centos-6.6"
  config.vm.network "forwarded_port", guest: 8080, host: 8080
  
  config.vm.provision "chef_solo" do |chef|
    chef.cookbooks_path = ["site-cookbooks", "cookbooks"]
    chef.run_list = ["recipe[animal-www::www]"]
  end
end

Running A Recipe (cont.)

Let's run Chef

$ vagrant provision
... Running provisioner: chef_solo...
...
... Run List is [recipe[animal-www::www]]
... Run List expands to [animal-www::www]
...
... link[/var/www] created

Running A Recipe (cont.)

Seeing if it worked

$ vagrant ssh
Last login: Tue Mar 17 14:39:00 2015 from 10.0.2.2
[vagrant@localhost ~]$ cat /var/www/index.html
...file contents...

The Plan

The Librarian

./Gemfile

source 'https://rubygems.org'

gem 'knife-solo'
gem 'librarian-chef'

The Librarian (cont.)

Install Librarian-Chef

$ chef exec bundle install --no-deployment
Fetching gem metadata...
...

The Librarian (cont.)

Create the Cheffile:

$ chef exec librarian-chef init
      create Cheffile
      

The Librarian (cont.)

./Cheffile

#!/usr/bin/env ruby
site 'https://supermarket.getchef.com/api/v1'

cookbook 'nginx'

The Librarian (cont.)

WINDOW ONLY — Set SSL_CERT_FILE

PS > [Environment]::SetEnvironmentVariable(
  "SSL_CERT_FILE",
  "C:\opscode\chefdk\embedded\ssl\certs\cacert.pem",
  "User")
  
  

The Librarian (cont.)

Install cookbook dependencies:

$ chef exec librarian-chef install
Installing apt (2.6.1)
Installing rsyslog (1.15.0)
[...]
Installing yum-epel (0.6.0)
Installing runit (1.5.18)
Installing nginx (2.7.4)

The Librarian (cont.)

./site-cookbooks/animal-www/metadata.rb

name             'animal-www'
maintainer       'YOUR_COMPANY_NAME'
maintainer_email 'YOUR_EMAIL'
license          'All rights reserved'
...
version          '0.1.0'

depends 'nginx'

Nginx Configuration

./site-cookbooks/animal-www/recipes/www.rb

[...cookbook_file stuff...]

include_recipe 'nginx'

template '/etc/nginx/sites-available/animals' do
  variables({
    :root => '/var/www',
    :port => 8080
  })
  notifies :reload, 'service[nginx]', :delayed
end

nginx_site 'animals' do
  enable true
end

Nginx Configuration (cont.)

./site-cookbooks/animal-wwww/templates/default/animals.erb

server {
  listen <%= @port %>;
  location / {
    root <%= @root %>;
    index index.html index.htm;
    sendfile off;
  }
}

Test It Out

$ vagrant provision
==> default: Running provisioner: chef_solo...
==> default: Detected Chef (latest) is already installed
Generating chef JSON and uploading...
==> default: Running chef-solo...
...

Then load http://localhost:8080/

Let's Make It Hit a REST Endpoint

Set Up Angular.js

First install Bower

$ sudo npm install -g bower
...
$ cd www
$ mkdir bower_components
$ bower install angular
...
$ cd ..

Set Up Angular.js (cont.)

./www/index.html

<!DOCTYPE html>
<html ng-app="animals">
<head>
  <title>Animals R Fun</title>
  <script type="text/javascript"
    src="bower_components/angular/angular.js"></script>
  <script type="text/javascript" src="main.js"></script>
</head>
<body ng-controller="AnimalsCtrl as ctrl">
  <p>Can I haz animals?</p>
  <ul><li>Aardvarks</li><li>Yeti Crab</li></ul>
  <p>More plz: {{ctrl.random}}</p>
</body>
</html>

Set Up Angular.js (cont.)

./www/main.js

angular.module('animals', [])
.controller('AnimalsCtrl', function ($http) {
  var self = this;
  $http.get('/api/random').success(function (data) {
    self.random = data;
  });
});

Set Up API

./site-cookbooks/animal-wwww/templates/default/animals.erb

server {
  listen <%= @port %>;
  location / {
    root <%= @root %>;
    index index.html index.htm;
    sendfile off;
  }
  location /api/ {
    proxy_pass http://localhost:3000/;
  }
}

Set Up API (cont.)

./api/app.js

var app = require('express')();
var as = ['Axolotl', 'Narwhal', 'Sloth'];
app.get('/random', function (req, res) {
  var r = Math.floor(Math.random()*100);
  res.send(as[r % as.length]);
});
app.listen(3000);

Set Up API (cont.)

Install Express from the host:

$ cd api
$ mkdir node_modules
$ npm install express
express@4.12.2 node_modules\express
...
$ cd ..

Set Up API (cont.)

./site-cookbooks/animal-www/recipes/api.rb

include_recipe 'nodejs'

link '/var/api' do
  to '/vagrant/api'
end

Set Up API (cont.)

./site-cookbooks/animal-www/metadata.rb

name             'animal-www'
maintainer       'YOUR_COMPANY_NAME'
maintainer_email 'YOUR_EMAIL'
license          'All rights reserved'
...
version          '0.1.0'

depends 'nginx'
depends 'nodejs'

Set Up API (cont.)

./Cheffile

#!/usr/bin/env ruby
site 'https://supermarket.getchef.com/api/v1'

cookbook 'nginx'
cookbook 'nodejs'

Set Up API (cont.)

Update cookbook dependencies:

$ chef exec librarian-chef install
[...]
Installing nginx (2.7.4)
Installing nodejs (2.2.0)

Set Up API (cont.)

./Vagrantfile

Vagrant.configure(2) do |config|
  config.vm.box = "chef/centos-6.6"
  config.vm.network "forwarded_port", guest: 8080, host: 8080
  
  config.vm.provision "chef_solo" do |chef|
    chef.cookbooks_path = ["site-cookbooks", "cookbooks"]
    chef.run_list = ["animal-www::api", "animal-www::www"]
  end
end

Set Up API (cont.)

Time to run Chef again

$ vagrant provision
... Running provisioner: chef_solo...
...
... Run List expands to [animal-www::www, animal-www::api]
...

Test The API

Start Express manually:

$ vagrant ssh
Last login: Tue Mar 17 21:33:51 2015 from 10.0.2.2
[vagrant@localhost ~]$ node /var/api/app.js

Then load http://localhost:8080/

Let's Run Express As a Service

Runit Service

Append to: ./site-cookbooks/animal-www/recipes/api.rb

# ...existing lines...

user 'api' do
  system true
  shell '/sbin/nologin'
end

include_recipe 'runit'
runit_service 'api' do
  default_logger true
  options({
    :user => 'api',
    :path => '/var/api/app.js'
  })
end

Runit Service (cont.)

./site-cookbooks/animal-www/templates/default/sv-api-run.erb

#!/bin/sh
exec 2>&1
exec chpst -u <%= @options[:user] %> \
  node "<%= @options[:path] %>"
  

Runit Service (cont.)

./site-cookbooks/animal-www/metadata.rb

name             'animal-www'
...
version          '0.1.0'

depends 'nginx'
depends 'nodejs'
depends 'runit'

Runit Service (cont.)

./Cheffile

#!/usr/bin/env ruby
site 'https://supermarket.getchef.com/api/v1'

cookbook 'nginx'
cookbook 'nodejs'
cookbook 'runit'

Runit Service (cont.)

Time to run Chef again

$ vagrant provision
... Running provisioner: chef_solo...
...
... user[api] created
...
... runit_service[api] enabled
...

Then load http://localhost:8080/

Refactoring Time

Default Recipe

./site-cookbooks/animal-www/recipes/default.rb

include_recipe 'animal-www::www'
include_recipe 'animal-www::api'

Default Recipe (cont.)

./Vagrantfile

Vagrant.configure(2) do |config|
  config.vm.box = "chef/centos-6.6"
  config.vm.network "forwarded_port", guest: 8080, host: 8080
  
  config.vm.provision "chef_solo" do |chef|
    chef.cookbooks_path = ["site-cookbooks", "cookbooks"]
    chef.run_list = ["animal-www"]
  end
end

Default Recipe (cont.)

Time to run Chef again

$ vagrant provision
... Running provisioner: chef_solo...
...
... Run List expands to [animal-www]
...

Attributes

./site-cookbooks/animal-www/recipes/api.rb

user 'api' do
  system true
  shell '/sbin/nologin'
end

runit_service 'api' do
  default_logger true
  options({
    :user => 'api',
    :path => '/var/api/app.js'
  })
end

Attributes (cont.)

./site-cookbooks/animal-www/attributes/default.rb

node.default['animal-www']['api']['user'] = 'api'

Attributes (cont.)

./site-cookbooks/animal-www/recipes/api.rb

user node['animal-www']['api']['user'] do
  system true
  shell '/sbin/nologin'
end

runit_service 'api' do
  default_logger true
  options({
    :user => node['animal-www']['api']['user'],
    :path => '/var/api/app.js'
  })
end

Attributes (cont.)

./Vagrantfile

Vagrant.configure(2) do |config|
  config.vm.box = "chef/centos-6.6"
  config.vm.network "forwarded_port", guest: 8080, host: 8080
  
  config.vm.provision "chef_solo" do |chef|
    chef.cookbooks_path = ["site-cookbooks", "cookbooks"]
    chef.run_list = ["animal-www"]
    chef.json = {
      "animal-www" => {
        "api" => { "user" => "derp" }
      }
    }
  end
end

Attributes (cont.)

Time to run Chef again

$ vagrant provision
... Running provisioner: chef_solo...
...
... user[derp] created
...

Attributes (cont.)

$ vagrant ssh
Last login: Wed Mar 18 19:45:42 2015 from 10.0.2.2
[vagrant@localhost ~]$ ps -eo user,pid,cmd | grep node
derp      8913 node /var/api/app.js
vagrant   8942 grep node

Foodcritic

Lint your cookbook:

$ foodcritic site-cookbooks/animal-www
FC008: Generated cookbook metadata needs updating...
FC008: Generated cookbook metadata needs updating...

What is FC008?

Now...

Go Forth And Chef

Topics Not Covered