今回はcapistranoでの自動デプロイの導入方法を備忘録のためまとめていきたいと思います。前回の記事からの続きとなります。
前提
手動でのデプロイができていることが前提の記事となります。また、細かい設定やインストールしているパッケージなどは下記事に手動でのデプロイ方法としてまとめてあるので、そちらからご確認いただけます。
https://qiita.com/shun0211/items/21871c82d385648b4bae
環境
- Amazon Linux2 (無料枠)
- RDS (MySQL8.0)
- capistrano 3.14.1
- Ruby 2.7.1
- Rails 6.0.3.4
- Unicorn 5.7.0
- Nginx 1.12.2
Gemのインストール
group :development, :test do
gem 'capistrano'
gem 'capistrano-rbenv'
gem 'capistrano-bundler'
gem 'capistrano-rails'
gem 'capistrano3-unicorn'
end
$ bundle install
$ bundle exec cap install
これにて、自動デプロイのための設定ファイルが作成されます
live_share
├ Capfile
├─ config
│ ├─ deploy
│ │ ├─production.rb #自動デプロイ時の本番環境での設定
│ │ └─staging.rb
│ └─deploy.rb #自動デプロイ時の共通設定
└─ lib
└─capistrano
└─tasks
Capistranoで自動デプロイをすると、サーバー上のディレクトリ構造は以下のようになります。(今回の設定の場合)
live_share
├─ current # 最新のデプロイしたフォルダやファイルが置かれる
├─ shared # 共通のファイルが置かれる
│ ├─ bundle
│ ├─ config
│ ├─ log
│ ├─ public
│ ├─ temp
│ └─ vendor
└─ release # 過去にデプロイしたフォルダやファイルが置かれる
見て分かるように、階層構造が一段深くなっているので、NginxやUnicornの設定ファイルも修正する必要があります。
各ファイルの編集
まずは、Capfile, config/deploy.rb, config/deploy/production.rbのファイルを編集します。
require "capistrano/setup"
require "capistrano/deploy"
require "capistrano/scm/git"
install_plugin Capistrano::SCM::Git
require "capistrano/rbenv"
require "capistrano/bundler"
require "capistrano/rails/assets"
require "capistrano/rails/migrations"
require 'capistrano3/unicorn'
Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }
# caipstranoのバージョンを記載します。バージョンはGemfile.lockに書いてあります。
lock "~> 3.14.1"
set :application, "conefan"
# pullしてくるgitのURLを書きます。
set :repo_url, "git@github.com:shun0211/live_share.git"
# デフォルトのブランチはmasterになっているので、mainに変更します。
set :branch, "main"
# デプロイするディレクトリを指定します。
set :deploy_to, "/var/www/rails/live_share"
# capistranoではデプロイ後にバグが起きた場合、デプロイ前の状態に戻れるようにデプロイ前のファイルをrelesaseフォルダに入れます。その際、いくつ前のバージョンまで残しておくのかをここで設定します。
set :keep_releases, 2
# ssh接続をする際に必要な設定を書きます。
set :ssh_options, {
# capistranoコマンド実行者の秘密鍵
port: 22,
keys: %w(~/.ssh/live_share_key_rsa),
forward_agent: true,
auth_methods: %w(publickey)
}
# Railsがproduction.keyを参照するためのシンボリックリンクを貼る記述をします。production.keyについては後述
append :linked_files, 'config/credentials/production.key'
# 同じくシンボリックリンクを貼るフォルダを指定します。記載したフォルダがshared下に作られます。
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system', 'public/uploads')
set :rbenv_type, :user
set :rbenv_ruby, '2.7.1'
set :unicorn_pid, -> { "#{shared_path}/tmp/pids/unicorn.pid" }
set :unicorn_config_path, -> { "#{current_path}/config/unicorn.conf.rb" }
after 'deploy:publishing', 'deploy:restart'
namespace :deploy do
task :restart do
invoke 'unicorn:restart'
end
end
server "18.178.91.188", user: "ec2-user", roles: %w{web app db}
また、前述したようにディレクトリ構造の変更により、NginxやUnicornの設定ファイルを変更する必要があるので、編集します。前回の記事ではUnicornの設定ファイルはサーバー上にしか書いていないので、サーバーにログインしてファイルの中身をローカルに持ってきて編集します。(たくさん試行錯誤をしていたので、前回の記事と書き方が異なっています)
Unicorn設定ファイル
#サーバ上でのアプリケーションコードが設置されているディレクトリを変数に入れておく
app_path = File.expand_path('../../../', __FILE__)
worker_processes 2
working_directory "#{app_path}/current" #currentにします
stderr_path "#{app_path}/shared/log/unicorn.stderr.log" #sharedにします
stdout_path "#{app_path}/shared/log/unicorn.stdout.log" #sharedにします
timeout 30
listen "#{app_path}/shared/tmp/sockets/.unicorn.sock" #sharedにします .unicorn.sockの.(ドット)を忘れずに
pid "#{app_path}/shared/tmp/pids/unicorn.pid" #sharedにします
preload_app true
GC.respond_to?(:copy_on_write_friendly=) && GC.copy_on_write_friendly = true
check_client_connection false
run_once = true
before_exec do |server|
ENV['BUNDLE_GEMFILE'] = "#{app_path}/current/Gemfile" #currentにします
end
before_fork do |server, worker|
defined?(ActiveRecord::Base) &&
ActiveRecord::Base.connection.disconnect!
if run_once
run_once = false
end
# 古いプロセスがあった場合はkillする処理
old_pid = "#{server.config[:pid]}.oldbin"
if File.exist?(old_pid) && server.pid != old_pid
begin
sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU
Process.kill(sig, File.read(old_pid).to_i)
rescue Errno::ENOENT, Errno::ESRCH => e
logger.error e
end
end
end
after_fork do |_server, _worker|
defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection
end
Nginx設定ファイル
error_log /var/www/rails/live_share/shared/log/nginx.error.log; #sharedにします
access_log /var/www/rails/live_share/shared/log/nginx.access.log; #sharedにします
upstream unicorn_server {
server unix:/var/www/rails/live_share/shared/tmp/sockets/.unicorn.sock fail_timeout=0; #sharedにします
}
server {
listen 80;
client_max_body_size 4G;
server_name conefan.com; #サーバー名
keepalive_timeout 5;
root /var/www/rails/live_share/current/public; #currentにします
location ~ ^/assets/ {
root /var/www/rails/live_share/current/public; #currentにします
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://unicorn_server;
break;
}
}
error_page 500 502 503 504 /500.html;
}
設定ファイルを再読み込みし再起動します。
$ sudo service nginx reload
$ sudo service nginx restart
秘密鍵をDockerコンテナ上に配置
筆者は開発環境にDockerを使っていたので、Dockerコンテナ内の~/.sshディレクトリ内にEC2にログインするための秘密鍵を置かなければいけません。
$ docker container cp ~/.ssh/<秘密鍵名> <コンテナ名>:/root/.ssh
これでコンテナ内に秘密鍵が設置できたので、実際に入ってコンテナ内からEC2へログインできるか確認します。
$ docker exec -it <コンテナ名> bash
# コンテナ内で以下コマンド
$ ssh -i ~/.ssh/<秘密鍵> ec2-user@18.178.91.188
今後、ビルドし直す度に秘密鍵をコピーするのがめんどくさいため、volume化して~/.sshディレクトリを共有します。(.ssh内に他の秘密鍵がある場合はディレクトリを一段深くしてそこをvolume化したほうがいいかもしれません。)
web:
# 省略
volumes:
- ~/.ssh:/root/.ssh
# 省略
環境変数の設定
また、database.yml内で環境変数を使っているので、サーバー内でも設定しないといけません。前回の記事ではdotenvのGemを使っていたのですが、自動デプロイのときにはうまくいかなかったので、サーバー内の環境変数を設定します。(gotenvは本番環境では非推奨らしいです。)
DB_NAME=データベース名_production #RDSで作成したDB名
DB_USERNAME=root #RDSのユーザー名
DB_PASSWORD=********* #RDSのパスワード
DB_HOSTNAME=***.ap-northeast-1.rds.amazonaws.com # RDSのエンドポイント
環境変数が設定されているか確認
$ source .env
$ echo $DB_NAME
$ echo $DB_USERNAME
$ echo $DB_PASSWORD
$ echo $DB_HOSTNAME
環境変数の設定はcredentials.yml.encを使ってやる方法もあるみたいです。
production環境用にCredentialsの設定
手動デプロイとは異なり、production環境用のcredentialsの設定が必要になります。
$ docker-compose run -e EDITOR=vim web rails credentials:edit --environment production
このコマンドを打つことによりconfig/credentialsディレクトリが作られ、その中にproduction.keyとproduction.yml.encが作られます。デフォルトではsecret_key_baseは生成されないので自分で設定を行います。下記コマンドでヘルプが見れます。
$ bin/rails credentials:edit --help
その後、生成されたproduction.keyをデプロイ先のサーバにコピーします。コピー先はshared/config/credentialsディレクトリに行います。
$ scp -i ~/.ssh/live_share_key_rsa production.key ec2-user@18.178.91.188:/var/www/rails/live_share/shared/config/credentials
ここまで来たらmainブランチにローカルで編集した設定の変更をマージしてデプロイを走らせます。
docker-compose run --rm web bundle exec cap production deploy
最後までデプロイが走れば成功です。お疲れさまでした!
何度もエラーが出ると思いますが、一個ずつ解決していけば必ずできるのでとにかく諦めないことが重要だと感じました。
ここからは筆者がハマったエラーについてまとめます。
エラーについて
ssh接続エラー
コンテナ内に鍵を設置してssh接続できており、設定もまちがってないのに下のようなエラーが出る場合は一旦コンテナをビルドし直したほうがいいかもしれません。筆者はビルドし直すことでエラーがでなくなりました。
Net::SSH::AuthenticationFailed: Authentication failed for ~~~
Gem::LoadError
下エラーが出る場合はエラーログにもありますが、ed25519とbcrypt_pbkdfのGemをインストールすることで解決しました。
Gem::LoadError : "ed25519 is not part of the bundle. Add it to your Gemfile."
Node.jsバージョンエラー
node.jsのバージョンが古い場合にエラーとなります。しかし、筆者の場合サーバー上で確認してもnodeのバージョンは10.21.0でした。なぜ6.17.1が使われているのかはなぞでしたが、新しいバージョンを再度インストールすることで解決しました。
The engine "node" is incompatible with this module. Expected version ">=8.16.0". Got "6.17.1"
# npmでnをインストール
$ sudo npm install n -g
# 安定版の最新node.jsをインストール
$ sudo n stable
バージョン確認
$ node -v
参考
https://qiita.com/gyu_outputs/items/c960c903e7bc6f684a44
https://qiita.com/tatama/items/aaa1300f55da5da2933a
https://loumo.jp/archives/24419