okochangの馬鹿でありがとう

ふらふら適当に世間を生きる日々でございます

インスタンスのAMI作成にSQSを使うようにしてみたら微妙だった話

こんにちは@oko_changです。
前回AWS SDK for RubyでSQSを使ってみましたが、お仕事がAWSのインフラ構築をメインとしていることもあり、あまり業務で使う機会がありません。
というわけで今回は日頃の業務に近いインスタンスのAMI作成の仕組みをちょっと強引にSQSを使った仕組みにしてみました。
SQSの使い方をイメージしやすいかなと試しにやっただけなので、実際の業務でこういう構成は組まないと思います。

テスト環境について

構成図

f:id:okochang:20130817135515p:plain

流れ

  1. 対象サーバはマウントされているEBS(/ebs01)をアンマウント
  2. 対象サーバはキューに自身のインスタンスIDが書かれたメッセージを送る
  3. バックアップサーバはキュー内のメッセージを定期的にチェックして取得
  4. バックアップサーバは受け取ったメッセージのインスタンスIDを指定してAMIを作成
  5. バックアップサーバはAMIが作成したら完了したメッセージをキューに送り、元のメッセージを削除
  6. 対象サーバはキュー内のメッセージを定期的にチェックして取得
  7. 対象サーバはバックアップ完了のメッセージを受け取るとEBSをマウントし、元のメッセージを削除

対象サーバのインスタンスをセットアップ

以下のようにテスト環境を用意してあるとして、マウントされているEBSボリュームも含めてバックアップ対象となります。
バックアップする時はアンマウント、バックアアップが完了したら再度マウントします。

$ sudo ln -f /usr/share/zoneinfo/Asia/Tokyo /etc/localtime 
$ sudo mkfs -t ext4 /dev/sdc
$ sudo mkdir /ebs01
$ sudo mount /dev/sdc /ebs01
$ df -h

バックアップ対象サーバ行う処理

# -*- coding: utf-8 -*- 
## 設定
require 'net/http'
require 'aws-sdk'

sqs_region = 'sqs.' + Net::HTTP.get('169.254.169.254', '/latest/meta-data/placement/availability-zone').chop + '.amazonaws.com'
instance_id = Net::HTTP.get('169.254.169.254', '/latest/meta-data/instance-id')
unmounted_instances_queue = "unmountend_instances"

## AWSの認証
sqs = AWS::SQS.new(
  :sqs_endpoint => sqs_region
).client

## アンマウントされたインスタスIDを管理するキューの接続先を指定
unmounted_instances_queue = sqs.list_queues(:queue_name_prefix => unmounted_instances_queue)[:queue_urls][0]

## 対象のEBSボリュームをアンマウントする
unmount_status = `sudo umount /ebs01`

## アンマウントが正常に完了していればキューにメッセージを投げ、そうでなければ終了する
if unmount_status.empty?
  sqs.send_message(:queue_url => unmounted_instances_queue, :message_body => instance_id)
else
  puts "Error check mount status"
  exit 1
end

exit 0

バックアップ(Create Image)を実行するインスタンスの処理

# -*- coding: utf-8 -*- 
## 設定
require 'net/http'
require 'aws-sdk'

sqs_region = 'sqs.' + Net::HTTP.get('169.254.169.254', '/latest/meta-data/placement/availability-zone').chop + '.amazonaws.com'
ec2_region = 'ec2.' + Net::HTTP.get('169.254.169.254', '/latest/meta-data/placement/availability-zone').chop + '.amazonaws.com'
unmounted_instances_queue = "unmountend_instances"

## AWSの認証
sqs = AWS::SQS.new(
  :sqs_endpoint => sqs_region
).client

ec2 = AWS::EC2.new(
  :ec2_endpoint => ec2_region
).client

## アンマウントされたインスタスIDを管理するキューの接続先を指定
unmounted_instances_queue = sqs.list_queues(:queue_name_prefix => unmounted_instances_queue)[:queue_urls][0]

## 上記キューからメッセージを取得します
message = sqs.receive_message(:queue_url => unmounted_instances_queue, :wait_time_seconds => 20)[:messages][0]
if message.nil?
  puts "No message available in queue"
  exit 0
