1. Qiita
  2. Items
  3. AWS

OpsWorksかLambda(スケジュール)を使ってEC2自動起動自動停止で費用節約する

  • 28
    Like
  • 0
    Comment
More than 1 year has passed since last update.

AWSの費用で何に一番費用が掛かるかという話だと大抵EC2だと思います。ただ、AWSの利点として時間課金なので検証環境など夜間に利用しない場合に停止しておくことで費用を大幅に節約できることができます。

上記について色々やる方法があると思いますが、現在だと以下2つのやり方が良いかと思います。

  • OpsWorksのTime-Basedインスタンスで自動起動、停止(別途費用なし)
  • Lambdaスケジュールを使って自動起動、停止(Lambda実行料金のみ。かなり安価)

上記についてやり方などをメモ。

結論

  • 対象EC2がインターネット接続が可能で新規に起動もしくはAMI取り直しができるならばOpsWorksが良い
  • 上記に当てはまらない場合にはLambdaスケジュールを利用
  • Lambdaスケジュールでやる場合、コードの作成が必要

そもそもどうやってやるのか

まず、OpsWorksについてですが、Time-basedインスタンスを使います。OpsWorksではインスタンスを時間もしくは負荷に応じて自動起動・停止する機能があり、その機能を利用します。起動、停止時間の設定は下記のようにGUI上で設定可能です。

Screen Shot 2015-10-10 at 11.17.01 AM.png

曜日ごと、毎日などの細かい時間をGUIで設定できるので変更する場合も簡単にできます。

なお、こちらを使うときの注意点は以下です。

  • GUIで表示される時間はJSTでなく、UTC
  • OpsWorksはインスタンスにエージェントが必要で、インターネットとの接続が必要
  • OpsWorksのレイヤーにTime-basedインスタンスとして既に起動しているインスタンスを追加はできない。上記より既に起動しているインスタンスの場合、AMIをとって別途新規起動が必要

Lambdaを使ったやり方としてはLambdaのスケジュール実行及びEC2のタグを利用することで可能です。

Lambdaの機能アップデートにより、現在、任意の周期(5分ごと、15分と、1時間ごと、1日ごと)またはcron形式での実行が可能となりましたので、1時間周期でLambdaを実行するように設定し、その処理でEC2のタグ情報をみて、必要に応じて対象のEC2の起動・停止を行うことができます。今まで上記のようなことをやる場合、EC2起動・停止を実施するためだけのサーバーが必要でしたが、Lambdaのスケジュール機能を使えばその必要がなくなります。

LambdaスケジュールでEC2自動起動・停止をやってみる

実際にLambdaでEC2自動起動・停止をやってみます。なお、コードを誤ると対象ではないEC2を停止してしまう危険性もあるので、くれぐれも確認の際には別環境や予め付与するロールの権限を制限するなどを行ってください。(以降に書いた私のコードも利用の際には別途確認ください)

今回試してみるにあたり、以下の仕様としてやってみました。

  • EC2のタグにType=devと書いたEC2群が自動起動・停止の対象(本番にはType=proとつけるもしくはTypeをつけずに対象外とする)
  • タグにStart=9:00,End=19:00と書くと9:00-19:00の間は起動し、それ以外は停止という意味とする
  • 曜日指定や1日で複数起動停止するなどはなし(作ろうと思えば作れますが)

まずは、上記の仕様で動作するLambda用のコードを書きます。
いくつかのモジュールを使うのでインストールします。

$mkdir AutoStartStop
$cd AutoStartStop
$npm install async aws-sdk moment

以下のようなコードを書いてみました。
(Node.js初級者であまりうまく書けている気がしませんが。。。)

index.js
var aws = require('aws-sdk');
var moment = require("moment");
var async = require('async');

aws.config.update({region: 'ap-northeast-1'});

function getHour(value) {
  return value.split(":", 2)[0];
}

function getMinute(value) {
  return value.split(":", 2)[1];
}

function stopInstance(ec2, instanceId, callback) {
  console.log("stop EC2. id = " + instanceId);
  var params = {
    InstanceIds: [
      instanceId
    ],
  };
  ec2.stopInstances(params, function(err, data) {
    if (err) console.log(err, err.stack);
    else console.log("stop success. instance id = " + instanceId);
    callback();
  });
}

function startInstance(ec2, instanceId, callback) {
  console.log("start EC2. id = " + instanceId);
  var params = {
    InstanceIds: [
      instanceId
    ],
  };
  ec2.startInstances(params, function(err, data) {
    if (err) console.log(err, err.stack);
    else console.log("start success. instance id = " + instanceId);
    callback();
  });
}

function handleInstance(state, start, end) {
  // not support
  if(start >= end) return 'not support';

  var now = getNow();

  if(now >= start && now < end) {
    console.log("running time");
    if(state === "stopped") {
      return "start";
    } else {
      console.log("state = " + state + ". nothing");
      return "nothing";
    }

  } else if(now < start || now >= end) {
    console.log("stopping time");
    if(state === "running") {
      return "stop";
    } else {
      console.log("state = " + state + ". nothing");
      return "nothing";
    }
  } else {
    console.log("nothing");
    return "nothing";
  }
}

