2012年5月12日土曜日

タグを利用したEC2のバックアップ(AMI取得)と世代管理

スズキです。

CDPネタです。今回の対象は「Cloud DIパターン」です。


このパターンの「利点」に
EC2インスタンスの構築だけでなく、AMIやスナップショットの自動取得を行う仕組みを作る場合にも利用できる。
といった記載があります。

今回はEC2のタグ情報を利用して、スナップショット(AMI)の取得や世代管理を
行なうPHPスクリプトを作成しました。

このPHPスクリプトを定期的に実行するだけで、あとはAWSマネジメントコンソールにて
EC2のタグを編集し、スナップショット(AMI)の取得対象にするかどうか、
世代をどれだけ残すか、が容易に管理することができます。

PHPスクリプトは長いの最後に掲載することとし、先に仕様をまとめておきます。
  • 指定のアカウント・リージョンのすべての稼働しているEC2に対して実施
    • 稼働は"instance-state-name"が"running"の状態
  • バックアップ対象EC2は"Backup-Generation"タグがついているもの
    • "Backup-Generation"の値は0以上の数字(0のときはバックアップしない)
  • バックアップは"create_image"つまりAMIの作成で実施
    • "NoReboot"オプションをつけてEC2のリブートは抑制
  • 作成したAMIにはタグを付与
    • "Name"タグはEC2と同じものを付与
    • 自動バックアップとわかるように"Backup-Type"タグを"auto"としても付与
    • 関連するスナップショットにも"Backup-Type"タグを"auto"として付与
    • スナップショットの"Name"タグの値はAMI名とデバイスの値からの文字列
    • ただしタグの付与は作成後すぐだとエラーの可能性があるので最後に実施
  • AMIは最新からEC2の"Backup-Generation"タグの値だけ維持
    • AMIの"Name"タグがEC2と同様のものが対象
    • 残りの古いAMIは削除
    • "Backup-Type"タグが"auto"になっていないものは対象外
  • 削除したAMIに関連するスナップショットも削除
    • "Backup-Type"タグが"auto"のものも対象(注意!)
  • AMIやスナップショットへのタグ付けは作成後に実施
    • 作成後すぐに行うとエラーになる可能性
実際の挙動は下記のようになります。

まずはバックアップ対象のEC2です。バックアップ(AMI)を2世代管理するように
"Backup-Generation"を付け、値を2としています。


この状態で最後に掲載するバックアップスクリプト(PHP)を実行すると、
"Backup-Generation"タグが付いているEC2に対してAMIを作成します。


"Backup-Generation"の値は2なので、バックアップは2世代残るようになっており、
タグも"Name"はEC2と同じもの、そして自動バックアップがわかるように、
"Backup-Type"も"auto"で付いています。

当然AMIの作成と同時にスナップショットも取得されています。


こちらのタグも自動バックアップがわかるように、"Backup-Type"が"auto"として付き、
"Name"はAMI名にアタッチしているデバイス名を付与したものが付いています。

図にすると、こんな感じでしょうか?


最後にPHPスクリプトですが、下記のようになっています。
#!/usr/bin/php
<?php
// 初期設定
require_once("/opt/aws/php/default/sdk.class.php");
date_default_timezone_set("Asia/Tokyo");
$ec2 = new AmazonEC2(array(
  "key"    => "ACCESS KEY",
  "secret" => "SECRET KEY"
));
$ec2->set_region(AmazonEC2::REGION_APAC_NE1);
error_log(date("Y/m/d H:i:s") . " [Info] Begin create images.");

// 対象EC2(runnning)の取得
$response = $ec2->describe_instances(array(
  "Filter" => array(
    array("Name" => "instance-state-name", "Value" => "running")
  )
));
if(!$response->isOK()) {
  error_log(date("Y/m/d H:i:s") . " [" . $response->body->Errors->Error->Code . "] " . $response->body->Errors->Error->Message);
}

// 対象EC2に対するバックアップ処理
if(isset($response->body->reservationSet->item)) {
  foreach($response->body->reservationSet->item as $reservation) {
    foreach($reservation->instancesSet->item as $instance) {
      error_log(date("Y/m/d H:i:s") . " [Info] Begin execute instance(" . $instance->instanceId . ").");
      $is_backup   = false;
      $instance_id = $instance->instanceId;
      $image_tag   = $instance_id;

      // バックアップ条件の確認
      if(isset($instance->tagSet->item)) {
        foreach($instance->tagSet->item as $tag) {
          if($tag->key == "Name" && $tag->value != null && trim($tag->value) != "") {
            $image_tag = $tag->value;
          }
          if($tag->key == "Backup-Generation" && is_numeric($tag->value->to_string()) && intval($tag->value) > 0) {
            $is_backup  = true;
            $generation = intval($tag->value);
          }
        }
      }

      // バックアップと世代管理の実施
      if($is_backup) {
        // AMI名の作成
        $image_name  = $image_tag . "-" . date("YmdHis");
        // AMIの作成
        $image_id    = create_image($ec2, $image_name, $instance_id);
        // 削除対象AMIの取得
        $images      = find_delete_images($ec2, $image_tag, $generation);
        // AMIとスナップショットの削除
        delete_images($ec2, $images);
        // AMIにタグ付け
        tag_image($ec2, $image_id, $image_tag);
        // スナップショットにタグ付け
        tag_snapshots($ec2, $image_id);
      } else {
        error_log(date("Y/m/d H:i:s") . " [Info] Skip  create image from " . $instance->instanceId . ".");
      }
      error_log(date("Y/m/d H:i:s") . " [Info] End   execute instance(" . $instance->instanceId . ").");
    }
  }
}