end

## 取得したメッセージの本体からインスタンスIDを取得します
instance_id = message[:body]

## 取得したインスタンスID用のバックアップ完了通知のキューを作成します
each_instance_queue = sqs.list_queues(:queue_name_prefix => instance_id)[:queue_urls][0]
if each_instance_queue.nil?
  sqs.create_queue(:queue_name => instance_id, :attributes => {"VisibilityTimeout" => "300"})
end

## 取得したインスタンスIDの本日分バックアップが完了しているかチェックします
## 本日分のバックアップがすでに完了している場合はメッセージを削除して完了にします
image_name = instance_id + '-' + Time.now.strftime("%Y%m%d")
if ec2.describe_images(:owners => ["self"], :filters => [{:name => 'name', :values => [image_name]}])[:images_set].any?
  puts "AMI is already available"
  sqs.delete_message(:queue_url => unmounted_instances_queue, :receipt_handle => message[:receipt_handle])
  exit 0
end

## 取得したインスタンスIDを指定してAMIを作成します
image_id = ec2.create_image(:instance_id => instance_id, :name => image_name, :no_reboot => true,)[:image_id]
count = 1
while ec2.describe_images(:image_ids => [image_id], :owners => ["self"])[:images_set][0][:image_state] != "available"
  sleep(30)
  count += 1
  if count > 10 then
    puts "Creating AMI was not completed"
    exit 1
  end
end

## AMIの作成が完了したら、完了通知用のキューにメッセージを送ります
sqs.send_message(:queue_url => each_instance_queue, :message_body => "Creating AMI completed")

## 元のメッセージを削除します
sqs.delete_message(:queue_url => unmounted_instances_queue, :receipt_handle => message[:receipt_handle])
puts "Backup job completed"

exit 0

再びバックアップ対象サーバで行う処理

# -*- coding: utf-8 -*- 
## 設定
require 'net/http'
require 'aws-sdk'

sqs_region = 'sqs.' + Net::HTTP.get('169.254.169.254', '/latest/meta-data/placement/availability-zone').chop + '.amazonaws.com'
instance_id = Net::HTTP.get('169.254.169.254', '/latest/meta-data/instance-id')

## AWSの認証
sqs = AWS::SQS.new(
  :sqs_endpoint => sqs_region
).client

## インスタンス毎のバックアップ完了通知のキューを指定します
each_instance_queue = sqs.list_queues(:queue_name_prefix => instance_id)[:queue_urls][0]

## 上記キューに格納されているバックアップ完了のメッセージを取得します
message = sqs.receive_message(:queue_url => each_instance_queue, :wait_time_seconds => 20)[:messages][0]
if message.nil?
  puts "no message available in queue"
  exit 0
end

## 対象のEBSがアンマウントされている状態かチェックします
## すでにマウントされている状態であれば、メッセージを削除します
unmount_status = `mount | grep "/ebs01"`
if unmount_status.include?("/ebs01")
  puts "Device is already mounted"
  sqs.delete_message(:queue_url => each_instance_queue, :receipt_handle => message[:receipt_handle])
  exit 0
end

## 対象のEBSボリュームをマウントします
mount_status = `sudo mount /dev/sdc /ebs01`

## マウントが正常に完了したかどうかチェックします
if mount_status.empty?
  puts "Mounting completed"
else
  puts "Error check the mount status"
  exit 1
end

## 取得したメッセージを削除します
sqs.delete_message(:queue_url => each_instance_queue, :receipt_handle => message[:receipt_handle])

exit 0

まとめ

  • Visiblitlity Timeoutはバックアップが完了する時間を見て調整した方が良い
    • この仕組は実際使わないけど
  • SQSはメッセージが複数届く場合があるので、きちんと考慮すべき
    • この仕組は実際使わないけど
  • マウントポイントとかEBSのデバイス名とかはタグとかAPIで取得出来ると、再利用性が高りそう
    • この仕組は実際使わないけど

インスタンス全てのAMIを日次で取らなくて良いようにしたいですね。
また微妙な仕組みだな〜( ´Д`)σ)Д`)プニョプニョと思いつつ、せっかくやってみたのでアップ。