読者です 読者をやめる 読者になる 読者になる

まちいろエンジニアブログ

南池袋のWebサービス開発会社、株式会社まちいろのエンジニアブログです。

AWS Lambda で EC2 インスタンスを自動起動・停止してみる (実装編)

AWS Lambda Node.js

こんにちは、まちいろの工藤です。

今回は前回の続きとして、AWS Lambda を用いてEC2 インスタンス自動起動・停止の実装に入って行きたいと思います。

仕様決め

まずは、今回実装する仕組みの仕様を決めたいと思います。

EC2 に OperatingSchedule という名称のタグを設け、起動時間-停止時間 (曜日)のフォーマットで設定可能とします。
この時間帯・曜日以外であれば自動的に停止するようにしたいと思います。

まちいろの就業時間である9時〜18時に稼働させたい場合の設定は以下のようになります。
起動はちょっと早めにしておきます。

f:id:mkudo-machiiro:20160615152325p:plain:w400

aws-sdk を用いて実装

Node.js で AWSAPI を実行するためのモジュール aws-sdk と、lodashmoment をインストールします。

$ npm install aws-sdk lodash moment --save

まず、OperatingSchedule タグを持つ EC2 インスタンスの情報を取得する必要があります。
aws-cliec2 describe-instances に相当する AWS.EC2.describeInstances メソッドを使用します。

  const ec2 = new AWS.EC2();
  ec2.describeInstances({
    Filters: [{Name: 'tag-key', Values: ['OperatingSchedule']}]
  }).promise().then((data) => {
    const all = _.flatten(_.map(data.Reservations, (reservation) => reservation.Instances));

    // 以降の処理を記述...
  }).catch((err) => {
    context.fail(err)
  });

次に、稼働時間帯かどうかを判定するフィルター処理を実装します。
この実装は別ファイル lib/schedule-filter.js として作成します。

const _ = require('lodash');
const moment = require('moment');
const REGEXP = /([0-9]{4})-([0-9]{4})\s\((.+)\)/;

/*
 * 現在稼働中であるべきかどうかを Boolean で返す.
 */
module.exports = (instance) => {
  // タグ設定が正規表現とマッチしない場合は、停止しないよう true を返す
  const tag = _.find(instance.Tags, (tag) => {return tag.Key === 'OperatingSchedule'});
  if (!tag) {
    return true;
  }

  // スケジュール設定を取得
  const schedule = tag.Value;
  const values = schedule.match(REGEXP);
  if (!values) {
    return true;
  }

  // 現在日時を取得
  const now = moment().utcOffset('+09:00');

  // 指定された曜日でない場合は false
  if (!_.includes(values[3].split(','), now.format('ddd'))) {
    return false;
  }

  // 稼働時間帯でない場合は false
  const start = now.clone().hour(parseInt(values[1].substring(0, 2))).minutes(parseInt(values[1].substring(2, 4))).seconds(0);
  const end = now.clone().hour(parseInt(values[2].substring(0, 2))).minutes(parseInt(values[2].substring(2, 4))).seconds(0);
  if (start.isAfter(now) || end.isBefore(now)) {
    return false;
  }

  return true;
};

このフィルター処理を組み合わせ、EC2 が稼働時間帯かつステータスが stopped となっている場合に起動するスクリプト start.js を完成させます。
停止スクリプト stop.js は、ステータスの判定 running に変わるのと、startInstancesstopInstances に変わるだけです。

const _ = require('lodash');
const AWS = require('aws-sdk');
const scheduleFilter = require('lib/schedule-filter');

exports.handler = function(event, context) {
  const ec2 = new AWS.EC2();
  ec2.describeInstances({
    Filters: [{Name: 'tag-key', Values: ['OperatingSchedule']}]
  }).promise().then((data) => {
    // 全ての EC2 インスタンスを取得
    const all = _.flatten(_.map(data.Reservations, (reservation) => reservation.Instances));
    // 対象となるインスタンスに絞り込む
    const filtered = _.filter(all, (instance) => {return instance.State.Name === 'stopped' && scheduleFilter(instance);});
    // インスタンスIDのリストに変換
    const ids = _.map(filtered, (instance) => {return instance.InstanceId;});

    if (_.isEmpty(ids)) {
      context.succeed();
      return;
    }

    ec2.startInstances({
      InstanceIds: ids
    }).promise().then((data) => {
        context.succeed(ids);
      }).catch((err) => {
        context.fail(err)
      });

  }).catch((err) => {
    context.fail(err)
  });
};

ちなみに Lambda では、Function の処理が成功の場合は context.succeed();、失敗の場合は context.fail(); をコールします。
詳細は下記公式ドキュメントを参照してください。

Context オブジェクト (Node.js) - AWS Lambda

デプロイタスクの修正

今回は起動と停止の2つの Lambda Function を追加したのと、新たに lib ディレクトリが追加されたため、前回作成したデプロイタスクを少し修正します。

const gulp = require('gulp');
const zip = require('gulp-zip');
const del = require('del');
const install = require('gulp-install');
const runSequence = require('run-sequence');
const awsLambda = require("node-aws-lambda");

gulp.task('clean', function() {
  return del(['./dist', './dist.zip']);
});

// start.js と stop.js をそれぞれコピー
gulp.task('js', function() {
  return gulp.src(['start.js', 'stop.js'])
    .pipe(gulp.dest('dist/'));
});

// lib をコピーする処理を追加
gulp.task('lib', function() {
  return gulp.src(['lib/**/*.js'])
    .pipe(gulp.dest('dist/lib'));
});

gulp.task('node-mods', function() {
  return gulp.src('./package.json')
    .pipe(gulp.dest('dist/'))
    .pipe(install({production: true}));
});

gulp.task('zip', function() {
  return gulp.src(['dist/**/*', '!dist/package.json'])
    .pipe(zip('dist.zip'))
    .pipe(gulp.dest('./'));
});

// deploy を2回実行
gulp.task('upload', function(callback) {
  awsLambda.deploy('./dist.zip', require("./lambda-config-start.js"), () => {
    awsLambda.deploy('./dist.zip', require("./lambda-config-stop.js"), callback);
  });
});

gulp.task('deploy', function(callback) {
  return runSequence(
    ['clean'],
    ['js', 'lib', 'node-mods'],
    ['zip'],
    ['upload'],
    callback
  );
});

再度デプロイすると、起動・停止の2つの Lambda Function が追加されます。

IAM ロールに権限を付与

IAM ロールに対して、スクリプト内で使用している API をコールするために必要な権限を追加します。
今回は ec2:describe-instancesec2:stop-instancesec2:start-instances を使用しているため、以下の設定を追記します。

{
    "Effect": "Allow",
    "Action": [
        "ec2:StartInstances",
        "ec2:StopInstances",
        "ec2:DescribeInstances"
    ],
    "Resource": "*"
}

Lambda Function の定期実行設定

ここまでの設定で、EC2 の起動・停止が可能となりました。
最後に、自動的に Lambda Function が実行されるよう、イベントソースの設定を行います。

Function の詳細画面にある [Event sources] タブから、以下のイベントソースを新規に追加します。

項目 設定値
Event source type CloudWatch Events - Schedule
Rule name rule-15minutes
Schedule expression rate (15 minutes)

これで、15分に1回定期実行され、自動起動・停止を実現することができました。

まとめ

いかがでしたでしょうか。現在まちいろでは本番環境での Lambda の利用を始めていますが、今後は開発フローをどのようにしていくかが課題だと感じています。

かなり長いエントリとなってしまいましたが、設定自体は非常に簡単なので、興味はあるけどまだ触ったことがないという方は是非挑戦してみてください。