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