定期的にEC2インスタンスを停止・起動する -1-

定期的に EC2・RDS インスタンスを停止・起動する仕組みの CloudFormation テンプレート | Developers.IO

AWS初学者なのですが、開発環境にこういうのを欲しいとちょうど思ってたので、勉強的な意味で自分で作ってみようと思います。

EC2を起動・停止するLambda

上のサイトだとPythonを使っているのですが、いかんせん、Pythonが全く分からない、、、

ので、ほんの少しだけ知っているJavaScriptで書いてみます。

AWS SDK for JavaScript - Start/Stop Instances

Managing Amazon EC2 Instances - AWS SDK for JavaScript を見ると、 EC2を起動・停止するスクリプトはこんな感じになるようです。

起動と停止を両方できる感じで書いてあるので、わりとゴチャっとしています。少しずつ調べます。

// Load the AWS SDK for Node.js
var AWS = require('aws-sdk');
// Set the region 
AWS.config.update({region: 'REGION'});

// Create EC2 service object
ec2 = new AWS.EC2({apiVersion: '2016-11-15'});

var params = {
  InstanceIds: [process.argv[3]],
  DryRun: true
};

if (process.argv[2].toUpperCase() === "START") {
  // call EC2 to start the selected instances
  ec2.startInstances(params, function(err, data) {
    if (err && err.code === 'DryRunOperation') {
      params.DryRun = false;
      ec2.startInstances(params, function(err, data) {
          if (err) {
            console.log("Error", err);
          } else if (data) {
            console.log("Success", data.StartingInstances);
          }
      });
    } else {
      console.log("You don't have permission to start instances.");
    }
  });
} else if (process.argv[2].toUpperCase() === "STOP") {
  // call EC2 to stop the selected instances
  ec2.stopInstances(params, function(err, data) {
    if (err && err.code === 'DryRunOperation') {
      params.DryRun = false;
      ec2.stopInstances(params, function(err, data) {
          if (err) {
            console.log("Error", err);
          } else if (data) {
            console.log("Success", data.StoppingInstances);
          }
      });
    } else {
      console.log("You don't have permission to stop instances");
    }
  });
}

AWS.config.update()

AWS.config.update({region: 'REGION'})のところで、SDKの接続先のリージョンを設定しています。他の設定オプションはClass: AWS.Config — AWS SDK for JavaScriptの「General Configuration Options」に列挙してあるもののようです。

new AWS.EC2()

ec2 = new AWS.EC2({apiVersion: '2016-11-15'});でEC2に接続するオブジェクトを作っています。ここでは、apiVersionを指定していますがClass: AWS.EC2 — AWS SDK for JavaScriptを確認すると、特に指定する意図がない場合は、省略可能なようです。

ec2.startInstances()

ec2.startInstances()のドキュメントは、Class: AWS.EC2 — AWS SDK for JavaScriptにあります。

一回目にec2.startInstancesを実行しているときは、引数(params)で DryRun: true を指定していて、要するに権限があるかどうかを確認しています。で、コールバックの第一引数(err)にその結果が渡されています。errってわかりにくいですね。

err.codeDryRunOperationの場合は権限があり実行可能と判定されています。権限が不足している場合などは、err.codeUnauthorizedOperationになります。

権限チェックが終わったら、DryRunfalseにして実際にEC2を起動します。この時のコールバックのerrには、エラーが発生した場合のみErrorが格納されます。

console.log()

AWS Lambdaでconsole.log()を実行すると、自動的にCloudWatchに送信されます。

ログ作成 (Node.js) - AWS Lambdaを確認すると、

CloudWatch ログ – CloudWatch でログを確認するには、ロググループ名とログストリーム名を把握しておく必要があります。コードに context.logGroupName メソッドと context.logStreamName メソッドを追加することで、この情報を取得できます。Lambda 関数を実行すると、コンソールまたは CLI の結果ログが、ロググループ名およびログストリーム名を表示します。

と書いてあるので、ふむ、という気持ちになりますが、AWS Lambda の Amazon CloudWatch ログへのアクセス - AWS Lambdaを確認すると、

