ELBにぶら下がっているEC2にCapistranoを利用してローリングデプロイする方法について書きました。
もしもっと良い方法があれば教えてください。
環境
- rails (4.2.0)
- unicorn (5.0.0)
- capistrano (3.4.1)
- aws-sdk (2.1.36)
ELBにEC2を2つぶら下げる構成でサービスを運用しています。
Capistranoで複数サーバーにデプロイする時の問題点
Capistranoで複数サーバーにデプロイする場合、以下のように記載すれば複数サーバーにデプロイできます。
server 'xx.xx.xx.xx', user: 'user', roles: %w[web web1]
server 'xx.xx.xx.xx', user: 'user', roles: %w[web web2]
ただ、このように記載して複数サーバーにデプロイした場合、以下のような問題がありました。
-
bundle install
やrake assets:precompile
でCPUに負荷がかかり、CPUが100%にはりつくことがある -
rake assets:precompile
に時間がかかるのでサーバーのレスポンスが劣化してしまう
ローリングデプロイにする前に考えたこと
ビルドサーバーを用意して成果物だけを各サーバーに配置する
そもそも各サーバーで bundle install
や rake assets:compile
する必要はなく、ビルドサーバーのようなものを用意してあげて、成果物だけ各サーバーに配信して、unicornをgraceful restartすれば良さそうです。
capistrano-bundle_rsyncを使えば、上記に書いたようなことを実現できます。
ただ、あまりメンテされてない印象だったので採用はいったん見送りました。
ブルーグリーンデプロイ
今のところプロビジョニングツールは導入できていないこともあり、複数サーバー(現状の構成だと4つ)をメンテするのもつらみがあります。
Immutable Infrastructureなどは大変興味はあるものの、今回の課題に対しては、yak shaving感があるので今回は採用を見送りました。(infrastructure as codeをしたさはすごいあるのですが、、、)
ローリングデプロイ
おもに対応コストの点から現状のデプロイフローにそってデプロイを行いつつ、デプロイの前後にELBからデプロイするEC2を切り離して、デプロイ終了後にELBにもう一度くっつけるようにしてみました。
デプロイの流れとしては、以下のようになります。
- web1をELBから切り離す
- デプロイ実行
- web1をELBにつけて、web1が in serviceになるまで待つ
- web2をELBから切り離す(以下同じ)
まず、以下のようにCapistranoのタスクに elb:remove
elb:add
をフックします。
config/deploy.rb
before 'deploy:starting', 'elb:remove'
before 'deploy:finishing', 'elb:add'
after 'deploy:publishing', 'deploy:restart'
after 'deploy:failed', 'elb:remove'
Capistranoで実行するrakeタスクをlib/capistrano/tasks/elb.rakeに定義します。
もし Capfile
に Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }
を記載していないとelb.rakeがロードされないので気をつけてください。
require 'aws-sdk'
require 'dotenv/load'
require 'active_support'
require 'active_support/core_ext'
require 'pry-byebug'
namespace :elb do
ELB_NAME = 'elb_name'.freeze
EC2_INSTANCE_IDS = {
'xx.xx.xx.xx' => 'instance_id', # web1
'xx.xx.xx.xx' => 'instance_id' # web2
}.freeze
def client
@client ||= Aws::ElasticLoadBalancing::Client.new(
access_key_id: ENV['AWS_ELB_ACCESS_KEY'],
secret_access_key: ENV['AWS_ELB_SECRET_KEY'],
region: 'ap-northeast-1' # 東京リージョン
)
end
def instance(hostname)
{
instances: [
{
instance_id: EC2_INSTANCE_IDS[hostname]
}
],
load_balancer_name: ELB_NAME
}
end
def in_service?
client.wait_until(:instance_in_service, { load_balancer_name: ELB_NAME }) do |w|
w.max_attempts = 8
w.delay = 10
end
rescue Aws::Waiters::Errors::WaiterFailed
raise 'Resource did not enter the desired state in time'
end
desc 'Remove EC2 instance from ELB'
task :remove do
on roles(:app) do |server|
# NOTE: ELBにぶら下がっていないサーバーにデプロイする場合があるため、ELBにぶら下げてないサーバーの場合は何もしない
if EC2_INSTANCE_IDS[server.hostname].present?
puts "Removing #{server.hostname} from ELB"
res = client.deregister_instances_from_load_balancer(instance(server.hostname))
raise "There is no EC2 instance on ELB!!!" if res[:instances].empty?
end
end
end
desc 'Add EC2 instance from ELB'
task :add do
on roles(:app) do |server|
if EC2_INSTANCE_IDS[server.hostname].present?
puts "Add #{server.hostname} to ELB"
res = client.register_instances_with_load_balancer(instance(server.hostname))
# NOTE: 2017.11.16時点でELBにぶら下がるインスタンスは2台なので1台しかぶらさがってないとaddできていない
raise 'Failed to add EC2 instance to ELB!!!' if res[:instances].length == 1
raise 'Some are not in service' unless in_service?
puts 'All instances on ELB are in service'
end
end
end
desc 'Health check of ELB'
task :health_check do
puts 'Health check of ELB'
if in_service?
puts 'In service'
else
puts 'Out of service'
end
end
end
単体のrakeタスクの動作は以下のコマンドで確認できます。
$ ROLES=web1 bundle exec cap production elb:health_check
$ ROLES=web1 bundle exec cap production elb:add
$ ROLES=web1 bundle exec cap production elb:remove
web1
というロールを指定することで web1
というロールが付与されているサーバーを対象にrakeタスクが実行されます。
ロールにの使い方は Capistranoで複数のサーバーそれぞれにデプロイをする方法 に記載しています。
ELBからインスタンスを切り離す、再びつける、の動作が確認できれば、確認環境などで $ cap production deploy
してみてください。
課題
- DB変更がともなうデプロイの時につらみがある
この記事はCapistranoでELB + EC2環境にローリングデプロイをQiita用に加筆修正したものです