Capistranoで異なるAvilabilityZoneにEC2インスタンスをラウンチする

前回、

次回はデータベースの作成や初期設定を直前タスクとして挿入して、さらにWordPressの設定ファイルの書き換え、GitHubからWordPressテーマをダウンロードするところまでやってみようと思います。

とか云っていたのですが、都合によりCapistrano3を使ってAWSのEC2インスタンス作成を行ったので、そのTIPSを残しておこうかと思います。 今回は、デプロイ用のEC2インスタンスから公式のAMIを利用してAvailabilityZonesが異なるエリアにそれぞれEC2インスタンスを立ててみました。試験的に、AMIは最新のインスタンスタイプ「t2.micro」に対応した「Amazon Linux AMI 2014.03.2 (HVM)」を利用して、Tokyoリージョンの1aと1cのAvailabilityZoneにそれぞれ2台ずつ、合計4インスタンスを同時に立ててみます。

※ 事前準備として、インスタンスを立てるAWSアカウントにてVPCやSubnet等EC2インスタンスを作成する上で必要最小限の設定をしておく必要があります。特に今回は異なるAvailabilityZoneへのインスタンスを立てるのでそれぞれのゾーンにSubnetを設定しておく必要があります。

さて、早速デプロイの手順です。 はじめに、デプロイ環境(サーバ)の設定を行うため、config/deploy/test.rbを編集します。 今回は動的に作成したEC2インスタンスをデプロイ環境とするため、事前のサーバ設定ができません。というのも、AWSで立ち上がる新規インスタンスにはElasticIPを付与しないので、毎回PublicIPが異なり、エンドポイントURLも動的に変わってしまうためです。なので、今回はインスタンスが起動した後にCapistoranoのタスクにて対象サーバを設定するようにします。 初期のサーバ設定は不要となるので、記述をコメントアウトしておきます。なお、SSHのオプションだけは最終的に共通で利用する予定なので残しておきます(今回のデプロイタスクでは使いませんが…)。

$ vim config/deploy/test.rb
#role :app, %w{deploy@localhost}
#role :web, %w{deploy@localhost}
#role :db, %w{deploy@localhost}

set :ssh_options, {
  keys: %w(/home/deploy/.ssh/id_rsa),
  forward_agent: true,
}

今回のデプロイでは、「AWS SDK for Ruby」を利用するので、あらかじめSDKをインストールしておきます。

$ sudo gem install aws-sdk

次に、config/deploy.rbにデプロイ内容を設定します。

$ vim config/deploy.rb
# config valid only for Capistrano 3.1
lock '3.2.1'

# AWS SDK for Ruby を読み込む
require 'aws-sdk'

# AWS SDK用の設定
AWS.config({
  :access_key_id => '<AWSアカウントのACCESS KEY ID>', 
  :secret_access_key => '<AWSアカウントのSECRET ACCESS KEY>', 
  :region => 'ap-northeast-1', # EC2 Instanceを作成するリージョン
})

# AMIのimage_id
# Amazon Linux AMI 2014.03.2 (HVM)
set :ami_image_id, 'ami-29dc9228'

# 作成するInstance数
set :instance_count, 4

# 作成するInstanceタイプ
set :ec2_instance_type, 't2.micro'

# 作成先のAvailability Zones
set :availability_zones, [ 'ap-northeast-1a', 'ap-northeast-1c' ]

# 作成先のsubnet_id
set :subnet_ids, [ 'subnet-********', 'subnet-********' ]

# Capistranoデフォルトのタスクを削除する
framework_tasks = [:starting, :started, :updating, :updated, :publishing, :published, :finishing, :finished]
framework_tasks.each do |t|
  Rake::Task["deploy:#{t}"].clear
end
Rake::Task[:deploy].clear

desc 'Launch an EC2 instance to each availability zone different'
task :launch do
  run_locally do
    ec2 = AWS::EC2.new

    created_instances = []
    cnt = 0
    while cnt < fetch(:instance_count) do
      if cnt.even? then
        current_az = fetch(:availability_zones)[0]
        current_sn = fetch(:subnet_ids)[0]
      else
        current_az = fetch(:availability_zones)[1]
        current_sn = fetch(:subnet_ids)[1]
      end
      i = ec2.instances.create(
        :image_id => fetch(:ami_image_id), 
        :availability_zone => current_az,
        :subnet => current_sn, 
        :instance_type => fetch(:ec2_instance_type), 
        :count => 1
      )
      sleep 10 while i.status == :panding
      created_instances << i.id
      cnt += 1
    end
    execute "echo -n #{created_instances} > ~/CREATED_INSTANCES"

  end
end

