April 21, 2015

Automating Deployment from GitHub

Let's get to automating! This video will show you how to use NodeJS to listen for GitHub webhooks, and kick off an automated (zero-downtime!) deployment script when we push to our master branch.

Let's get to automating! This video will show you how to use NodeJS to listen for GitHub webhooks, and kick off an automated (zero-downtime!) deployment script when we push to our master branch.We'll setup an SSH keypair and install NodeJS so that the server can listen for web hooks and can push/pull to and from the project repository.

Deploy Hook

On the production server, generate an SSL certificate for the deploy user, and add the resulting public key to Github as a Deploy hook.

Create id_rsa if not exists, else use id_rsa.pub:

cd ~/.ssh
ssh-keygen -t rsa -b 4096 -f id_rsa

Paste the contents of the id_rsa.pub file into the Github repository as a deploy hook within the repository settings.

Web Listener

We need a web listener listening for Github webhooks. Anything that can listen for web hooks will do, but I like NodeJS because small, can listen over HTTP without need of being proxied to from a web server (Nginx, Apache, etc), and can run shell scripts in a solid fashion.

Here's a Node.JS version of a webhook listener!

Install Node.JS

curl -sL https://deb.nodesource.com/setup | sudo bash -
sudo apt-get install -y nodejs

Create Listener

Within our "production" server:

# Log in as user fideloper for sudo
# but use our deployer user's home directory
# Run commands as user "deployer"
sudo -u deployer mkdir /home/deployer/deploy
cd /home/deployer/deploy
sudo -u deployer npm init
sudo -u deployer npm install --save githubhook

Use githubhook library at /home/deployer/deploy/server.js

var execFile = require('child_process').execFile;
var githubhook = require('githubhook');
var github = githubhook({
    host: '0.0.0.0',
    port: 3420,
    path: '/gitback',
    secret: 'my-secret',
    logger: console,
});

github.listen();

github.on('deploy-ex:refs/heads/master', function (data) {
    // Exec a shell script
    var execOptions = {
        maxBuffer: 1024 * 1024 // Increase max buffer to 1mb
    };
    execFile('/home/deployer/deploy/deploy.sh', execOptions, function(error, stdout, stderr) {
        if( error )
        {
            console.log(error)
        }
    });
});

Within GitHub repository settings, we'll need to add the Webhook settings to point to our production server on the correct port. Add the secret used in the NodeJS script as well:

http://104.236.85.162:3420/gitback
my-secret

Create Deploy Shell Script

This will do basically what we configured Envoy to do, but on the server as a shell script at /home/deployer/deploy/deploy.sh:

#!/usr/bin/env bash

REPO='git@github.com:Servers-for-Hackers/deploy-ex.git';
RELEASE_DIR='/var/www/releases';
APP_DIR='/var/www/app';
RELEASE="release_`date +%Y%m%d%H%M%s`";

# Fetch Latest Code
[ -d $RELEASE_DIR ] || mkdir $RELEASE_DIR;
cd $RELEASE_DIR;
git clone -b master $REPO $RELEASE;

# Composer
cd $RELEASE_DIR/$RELEASE;
composer install --prefer-dist --no-scripts;
php artisan clear-compiled --env=production;
php artisan optimize --env=production;

# Symlinks
ln -nfs $RELEASE_DIR/$RELEASE $APP_DIR;
chgrp -h www-data $APP_DIR;

## Env File
cd $RELEASE_DIR/$RELEASE;
ln -nfs ../../.env .env;
chgrp -h www-data .env;

## Logs
rm -r $RELEASE_DIR/$RELEASE/storage/logs;
cd $RELEASE_DIR/$RELEASE/storage;
ln -nfs ../../../logs logs;
chgrp -h www-data logs;

## Update Currente Site
ln -nfs $RELEASE_DIR/$RELEASE $APP_DIR;
chgrp -h www-data $APP_DIR;

## PHP
sudo service php5-fpm reload;

Test It

We'll update our code with a new route and then push that to the master branch. If the /deployed page works, then our automated deployment was a success!

Ensure the NodeJS listener is active:

node /home/deployer/deploy/server.js

Then we add a route to the framework's routes.php file:

Route::get('/deployed', function() {
    return "New Route!";
});

Monitoring NodeJS Listener

We can't leave NodeJS to its own devices - we should install something to make sure it's always up and running.

The video uses Debian 7 with Systemd enabled/installed. For systems with Systemd, we can add a unit file (service) to monitor the NodeJS listener.

Systemd

Add the following service configuration to file /lib/systemd/system/deploy.service:

[Unit]
Description=Webhook

[Service]
User=deployer
Group=www-data
Restart=on-failure
ExecStart=/usr/bin/node /home/deployer/deploy/server.js`

[Install]
WantedBy=multi-user.target

Then run:

# Enable service unit file
systemctl enable deploy.service

# Check its status and start it
sudo service deploy status
sudo service deploy start

Upstart

On Ubuntu, or any system using Upstart, we can add the following configuration and use that instead:

Add this configuration to file /etc/init/deploy.conf:

description "NodeJS Webhook Listener"

start on filesystem or runlevel [2345]
stop on runlevel [!2345]

setuid deployer
setgid www-data

respawn
respawn limit 5 2

script
    /usr/bin/node /home/deployer/deploy/server.js`
end script

Then get its status and start it:

sudo service deploy status
sudo service deploy start 

Firewalls

Lastly we need to ensure Git can send webhooks to our listen. Configure it to accept connections on port 3420:

# Place rule at the 4th position of the INPUT chain
# This assumes the 4th position is before any rule 
#  potentially dropping tcp traffic over port 3420
sudo iptables -I INPUT 4 -p tcp --dport 3420 -j ACCEPT

All Topics