Lambda は、自動的に CloudWatch Logs と統合され、Lambda 関数に関連付けられた CloudWatch Logs グループ (/aws/lambda/<関数名> という名前) にすべてのログをコードからプッシュできます。

と書かれていて、少し安心します。きっと、ログストリーム名はいつ起動されたかわかるような感じの名前になるんじゃないかな、って感じでしょうね。

ec2.stopInstances()

ec2.stopInstances()のドキュメントは、Class: AWS.EC2 — AWS SDK for JavaScriptにあります。

強制的に止めるForceというオプションが増えていますが、ec2.startInstances()と基本的に同じみたいですね。

AWS Lambdaで動かす

中身をそこそこ理解できて来たので、AWS Lambdaで動かしてみようと思います。とりあえず、EC2は一つ用意してあって、インスタンスIDを控えておきます。EC2の「シャットダウン動作」は「停止」にしておきました。

まずは、シンプルな Lambda 関数を作成する - AWS Lambdaから入門してみます。

関数の作成

「一から作成」を選んだ状態で進めます。ランタイムはNode.js 8.10にしていて、ロールはEC2の起動停止に必要な権限を付け加えたポリシーを使いました。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ec2:StartInstances",
        "ec2:StopInstances"
      ],
      "Resource": "*"
    }
  ]
}

デフォルトで、こんな感じの関数が入ってました。

exports.handler = async (event) => {
    // TODO implement
    return 'Hello from Lambda!'
};

AWS Lambdaで起動する関数は、exports.handlerに設定された関数になるってことで、Lambdaをキックしたイベントの情報をeventから受け取れそうな感じですね。

とりあえず、コンソールにこんにちはしてみます。

exports.handler = async (event) => {
    console.log('Hello, CloudWatch!!!')
    return 'Hello from Lambda!'
};

「保存」して、「テスト」を実行します。「保存」しないで「テスト」すると、変わんないなぁって悩むことになります。

console.log()で吐き出したものは、確かにCloudWatchに送信されていて、「/aws/lambda/」というロググループの「/yyyy/MM/dd/[$LATEST]」ってログストリームに出力されてました。

どうやら、$LATESTってところにはバージョン番号が入りそうな感じですかね。

ひとまず停止してみる

とりあえず、EC2が起動状態なので、停止してみます。環境変数EC2_INSTANCE_IDに止めたいEC2のインスタンスIDを設定しておきます。

exports.handler = async (event) => {
    const AWS = require('aws-sdk');
    AWS.config.update({region: 'ap-northeast-1'});
    const params = {
      InstanceIds: [process.env.EC2_INSTANCE_ID],
      DryRun: true
    };

    const ec2 = new AWS.EC2()

    ec2.stopInstances(params, function(err, data) {
      if (err && err.code === 'DryRunOperation') {
        console.info(`You can stop instances ${params.InstanceIds}`)
      } else {
        console.error("You don't have permission to stop instances");
      }
    });
};

3秒以内に終わんなかったよ、失敗だよ、って言われました。タイムアウトを5分にしてもう一回チャレンジです。そうすると、正常に完了するんですが、ログが出ないです。asyncだから少し待てばいいのかなぁ、なんて思ってたんですが、待てど暮らせど出ないです。

LambdaのLambda 関数ハンドラー (Node.js) - AWS Lambdaというドキュメントを確認したら、そもそもNode.js 8.10は存在すら認められてなくて、asyncについて一切言及がないので、やめました。

とりあえず、async外せばログは出ます。なので、asyncは外しておくことにします。

では、実際に止めてやります。

exports.handler = (event) => {
    const AWS = require('aws-sdk');
    AWS.config.update({region: 'ap-northeast-1'});
    const params = {
      InstanceIds: [process.env.EC2_INSTANCE_ID],
      DryRun: true
    };

    const ec2 = new AWS.EC2()

    ec2.stopInstances(params, function(err, data) {
      if (err && err.code === 'DryRunOperation') {
      params.DryRun = false;
      ec2.stopInstances(params, function(err, data) {
          if (err) {
            console.error(`Failed to stop instances ${params.InstanceIds}.\n`, err);
          } else if (data) {
            console.info(`Successfully stopping instances ${params.InstanceIds}.\n`, data.StoppingInstances);
          } else {
            console.info('Unknown state. No error occurred and no data passed.')
          }
      });
      } else {
        console.error("You don't have permission to stop instances");
      }
    });
};

