Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds

Deployment and Maintenance

Save for later
  • 21 min read
  • 20 Jul 2015

article-image

 In this article by Sandro Pasquali, author of Deploying Node.js, we will learn about the following:

  • Automating the deployment of applications, including a look at the differences between continuous integration, delivery, and deployment
  • Using Git to track local changes and triggering deployment actions via webhooks when appropriate
  • Using Vagrant to synchronize your local development environment with a deployed production server
  • Provisioning a server with Ansible

Note that application deployment is a complex topic with many dimensions that are often considered within unique sets of needs. This article is intended as an introduction to some of the technologies and themes you will encounter. Also, note that the scaling issues are part and parcel of deployment.

(For more resources related to this topic, see here.)

Using GitHub webhooks

At the most basic level, deployment involves automatically validating, preparing, and releasing new code into production environments. One of the simplest ways to set up a deployment strategy is to trigger releases whenever changes are committed to a Git repository through the use of webhooks. Paraphrasing the GitHub documentation, webhooks provide a way for notifications to be delivered to an external web server whenever certain actions occur on a repository.

In this section, we'll use GitHub webhooks to create a simple continuous deployment workflow, adding more realistic checks and balances.

We'll build a local development environment that lets developers work with a clone of the production server code, make changes, and see the results of those changes immediately. As this local development build uses the same repository as the production build, the build process for a chosen environment is simple to configure, and multiple production and/or development boxes can be created with no special effort.

The first step is to create a GitHub (www.github.com) account if you don't already have one. Basic accounts are free and easy to set up.

Now, let's look at how GitHub webhooks work.

Enabling webhooks

Create a new folder and insert the following package.json file:

{
"name": "express-webhook",
"main": "server.js",
"dependencies": {
"express": "~4.0.0",
"body-parser": "^1.12.3"
}
}

This ensures that Express 4.x is installed and includes the body-parser package, which is used to handle POST data. Next, create a basic server called server.js:

var express = require('express');
var app = express();
var bodyParser = require('body-parser');
var port = process.env.PORT || 8082;
app.use(bodyParser.json());
app.get('/', function(req, res) {
res.send('Hello World!');
});
app.post('/webhook', function(req, res) {
// We'll add this next
});
app.listen(port);
console.log('Express server listening on port ' + port);

Enter the folder you've created, and build and run the server with npm install; npm start. Visit localhost:8082/ and you should see "Hello World!" in your browser.

Whenever any file changes in a given repository, we want GitHub to push information about the change to /webhook. So, the first step is to create a GitHub repository for the Express server mentioned in the code. Go to your GitHub account and create a new repository with the name 'express-webhook'. The following screenshot shows this:

deployment-and-maintenance-img-0

Once the repository is created, enter your local repository folder and run the following commands:

git init
git add .
git commit -m "first commit"
git remote add origin [email protected]:<your username>/express-webhook

You should now have a new GitHub repository and a local linked version. The next step is to configure this repository to broadcast the push event on the repository. Navigate to the following URL:

https://github.com/<your_username>/express-webhook/settings

From here, navigate to Webhooks & Services | Add webhook (you may need to enter your password again). You should now see the following screen:

deployment-and-maintenance-img-1

This is where you set up webhooks. Note that the push event is already set as default, and, if asked, you'll want to disable SSL verification for now. GitHub needs a target URL to use POST on change events. If you have your local repository in a location that is already web accessible, enter that now, remembering to append the /webhook route, as in http://www.example.com/webhook.

If you are building on a local machine or on another limited network, you'll need to create a secure tunnel that GitHub can use. A free service to do this can be found at http://localtunnel.me/. Follow the instructions on that page, and use the custom URL provided to configure your webhook.

Other good forwarding services can be found at https://forwardhq.com/ and https://meetfinch.com/.

