Capistranoのタスクを新EC2インスタンスが完全起動するまでsleepさせる

Capistranoで新規作成したEC2インスタンスが完全に起動し切っていない状態で、そのインスタンスに対してSSHアクセスするタスクを実行すると、どこかしらでエラーになってタスクが完了しません。そこで、デプロイ対象となるEC2インスタンスの起動状態をチェックして、完全に起動していない状態の場合sleepして起動を待つようなタスクを作りました。

AWSのEC2インスタンスには3つのステータス情報があり、この全てのステータスを確認しないと、インスタンスの完全起動状態とは言えないので、注意が必要でした(下図参照)。

AWSマネージメントコンソールで確認できるEC2インスタンスステータス

インスタンスが起動しているかどうかの確認は、AWSマネージメントコンソールでいうところの「Instance State」がrunningであるかを判定すればOKなのですが、実際にインスタンスにSSH接続ができるかどうかの確認は「Status Checks」欄に「2/2 checks」とあるように2つのステータス(INSTANCESTATUSとSYSTEMSTATUSのreachability)が共にpassedであるかまでを確認する必要があるのです。通常、インスタンスが起動すると「Instance State」は数秒から十数秒程度でrunningになるのですが、「Status Checks」は数分程度initializingで初期化処理を行っています。このステータスが共にpassedにならないとSSHアクセスでコケます。

今まで私が使っていたcheckタスクだと、「Instance State」のステータス1つしか確認していなかったので、後続タスクがSSHアクセスで中断したりしていました。それを回避するためのタスクが今回のcheckタスクになります。

task :check do
  run_locally do
    created_instances_list = 'CREATED_INSTANCES'

    def check_instance_status(instance_ids=[]) 
      ec2 = AWS::EC2.new
      AWS.memoize do
        ec2info = ec2.client.describe_instance_status({'instance_ids' => instance_ids})
        sys_status = ec2info.instance_status_set.map { |i| i.system_status.details[0].status }
        ins_status = ec2info.instance_status_set.map { |i| i.instance_status.details[0].status }
        status = sys_status + ins_status
        return status.include?('initializing') ? false : true
      end
    end

    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(&:id)
          raise "Is not still created all instances" if target_instances.length < fetch(:instance_count)
          if target_instances.length == fetch(:instance_count) then
            # 全インスタンス起動(Instance Stateがrunning)
            chk_retry = 0

            while !check_instance_status(target_instances) do
              # インスタンスステータスが全てOKでない場合は15秒待つ(※20回までリトライする)
              info "In preparation of the instance: Status check " + (chk_retry > 0 ? "(retry #{chk_retry + 1} times)" : "")
              sleep 15
              if check_instance_status(target_instances) then
                break
              end
              chk_retry += 1
              raise "Instance is not still ready. Please run the task again after waiting for a while." if chk_retry >= 20
            end

            # ここからが全インスタンス完全起動後の処理(初回ssh接続設定)
            target_instance_private_ips = ec2.instances.select { |i| i.exists? && i.status == :running && ci.include?(i.id) }.map(&:private_ip_address)
            pkfn = fetch(:private_key_file)
            target_instance_private_ips.each { |var| 
              server var, user: 'ec2-user', roles: %w{web app}, ssh_options: { keys: %W(/home/deploy-user/#{pkfn}), forward_agent: true }
            }
          end
        end
      rescue => e
        info e
        exit
      end
  end

  # 後続タスクのサンプル(SSHしてhostnameを表示する)
  on roles(:web) do
    info capture "hostname"
  end
end

上記設定から、後続タスクのサンプル部分を削除したタスクを、CapistranoでEC2インスタンスを作成した後のデプロイタスクの直前に挿入してやる感じです。

参考サイト