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

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

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

+ 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](https://qiita-image-store.s3.amazonaws.com/0/20193/d434b95d-ae1c-30b6-d597-59a5f3d50e88.png "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分と、１時間ごと、１日ごと）または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](https://qiita-image-store.s3.amazonaws.com/0/20193/5c3132ef-d2c7-2297-cf32-f1aa6bad6eda.png "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](https://qiita-image-store.s3.amazonaws.com/0/20193/f7d02a47-a703-774e-d126-ff972f038275.png "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](https://qiita-image-store.s3.amazonaws.com/0/20193/8d93682c-6a06-6514-5f20-7e8a719e737f.png "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に通知などもコードを変えることによって自由に変更可能だと思います。