function validValue(key, value) {

  // null
  if(!value)  {
    console.log(key + " = null or undefined");
    return false;
  }

  // format
  if(!(value.match(/^[0-9]{1,2}:[0-9][0-9]$/))) {
    console.log("not support format. " + key + " = " + value);
    return false;
  }

  // hour
  if(24 < getHour(value) || 0 > getHour(value)) {
    console.log("not support format(hour). " + key + " = " + value);
    return false;
  }

  // minute
  if(60 < getMinute(value) || 0 > getMinute(value)) {
    console.log("not support format(minute). " + key + " = " + value);
    return false;
  }

  return true;
}

function getNow() {
  return  moment().utcOffset("+09:00");
}

function getDateValue(instance, tagName) {
  var value = "";
  var tagValue = "";
  instance.Tags.forEach(function(tag) {
    if(tag.Key === tagName) tagValue= tag.Value;
  });
  if(!(validValue(tagName, tagValue))) return "";
  var now = getNow();
  var month = now.get('month') + 1; 
  var value = moment(now.get('year') + '-' + month + '-' + now.get('date') + ' ' + getHour(tagValue) + ':' + getMinute(tagValue) + ' +09:00', 'YYYY-MM-DD HH:mm Z');
  console.log(tagName + " = " + value.format());
  return value;
}

// main
exports.handler = function(event, context) {
  console.log("start");
  var ec2 = new aws.EC2();
  params = {
    Filters: [
      {
        Name: 'tag-key',
        Values: ['Type']
      },
      {
        Name: 'tag-value',
        Values: ['dev']
      },
    ]
  };
  ec2.describeInstances(params, function(err, data) {
    if (err) console.log(err, err.stack);
    else if(data.Reservations.length == 0) console.log("don't find ec2");
    else {
      console.log(data);
      async.forEach(data.Reservations, function(reservation, callback) {
        var instance = reservation.Instances[0];
        console.log("check instance(id = " + instance.InstanceId +  ")");
        var start = getDateValue(instance, 'Start');
        var end = getDateValue(instance, 'End');
        if(start != "" && end != "") {
          var result =  handleInstance(instance.State.Name, start, end);
          if(result === "start") {
            startInstance(ec2, instance.InstanceId, function() {
              callback();
            });
          } else if(result === "stop") {
            stopInstance(ec2, instance.InstanceId, function() {
              callback();
            });
          } else {
            callback();
          }
        } else {
          callback();
        }
      }, function() {
        console.log('all done.');
        context.succeed('OK');
      });
    }
  });
};

処理としては以下の流れです。

  • タグにType=devとなっているEC2群を取得
  • それぞれのEC2でタグ情報を取得
  • タグStartとEndの情報と現在の時刻(JST)を比較し、必要があれば起動もしくは停止を行う

同期処理をしたい部分はasyncモジュール、JSTの利用にはmomentモジュールを利用してみました。

以下でzip圧縮し、Lambdaにアップロードして利用します。

$zip -r AutoStartStop.zip index.js node_modules

なお、コード内でAWS SDKでEC2の操作をしているのRoleにはEC2のdescribe,star,stopが利用出来るRoleを設定しておいてください。また、実行時間も1分など少し長くしておくと良いと思います。(実行時間は台数による)

では実際にやってみます。
現在、EC2が2台起動しており、その状態でタグの情報を以下のように変更します。

Screen Shot 2015-10-10 at 12.25.17 PM.png

試験実施時が12時以降なのでLambdaの実行により、EC2がstopすればOKです。Lambdaのマネジーメントコンソールから実行できるのでやってみます。実行後、以下のようにOKと出ていれば処理成功です。

Screen Shot 2015-10-10 at 1.23.32 PM.png

LogOutput(CloudWatch)では以下のようになっており、実際に対象のインスタンも停止状態になっていました。

START RequestId: 5b89ae36-6f04-11e5-adfa-658072629e05 Version: $LATEST
2015-10-10T04:07:01.612Z    5b89ae36-6f04-11e5-adfa-658072629e05    start
2015-10-10T04:07:03.562Z    5b89ae36-6f04-11e5-adfa-658072629e05    { Reservations: 
   [ { ReservationId: 'r-xxxxxx',
       OwnerId: 'xxxxxxx',
       Groups: [],
       Instances: [Object] },
     { ReservationId: 'r-xxxxx',
       OwnerId: 'xxxxxxx',
       Groups: [],
       Instances: [Object] } ] }