error_log(date("Y/m/d H:i:s") . " [Info] End   create images.");
exit(0);

// AMIの作成
function create_image($ec2, $image_name, $instance_id) {
  error_log(date("Y/m/d H:i:s") . " [Info] Begin create image(" . $image_name . ") from " . $instance_id . ".");
  $response = $ec2->create_image(
    $instance_id,
    $image_name,
    array(
      "Description" => "Create from " . $instance_id . ".",
      "NoReboot"    => true
    )
  );
  if(!$response->isOK()) {
    error_log(date("Y/m/d H:i:s") . " [" . $response->body->Errors->Error->Code . "] " . $response->body->Errors->Error->Message);
  }
  error_log(date("Y/m/d H:i:s") . " [Info] End   create image(" . $image_name . ") from " . $instance_id . ".");
  return $response->body->imageId;
}

// 削除対象AMIの取得
function find_delete_images($ec2, $image_tag, $generation) {
  $response = $ec2->describe_images(array("Filter" => array(
    array("Name" => "tag:Name"       , "Value" => $image_tag),
    array("Name" => "tag:Backup-Type", "Value" => "auto")
  )));
  if(!$response->isOK()) {
    error_log(date("Y/m/d H:i:s") . " [" . $response->body->Errors->Error->Code . "] " . $response->body->Errors->Error->Message);
  }
  $images = array();
  foreach($response->body->imagesSet->item as $image) {
    $images["$image->name"] = array(
      "id"        => $image->imageId,
      "snapshots" => $image->blockDeviceMapping
    );
  }
  krsort($images);
  return array_slice($images, $generation - 1);
}

// AMIとスナップショットの削除
function delete_images($ec2, $images) {
  foreach($images as $image_name => $image) {
    error_log(date("Y/m/d H:i:s") . " [Info] Begin delete image(" . $image_name . ").");
    $image_id = $image["id"];
    $response = $ec2->deregister_image($image_id);
    if(!$response->isOK()) {
      error_log(date("Y/m/d H:i:s") . " [" . $response->body->Errors->Error->Code . "] " . $response->body->Errors->Error->Message);
    }
    error_log(date("Y/m/d H:i:s") . " [Info] End   delete image(" . $image_name . ").");
    foreach($image["snapshots"]->item as $snapshot) {
      $snapshot_id = $snapshot->ebs->snapshotId;
      error_log(date("Y/m/d H:i:s") . " [Info] Begin delete snapshot(" . $snapshot_id . ").");
      $response = $ec2->delete_snapshot($snapshot_id);
      if(!$response->isOK()) {
        error_log(date("Y/m/d H:i:s") . " [" . $response->body->Errors->Error->Code . "] " . $response->body->Errors->Error->Message);
      }
      error_log(date("Y/m/d H:i:s") . " [Info] End   delete snapshot(" . $snapshot_id  . ").");
    }
  }
}

// AMIにタグ付け
function tag_image($ec2, $image_id, $image_tag) {
  error_log(date("Y/m/d H:i:s") . " [Info] Begin tag(" . $image_tag . ") image to " . $image_id . ".");
  $response = $ec2->create_tags($image_id, array(
    array("Key" => "Name"       , "Value" => $image_tag),
    array("Key" => "Backup-Type", "Value" => "auto"),
  ));
  if(!$response->isOK()) {
    error_log(date("Y/m/d H:i:s") . " [" . $response->body->Errors->Error->Code . "] " . $response->body->Errors->Error->Message);
  }
  error_log(date("Y/m/d H:i:s") . " [Info] End   tag(" . $image_tag . ") image to " . $image_id . ".");
}

// スナップショットにタグ付け
function tag_snapshots($ec2, $image_id) {
  $response = $ec2->describe_images(array("ImageId" => $image_id));
  foreach($response->body->imagesSet->item as $image) {
    foreach($image->blockDeviceMapping->item as $snapshot) {
      $snapshot_id   = $snapshot->ebs->snapshotId;
      $snapshot_tag = $image->name . "-" . basename($snapshot->deviceName);
      error_log(date("Y/m/d H:i:s") . " [Info] Begin tag(" . $snapshot_tag . ") snapshot to " . $snapshot_id . ".");
      $response = $ec2->create_tags($snapshot_id, array(
        array("Key" => "Name"       , "Value" => $snapshot_tag),
        array("Key" => "Backup-Type", "Value" => "auto")
      ));
      if(!$response->isOK()) {
        error_log(date("Y/m/d H:i:s") . " [" . $response->body->Errors->Error->Code . "] " . $response->body->Errors->Error->Message);
      }
      error_log(date("Y/m/d H:i:s") . " [Info] End   tag(" . $snapshot_tag . ") snapshot to " . $snapshot_id . ".");
    }
  }
}
?>

軽い気持ちで書き始めたら、予想以上に時間がかかった...
--------
http://www.suz-lab.com/

1 コメント:

debiancdn さんのコメント...

Cloud DI の DI ってなんでしょうか。略だけだとわからないですよ。