Zero downtime deployment for Rails with Capistrano and Unicorn
This post hasn't been updated for 4 years
Zero downtime deployment for Rails with Capistrano and Unicorn
Trên Viblo cũng có khá nhiều bài viết về việc auto deploy một ứng dụng Ruby on Rails với Capistrano. Nhưng mình cũng vẫn chia sẻ bài viết này với mục đích hướng dẫn mọi người chi tiết hơn trong việc cài đặt một server từ chưa có gì cho tới khi ứng dụng của chúng ta được chạy và có khả năng deploy hoàn chỉnh. Nằm ngoài mục đích đó là mình cũng muốn note lại và tổng kết những gì mình đã tìm hiểu, học được từ việc cài đặt và sử dụng Capistrano. Chúng ta cùng vừa đọc vừa thực hành luôn nhé. Việc khó khăn nhất là chúng ta cần phải có một server để thực hiện, nhưng may mắn thay, chúng ta có thể sử dụng Docker để giả lập một server. Về việc cài đặt và sử dụng Docker như thế nào thì mình không bàn tới trong bài viết này nhé. Mặc định là mọi người đã cài và có thể sử dụng Docker căn bản. Chúng ta cùng đi từng bước một nhé. Phần đầu là chúng ta thực hiện cài đặt một server có thể SSH vào được từ host (giống với môi trường server thật).
Cài đặt server
Việc sử dụng image nào là tùy mọi người. Mình sử dụng image Ubuntu 14.04 nhé. Pull image (nếu chưa có):
sudo docker pull ubuntu:14.04
Sau khi pull image xong. Bạn có thể kiểm tra danh sách các images bằng lệnh:
sudo docker images
Khi đã có image của Ubuntu, chúng ta khởi tạo một Docker container bằng lệnh:
sudo docker run --name virtual_server -it ubuntu:14.04 /bin/bash
Sau khi khởi tạo xong, bạn đã ở trong container, mặc định chúng ta đang ở quyền cao nhất rồi (root) nên các câu lệnh không cần prefix là
sudo
nữa nhé (nhưng ở môi trường thật nếu chúng ta chưa ở root thì vẫn cần phải có prefix này để thực hiện các câu lệnh liên quan đến hệ thống yêu cầu quyền super user).
Tiếp theo, chúng ta sẽ cài đặt một package giúp chúng ta có thể SSH vào container là OpenSSH server. Đầu tiên, update lại APT cache bằng lệnh:
apt-get update
Sau đó, cài OpenSSH server và VIM để cài thực hiện cài đặt nhé:
apt-get install -y vim openssh-server
Sau khi cài đặt xong, chúng ta sang phần cấu hình OpenSSH server để có thể SSH từ host vào container nhé. Trước khi sửa file cấu hình của OpenSSH, chúng ta cứ backup lại một bản cho chắc:
cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
Thực hiện sửa file sshd_config
bằng lệnh vim /etc/ssh/sshd_config
và sửa các cài đặt sang các giá trị sau:
Note: Trong VIM, bạn có thể search bằng cách dùng
/<pattern|keyword|string>
để search. Ví dụ:/MaxAuthTries
MaxAuthTries 3 # Bạn có thể thay đổi sang số khác, nếu thích
RSAAuthentication yes
PubkeyAuthentication yes
ChallengeResponseAuthentication no
PasswordAuthentication no
UsePAM no
Thoát VIM và khởi động lại SSH server bằng lệnh:
service ssh restart
Tạo thư mục .ssh
để chứa publickey của máy mà chúng ta có thể SSH lên:
ssh-keygen -t rsa
# Sau đó bạn cứ enter cho đến hết (nếu muốn :D)
Vào thư mục ~/.ssh
và tạo một file với tên authorized_keys
để chứa publickey với lệnh:
touch authorized_keys && chmod 600 authorized_keys
Add publickey của host vào authorized_keys
của container bằng cách bước sau:
- Host:
cat ~/.ssh/id_rsa.pub
và copy đoạn SSH key
- Container
vi ~/.ssh/authorized_keys
và paste đoạn SSH của host vào
Chúng ta thử SSH từ host vào container nhé. Để xem IP của container, chúng ta có thể sử dụng ifconfig
trong container hoặc lệnh sau trên host:
sudo docker inspect virtual_server | grep IPAddress
Của mình là 172.17.0.2
(còn của bạn bạn có thể sẽ khác, nếu có nhiều container đã được khởi tạo trước đó). Thử SSH vào xem sao:
ssh root@172.17.0.2
OK, thế là xong phần basic. Bây giờ chúng ta đi cài đặt các thành phần cần thiết để có thể chạy được một ứng dụng Rails là Ruby, MySQL, NginX, ... nhé. Chúng ta thực hiện cài đặt các packages cần thiết như khi làm việc với một server thật là không dùng tới tab access vào docker container trước mà chúng ta sử dụng tab mới vừa SSH ở trên để thực hiện nhé !
Do chúng ta tạo container từ image nên bạn luôn phải giữ cho Docker container được up. Mọi công việc khác trên host nên làm ở tab terminal khác và không được terminate tab Docker hiện tại.
Cài đặt Ruby
Chúng ta sử dụng RVM cho tiện nhé. Cài đặt CURL (nếu chưa có - chắc chắn là image 14.04 này chưa có package CURL đâu ):
apt-get install -y curl
Cài đặt RVM (theo trang chủ của RVM.io):
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
curl -sSL https://get.rvm.io | bash -s stable
Sau khi cài đặt xong, chúng ta thực hiện lệnh: source /usr/local/rvm/scripts/rvm
để có thể dùng luôn RVM trên terminal hiện tại. Ngoài ra, chúng ta nên thêm đoạn đó vào file ~/.bashrc
, ~/.profile
hoặc ~/.bash_profile
để lần sau chúng ta khởi động và access vào container là có thể sử dụng luôn RVM bằng lệnh:
echo source /usr/local/rvm/scripts/rvm | tee -a ~/.bashrc
Sau khi cài đặt xong RVM, chúng ta thực hiện lệnh rvm requirements
để RVM kiểm tra và cài đặt các packages cần thiết để có thể cài đặt Ruby.
Sau khi RVM cài đặt xong các packages cần thiết, chúng ta cài Ruby (mình cài bản 2.3.1) bằng lệnh:
rvm install ruby-2.3.1
Tiếp theo, chúng ta cài Bundler
và Rails
bằng lệnh:
gem install bundler
gem install rails
Đến đây là chúng ta xong phần cài đặt Ruby, giờ chúng ta chuyển sang cài đặt các thành phần khác như NginX, MySQL nhé
Cài đặt MySQL
Để cài đặt MySQL, ở trên môi trường server có lẽ bạn không cần phải đi qua các bước như với Docker image 14.04 này. Mình sử dụng MySQL 5.6 nhé (nhưng do image này chỉ có bản 5.5).
apt-get install -y software-properties-common python-software-properties libmysqlclient-dev
add-apt-repository 'deb http://archive.ubuntu.com/ubuntu trusty universe'
apt-get update
apt-get install mysql-server-5.6
Sau khi cài đặt xong MySQL, chúng ta start MySQL bằng lệnh: service mysql start
hoặc /etc/init.d/mysql start
. Sau khi bật MySQL service, chúng ta thử xem mọi thứ có hoạt động không bằng lệnh sau:
mysql -uroot -p -e "show databases;"
Sau đó bạn nhập password khi cài đặt. Chúng ta tiếp tục sang phần cài đặt NginX, Git, Node.JS và NPM nhé !
Cài đặt NginX, Git, Node.JS và NPM
Cài đặt Git
Chúng ta đi cài đặt Git để có thể pull source code khi deploy nhé.
apt-get install git
Sau khi cài xong, bạn có thể kiểm tra version của git bằng lệnh git --vesion
Cài đặt Node.JS và NPM
Node.JS và NPM để dùng khi precompile assets trong Rails.
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
Sau đó, cài Node.JS (bao gồm cả NPM) bằng lệnh:
apt-get install nodejs
Sau khi cài đặt xong, bạn thử một trong hai lệnh sau để kiểm tra: nodejs -v
hoặc node -v
. Nếu lệnh node -v
không ra kết quả, bạn có thể dùng lệnh sau để tạo symlink (vì có một số package dùng node
chứ không dùng nodejs
):
ln -s `which nodejs` /usr/local/bin/node
Tiếp theo, chúng ta sẽ cài NginX để chạy ứng dụng Rails của chúng ta nhé
Cài đặt NginX
Thêm repository của NginX:
add-apt-repository ppa:nginx/stable
Sau đó, update lại APT cache:
apt-get update
Cài đặt NginX:
apt-get install nginx
Sau khi cài đặt xong, bật NginX bằng lệnh: service nginx start
và bạn có thể kiểm tra bằng lệnh: curl localhost
hoặc dùng browser truy cập vào IP của Docker container.
OK, đã xong phần cài đặt server. Giờ chúng ta sang phần cài đặt trong source code để có thể auto deploy nhé.
Do chúng ta cài đặt container từ image nên khi bạn start lại Docker container, bạn cần vào container bật các dịch vụ như NginX, MySQL, SSH. Các services này không startup cùng container khi nó được start Các service cần bật:
service ssh start /etc/init.d/mysql start service nginx start
Cài đặt source code
Bạn có thể áp dụng với một project có sẵn (hoặc có thể tạo mới để kiểm thử). Chúng ta cùng đi vào các bước chi tiết nhé. Đầu tiên, bạn thêm các gem sau vào file Gemfile
của project:
gem "capistrano"
gem "capistrano-rails"
gem "capistrano3-unicorn"
gem "unicorn"
gem "capistrano-rvm"
Sau đó, chạy lệnh bundle install
để cài đặt các gem mới. Sau khi đã thêm các gem cần thiết xong, chúng ta sang phần cài đặt cho Capistrano nhé. Để khởi tạo file cài đặt cho Capistrano, bạn thực hiện lệnh dưới đây:
bundle exec cap install
Sau khi Capistrano đã tạo ra các file và thư mục cần thiết, bạn hãy mở file Capfile
và thực hiện các cài đặt sau:
# Uncomment các require sau
require "capistrano/rvm"
require "capistrano/bundler"
require "capistrano/rails/assets"
require "capistrano/rails/migrations"
# Thêm require sau
require "capistrano3/unicorn"
Xong, bạn có thể thử xem đã có tasks của Unicorn trong Capistrano chưa bằng lệnh: bundle exec cap -T | grep unicorn
. Tiếp theo, chúng ta mở file config/deploy.rb
để cài đặt một chút nhé.
set :application, "test_deploy_v3"
set :repo_url, "git@github.com:namnv609/test-deploy-ruby-v3.git"
set :bundle_binstubs, nil
# Default deploy_to directory is /var/www/my_app_name
set :deploy_to, "/var/www/html/#{fetch(:application)}"
# Default value for :linked_files is []
set :linked_files, fetch(:linked_files, [])
.push("config/database.yml", "config/secrets.yml")
# Default value for linked_dirs is []
set :linked_dirs, fetch(:linked_dirs, [])
.push("log", "tmp/pids", "tmp/cache", "tmp/sockets", "public/system", "vendor/bundle")
# Default value for keep_releases is 5
set :keep_releases, 5
after "deploy:publishing", "deploy:restart"
# Khởi động lại unicorn sau khi deploy
namespace :deploy do
task :restart do
invoke "unicorn:restart"
end
end
Mình giải thích một chút nhé:
:application
là tên ứng dụng sẽ deploy:repo_url
là Github repository URL:deploy_to
là thư mục sẽ chứa code deploy:linked_files
là các file dùng chung cho các bản deploy nhưsecrets.yml
,.env
,database.yml
, ...:linked_dirs
là các thư mục dùng chung cho các bản deploy:keep_releases
là số lượng bản deploy sẽ giữ lại. Tương đương với số lần bạn có thể rollback lại
Sau đó, bạn mở file config/deploy/staging.rb
(bạn có thể sửa file production.rb
hoặc có thể tạo các stage khác ví dụ như aws_staging.rb
, testing.rb
, ... nếu bạn muốn, trong ví dụ này thì mình dùng file staging.rb
) và thêm/sửa các dòng sau:
set :user, "root"
set :deploy_via, :remote_cache
set :conditionally_migrate, true
set :rails_env, "staging"
# Phần IP thì bạn thay thế cho phù hợp với IP của Docker container nhé
server "172.17.0.2", user: fetch(:user), port: fetch(:port), roles: %w(web app db)
Trong phần trên, mình giải thích sơ qua một chút. :user
là user sẽ sử dụng để deploy trên server (trong ví dụ này là root
, ngoài thực tế có thể là user khác), :roles
các các roles mà Capistrano sẽ sử dụng, nếu bạn có nhiều hơn một server, thì chỉ cần một server có chứa role db thôi (vì nhiều server cũng chỉ chung nhau một database và chỉ cần chạy migrate trên một server là đủ), ví dụ:
server "172.17.0.2", user: fetch(:user), port: fetch(:port), roles: %w(web app db)
server "172.17.0.3", user: fetch(:user), port: fetch(:port), roles: %w(web app)
Sau khi xong, bạn thử xem Capistrano có thể tương tác với server được hay chưa bằng lệnh:
bundle exec cap staging deploy:check
Sẽ có một số lỗi liên quan đến việc Capistrano không tìm thấy các file được khai báo trong :linked_files
, bạn cần phải vào server và tạo các file đó, ở đây là chúng ta sẽ tạo hai files là secrets.yml
và database.yml
trong thư mục /var/www/html/#{fetch(:application)}/shared/config/
với nội dung là các cài đặt riêng cho môi trường staging. Sau khi xong, bạn có thể thử lại lệnh trên và xem kết quả !
Tiếp, chúng ta sẽ tạo thư mục unicorn
trong thư mục config
để chứa các cài đặt cho Unicorn theo từng stage nhé. Các cài đặt này giúp cho việc sẽ không bị delay ảnh hưởng đến người dùng trong quá trình deploy. Ở đây, chúng ta sẽ tạo file config/unicorn/staging.rb
và thêm vào nội dung sau:
app_path = "/var/www/html/test_deploy_v3/current"
working_directory app_path
pid "#{app_path}/tmp/pids/unicorn.pid"
stderr_path "#{app_path}/log/unicorn.err.log"
stdout_path "#{app_path}/log/unicorn.out.log"
worker_processes 3
timeout 30
preload_app true
listen "#{app_path}/tmp/sockets/unicorn.sock", backlog: 64
before_exec do |_|
ENV["BUNDLE_GEMFILE"] = File.join(app_path, "Gemfile")
end
before_fork do |server, worker|
defined?(ActiveRecord::Base) and ActiveRecord::Base.connection.disconnect!
old_pid = "#{app_path}/tmp/pids/unicorn.pid.oldbin"
if File.exists?(old_pid) && server.pid != old_pid
begin
Process.kill("QUIT", File.read(old_pid).to_i)
rescue Errno::ENOENT, Errno::ESRCH
end
end
end
after_fork do |server, worker|
defined?(ActiveRecord::Base) and ActiveRecord::Base.establish_connection
end
Trong file trên, bạn chỉ cần quan tâm đến app_path
và sửa sao cho đúng với đường dẫn trên server là được nhé.
Tiếp theo, chúng ta tạo một file trong thư mục /etc/init.d/
trên server để có thể start/restart/reload/stop ứng dụng Rails của chúng ta từ terminal nhé. Tạo file với tên tùy chọn (trong trường hợp này mình đặt là unicorn_deploy
):
vi /etc/init.d/unicorn_deploy
Sau đó, thêm đoạn sau:
#!/bin/sh
set -u
set -e
# Example init script, this can be used with nginx, too,
# since nginx and unicorn accept the same signals
#[[ -s '/usr/local/rvm/scripts/rvm' ]] && source '/usr/local/rvm/scripts/rvm'
# Feel free to change any of the following variables for your app:
USER=root
GEM_HOME="/var/www/html/test_deploy_v3/shared/bundle"
APP_ROOT="/var/www/html/test_deploy_v3/current"
SET_PATH="export GEM_HOME=$GEM_HOME"
PID="$APP_ROOT/tmp/pids/unicorn.pid"
ENV="staging"
CMD="$SET_PATH; cd $APP_ROOT && bundle exec unicorn -D -E $ENV -c $APP_ROOT/config/unicorn/$ENV.rb"
old_pid="$PID.oldbin"
#cd $APP_ROOT || exit 1
$SET_PATH || exit 1
sig () {
test -s "$PID" && kill -$1 `cat $PID`
}
oldsig () {
test -s $old_pid && kill -$1 `cat $old_pid`
}
case $1 in
start)
sig 0 && echo >&2 "Already running" && exit 0
su - $USER -c "$CMD"
;;
stop)
sig QUIT && exit 0
echo >&2 "Not running"
;;
force-stop)
sig TERM && exit 0
echo >&2 "Not running"
;;
restart|reload)
sig HUP && echo reloaded OK && exit 0
echo >&2 "Couldn't reload, starting '$CMD' instead"
su - $USER -c "$CMD"
;;
upgrade)
sig USR2 && echo upgraded OK && exit 0
echo >&2 "Couldn't upgrade, starting '$CMD' instead"
su - $USER -c "$CMD"
;;
rotate)
sig USR1 && echo rotated logs OK && exit 0
echo >&2 "Couldn't rotate logs" && exit 1
;;
*)
echo >&2 "Usage: $0 <start|stop|restart|upgrade|rotate|force-stop>"
exit 1
;;
esac
Ở đoạn trên, bạn cần quan tâm các biến sau:
USER
là user sẽ sử dụng để chạy ứng dụng, giống với user trongconfig/deploy/<stage>.rb
GEM_HOME
là thư mục chứa Gem của ứng dụng, bạn sửa lại đường dẫn cho phù hợpAPP_ROOT
là thư mục chứ ứng dụng, bạn sửa lại cho phù hợpENV
là môi trường của ứng dụng, bạn sửa lại cho phù hợp
Sau khi lưu lại, bạn cần cấp quyền thực thi (executable) cho file đó với lệnh:
chmod +x /etc/init.d/unicorn_deploy
Vậy là xong, từ giờ, bạn có thể start/restart/reload/stop ứng dụng Rails từ terminal với lệnh: /etc/init.d/unicorn_deploy <start|stop|reload|restart>
rồi ! Chúng ta sang phần cài đặt NginX trên server nhé. Đầu tiên, bạn vào thư mục /etc/nginx/sites-available/
và backup lại file default
trước khi sửa đổi:
cp default default.bak
Sau đó, sửa file default
với nội dung sau:
upstream test_deploy_v3 {
server unix:/var/www/html/test_deploy_v3/current/tmp/sockets/unicorn.sock fail_timeout=0;
}
server {
listen 80 default deferred;
# server_name example.com;
root /var/www/html/test_deploy_v3/current/public;
location ^~ /assets/ {
gzip_static on;
expires max;
add_header Cache-Control public;
}
location ~ ^/(robots.txt|sitemap.xml.gz)/ {
root /var/www/html/test_deploy_v3/current/public;
}
try_files $uri/index.html $uri @test_deploy_v3;
location @test_deploy_v3 {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://test_deploy_v3;
}
error_page 500 502 503 504 /500.html;
client_max_body_size 4G;
keepalive_timeout 10;
}
Bạn cần phải sửa lại các đường dẫn đến thư mục chứa project sao cho phù hợp nhé. Lưu file lại và khởi động lại NginX bằng lệnh:
service nginx restart
OK, bây giờ bạn push source code lên Github và thử bắt đầu deploy nhé. Chạy lệnh sau trong thư mục project trên host:
bundle exec cap staging deploy
Sau khi deploy xong, bạn có thể truy cập vào IP của Docker container bằng trình duyệt để kiểm tra xem ứng dụng của chúng ta đã deploy thành công hay chưa.
Nếu bạn có sửa source code (thay đổi config, ...) trực tiếp trên server thì bạn cần phải khởi động lại Unicorn để những thay đổi đó có tác dụng. Bạn có thể dùng lệnh sau:
kill -USR2 `cat <deploy_to path>/shared/tmp/pids/unicorn.pid`
Đến đây, bài viết giới thiệu về auto deployment cho Rails đã kết thúc. Các bạn có thắc mắc hay gặp vấn đề gì thì để lại comment, chúng ta cùng bàn luận nhé ! Hẹn gặp lại mọi người trong các bài viết tiếp theo. Thân!
Link Github project: https://github.com/namnv609/test-deploy-ruby-v3
Original post: https://namnv609.cf/posts/zero-downtime-deployment-for-rails-with-capistrano-and-unicorn.html
All Rights Reserved