Projects

How I delivered SanctionedList.com with a solo developer setup (Part 5)

This guide series will be about setting up http://sanctionedlist.com with configuration management and TDD.

We need the webserver, lets work on that next. I’ll be using nginx. Lets look at official nginx cookbook and how to use it.

Yes, a bit overwhelming for a new Chef user (especially one to Nginx). However, the recipe is very powerful with all these options. Under usage section, it recommends “Package installation using the nginx.org repositories”. We’ll go with this.

Add the dependency and include the recipe. Berks install will silently install the cookbook once we run tests!

$ chef exec knife cookbook site show chef_nginx | grep latest_version
latest_version: https://supermarket.chef.io/api/v1/cookbooks/chef_nginx/versions/5.0.7
[…]
depends 'chef_nginx', '~> 5.0.7'
include_recipe 'chef_nginx::default'
[…]
- recipe[sanctionedlist::webserver]
[…]

Now we add a basic test and an integration test

require 'spec_helper'

describe 'sanctionedlist::webserver' do
let(:chef_run) { ChefSpec::ServerRunner.converge(described_recipe) }

it 'installs a package with the default action' do
expect(chef_run).to install_package('nginx')
end
end
describe package('nginx') do
it { should be_installed }
end

describe service('nginx') do
it { should be_enabled }
it { should be_running }
end
$ chef exec rspec
3 examples, 0 failures

$ kitchen converge; kitchen verify

System Package
✔ nginx should be installed
Service nginx
✔ should be enabled
✔ should be running
[…]
User root
✔ should exist
↺ This is an example test, replace with your own test.
Port 80
∅ should not be listening
expected 'Port 80.listening?' to return false, got true
↺ This is an example test, replace with your own test.

Test Summary: 7 successful, 1 failures, 2 skipped

Success! Somewhat. Clear or remove your default_test.rb from test/smoke/default
Test Summary: 6 successful, 0 failures, 0 skipped
Excellent but we need to set up our nginx conf because nginx doesn’t know if we want connections to go to php. It’s just two files that nginx uses, so we will want to use our own.

# Our nginx config
describe file('/etc/nginx/nginx.conf') do
it { should be_a_file }
its(:content) { should match(%r{sanctionedlist}) }
end

# Our site config
describe file('/etc/nginx/sites-enabled/sanctions') do
it { should be_a_file }
its(:content) { should match(%r{sanctionedlist}) }
end

describe port(80) do
it { should be_listening }
its('processes') { should include 'nginx:' }
end

Lets run kitchen. There’s missing conf files but at least it’s listening on port 80!

$ kitchen converge; kitchen verify
[…]
File /etc/nginx/nginx.conf
✔ should be a file
∅ content should match /sanctionedlist/
[…]
File /etc/nginx/sites-enabled/sanctions
∅ should be a file
expected 'File /etc/nginx/sites-enabled/sanctions.file?' to return true, got false
∅ content should match /listen\s+80;/
expected nil to match /listen\s+80;/
∅ content should match /sanctionedlist/
expected nil to match /sanctionedlist/
Port 80
✔ should be listening
✔ processes should include "nginx:"

Lets pass the tests with below.

# We use node.default to override chef_nginx attributes.
node.default['nginx']['conf_template'] = 'nginx.conf.erb'
node.default['nginx']['conf_cookbook'] = 'sanctionedlist'
node.default['nginx']['default_site_enabled'] = false
# If it still doesn't work, abuse this resource collection hack
# resources('template[nginx.conf]').cookbook 'sanctionedlist'
# https://blog.chef.io/2013/12/03/doing-wrapper-cookbooks-right/

# Ensure include_recipe comes before the attributes. Otherwise Chef_nginx will
# override it.
include_recipe 'chef_nginx::default'

template "#{node['nginx']['dir']}/sites-enabled/sanctions" do
source 'sanctions.erb'
notifies :reload, 'service[nginx]', :delayed
end
$ mkdir -p ~/sanctionedlist/chef/cookbooks/sanctionedlist/templates/default
# sanctionedlist server
user www-data;
worker_processes 4;
pid /run/nginx.pid;

events {
worker_connections 768;
# multi_accept on;
}

http {

##
# Basic Settings
##

sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# server_tokens off;

# server_names_hash_bucket_size 64;
# server_name_in_redirect off;

include /etc/nginx/mime.types;
default_type application/octet-stream;

##
# Logging Settings
##

access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;

##
# Gzip Settings
##

gzip on;
gzip_disable "msie6";

gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

##
# Virtual Host Configs
##

include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
# sanctionedlist site
server {
listen 80;
listen [::]:80 default_server ipv6only=on;

root /var/www/sanctions/public;
index index.php index.html index.htm;

server_name localhost;

location / {
try_files $uri $uri/ /index.php?$query_string;
}

location ~ \.php$ {
try_files $uri /index.php =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/run/php/php7.0-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}

# Media: images, icons, video, audio, HTC
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|htc)$ {
expires 1M;
access_log off;
add_header Cache-Control "public";
}

# CSS and Javascript
location ~* \.(?:css|js)$ {
expires 1y;
access_log off;
add_header Cache-Control "public";
}

}
$ kitchen converge; kitchen verify
Test Summary: 13 successful, 0 failures, 0 skipped
# Protip: If you're getting issues with running nginx, you can test the nginx conf with the kitchen like this:
$ kitchen exec default-ubuntu-1604 -c 'sudo nginx -t -c /etc/nginx/nginx.conf'

Now for PHP, or the application server

$ chef generate recipe appserver
$ chef exec knife cookbook site show php | grep latest_version
latest_version: https://supermarket.chef.io/api/v1/cookbooks/php/versions/2.2.1

[…]
depends 'php', '~> 2.2.1'
include_recipe 'php::default'

php_fpm_pool "default" do
action :install
end
[…]
- recipe[sanctionedlist::appserver]
[…]

Note: If you're sick of running the database and webserver recipes, comment them out in the kitchen.yml file (Or put them in their own cookbooks!)

require 'spec_helper'

describe 'sanctionedlist::appserver' do
let(:chef_run) { ChefSpec::ServerRunner.converge(described_recipe) }

it 'installs php' do
expect(chef_run).to install_package('php')
end
end
$ chef exec rspec
Failures:

1) sanctionedlist::appserver installs php
Failure/Error: expect(chef_run).to install_package('php')

expected "package[php]" with action :install to be in Chef run. Other package resources:

apt_package[php7.0-cgi]
apt_package[php7.0]
apt_package[php7.0-dev]
apt_package[php7.0-cli]
apt_package[php-pear]

# ./spec/unit/recipes/appserver_spec.rb:13:in 'block (2 levels) in '

Finished in 12.53 seconds (files took 1.22 seconds to load)
4 examples, 1 failure

Failed examples:

rspec ./spec/unit/recipes/appserver_spec.rb:12 # sanctionedlist::appserver installs php

Chef doesn't know which package we want installed. We'll go with php7.0

expect(chef_run).to install_package('php7.0')
$ chef exec rspec
Finished in 12.31 seconds (files took 1.23 seconds to load)
4 examples, 0 failures
describe package('php7.0') do
it { should be_installed }
end

describe service('php7.0-fpm') do
it { should be_enabled }
it { should be_running }
end
$ kitchen converge; kitchen verify
[…]
Test Summary: 16 successful, 0 failures, 0 skipped

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s