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.
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.
Now that we have the architecture plan in place, let's go ahead and automate the process.
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:
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
.
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.
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
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 communityconfig/deploy.rb
: This sets the variables that we are going to use through our tasks such as repository, application name, and so onconfig/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 forthlib/capistrano/tasks/*.rake
: This contains all the additional tasks, and can be invoked from the deploy.rb
scriptCapistrano 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 readydeploy: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 filesdeploy: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.
Full documentation on Capistrano can be found at http://capistranorb.com.
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
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
Time to automate the deployment! Capistrano has a default task which does the following:
ENV
variables, create symlinks, release version, and so on.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.
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.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" }, } ] }
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.
Nginx has different strategies for load balancing applications:
We are going to use ip_hash
because of our WebSocket/SocketIO requirements.
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.
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.
18.223.206.69