【開発環境にDocker】Capistrano導入方法

今回は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のインストール
```ruby:gemfile
group :development, :test do
  gem 'capistrano'
  gem 'capistrano-rbenv'
  gem 'capistrano-bundler'
  gem 'capistrano-rails'
  gem 'capistrano3-unicorn'
end
```
```bash:terminal
$ bundle install
$ bundle exec cap install
```
これにて、自動デプロイのための設定ファイルが作成されます

```bash
live_share
├  Capfile
├─  config
│ ├─  deploy
│ │ ├─production.rb #自動デプロイ時の本番環境での設定
│ │ └─staging.rb
│ └─deploy.rb #自動デプロイ時の共通設定
└─  lib
    └─capistrano
        └─tasks
```
Capistranoで自動デプロイをすると、サーバー上のディレクトリ構造は以下のようになります。(今回の設定の場合)

```bash:/var/www/rails(サーバー環境)
live_share
├─ current # 最新のデプロイしたフォルダやファイルが置かれる
├─ shared # 共通のファイルが置かれる
│   ├─ bundle
│   ├─ config
│   ├─ log
│   ├─ public
│   ├─ temp
│   └─ vendor
└─ release # 過去にデプロイしたフォルダやファイルが置かれる
```
見て分かるように、階層構造が一段深くなっているので、NginxやUnicornの設定ファイルも修正する必要があります。

## 各ファイルの編集
まずは、Capfile, config/deploy.rb, config/deploy/production.rbのファイルを編集します。

```ruby:capfile
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 }
```
```ruby:config/deploy.rb
# 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
```
```ruby:config/deploy/production.rb
server "18.178.91.188", user: "ec2-user", roles: %w{web app db}
```

また、前述したようにディレクトリ構造の変更により、NginxやUnicornの設定ファイルを変更する必要があるので、編集します。前回の記事ではUnicornの設定ファイルはサーバー上にしか書いていないので、サーバーにログインしてファイルの中身をローカルに持ってきて編集します。(たくさん試行錯誤をしていたので、前回の記事と書き方が異なっています)

### Unicorn設定ファイル
```ruby:config/unicorn.conf.rb(ローカル)
#サーバ上でのアプリケーションコードが設置されているディレクトリを変数に入れておく
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設定ファイル
```nginx:/etc/nginx/conf.d/live_share.conf(サーバー環境)
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;
}
```
設定ファイルを再読み込みし再起動します。

```bash:/(サーバー環境)
$ sudo service nginx reload
$ sudo service nginx restart
```

## 秘密鍵をDockerコンテナ上に配置
筆者は開発環境にDockerを使っていたので、Dockerコンテナ内の~/.sshディレクトリ内にEC2にログインするための秘密鍵を置かなければいけません。

```bash:live_share(ローカル)
$ docker container cp ~/.ssh/<秘密鍵名> <コンテナ名>:/root/.ssh
```
これでコンテナ内に秘密鍵が設置できたので、実際に入ってコンテナ内からEC2へログインできるか確認します。

```bash:live_share(ローカル)
$ docker exec -it <コンテナ名> bash

# コンテナ内で以下コマンド
$ ssh -i ~/.ssh/<秘密鍵> ec2-user@18.178.91.188 
```

今後、ビルドし直す度に秘密鍵をコピーするのがめんどくさいため、volume化して~/.sshディレクトリを共有します。(.ssh内に他の秘密鍵がある場合はディレクトリを一段深くしてそこをvolume化したほうがいいかもしれません。)

```yml:docker-compose.yml
  web:
    # 省略
    volumes:
      - ~/.ssh:/root/.ssh
    # 省略
```

## 環境変数の設定
また、database.yml内で環境変数を使っているので、サーバー内でも設定しないといけません。前回の記事ではdotenvのGemを使っていたのですが、自動デプロイのときにはうまくいかなかったので、サーバー内の環境変数を設定します。(gotenvは本番環境では非推奨らしいです。)

```plaintext:/etc/environmemt
DB_NAME=データベース名_production #RDSで作成したDB名
DB_USERNAME=root #RDSのユーザー名
DB_PASSWORD=********* #RDSのパスワード
DB_HOSTNAME=***.ap-northeast-1.rds.amazonaws.com # RDSのエンドポイント
```
環境変数が設定されているか確認

```bash:terminal
$ source .env
$ echo $DB_NAME
$ echo $DB_USERNAME
$ echo $DB_PASSWORD
$ echo $DB_HOSTNAME
```
環境変数の設定はcredentials.yml.encを使ってやる方法もあるみたいです。

## production環境用にCredentialsの設定
手動デプロイとは異なり、production環境用のcredentialsの設定が必要になります。

```bash:live_share(ローカル)
$ docker-compose run -e EDITOR=vim web rails credentials:edit --environment production
```

このコマンドを打つことによりconfig/credentialsディレクトリが作られ、その中にproduction.keyとproduction.yml.encが作られます。デフォルトではsecret_key_baseは生成されないので自分で設定を行います。下記コマンドでヘルプが見れます。

```bash:live_share
$ bin/rails credentials:edit --help
```

その後、生成されたproduction.keyをデプロイ先のサーバにコピーします。コピー先はshared/config/credentialsディレクトリに行います。

```bash:live_share(ローカル)
$ scp -i ~/.ssh/live_share_key_rsa production.key ec2-user@18.178.91.188:/var/www/rails/live_share/shared/config/credentials
```

ここまで来たらmainブランチにローカルで編集した設定の変更をマージしてデプロイを走らせます。

```bash:live_share
docker-compose run --rm web bundle exec cap production deploy
```

最後までデプロイが走れば成功です。お疲れさまでした！
何度もエラーが出ると思いますが、一個ずつ解決していけば必ずできるのでとにかく諦めないことが重要だと感じました。
ここからは筆者がハマったエラーについてまとめます。

## エラーについて
### ssh接続エラー
コンテナ内に鍵を設置してssh接続できており、設定もまちがってないのに下のようなエラーが出る場合は一旦コンテナをビルドし直したほうがいいかもしれません。筆者はビルドし直すことでエラーがでなくなりました。

```bash:エラーログ
Net::SSH::AuthenticationFailed: Authentication failed for ~~~
```

### Gem::LoadError
下エラーが出る場合はエラーログにもありますが、ed25519とbcrypt_pbkdfのGemをインストールすることで解決しました。

```bash:エラーログ
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が使われているのかはなぞでしたが、新しいバージョンを再度インストールすることで解決しました。

```bash:エラーログ
The engine "node" is incompatible with this module. Expected version ">=8.16.0". Got "6.17.1"
```

```bash:/(サーバー上)
# 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