How I wrote a Chef Server
while waiting in line at Best Buy
John Keiser
Development Lead at Opscode
Things that annoy me
Takes too long to get started with Chef
chef-client and knife tests are hard
chef-solo is ... unique
I like rewriting things
Common solution to these problems:
Write a tiny Chef server.
How hard could it be?
A Chef Server ...
Just stores things
One JSON for each thing
Nodes, roles, environments, cookbooks ...
Processing is done on the client (mostly)
chef-client and knife grab stuff with REST
Recipe for a chef server
-
Laptop
-
Folding chair
-
12 pack of soda
-
Desire for cheap TV
-
Get in line
-
Unfold laptop and chair
-
Mix in soda slowly
-
???
-
Chef server
What I made
In-memory storage
Fast startup
No dependencies
Chef 11 compatible
REST server
I didn't do ...
Security
Persistence
Scalability
These are left as an exercise for the reader.
A beginning: /roles
CRUD for role JSON:
{
"name": "base",
"description": "That Which Belongs To Us",
"override_attributes": {
"build_essential": {
"compiletime": true
}
},
"run_list": [
"recipe[build_essential]",
"role[core-packages]"
]
}
Endpoints
-
GET /roles
-
POST /roles
-
GET /roles/web
-
PUT /roles/web
-
DELETE /roles/web
The Code: Read
require 'sinatra'
require 'json'
before { content_type 'application/json' }
roles = {}
get('/roles/*') { |role| roles[role] }
get('/roles') do
result = {}
roles.keys.each { |role| result[role] = url("/roles/#{role}") }
JSON.generate(result)
end
The Code: Write
post('/roles') do
role_body = request.body.read
role = JSON.parse(role_body)['name']
roles[role] = role_body
end
put('/roles/*') { |role| roles[role] = request.body.read }
delete('/roles/*') { |role| roles.delete(role) }
It works!
$ knife role create blah -s http://127.0.0.1:4567
Created role[blah]
$ knife role list -s http://127.0.0.1:4567
blah
$ knife role show blah -s http://127.0.0.1:4567
chef_type: role
default_attributes:
description:
env_run_lists:
json_class: Chef::Role
name: blah
override_attributes:
run_list:
Repeat for
-
Clients
-
Cookbooks
-
Data Bags
-
Environments
-
Nodes
-
Roles
-
Users
Wait, cookbooks too?
Cookbook = JSON with version and list of files.
Liar.
OK, cookbooks aren't QUITE that simple.
-
Two-stage upload (Sandboxes)
-
Depsolver
Two-stage Upload
- Ask the server where it wants the files
-
Upload the files
- Upload the cookbook JSON pointing at the files
Depsolver
-
Finds cookbook dependencies
-
Finds latest possible cookbook version
- Tells nodes what cookbooks to get
Search
Chef Server:
Indexed by solr
Query is solr syntax
Results not always fresh
Chef Zero:
No index
Slow, accurate and fresh
The code is too large to fit in this margin.
Pedant is fucking awesome
2712 tests and counting
What can it do?
-
Quick way to learn Chef
- knife and chef-client tests
- Testing cookbooks (test-kitchen)
- And more ...
Using chef-zero in tests
require 'chef_zero/rspec'
describe 'knife list' do
include ChefZero::RSpec
when_the_chef_server "has plenty of stuff in it" do
cookbook 'cookbook1', '1.0.0', { 'metadata.rb' => '' }
data_bag 'bag1', { 'item1' => {}, 'item2' => {} }
environment 'environment1', {}
role 'role1', {}
it "knife cookbook list lists cookbooks" do
knife('cookbook list').should_succeed "cookbook1\n"
end
end
end
A Modest Proposal
chef-client --local-server
- No need for configuration or knife.rb
- Load your repo into a chef-zero
- Supports all recipes
- Use data bags, search, roles, environments
- Save node after chef-client run (convergence!)
Testing in "Production"
Ignorance Is Bliss
-
knife download / from production
-
chef-zero
-
knife upload -s http://127.0.0.1:8889
-
vagrant up
-
knife bootstrap -s http://127.0.0.1:8889
Chef on a Stick
-
Stick chef-zero and your repo on a stick ...
-
/mnt/UsbStick/chef-client --local-server
-
Carry it to the next server
- Repeat
The cloud is now a stick.
Getting and Using It
gem install chef-zero
chef-zero
knife.rb:
chef_server_url "http://127.0.0.1:8889"
gem install chef-zero
chef-zero
Questions?
@jkeiser2