이 글에서는 우분투 서버에 레일즈 프로덕션(Production) 어플리케이션을 배포(Deployment)하는 법을 단계별로 기술한다. 사용된 환경 및 툴은 다음과 같다.
  • Ubuntu 14.04 Server
  • RVM 1.26.x
  • Ruby 2.2
  • Rails 4.2
  • Capistrano 3.4
  • Nginx
  • Unicorn
  • Postgresql 9.3
프로젝트 준비
project 라는 이름의 프로젝트를 준비한다.
Github.com 연결
프로젝트 저장공간(Repository) 생성 git@github.com:zooliet/project.git 로컬 머신 유저에 대한 ssh key 등록 ssh-keygen -t rsa 실행 후 ~/.ssh/id_rsa.pub 내용을 github.com에 등록
ssh git@github.com 을 실행해서 github.com에서 이 클라이언트 머신을 인지하도록 함
주1. 머신 당 최초 한번만 실행 git 초기화 cd project
git init
git remote add origin git@github.com:zooliet/project.git .gitignore 소스 콘트롤에서 제외할 파일 목록 기재
주1. 레일즈 4.2 부터는 database.yml, secrets.yml을 소스 콘트롤하에 두어도 문제없음 첫번째 커밋과 체크인 git add -A
git commit -m 'First commit'
git push -u origin master
서버 확인
OS 버전 Ubuntu 14.04 LTS 64 bit (Ubunut disto codename: trusty)
주1. cat /etc/lsb-release
주2. uname -a 최소 패키지 설치 sudo apt-get install openssh-server git curl 'deploy' 유저 생성 sudo adduser deploy -ingroup sudo
주1. 서버의 모든 작업은 이 계정을 통해서 진행 'deploy' 유저에 대한 패스워드리스(Passwordless) sudo 설정 sudo EDITOR=vim visudo
주1. Capistrano 3 부터는 원격 액션(remote action)에 대해서 패스워드 프롬프트를 제공하는 기능이 사라져서 sudo 명령시 패스워드를 입력할 방법이 없음
주2. deploy 사용자에 대해 NOPASSWD를 설정하는 것으로 대응
로컬 머신 유저의 ssh key 등록 [Local] cat ~/.ssh/id_rsa.pub | ssh deploy@xxx 'cat >> ~/.ssh/authorized_keys'
주1. 로컬 머신 사용자의 공개 키(public key)를 서버 계정(=deploy)에 옮기는 절차임
주2. 로컬 머신 사용자가 서버의 원격 사용자(=deploy) 계정으로 접속시 패스워드를 생략할 수 있음
주3. [Local] brew install ssh-copy-id && ssh-copy-id deploy@xxx를 사용해도 됨 ssh 에이전트 설정 ssh-add -K
주1. ssh 에이전트는 github.com을 사용할 때 이 서버의 사용자(=deploy)가 아닌 로컬 머신의 사용자로서 접근할 수 있게하는 장치임
주2. ssh git@github.com을 최초 한번 실행해야 함 로케일(Locale) 설정 sudo apt-get install language-pack-ko
sudo vi /etc/default/locale
LANG="ko_KR.UTF-8"
LC_CTYPE="ko_KR.UTF-8"
LANGUAGE="ko_KR:kr"
#LANG="en_US.UTF-8"
#LC_CTYPE="en_US.UTF-8"
#LANGUAGE="en_US:en"
주1. 재부팅 필요
주2. locale -a 쉘(Shell) 초기화 파일 vi ~/.bash_profile
export RAILS_ENV=production
vi ~/.bashrc
source ~/.bash_profile
주1. ssh 로그시 .bash_profile 만 실행
주2. 카피스트라노 태스크(capistrano task)시 .bashrc가 먼저 실행된 뒤에 .bash_profile가 실행
[서버] RVM과 루비 설치
RVM curl -L https://get.rvm.io | bash -s stable
rvm requirements
주1. rvm --version
주2. rvm 자체 업데이트는 rvm get stable 루비 설치 rvm install 2.2
rvm use 2.2 --default
rvm current
figaro 젬(gem)과 환경 변수 관리
figaro 설치 vi Gemfile
gem 'figaro'
bundle install; figaro install
주1. figaro install에 의해 application.yml이 생성되고 자동으로 .gitignore에 등록 figaro 사용 vi config/application.yml
secret_key_base: '12ab32c...720dfa34'
vi config/secrets.yml
production:
  secret_key_base: Figaro.env.secret_key_base
