Deploying a new version of the app

Deploying can be a repetitive and error-prone task. However, we are going to automate it to make it more efficient. We are going to use a deployment tool called Capistrano to ease the task of deploying new versions of our app. We are also aiming for zero-downtime upgrades.

Zero-downtime deployments

It's not convenient for users to see a message, such as Site down for maintenance, so we are going to avoid that at all costs. We would also like to able to update our app as often as needed without the users even noticing. This can be accomplished with a zero-downtime architecture. Using two node applications, we can update one first while the other is still serving new requests. Then, we update the second app while the updated first app starts serving clients. That way, there's always an instance of the application serving the clients.

Zero-downtime deployments

Figure 2: Zero-downtime deployment setup

Now that we have the architecture plan in place, let's go ahead and automate the process.

Setting up the zero-downtime production server

At this point, you should create a server with at least two CPUs, with the help of the instructions given in the previous chapter (using the $10 bonus), or you can follow along with any other server that you prefer. Our setup might look like this:

Setting up the zero-downtime production server

Figure 3: Creating VM with two CPUs

Write down the private and public IP addresses. NodeJS applications use the private address to bind to ports 8080 and 8081, while Nginx will bind to the public IP address on port 80.

Getting started with Capistrano

Capistrano is a remote multi-server automation tool that will allow us to deploy our app in different environments such as Staging/QA and production. Also, we can update our app as often as needed without worrying about the users getting dropped.

Installing Capistrano