desc 'Check the activation status of new instances'
task :check do
  created_instances_list = 'CREATED_INSTANCES'

  run_locally do
    ec2 = AWS::EC2.new

    begin
      if test "[ -f ~/#{created_instances_list} ]"
        created_instances = capture("cd ~; cat #{created_instances_list}").chomp
        ci = created_instances.gsub(/(\[|\s|\])/, '').split(',')
        target_instances = ec2.instances.select { |i| i.exists? && i.status == :running && ci.include?(i.id) }.map(&:private_ip_address)
        if target_instances.length == 0 then
          raise "No created instances"
        end
        target_instances.each { |var| 
          server var, user: 'ec2-user', roles: %w{web app}
        }
      end
    rescue => e
      info e
      exit
    end

  end
end

task :deploy => :check do
  run_locally do

    info roles(:all)
    info 'Next task of deploy on new instances'

    # deploy start

  end
end

実際にインスタンスのラウンチをしてみる。

$ cap test launch
INFO[10af60ae] Running /usr/bin/env echo -n ["i-7497be72", "i-01c31718", "i-4d94bd4b", "i-00c31719"] > ~/CREATED_INSTANCES on localhost
DEBUG[10af60ae] Command: echo -n ["i-7497be72", "i-01c31718", "i-4d94bd4b", "i-00c31719"] > ~/CREATED_INSTANCES
INFO[10af60ae] Finished in 0.003 seconds with exit status 0 (successful).

上記のようなログが出力され、インスタンスのラウンチは完了したようなので、AWSのマネージメント・コンソール側で確認してみます。

AWSのマネージメント・コンソールで新規インスタンスを確認

確かに4つのインスタンスが、交互に別々のAvailabilityZonesで起動していました。理想どおりの動きです。 なお、AWSのインスタンスが起動する時間は環境によってまちまちのため、上記の「launch」タスクはEC2インスタンスの起動状況までは確認せずに終了させています。ただ、このままだとCapistoranoの次のタスクが起動した際にインスタンスの情報が引き継げないので、「launch」タスクで起動したインスタンスのIDを「CREATED_INSTANCES」というファイルに書き出して終了させています。 次のタスクでは、この「CREATED_INSTANCES」に書き出されているインスタンスIDを拾って、インスタンスの起動状況をチェックし、ステータスが「:running」であれば、デプロイ先のサーバとして設定を行うようにしています。 また、「check」タスクではステータスが「:running」である新規インスタンスのプライベートIPアドレスを取得して、それをデプロイ先サーバのホスト名として利用していますが、新インスタンスにパブリックIPアドレスが割り当てられてパブリックDNSが設定されているのであれば、ここを{}.map(&:dns_name)と名前引きにしても大丈夫です。

後続タスクを実行してみます。

$ cap test deploy
DEBUG[79c503fb] Running /usr/bin/env [ -f ~/CREATED_INSTANCES ] on localhost
DEBUG[79c503fb] Command: [ -f ~/CREATED_INSTANCES ]
DEBUG[79c503fb] Finished in 0.003 seconds with exit status 0 (successful).
DEBUG[9dd1f93d] Running /usr/bin/env cd ~; cat CREATED_INSTANCES on localhost
DEBUG[9dd1f93d] Command: cd ~; cat CREATED_INSTANCES
DEBUG[9dd1f93d]         [i-7497be72, i-01c31718, i-4d94bd4b, i-00c31719]
DEBUG[9dd1f93d] Finished in 0.002 seconds with exit status 0 (successful).
INFO[#<Capistrano::Configuration::Server:0x0000000189a7c8 @user="ec2-user", @hostname="176.34.61.80", @port=nil, @properties=#<Capistrano::Configuration::Server::Properties:0x00000001899238 @properties={}, @roles=#<Set: {:web, :app}>>>, #<Capistrano::Configuration::Server:0x000000018a3a08 @user="ec2-user", @hostname="176.34.62.168", @port=nil, @properties=#<Capistrano::Configuration::Server::Properties:0x000000018a3080 @properties={}, @roles=#<Set: {:web, :app}>>>, #<Capistrano::Configuration::Server:0x000000018a0d58 @user="ec2-user", @hostname="176.34.62.75", @port=nil, @properties=#<Capistrano::Configuration::Server::Properties:0x000000018a6910 @properties={}, @roles=#<Set: {:web, :app}>>>, #<Capistrano::Configuration::Server:0x000000018a8ee0 @user="ec2-user", @hostname="176.34.61.75", @port=nil, @properties=#<Capistrano::Configuration::Server::Properties:0x000000018b3818 @properties={}, @roles=#<Set: {:web, :app}>>>]
INFONext task of deploy on new instances

前タスクで作成したインスタンスの情報を引き継いで、無事にデプロイ先のサーバとして設定されました。 この後のタスク(今回の例では「deploy」タスク)で、各新規インスタンスに対してApacheやPHP、MySQLのインストールを行えば、晴れて前回のWordPressのデプロイに繋げられるようになります。

最終的には、Capistranoのコマンドを2~3回入力するだけで、サーバ作成からWordPressデプロイまで一気に出来てしまうという夢のようなデプロイ環境が作れそうです。

参考サイト