주1. application.yml 파일에서 yaml 형식으로 환경 변수를 설정
주2. 환경 변수가 필요한 곳에서 Figaro.env를 통해 값을 가져옴
주3. application.yml은 소스 콘트롤 관리 대상에서 제외해야 하며 별도의 과정을 통해 서버로 업로드
카피스트라노(Capistrano) 3 설치 및 셋업
vi Gemfile

group :development do
  # gem 'capistrano'
  gem 'capistrano-rails'
  gem 'capistrano-bundler'
  gem 'capistrano-rvm'
  gem 'highline', '~> 1.7.2'
end
bin/bundle install
bin/bundle exec cap install
# 디렉토리 구조
Capfile
config/
│__ deploy/
│   │__ templates/*.erb, *.yml
│   |__ production.rb
│   |__ staging.rb
│  
│__ deploy.rb
│ 
lib/capistrono/tasks/*.rake
vi Capfile

require 'capistrano/setup'
require 'capistrano/deploy'
require 'capistrano/rails'
require 'capistrano/bundler'
require 'capistrano/rvm'
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
vi config/deploy.rb

lock '3.4.0'
set :application, 'project'
set :repo_url, 'git@github.com:zooliet/project.git'
ask :branch, proc { `git rev-parse --abbrev-ref HEAD`.chomp }.call
set :deploy_to, "/home/deploy/apps/#{fetch(:application)}"
# set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/application.yml')
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system')
set :rvm_type, :auto
set :rvm_ruby_version, '2.2'
namespace :deploy do
  ...
end
lock '3.4.0'
카피스트라노 버전으로써 일치하지 않으면 오류가 발생 (주석 처리 시 검사 안함)
ask :branch, proc { ... }
디플로이(deploy)시 git 브랜치 이름을 사용자가 입력하도록 함 (주석 처리 시 master 브랜치 사용)
set :linked_files, [...]
이곳에 지정된 파일을 shared/에서 찾아서 current/에서 심볼릭 링크한다.
set :linked_dirs, [...]
이곳에 지정된 디렉토리를 shared/에 생성한 후 current/에서 심볼릭 링크한다. 가령, public/uploads를 지정하게 되면 일단 shared/public/uploads/를 생성한 후 current/public/uplodads/는 이곳으로 링크를 걸어서 사용하게된다. 이렇게 함으로써 매 디플로이먼트마다 디렉토리의 내용이 변경되는 것을 방지할 수 있다.
set :rvm_ruby_version, '2.2'
서버의 루비 버전으로써 일치하지 않으면 오류가 발생 (주석 처리 시 검사 안함)
vi config/deploy/production.rb

server 'xxx.com', user: 'deploy', roles: %w{web app db}
# server '11.22.33.44', user: 'deploy', roles: %w{web app db}
bin/bundle exec cap production deploy:check 를 실행해서 서버의 ./apps/에 다음과 같은 디렉토리가 만들어지는지 확인한다.
 # /home/deploy/apps/ 구조
 project
├── releases
└── shared
    ├── log
    ├── public
    │   ├── assets
    │   └── system
    ├── tmp
    │   ├── cache
    │   ├── pids
    │   └── sockets
    └── vendor
        └── bundle
기본 패키지 설치
vi lib/capistrano/tasks/base.rake

namespace :deploy do
  desc "Install basic packages"
  task :install do
    on roles(:all), in: :groups, limit: 3, wait: 10 do |host|
      packages = %w(
        build-essential openssl libreadline6 libreadline6-dev
        wget zlib1g zlib1g-dev libssl-dev libyaml-dev libsqlite3-0
        libsqlite3-dev sqlite3 libxml2-dev libxslt-dev autoconf libc6-dev
        ncurses-dev automake libtool bison python-software-properties
      )
      # packages.push("imagemagick", "logrotate")
      sudo "apt-get -y update"
      packages.each do |package|
        sudo "apt-get -y install #{package}"
      end
    end
  end
end
bin/bundle exec cap production deploy:install 을 입력해서 필요한 패키지들을 설치한다.
Postgresql 및 database.yml 레시피
vi Gemfile

group :development do
  ...
  gem 'capistrano-rvm'
  gem 'capistrano-postgresql'
  gem 'highline', '~> 1.7.2'
end
bin/bundle install
vi Capfile

...
require 'capistrano/postgresql'
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
vi config/deploy/production.rb

# Configuration
set :pg_user, 'deploy'
set :pg_ask_for_password, true
set :pg_pool, 25
set :pg_use_hstore, true
# set :pg_host, -> do
#  if release_roles(:all).count == 1 && release_roles(:all).first == primary(:db)
#    'localhost'
#  else
#    primary(:db).hostname
#  end
# end
vi lib/capistrano/tasks/postgresql.rake

namespace :postgresql do
  desc "Install the latest stable release of PostgreSQL."
  task :install do
    on roles(:db) do |host|
      sudo "apt-get -y update"
      sudo "apt-get -y install postgresql postgresql-contrib libpq-dev"
    end
  end
end
# after 'deploy:install', 'postgresql:install'
bin/bundle exec cap production postgresql:install
최신 버전의 PostgreSQL을 설치한다.
주1. 마지막 라인 주석을 제거하면 cap production deploy:install 실행 후 자동으로 실행된다.
bin/bundle exec cap production postgresql:setup
아래의 태스크들을 순차적으로 호출한다.
  • postgresql:create_db_user
  • postgresql:create_database
  • postgresql:add_hstore
  • postgresql:add_extensions
  • postgresql:generate_database_yml_archetype
  • postgresql:generate_database_yml
특히 마지막 태스크가 흥미로운데 여기서 서버의 shared/config/database.yml 파일을 생성한다. 이 파일은 cap production deploy 과정에서 after hook를 통해 호출되는 postgresql:database_yml_symlink에서 current/config/database.yml 링크를 생성한다.
주1. 이 동작 때문에 config/deploy.rb 파일의 :linked_files에 database.yml 지정이 필요없게 된다.
주2. bin/rails g capistrano:postgresql:template 를 실행하면 config/templates/postgresql.yml.erb 라는 템플릿 파일이 생성되는데 이 파일을 커스터마이즈하면 이 내용이 서버의 shared/config/datqbase.yml에 업로드 된다.
application.yml 레시피

namespace :figaro do
 desc "upload application.yml to shared_path/config/"
  task :setup do
    on release_roles :all do
      application_yml_file = shared_path.join('config/application.yml')
      execute :mkdir, '-pv', shared_path.join('config')
      upload! 'config/application.yml', application_yml_file
    end
  end

  task :application_yml_symlink do
    set :linked_files, fetch(:linked_files, []).push('config/application.yml')
  end

  after 'deploy:started', 'figaro:application_yml_symlink'
end

desc 'Server setup tasks'
task :setup do
  invoke "figaro:setup"
end
bin/bundle exec cap production figaro:setup
로컬의 config/application.yml이 서버의 shared/config/application.yml로 업로드된다.
bin/bundle exec cap production figaro:application_yml_symlink
deploy:started에 after hook에 의해 작동한다. 이 동작으로 인해 current/config/application.yml 심볼릭 링크 파일이 생성된다.
Nginx/Unicorn 레시피
vi Gemfile

gem 'unicorn'
group :development do
  ...
  gem 'capistrano-postgresql'
  gem 'capistrano-unicorn-nginx'
  gem 'highline', '~> 1.7.2'
end
bin/bundle install
vi Capfile

...
require 'capistrano/postgresql'
require 'capistrano/unicorn_nginx'
Dir.glob('lib/capistrano/tasks/*.rake').each { |r| import r }
vi config/deploy/production.rb

# Configuration
set :nginx_server_name, 'xxx.com'
# set :nginx_use_ssl, true

set :unicorn_workers, 4
vi lib/capistrano/tasks/nginx.rake

namespace :nginx do
  desc "Install latest stable release of nginx"
  task :install do
    on roles(:web) do |host|
      sudo "add-apt-repository ppa:nginx/stable  --yes"
      sudo "apt-get -y update"
      sudo "apt-get -y install nginx"
    end
  end

  %w[start stop restart].each do |command|
    desc "#{command} nginx"
    task command do
      on roles(:web) do |host|
        sudo "service nginx #{command}"
      end
    end
  end
end
# after 'deploy:install', 'nginx:install'
bin/bundle exec cap production nginx:install
최신 버전의 Nginx을 설치한다. 주1. 마지막 라인 주석을 제거하면 cap production deploy:install 실행 후 자동으로 실행된다.
bin/bundle exec cap production nginx:setup
이 태스크는 /etc/nginx/sites-available/project_production 파일을 업로드하고 이곳을 심볼릭 링크한 /etc/nginx/sites-enabled/project_production 파일을 생성한다.
주1. 업로드하는 파일은 디폴트로는 내부 템플릿으로 부터 동적으로 생성이 되는데 만약 내용을 커스터마이즈 하고 싶으면 bin/bundle exec rails g capistrano:unicorn_nginx:config를 실행후 생성하는 /config/deploy/templates/nginx_conf.erb 파일을 고치면 된다.
주2. 로컬의 config/deploy/production.rb의 nginx 관련 셋팅이 바뀔때 마다 nginx:setup을 수행한다.
bin/bundle exec cap production nginx:[start | stop | restart]
nginx를 시작, 중단, 재시작 할 때 사용한다.
bin/bundle exec cap production unicorn:setup_initializer
이 태스크는 /etc/init.d/unicorn_project_production 스크립트 파일을 업로드하고 update-rc.d를 실행해서 이 스크립트를 /etc/rc*.d/에 배치한다.
주1. 업로드하는 파일은 디폴트로는 내부 템플릿으로 부터 동적으로 생성이 되는데 만약 내용을 커스터마이즈 하고 싶으면 bin/bundle exec rails g capistrano:unicorn_nginx:config를 실행후 생성하는 /config/deploy/templates/unicorn_init.erb 파일을 고치면 된다.
bin/bundle exec cap production unicorn:setup_app_config
이 태스크는 app/project/shared/config/unicorn.rb 파일을 업로드한다. 이 파일은 unicorn 프로세스가 실행하면서 참조하게 된다.
주1. 업로드하는 파일은 디폴트로는 내부 템플릿으로 부터 동적으로 생성이 되는데 만약 내용을 커스터마이즈 하고 싶으면 bin/bundle exec rails g capistrano:unicorn_nginx:config를 실행후 생성하는 /config/deploy/templates/unicorn.rb.erb 파일을 고치면 된다.
bin/bundle exec cap production uincorn:[start | stop | restart]
unicorn을 시작, 중단, 재시작 할 때 사용한다. 특별히 unicorn:restart는 디플로이먼트 과정에서 발생하는 deploy:publishing에 after hook 되어있다.
Node.js 설치 레시피
레일즈의 커피스크립트(coffeescript)와 자바스크립트 파일을 압축하는 uglifier는 자바스크립트 런타임을 요구하게된다. OS X 에서는 자바스크립트 런타임이 내장되어 있으므로 별도의 설치가 필요없지만 프로덕션 환경이 실행되는 Linux에서는 별도로 설치가 필요한다. therubyracer 젬 또는 node.js를 설치하면 자바스크립트 런타임이 같이 설치된다. vi lib/capistrano/tasks/nodejs.rake

namespace :nodejs do
  desc "Install the latest relase of Node.js"
  task :install do
    on roles(:app) do |host|
      sudo "add-apt-repository ppa:chris-lea/node.js --yes"
      sudo "apt-get -y update"
      sudo "apt-get -y install nodejs"
    end
  end
end

# after "deploy:install", "nodejs:install"
bin/bundle exec cap production nodejs:install
실행 순서
bin/bundle exec cap -T
가용한 명령어 전체 보기
bin/bundle exec cap install
로컬에 카피스트라노 관련 디렉토리 및 파일 생성
bin/bundle exec cap production deploy:install
  • bin/bundle exec cap production postgresql:install
  • bin/bundle exec cap production nginx:install
  • bin/bundle exec cap production nodejs:install
bin/bundle exec cap production setup
  • bin/bundle exec cap production postgresql:setup
  • bin/bundle exec cap production nginx:setup
  • bin/bundle exec cap production nginx:setup_ssl
  • bin/bundle exec cap production figaro:setup
  • bin/bundle exec cap production unicorn:setup_initializer
  • bin/bundle exec cap production unicorn:setup_app_config
  • bin/bundle exec cap production unicorn:setup_logrotate
bin/bundle exec cap production deploy
원격(Remote) 레일즈 콘솔 레시피
vi Gemfile; bin/bundle install

group :development do
  gem 'capistrano-rails-console'
end
vi Capfile

require 'capistrano/rails/console'
bin/bundle exec cap production rails:console
로컬에서 서버의 레일즈 콘솔을 사용할 수 있게 해준다.
Redis 레시피
vi config/deploy/production.rb

set :redis_maxmemory, '128MB'
vi lib/capistrano/tasks/redis.rake

namespace :redis do
  desc "Install the latest stable release of Redis."
  task :install do
    on primary :app do
      sudo "add-apt-repository 'deb http://packages.dotdeb.org stable all' --yes"
      sudo "wget -q -O - http://www.dotdeb.org/dotdeb.gpg | sudo apt-key add -"
      sudo "apt-get -y update"
      sudo "apt-get -y install redis-server"
    end
  end

  desc "Setup redis configuration for this application"
  task :setup do
    on primary :app do
      redis_maxmemory = fetch :redis_maxmemory
      sudo "cp /etc/redis/redis.conf /tmp"
      sudo %Q{sh -c "echo maxmemory #{redis_maxmemory} >> /tmp/redis.conf"}
      sudo "mv /tmp/redis.conf /etc/redis/redis.conf"
    end
  end

  %w[start stop restart].each do |command|
    desc "#{command} redis-server"
    task command do
      on primary :app do |host|
        sudo "service redis-server #{command}"
      end
    end
  end
end

after "deploy:install", "redis:install"
after "setup", "redis:setup"
after "redis:setup", "redis:restart"
원격 레이크 태스크(Remote Rake Task) 레시피
vi lib/capistrano/tasks/remote_rake.rake

namespace :deploy do
  namespace :rake do
    desc "Invoke rake task"
    task :invoke do
      on roles(:all), in: :sequence, wait: 5 do
        within current_path do
          execute(:rake, "RAILS_ENV=production #{ENV['task']}")
        end
      end
    end
  end
  # 사용법: cap production deploy:rake:invoke task=zoolu:dummy_test
end
cap production deploy:rake:invoke task=zoolu:dummy_test
메인터넌스 모드 레시피
vi Gemfile

gem 'capistrano-maintenance', github: "capistrano/maintenance", require: false
vi Capfile

require 'capistrano/maintenance'
bin/bundle exec cap production maintenance:enable
bin/bundle exec cap production maintenance:disable
SSL 설정 레시피

작성 중입니다

Sidekiq 레시피

작성 중입니다