今回は、Cloud Design Pattern(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
// 初期設定
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 . ".");
    }
  }
}
?>

こちらの記事はなかの人(suz-lab)監修のもと掲載しています。
元記事は、こちら