無事に、止まりました。

起動してみる

停止の時とほとんど同じ方法で、今度は起動してみます。といっても、ただのコピペになるので、少し工夫してみます。

Scheduled Eventsで起動するつもりなので、ルール名がStartScheduledRuleStopScheduledRuleかに応じて、起動と停止を呼び分けるようにします。

CloudWatch Events Event Examples From Each Supported Service - Amazon CloudWatch Eventsを確認すると、Scheduled Eventsの場合はデータとして次のようなものが渡されるようです。

{
  "id": "53dc4d37-cffa-4f76-80c9-8b7d4a4d2eaa",
  "detail-type": "Scheduled Event",
  "source": "aws.events",
  "account": "123456789012",
  "time": "2015-10-08T16:53:06Z",
  "region": "us-east-1",
  "resources": [ "arn:aws:events:us-east-1:123456789012:rule/MyScheduledRule" ],
  "detail": {}
}

resourcesに含まれるARNが、キック元のルール名に対応するので、そこに起動用のルール名が含まれていればEC2を起動し、停止用のルール名が含まれていればEC2を停止するようにしてみます。

function stopEC2Instances(ec2, params) {
    ec2.stopInstances(params, function(err, data) {
      if (err && err.code === 'DryRunOperation') {
      params.DryRun = false;
      ec2.stopInstances(params, function(err, data) {
          if (err) {
            console.error(`Failed to stop instances ${params.InstanceIds}.\n`, err);
          } else if (data) {
            console.info(`Successfully stopping instances ${params.InstanceIds}.\n`, data.StoppingInstances);
          } else {
            console.info('Unknown state. No error occurred and no data passed.')
          }
      });
      } else {
        console.error("You don't have permission to stop instances");
      }
    });
}

function startEC2Instances(ec2, params) {
    ec2.startInstances(params, function(err, data) {
      if (err && err.code === 'DryRunOperation') {
      params.DryRun = false;
      ec2.startInstances(params, function(err, data) {
          if (err) {
            console.error(`Failed to start instances ${params.InstanceIds}.\n`, err);
          } else if (data) {
            console.info(`Successfully starting instances ${params.InstanceIds}.\n`, data.StartingInstances);
          } else {
            console.info('Unknown state. No error occurred and no data passed.')
          }
      });
      } else {
        console.error("You don't have permission to start instances");
      }
    });
}

exports.handler = (event) => {
    const AWS = require('aws-sdk');
    AWS.config.update({region: 'ap-northeast-1'});
    const params = {
      InstanceIds: [process.env.EC2_INSTANCE_ID],
      DryRun: true
    };

    const ec2 = new AWS.EC2()

    if(event['resources'].find(r => r.includes('StopScheduledRule'))) {
      stopEC2Instances(ec2, params)
    }

    if(event['resources'].find(r => r.includes('StartScheduledRule'))) {
      startEC2Instances(ec2, params)
    }
};

起動できました。が、あまりにもソースコードがきちゃない、、、ので、少し見直してみた。こんな感じでよいのだろうか。

あと、eventのデータをカスタマイズして、event.commandを入れる前提にしてみました。ルール名で縛るのは、不便な気がしたので。

const {promisify} = require('util')
const AWS = require('aws-sdk');

function instanceIds(ec2) {
  return [process.env.EC2_INSTANCE_ID]
}

exports.handler = async event => {
  console.log(event)
  
  const ec2 = new AWS.EC2()

  const actions = {
    start: (p, cb) => ec2.startInstances(p, cb),
    stop: (p, cb) => ec2.stopInstances(p, cb),
  }

  await promisify(actions[event.action])({InstanceIds: instanceIds(ec2), DryRun: false})
      .then(r => console.log(JSON.stringify(r, null, '  ')))
      .catch(e => { throw e })
}

これで、EC2を起動・停止するスクリプトを作成することができました。

Comments

comments powered by Disqus