Chef Workshop
Outline
Let's use Chef to make a site that does stuff
Slides
- Follow along at:
http://mkropat.github.io/presentation-chef-workshop/ - Keep in mind: the slides are boring
 - All the interesting bits come from the questions you ask
 - Also: typing in the Chef-y bits helps you learn
 
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
- We want to serve the page in nginx
 - So we need to create an nginx config file
 - So we need to install nginx
 - So we need an nginx cookbook
 - So we need a way to pull in cookbooks
 
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...
        
      Now...
Go Forth And Chef
Topics Not Covered
- Idempotency
 - LWRPs