Capistrano is a Ruby program, so we need to install Ruby (if you haven't done so yet).

For Windows, go to: http://rubyinstaller.org/.

For Ubuntu, we are going to install a Ruby version manager (rvm):

sudo apt-get install ruby

Or for MacOS:

brew install ruby

We can install Capistrano as follows:

gem install Capistrano -v 3.4.0

Now we can bootstrap it in the meanshop folder:

cap install

Understanding Capistrano

The way Capistrano works is through tasks (rake tasks). Those tasks perform operations on servers such as installing programs, pulling code from a repository, restarting a service, and much more. Basically, we can automate any action that we can perform through a remote shell (SSH). We can scaffold the basic files running cap install.

During the installation process, a number of files and directories are added to the project, which are as follows:

  • Capfile: This loads the Capistrano tasks, and can also load predefined tasks made by the community
  • config/deploy.rb: This sets the variables that we are going to use through our tasks such as repository, application name, and so on
  • config/deploy/{production.rb, staging.rb}: While deploy.rb sets the variables that are common for all environments, production/staging.rb set the variables specific to the deployment stage, for example, NODE_ENV, servers IP addresses, and so forth
  • lib/capistrano/tasks/*.rake: This contains all the additional tasks, and can be invoked from the deploy.rb script

Capistrano comes with a default task called cap production deploy. This task executes the following sequence:

  • deploy:starting: This starts a deployment, making sure everything is ready
  • deploy:started: This is the started hook (for custom tasks)
  • deploy:updating: This updates server(s) with a new release (for example, git pull)
  • deploy:updated: This is the updated hook (for custom tasks)
  • deploy:publishing: This publishes the new release (for example, create symlinks)
  • deploy:published: This is the published hook (for custom tasks)
  • deploy:finishing: This finishes the deployment, cleans up temp files
  • deploy:finished: This is the finished hook (for custom tasks)

This deploy task pulls the code, and creates a release directory where the last five are kept. The most recent release has a symlink to current where the app lives.

Tip

Full documentation on Capistrano can be found at http://capistranorb.com.

Preparing the server

Now, we need a deployer user that we can use in Capistrano. Let's ssh into the server where we just created the user:

root@remote $ adduser deployer

Optionally, to avoid typing the password every time, let's add the remote keys. In Ubuntu and MacOS you can do the following:

root@local $ ssh-copy-id deployer@remote

Setting up Capistrano variables

Set the variables in config/deploy.rb, for instance:

/* config/deploy.rb */

# config valid only for current version of Capistrano
lock '3.4.0'

set :application, 'meanshop'
set :repo_url, '[email protected]:amejiarosario/meanshop.git'
set :user, 'deployer'
set :node_version, '0.12.7'
set :pty, true
set :forward_agent, true
set :linked_dirs, %w{node_modules}

namespace :deploy do
  # after :deploy, 'app:default'
  # after :deploy, 'nginx:default'
  # before 'deploy:reverted', 'app:default'end

The production server settings are done as follows:

/* config/deploy/production.rb */

server '128.0.0.0', user: 'deployer', roles: %w{web app db},
  private_ip: '10.0.0.0', primary: true

set :default_env, {
  NODE_ENV: 'production',
  path: "/home/#{fetch(:user)}/.nvm/versions/node/#{fetch (:node_version)}/bin:$PATH"
}

The next step is to forward our SSH keys to our server by running:

ssh-add ~/.ssh/id_rsa
ssh-add -L

Finally, you can deploy the application code to the server by running:

cap production deploy

If everything goes well, the application will be deployed to /var/www/meanshop/current.

Note: Refer to the previous chapter to install NodeJS, MongDB, pm2, grunt-cli, and all the required components in only one server:

$ sudo apt-get update
$ sudo apt-get install -y build-essential openssl libssl-dev pkg-config git-core mongodb ruby
$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.29.0/install.sh | bash
$ source ~/.bashrc
$ nvm install 0.12.7
$ nvm use 0.12.7
$ nvm alias default 0.12.7
$ npm install grunt-contrib-imagemin
$ npm install -g grunt-cli bower pm2
$ sudo gem install sass

Capistrano tasks

Time to automate the deployment! Capistrano has a default task which does the following:

  1. Create a file structure on the remote server.
  2. Set up ENV variables, create symlinks, release version, and so on.
  3. Check out the Git repository.
  4. Clean up.

We can use hooks to add tasks to this default workflow. For instance, we can run npm install, build assets, and start servers after Git is checked out.

Adding new tasks

Let's add a new task, app.rake, which prepares our application for serving the client, and also updates the servers one by one (zero-downtime upgrade). First, let's uncomment the scripts in config/deploy that invoke the app:default task (in the app.rake script). And now, let's add app.rake:

# lib/capistrano/tasks/app.rake

namespace :app do
  desc 'Install node dependencies'
  task :install do
    on roles :app do
      within release_path do
        execute :npm, 'install', '--silent', '--no-spin'
        execute :bower, 'install', '--config.interactive=false', '--silent'
        execute :npm, :update, 'grunt-contrib-imagemin'
        execute :grunt, 'build'
      end
    end
  end

  desc 'Run the apps and also perform zero-downtime updates'
  task :run do
    on roles :app do |host|
      null, app1, app2 = capture(:pm2, 'list', '-m').split('+---')
      if app1 && app2 && app1.index('online') && app2.index('online')
        execute :pm2, :restart, 'app-1'
        sleep 15
        execute :pm2, :restart, 'app-2'
      else
        execute :pm2, :kill
        template_path = File.expand_path('../templates/pm2.json.erb', __FILE__)
        host_config   = ERB.new (File.new(template_path).read).result(binding)
        config_path = "/tmp/pm2.json"
        upload! StringIO.new(host_config), config_path
        execute "IP=#{host.properties.private_ip}", "pm2", "start", config_path
      end
    end
  end

  task default: [:install, :run]
end

Don't worry too much if you don't understand everything that's going on here. The main points about app.rake are:

  • app:install: This downloads the npm and the bower package, and builds the assets.
  • app:run: This checks if the app is running and if it is going to update one node instance at a time at an interval of 15 seconds (zero-downtime). Otherwise, it will start both the instances immediately.

Tip

More information about other things that can be done with Rake tasks can be found at https://github.com/ruby/rake, as well as the Capistrano site at http://capistranorb.com/documentation/getting-started/tasks/.

Notice that we have a template called pm2.json.erb; let's add it:

/* lib/capistrano/tasks/templates/pm2.json.erb */

{
  "apps": [
    {
      "exec_mode": "fork_mode",
      "script": "<%= release_path %>/dist/server/app.js",
      "name": "app-1",
      "env": {
        "PORT": 8080,
        "NODE_ENV": "production"
      },
    },
    {
      "exec_mode": "fork_mode",
      "script": "<%= release_path %>/dist/server/app.js",
      "name": "app-2",
      "env": {
        "PORT": 8081,
        "NODE_ENV": "production"
      },
    }
  ]
}

Preparing Nginx

This time we are using Nginx as a load balancer between our two node instances and the static file server. Similar to app.rake, we are going to add new tasks that install Nginx, set up the config file, and restart the service:

# lib/capistrano/tasks/nginx.rake

namespace :nginx do
  task :info do
    on roles :all do |host|
      info "host #{host}:#{host.properties.inspect} (#{host.roles.to_a.join}): #{capture(:uptime)}"
    end
  end

  desc 'Install nginx'
  task :install do
    on roles :web do
      execute :sudo, 'add-apt-repository', '-y', 'ppa:nginx/stable'
      execute :sudo, 'apt-get', '-y', 'update'
      execute :sudo, 'apt-get', 'install', '-y', 'nginx'
    end
  end

  desc 'Set config file for nginx'
  task :setup do
    on roles :web do |host|
      template_path = File.expand_path('../templates/nginx.conf.erb', __FILE__)
      file = ERB.new(File.new(template_path).read).result(binding)
      file_path = '/tmp/nginx.conf'
      dest = "/etc/nginx/sites-available/#{fetch(:application)}"
      upload! StringIO.new(file), file_path
      execute :sudo, :mv, file_path, dest
      execute :chmod, '0655', dest
      execute :sudo, :ln, '-fs', dest, "/etc/nginx/sites-enabled/#{fetch(:application)}"
    end
  end

  task :remove do
    on roles :web do
      execute :sudo, :'apt-get', :remove, :'-y', :nginx
    end
  end

  %w[start stop restart status].each do |command|
    desc "run #{command} on nginx"
    task command do
      on roles :web do
        execute :sudo, 'service', 'nginx', command
      end
    end
  end

  desc 'Install nginx and setup config files'
  task default: [:install, :setup, :restart]
end

We also need to add the new template for Nginx config:

# lib/capistrano/tasks/templates/nginx.conf.erb

upstream node_apps {
  ip_hash;
  server <%= host.properties.private_ip %>:8080;
  server <%= host.properties.private_ip %>:8081;
}

server {
  listen 80;
  server_name localhost; # or your hostname.com
  root <%= release_path %>/dist/public;
  try_files $uri @node;

  location @node {
    proxy_pass http://node_apps;
    proxy_http_version 1.1;
    # server context headers
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    # headers for proxying a WebSocket
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
  }
}

Run cap production deploy, and after it finishes, you will see the app running in the public IP!

There are three main points of interest in this configuration: load balancing, static file server, and WebSockets.

Load balancing

Nginx has different strategies for load balancing applications:

  • Round-robin: The requests are distributed on the servers in the same order as defined. When one request reaches the last server, the next one goes to the first server.
  • Least-connected: The requests are distributed on the basis of the number of connections. The next request is sent to the server with the least connections.
  • IP-hash: The requests are distributed based on the IP address. This ensures that the client always uses the same server on each request. This is useful for sticky clients using WebSockets.

We are going to use ip_hash because of our WebSocket/SocketIO requirements.

Static file server

Static assets such as images, JS, and CSS files are not changed very often in production environments. So, they can be safely cached and served directly from Nginx without having to hit the node instances:

root /home/deployer/meanshop/current/dist/public;
try_files $uri @node;

Nginx will look for a static file in the file system first (root path). If it doesn't find it, Nginx will assume that it is a dynamic request, and hand it off to the node instances.

WebSockets

We are using WebSockets (WS) to establish a bidirectional communication between the servers and the clients. This allows our store application to have realtime updates. For that, we have headers in the configuration that advises the clients to upgrade from HTTP 1.0 to HTTP 1.1 to enable the WS connections.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.206.69