Now that webhooks are enabled, the next step is to test the system by triggering a push event. Create a new file called readme.md (add whatever you'd like to it), save it, and then run the following commands:

git add readme.md
git commit -m "testing webhooks"
git push origin master

This will push changes to your GitHub repository. Return to the Webhooks & Services section for the express-webhook repository on GitHub. You should see something like this:

deployment-and-maintenance-img-2

This is a good thing! GitHub noticed your push and attempted to deliver information about the changes to the webhook endpoint you set, but the delivery failed as we haven't configured the /webhook route yet—that's to be expected. Inspect the failed delivery payload by clicking on the last attempt—you should see a large JSON file. In that payload, you'll find something like this:

"committer": {
"name": "Sandro Pasquali",
"email": "[email protected]",
"username": "sandro-pasquali"
},
"added": [
"readme.md"
],
"removed": [],
"modified": []

It should now be clear what sort of information GitHub will pass along whenever a push event happens. You can now configure the /webhook route in the demonstration Express server to parse this data and do something with that information, such as sending an e-mail to an administrator. For example, use the following code:

app.post('/webhook', function(req, res) {
console.log(req.body);
});

The next time your webhook fires, the entire JSON payload will be displayed.

Let's take this to another level, breaking down the autopilot application to see how webhooks can be used to create a build/deploy system.

Implementing a build/deploy system using webhooks

To demonstrate how to build a webhook-powered deployment system, we're going to use a starter kit for application development. Go ahead and use fork on the repository at https://github.com/sandro-pasquali/autopilot.git. You now have a copy of the autopilot repository, which includes scaffolding for common Gulp tasks, tests, an Express server, and a deploy system that we're now going to explore.

The autopilot application implements special features depending on whether you are running it in production or in development. While autopilot is a little too large and complex to fully document here, we're going to take a look at how major components of the system are designed and implemented so that you can build your own or augment existing systems. Here's what we will examine:

  • How to create webhooks on GitHub programmatically
  • How to catch and read webhook payloads
  • How to use payload data to clone, test, and integrate changes
  • How to use PM2 to safely manage and restart servers when code changes

If you haven't already used fork on the autopilot repository, do that now. Clone the autopilot repository onto a server or someplace else where it is web-accessible. Follow the instructions on how to connect and push to the fork you've created on GitHub, and get familiar with how to pull and push changes, commit changes, and so on.

PM2 delivers a basic deploy system that you might consider for your project (https://github.com/Unitech/PM2/blob/master/ADVANCED_README.md#deployment).

Install the cloned autopilot repository with npm install; npm start. Once npm has installed dependencies, an interactive CLI application will lead you through the configuration process. Just hit the Enter key for all the questions, which will set defaults for a local development build (we'll build in production later). Once the configuration is complete, a new development server process controlled by PM2 will have been spawned. You'll see it listed in the PM2 manifest under autopilot-dev in the following screenshot:

deployment-and-maintenance-img-3

You will make changes in the /source directory of this development build. When you eventually have a production server in place, you will use git push on the local changes to push them to the autopilot repository on GitHub, triggering a webhook. GitHub will use POST on the information about the change to an Express route that we will define on our server, which will trigger the build process. The build runner will pull your changes from GitHub into a temporary directory, install, build, and test the changes, and if all is well, it will replace the relevant files in your deployed repository. At this point, PM2 will restart, and your changes will be immediately available.

Schematically, the flow looks like this:

deployment-and-maintenance-img-4

To create webhooks on GitHub programmatically, you will need to create an access token. The following diagram explains the steps from A to B to C:

deployment-and-maintenance-img-5

We're going to use the Node library at https://github.com/mikedeboer/node-github to access GitHub. We'll use this package to create hooks on Github using the access token you've just created.

Once you have an access token, creating a webhook is easy:

var GitHubApi = require("github");
github.authenticate({
type: "oauth",
token: <your token>
});
github.repos.createHook({
"user": <your github username>,
"repo": <github repo name>,
"name": "web",
"secret": <any secret string>,
"active": true,
"events": [
"push"
],
"config": {
"url": "http://yourserver.com/git-webhook",
"content_type": "json"
}
}, function(err, resp) {
...
});

Autopilot performs this on startup, removing the need for you to manually create a hook.

Now, we are listening for changes. As we saw previously, GitHub will deliver a payload indicating what has been added, what has been deleted, and what has changed. The next step for the autopilot system is to integrate these changes.

It is important to remember that, when you use webhooks, you do not have control over how often GitHub will send changesets—if more than one person on your team can push, there is no predicting when those pushes will happen. The autopilot system uses Redis to manage a queue of requests, executing them in order. You will need to manage multiple changes in a way. For now, let's look at a straightforward way to build, test, and integrate changes.

In your code bundle, visit autopilot/swanson/push.js. This is a process runner on which fork has been used by buildQueue.js in that same folder. The following information is passed to it:

  • The URL of the GitHub repository that we will clone
  • The directory to clone that repository into (<temp directory>/<commit hash>)
  • The changeset
  • The location of the production repository that will be changed

Go ahead and read through the code. Using a few shell scripts, we will clone the changed repository and build it using the same commands you're used to—npm install, npm test, and so on. If the application builds without errors, we need only run through the changeset and replace the old files with the changed files.

The final step is to restart our production server so that the changes reach our users. Here is where the real power of PM2 comes into play.

When the autopilot system is run in production, PM2 creates a cluster of servers (similar to the Node cluster module). This is important as it allows us to restart the production server incrementally. As we restart one server node in the cluster with the newly pushed content, the other clusters continue to serve old content. This is essential to keeping a zero-downtime production running.

Hopefully, the autopilot implementation will give you a few ideas on how to improve this process and customize it to your own needs.

Synchronizing local and deployed builds

One of the most important (and often difficult) parts of the deployment process is ensuring that the environment an application is being developed, built, and tested within perfectly simulates the environment that application will be deployed into. In this section, you'll learn how to emulate, or virtualize, the environment your deployed application will run within using Vagrant. After demonstrating how this setup can simplify your local development process, we'll use Ansible to provision a remote instance on DigitalOcean.

Developing locally with Vagrant

For a long while, developers would work directly on running servers or cobble together their own version of the production environment locally, often writing ad hoc scripts and tools to smoothen their development process. This is no longer necessary in a world of virtual machines. In this section, we will learn how to use Vagrant to emulate a production environment within your development environment, advantageously giving you a realistic box to work on testing code for production and isolating your development process from your local machine processes.

Unlock access to the largest independent learning library in Tech for FREE!
Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
Renews at €14.99/month. Cancel anytime

By definition, Vagrant is used to create a virtual box emulating a production environment. So, we need to install Vagrant, a virtual machine, and a machine image. Finally, we'll need to write the configuration and provisioning scripts for our environment.

Go to http://www.vagrantup.com/downloads and install the right Vagrant version for your box. Do the same with VirtualBox here at https://www.virtualbox.org/wiki/Downloads.

You now need to add a box to run. For this example, we're going to use Centos 7.0, but you can choose whichever you'd prefer. Create a new folder for this project, enter it, and run the following command:

vagrant box add chef/centos-7.0

Usefully, the creators of Vagrant, HashiCorp, provide a search service for Vagrant boxes at https://atlas.hashicorp.com/boxes/search.

You will be prompted to choose your virtual environment provider—select virtualbox. All relevant files and machines will now be downloaded. Note that these boxes are very large and may take time to download.

You'll now create a configuration file for Vagrant called Vagrantfile. As with npm, the init command quickly sets up a base file. Additionally, we'll need to inform Vagrant of the box we'll be using:

vagrant init chef/centos-7.0

Vagrantfile is written in Ruby and defines the Vagrant environment. Open it up now and scan it. There is a lot of commentary, and it makes a useful read. Note the config.vm.box = "chef/centos-7.0" line, which was inserted during the initialization process.

Now you can start Vagrant:

vagrant up

If everything went as expected, your box has been booted within Virtualbox. To confirm that your box is running, use the following code:

vagrant ssh

If you see a prompt, you've just set up a virtual machine. You'll see that you are in the typical home directory of a CentOS environment.

To destroy your box, run vagrant destroy. This deletes the virtual machine by cleaning up captured resources. However, the next vagrant up command will need to do a lot of work to rebuild. If you simply want to shut down your machine, use vagrant halt.

Vagrant is useful as a virtualized, production-like environment for developers to work within. To that end, it must be configured to emulate a production environment. In other words, your box must be provisioned by telling Vagrant how it should be configured and what software should be installed whenever vagrant up is run.

One strategy for provisioning is to create a shell script that configures our server directly and point the Vagrant provisioning process to that script. Add the following line to Vagrantfile:

config.vm.provision "shell", path: "provision.sh"

Now, create that file with the following contents in the folder hosting Vagrantfile:

# install nvm
curl https://raw.githubusercontent.com/creationix/nvm/v0.24.1/install.
sh | bash
# restart your shell with nvm enabled
source ~/.bashrc
# install the latest Node.js
nvm install 0.12
# ensure server default version
nvm alias default 0.12

Destroy any running Vagrant boxes. Run Vagrant again, and you will notice in the output the execution of the commands in our provisioning shell script.

When this has been completed, enter your Vagrant box as the root (Vagrant boxes are automatically assigned the root password "vagrant"):

vagrant ssh
su

You will see that Node v0.12.x is installed:

node -v

It's standard to allow password-less sudo for the Vagrant user. Run visudo and add the following line to the sudoers configuration file:

vagrant ALL=(ALL) NOPASSWD: ALL

Typically, when you are developing applications, you'll be modifying files in a project directory. You might bind a directory in your Vagrant box to a local code editor and develop in that way. Vagrant offers a simpler solution. Within your VM, there is a /vagrant folder that maps to the folder that Vagrantfile exists within, and these two folders are automatically synced. So, if you add the server.js file to the right folder on your local machine, that file will also show up in your VM's /vagrant folder.

Go ahead and create a new test file either in your local folder or in your VM's /vagrant folder. You'll see that file synchronized to both locations regardless of where it was originally created.

Let's clone our express-webhook repository from earlier in this article into our Vagrant box. Add the following lines to provision.sh:

# install various packages, particularly for git
yum groupinstall "Development Tools" -y
yum install gettext-devel openssl-devel perl-CPAN perl-devel zlib-devel
-y
yum install git -y
# Move to shared folder, clone and start server
cd /vagrant
git clone https://github.com/sandro-pasquali/express-webhook
cd express-webhook
npm i; npm start

Add the following to Vagrantfile, which will map port 8082 on the Vagrant box (a guest port representing the port our hosted application listens on) to port 8000 on our host machine:

config.vm.network "forwarded_port", guest: 8082, host: 8000

Now, we need to restart the Vagrant box (loading this new configuration) and re-provision it:

vagrant reload
vagrant provision

This will take a while as yum installs various dependencies. When provisioning is complete, you should see this as the last line:

==> default: Express server listening on port 8082

Remembering that we bound the guest port 8082 to the host port 8000, go to your browser and navigate to localhost:8000. You should see "Hello World!" displayed.

Also note that in our provisioning script, we cloned to the (shared) /vagrant folder. This means the clone of express-webhook should be visible in the current folder, which will allow you to work on the more easily accessible codebase, knowing it will be automatically synchronized with the version on your Vagrant box.

Provisioning with Ansible

Configuring your machines by hand, as we've done previously, doesn't scale well. For one, it can be overly difficult to set and manage environment variables. Also, writing your own provisioning scripts is error-prone and no longer necessary given the existence of provisioning tools, such as Ansible.

With Ansible, we can define server environments using an organized syntax rather than ad hoc scripts, making it easier to distribute and modify configurations. Let's recreate the provision.sh script developed earlier using Ansible playbooks:

Playbooks are Ansible's configuration, deployment, and orchestration language. They can describe a policy you want your remote systems to enforce or a set of steps in a general IT process.

Playbooks are expressed in the YAML format (a human-readable data serialization language). To start with, we're going to change Vagrantfile's provisioner to Ansible. First, create the following subdirectories in your Vagrant folder:

provisioning
common
tasks

These will be explained as we proceed through the Ansible setup.

Next, create the following configuration file and name it ansible.cfg:

[defaults]
roles_path = provisioning
log_path = ./ansible.log

This indicates that Ansible roles can be found in the /provisioning folder, and that we want to keep a provisioning log in ansible.log. Roles are used to organize tasks and other functions into reusable files. These will be explained shortly.

Modify the config.vm.provision definition to the following:

config.vm.provision "ansible" do |ansible|
ansible.playbook = "provisioning/server.yml"
ansible.verbose = "vvvv"
end

This tells Vagrant to defer to Ansible for provisioning instructions, and that we want the provisioning process to be verbose—we want to get feedback when the provisioning step is running. Also, we can see that the playbook definition, provisioning/server.yml, is expected to exist. Create that file now:

---
- hosts: all
sudo: yes
roles:
- common
vars:
env:
user: 'vagrant'
nvm:
version: '0.24.1'
node_version: '0.12'
build:
repo_path: 'https://github.com/sandro-pasquali'
repo_name: 'express-webhook'

Playbooks can contain very complex rules. This simple file indicates that we are going to provision all available hosts using a single role called common. In more complex deployments, an inventory of IP addresses could be set under hosts, but, here, we just want to use a general setting for our one server. Additionally, the provisioning step will be provided with certain environment variables following the forms env.user, nvm.node_version, and so on. These variables will come into play when we define the common role, which will be to provision our Vagrant server with the programs necessary to build, clone, and deploy express-webhook. Finally, we assert that Ansible should run as an administrator (sudo) by default—this is necessary for the yum package manager on CentOS.

We're now ready to define the common role. With Ansible, folder structures are important and are implied by the playbook. In our case, Ansible expects the role location (./provisioning, as defined in ansible.cfg) to contain the common folder (reflecting the common role given in the playbook), which itself must contain a tasks folder containing a main.yml file. These last two naming conventions are specific and required.

The final step is creating the main.yml file in provisioning/common/tasks. First, we replicate the yum package loaders (see the file in your code bundle for the full list):

---
- name: Install necessary OS programs
yum: name={{ item }} state=installed
with_items:
- autoconf
- automake
...
- git

Here, we see a few benefits of Ansible. A human-readable description of yum tasks is provided to a looping structure that will install every item in the list. Next, we run the nvm installer, which simply executes the auto-installer for nvm:

- name: Install nvm
sudo: no
shell: "curl https://raw.githubusercontent.com/creationix/nvm/v{{ nvm.
version }}/install.sh | bash"

Note that, here, we're overriding the playbook's sudo setting. This can be done on a per-task basis, which gives us the freedom to move between different permission levels while provisioning. We are also able to execute shell commands while at the same time interpolating variables:

- name: Update .bashrc
sudo: no
lineinfile: >
dest="/home/{{ env.user }}/.bashrc"
line="source /home/{{ env.user }}/.nvm/nvm.sh"

Ansible provides extremely useful tools for file manipulation, and we will see here a very common one—updating the .bashrc file for a user. The lineinfile directive makes the addition of aliases, among other things, straightforward.

The remainder of the commands follow a similar pattern to implement, in a structured way, the provisioning directives we need for our server. All the files you will need are in your code bundle in the vagrant/with_ansible folder. Once you have them installed, run vagrant up to see Ansible in action.

One of the strengths of Ansible is the way it handles contexts. When you start your Vagrant build, you will notice that Ansible gathers facts, as shown in the following screenshot:

deployment-and-maintenance-img-6

Simply put, Ansible analyzes the context it is working in and only executes what is necessary to execute. If one of your tasks has already been run, the next time you try vagrant provision, that task will not run again. This is not true for shell scripts! In this way, editing playbooks and reprovisioning does not consume time redundantly changing what has already been changed.

Ansible is a powerful tool that can be used for provisioning and much more complex deployment tasks. One of its great strengths is that it can run remotely—unlike most other tools, Ansible uses SSH to connect to remote servers and run operations. There is no need to install it on your production boxes. You are encouraged to browse the Ansible documentation at http://docs.ansible.com/index.html to learn more.

Summary

In this article, you learned how to deploy a local build into a production-ready environment and the powerful Git webhook tool was demonstrated as a way of creating a continuous integration environment.

Resources for Article:


Further resources on this subject: