aws-sdk ruby で EC2 instance snapshot からの復元を自動化 #ec2

# aws-sdk ruby で EC2 instance :camera: snapshot からの :recycle: 復元を自動化
## :musical_score: 概要
aws-sdk ruby で EC2 の Instance の :camera: スナップショットからの :recycle: 復元を自動化します。

## :hand: この記事を読む前に
手動で行った場合の記事が以下になります。（参照先は snapshot 込）
<i class="fa fa-external-link" style="font-size:1em;"></i> [EC2 instance snapshot and restore](http://qiita.com/tbpgr/items/2456a28506ec44d23a0a)


## :musical_note: 前提知識
* 環境変数の設定には dotenv を利用します。
    * <i class="fa fa-external-link-square" style="font-size:1em;"></i> [Ruby | dotenv gem で 環境変数をスマートに管理](http://qiita.com/tbpgr/items/b7126c3d2d8f0329b11c)
* AWS の キーの取得については下記を参照
    * <i class="fa fa-external-link-square" style="font-size:1em;"></i> [AWS-SDK Ruby で EC2 のインスタンス名からインスタンス id / ip address を取得する](http://qiita.com/tbpgr/items/580651b875ecdfdf0551)

## :musical_note: 前提
* Snapshot の取得は、下記記事の手順で完了済みとする
    * <i class="fa fa-external-link-square" style="font-size:1em;"></i> [aws-sdk ruby で EC2 instance :camera: snapshot の取得を自動化](http://qiita.com/tbpgr/items/bc834c528c30a25bd556)
* Instance は起動中かつ 1 つの Volume が割り当て済み

## <i class="fa fa-gears" style="font-size:1em;"></i> 設定ファイル
### <i class="fa fa-gear" style="font-size:1em;"></i> Gemfile の設定
~~~
source "https://rubygems.org"

gem "dotenv"
gem "aws-sdk"
~~~

### <i class="fa fa-gear" style="font-size:1em;"></i> .env ファイルの設定
* AWS Management Console で必要な値を取得し、 .env ファイルに設定をする

~~~
ACCESS_KEY_ID = "your access key"
SECRET_ACCESS_KEY = "your secret access key"
EC2_ENDPOINT = "end point"
~~~

## <i class="fa fa-code" style="font-size:1em;"></i> 主処理

~~~ruby
require 'aws-sdk'
require 'dotenv'

class RestoreExecutor
  def initialize(instance_search_word , snapshot_search_word, device)
    Dotenv.load
    @ec2 = AWS::EC2.new(
      access_key_id:     ENV["ACCESS_KEY_ID"],
      secret_access_key: ENV["SECRET_ACCESS_KEY"],
      ec2_endpoint:      ENV["EC2_ENDPOINT"]
    )
    @snapshot_search_word = snapshot_search_word
    @instance_search_word = instance_search_word
    @device = device
    @volume_name = "#{snapshot_search_word} #{Time.now}"
  end

  def restore
    snapshot = find_snapshot
    instance = find_instance
    old_volume = old_volume(instance.instance_id)
    new_volume = create_volume_from_snapshot(instance, old_volume, snapshot)
    wait_until_status(new_volume, :available)
    update_volume_name(new_volume)
    wait_until_status(new_volume, :available)
    instance.stop
    wait_until_status(instance, :stopped)
    old_volume.detach_from(instance, @device)
    wait_until_status(old_volume, :available)
    new_volume.attach_to(instance, @device)
    wait_until_status(new_volume, :in_use)
    instance.start
    wait_until_status(instance, :running)
    wait_until_system_check_complete(instance)
  end

  private

  def find_snapshot
    snapshots = @ec2.snapshots.select { |e|e.description  =~ /#{@snapshot_search_word}/ }
    validate_existence_snapshot(snapshots)
    choose_latest_snapshot(snapshots)
  end

  def find_instance
    instances = @ec2.instances.select { |e|e.tags['Name'] =~ /#{@instance_search_word}/ }
    validate_empty_instance(instances)
    validate_too_many_instance(instances)
    instances.first
  end

  def old_volume(instance_id)
    @ec2.volumes.select { |e|e.attachments.to_a.size == 1}
                .find { |e|e.attachments.first.instance.id == instance_id }
  end

  def create_volume_from_snapshot(instance, volume, snapshot)
    options = {
      snapshot_id: snapshot.id,
      availability_zone: instance.availability_zone,
      volume_type: volume.type
    }
    response = @ec2.client.create_volume(options)
    new_volume(response.data[:volume_id])
  end

  def update_volume_name(volume)
    volume.tag('Name', :value => @volume_name)
  end

  def wait_until_status(target, status)
    print "Waiting until #{status} #{Time.now}"
    loop {
      break if [status, :error].include?(target.status)
      sleep 1
      print "."
    }
    puts
    puts Time.now
    puts
  end

  def wait_until_system_check_complete(instance)
    print "Waiting until system check #{Time.now}"
    loop {
      response = @ec2.client.describe_instance_status({"instance_ids" => [instance.instance_id]})
      statuses = response.instance_status_set.map { |i| i.system_status.details[0].status }
      break unless statuses.include?('initializing')
      sleep 1
      print "."
    }
    puts
    puts Time.now
    puts
  end

  def validate_existence_snapshot(snapshots)
    return unless snapshots.size.zero?
    STDERR.puts ("No snapshots. Confirm your snapshots search keyword '#{@snapshot_search_word}'.")
    exit(false)
  end

  def choose_latest_snapshot(snapshots)
    snapshots.sort_by { |e|e.description }.last
  end

  def validate_empty_instance(instances)
    return unless instances.empty?
    STDERR.puts ("No instance. Confirm your instance search keyword '#{@instance_search_word}'.")
    exit(false)
  end

  def validate_too_many_instance(instances)
    return if instances.size < 2
    STDERR.puts ("Too many instance. Confirm your instance search keyword '#{@instance_search_word}'.")
    exit(false)
  end

  def new_volume(volume_id)
    @ec2.volumes[volume_id]
  end
end

se = RestoreExecutor.new('vagrant', 'test_snapshot_vagrant', '/dev/sda')
se.restore
~~~

## <i class="fa fa-check-circle" style="font-size:1em;"></i> 結果確認

* 実行

~~~bash
$ ruby restore.rb
Waiting until available 2015-01-14 22:01:14 +0900....
2015-01-14 22:01:18 +0900

Waiting until available 2015-01-14 22:01:18 +0900
2015-01-14 22:01:18 +0900

Waiting until stopped 2015-01-14 22:01:19 +0900..........................
2015-01-14 22:01:47 +0900

Waiting until available 2015-01-14 22:01:48 +0900.
2015-01-14 22:01:49 +0900

Waiting until in_use 2015-01-14 22:01:49 +0900
2015-01-14 22:01:49 +0900

Waiting until running 2015-01-14 22:01:50 +0900....................
2015-01-14 22:02:12 +0900

Waiting until system check 2015-01-14 22:02:12 +0900........................
2015-01-14 22:10:12 +0900
~~~

* ブラウザで確認

![snap.png](https://qiita-image-store.s3.amazonaws.com/0/43480/06d44a91-5737-6188-4cb7-4822362d5e14.png)

## :memo: 補足
* volume の Name ですが、  EC2::Client#create_volume 時に設定する方法が分からなかったので、新規作成後に改めて Volume に include されている TaggedItem#add_tag (tag) で設定している

## :books: 外部資料
* <i class="fa fa-external-link" style="font-size:1em;"></i> [AWS SDK for Ruby | API Document](http://docs.aws.amazon.com/AWSRubySDK/latest/frames.html)