2015-10-10T04:07:03.564Z    5b89ae36-6f04-11e5-adfa-658072629e05    check instance(id = i-e7904442)
2015-10-10T04:07:03.682Z    5b89ae36-6f04-11e5-adfa-658072629e05    Start = 2015-10-09T23:00:00+00:00
2015-10-10T04:07:03.683Z    5b89ae36-6f04-11e5-adfa-658072629e05    End = 2015-10-10T03:00:00+00:00
2015-10-10T04:07:03.683Z    5b89ae36-6f04-11e5-adfa-658072629e05    stopping time
2015-10-10T04:07:03.683Z    5b89ae36-6f04-11e5-adfa-658072629e05    stop EC2. id = i-e7904442
2015-10-10T04:07:03.744Z    5b89ae36-6f04-11e5-adfa-658072629e05    check instance(id = i-df26f17a)
2015-10-10T04:07:03.744Z    5b89ae36-6f04-11e5-adfa-658072629e05    Start = 2015-10-09T19:00:00+00:00
2015-10-10T04:07:03.744Z    5b89ae36-6f04-11e5-adfa-658072629e05    End = 2015-10-10T03:00:00+00:00
2015-10-10T04:07:03.744Z    5b89ae36-6f04-11e5-adfa-658072629e05    stopping time
2015-10-10T04:07:03.744Z    5b89ae36-6f04-11e5-adfa-658072629e05    stop EC2. id = i-df26f17a
2015-10-10T04:07:04.116Z    5b89ae36-6f04-11e5-adfa-658072629e05    stop success. instance id = i-df26f17a
2015-10-10T04:07:04.142Z    5b89ae36-6f04-11e5-adfa-658072629e05    stop success. instance id = i-e7904442
2015-10-10T04:07:04.142Z    5b89ae36-6f04-11e5-adfa-658072629e05    all done.
END RequestId: 5b89ae36-6f04-11e5-adfa-658072629e05
REPORT RequestId: 5b89ae36-6f04-11e5-adfa-658072629e05  Duration: 2531.55 ms    Billed Duration: 2600 ms    Memory Size: 128 MB Max Memory Used: 19 MB  

ログのstart及びendの時刻はUTCで表示されているので+9時間するとJSTでの時間となります。

次に今度は起動するか確認します。
タグを以下のように変えて、自動起動するか確認します。

Screen Shot 2015-10-10 at 12.34.59 PM.png

実行時のログは以下のようになっており、実際にも起動処理が終わっていることが確認できました。

START RequestId: 1939a7d7-6f00-11e5-b2d4-2117e61043df Version: $LATEST
2015-10-10T03:36:32.206Z    1939a7d7-6f00-11e5-b2d4-2117e61043df    start
2015-10-10T03:36:33.849Z    1939a7d7-6f00-11e5-b2d4-2117e61043df    { Reservations: 
   [ { ReservationId: 'r-xxxxx',
       OwnerId: 'xxxxxx',
       Groups: [],
       Instances: [Object] },
     { ReservationId: 'r-xxxxxx',
       OwnerId: 'xxxxxx',
       Groups: [],
       Instances: [Object] } ] }
2015-10-10T03:36:33.907Z    1939a7d7-6f00-11e5-b2d4-2117e61043df    check instance(id = i-e7904442)
2015-10-10T03:36:33.968Z    1939a7d7-6f00-11e5-b2d4-2117e61043df    Start = 2015-10-09T23:00:00+00:00
2015-10-10T03:36:33.969Z    1939a7d7-6f00-11e5-b2d4-2117e61043df    End = 2015-10-10T10:00:00+00:00
2015-10-10T03:36:33.969Z    1939a7d7-6f00-11e5-b2d4-2117e61043df    running time
2015-10-10T03:36:33.969Z    1939a7d7-6f00-11e5-b2d4-2117e61043df    start EC2. id = i-e7904442
2015-10-10T03:36:34.085Z    1939a7d7-6f00-11e5-b2d4-2117e61043df    check instance(id = i-df26f17a)
2015-10-10T03:36:34.086Z    1939a7d7-6f00-11e5-b2d4-2117e61043df    Start = 2015-10-09T19:00:00+00:00
2015-10-10T03:36:34.086Z    1939a7d7-6f00-11e5-b2d4-2117e61043df    End = 2015-10-10T10:00:00+00:00
2015-10-10T03:36:34.086Z    1939a7d7-6f00-11e5-b2d4-2117e61043df    running time
2015-10-10T03:36:34.086Z    1939a7d7-6f00-11e5-b2d4-2117e61043df    start EC2. id = i-df26f17a
2015-10-10T03:36:34.667Z    1939a7d7-6f00-11e5-b2d4-2117e61043df    start success. instance id = i-e7904442
2015-10-10T03:36:34.756Z    1939a7d7-6f00-11e5-b2d4-2117e61043df    start success. instance id = i-df26f17a
2015-10-10T03:36:34.756Z    1939a7d7-6f00-11e5-b2d4-2117e61043df    all done.
END RequestId: 1939a7d7-6f00-11e5-b2d4-2117e61043df
REPORT RequestId: 1939a7d7-6f00-11e5-b2d4-2117e61043df  Duration: 2564.25 ms    Billed Duration: 2600 ms    Memory Size: 128 MB Max Memory Used: 19 MB

上記のように確認できれば、Lambdaで本functionを1時間に1回実行するようにすることでタグ情報に従ってEC2の自動起動、停止が可能です。また、タグに曜日情報を入れる、起動・停止の際にSlackに通知などもコードを変えることによって自由に変更可能だと思います。