AWSのEC2サーバーをLambdaを使って自動起動


目的:EC2サーバーの自動起動&停止でコストダウン

 AWSを使用することが多くなってくると、運用コストが気になってきます。他(社)のクラウドサービスと異なり、AWSは基本的には従量制です。インスタンス(CPU)やストレージ(ディスク)、トラフィックのデータまでほとんどのサービスが使った量・時間に対して課金されます。
 EC2インスタンスは停止すれば課金されない(正確にはストレージや固定IPアドレス等は停止中も課金される)ので、使わないときはEC2インスタンスを停止しておき、使用時のみ起動して立ち上げる仕組みを、AWS Lambdaを使って構築します。

 弊社ではソーシャルゲームを開発・運用していますが、イベント実行時には普段の十数倍のアクセス集中や負荷があります。AWSではオートスケール機能もありますが、あらかじめ予定されているのであれば時間前にサーバーを追加しておくのが安全です。
また、 バッチファイル実行やログ収集を夜中や早朝に行う場合などにも、必要時のみ起動できればコスト削減に役立つでしょう。

AWSドキュメントの参照

 AWSを解説したサイトは多いですが、Lambdaを解説したサイトは以外に少ないです。さらにAWSの機能追加やUI(ユーザーインターフェース)の変更も頻繁なため、情報が古い場合も多いです。
 まずはAWSの公式ドキュメントを参照しましょう。Lambdaの公式日本語ドキュメントは2020/5/5現在は下記です。

日本語ドキュメントは、英語のままの部分や実際のUIの変更に追いついていない部分、技術者ではなく人が翻訳したのでは無いかと思われる部分もありますので、注意が必要です。

例えば、2020/5/5現在下記のドキュメントページで[Create a function]は、コンソールではすでに[関数の作成]と翻訳表示されています。

コンソールで Lambda 関数を作成する

AWSドキュメントページでは、各国語に切り替えられるので、原文の英語と日本語を見比べることも必要です。

AWS Lambdaコンソールで関数を作成

 EC2インスタンスを起動させるには、スクリプトを作成してAWSのAPIを呼び出すことになります。Lambdaではこのスクリプト(関数)を呼び出すことになります。
 早速AWSコンソール(AWSにログインしてメニュー一覧)から「Lambda」を選択します。

 Lambdaのダッシュボード、または初回はウイザードが表示されるかもしれません。
左側のメニューから「関数」を選択します。直接「関数の作成」をクリックすると、下の画面がスキップされて、関数の作成ウイザードに進みます。

 この画面には作成した関数が一覧表示されます。まだ関数を作成していないので「関数の作成」をクリックします。関数を作成するためのオプションが表示されます。

 ここで「一から作成」を選択し、関数名に「Start_webserver」と入力。ランタイムにはJavaScriptで記述するため「Node_js 12.x」を選びます。実行ロールの選択または作成では「基本的なLambdaアクセス権限で新しいロールを作成」を選びます。
 入力・選択をしたら「関数の作成」をクリックします。

テンプレートとなるスクリプト(関数コード)が表示されています。このプロジェクトを実行してみましょう。ウインドウ上部の「テスト」または、コードが表示されている部分のタブの上の「Save」の右の「Test」をクリックすると次のようなウインドウが表示されます。

これは、スクリプトが呼び出される時に、引き渡すパラメータの設定画面です。今回は使わないのですが、テストイベント名は設定しなくてはならないので、イベント名に「TestEvent」と入力して「作成」ボタンを押します。
 再度「テスト」または「Test」をクリックしてスクリプトを実行します。

このようにコードの下部に「Execution Result」のタブが表示され、実行結果(Response)や、Lambda内部のリクエストID、実行ログが表示され、実行(テスト)がエラーも無く正常に終了したことが確認できます。

スクリプトはどこから実行されるのか

 テンプレートサンプルの実行まではできましたが、モヤモヤが残ります。
 AWSのLambdaを解説しているサイトなども多いですが、スクリプトはどこから実行されるのか、言い換えると最初はどの関数が呼び出されるのかがはっきりとわかりませんでした。
 前項の短いテンプレートのスクリプトでは、関数がexports.handler の1つしかないのでこれが実行されるのは理解できますが、ドキュメントで明確に説明しているところが見つかりません。
AWSの公式ドキュメントを見ながら悩んでましたが、ふと見つけました。コードのハンドラの右側の「情報」をクリックするとヘルプが表示されました。

 これで最初に実行される関数も、ファイル名?の「index.js」も理解できました。
 ハンドラで表示されている、XXX.YYYの部分で、XXX.jsのファイルタブの中の、export.YYY関数が実行されるということです。

実行スクリプトにEC2インスタンスのロール(権限)を付与

 これからサーバーを起動・停止させるスクリプト(関数)を作成します。その前に、スクリプトを実行する際に、EC2インスタンスを起動・停止させることのできるロール(権限)を付与します。
 関数名の下にある、「アクセス権限」をクリックします。

アクセス権限の画面に移行すると、この関数に割当てられているロール名が表示されます。この名前は自動的に割り当てられたものです。このロール名をクリックします。

 新しいウインドウ(またはタブ)で IAM(ユーザーアクセスと暗号化キーの管理)の画面が開きます。

ここで1つだけ設定されているポリシーを展開(▶をクリック)します。

次にポリシーの編集をクリックします。

「+さらにアクセス許可を追加する」をクリックします。
 サービスの追加画面が出るので、以下のように追加します。

・サービス→EC2
・アクション→検索バーに「Instances」を入力し、出てきた一覧から「StartInstances」「StopInstances」をチェック
・リソース→全てのリソース

 設定が終わったら「ポリシーの確認」をクリックします。

確認し「変更の保存」をクリックします。

 これで実行するスクリプトに、EC2インスタンスサーバーを起動・停止できる権限が付与されました。権限が無いと、APIを呼び出しても実行されません。

EC2インスタンス起動スクリプト

 テンプレートプロジェクトの動作確認ができ、ロール(権限)の設定準備ができたので、実際にEC2インスタンスを起動させるスクリプトを記述します。
 テンプレートプロジェクトのコード部分(index.jsのタブ内)を下記のコードに置き換えます。

index.js
const INSTANCE_ID = ['i-052838b19d5xxxxxx']; 
var AWS = require('aws-sdk'); 
AWS.config.region = 'ap-northeast-1';

exports.handler =  function(){
    var ec2 = new AWS.EC2();
    var params = {
        InstanceIds: INSTANCE_ID
    };
    console.log('Script Start!');
    ec2.startInstances(params, function(err, data) {
        if (err) console.log(err, err.stack);
        else     console.log(data);
    });
};

 行始めの INSTANCE_IDには、起動させたいEC2インスタンスIDを設定して下さい。インスタンスIDは、i-から始まるIDで、EC2インスタンス一覧から、インスタンスを選択すると表示されます。
 このINSTANCE_IDは、下記のように複数指定することが可能ですので、同時に複数のインスタンスの起動ができます。

const INSTANCE_ID = ['i-052838b19d5xxxxx1', 'i-052838b19d5xxxxx2', 'i-052838b19d5xxxxx3']; 

置き換えが終わったら、コード上部の「Save」または右上の「保存」をクリックし、「Test」または「テスト」をクリックします。
 以下のような実行結果が表示されるはずです。

エラーが出ていなければ、正常にEC2インスタンスの起動指示が完了したということです。あくまでも起動指示なので、停止状態のインスタンスであれば、ここから起動の準備からブートなどに時間がかかります。エラーならばどうなるのかを確認したければ、INSANCE_ID部分を不正な値にして実行してみて下さい。
 気になるのは実行結果のログ表示で、CurrentState: [Object]PreviousState: [Object]の部分です。このObjectの中身を展開するためには、ログ出力部分を、

console.log(JSON.stringify(data)); 

として、JSON形式データの中身を表示するようにしてみてください。

指定の時刻にEC2インスタンスを起動

 EC2インスタンスを起動させることができましたが、このスクリプト関数を任意の時刻に実行するようにします。
「+トリガーを追加」をクリックして表示される、トリガーの設定画面から「Cloudwatch Events/EventsBridge」を選択します。

ルールで「新規ルールの作成」を選び、ルール名に任意のルール名を英字で設定します。ここでは「EC2_web2_launch_at_PM8」としています。ルールの意味がわかるように設定して下さい。
 ルールタイプで「スケジュール式」を選び、スケジュール式に起動時刻を設定します。起動時刻の指定方法は、Linuxサーバーで使われているCRONに似た形式で指定し、「cron(0 11 * * ? *)」と入力しています。
 指定方法は下記ドキュメントに詳しく説明してあります。

 Rate または Cron を使用したスケジュール式

 ここで気をつけるのは、時刻をGMT(グリニッジ標準時=世界標準時=UTC)で指定するということです。日本時間はGMT+9時間ですので、起動させたい日本時間より9時間前(マイナス9時間)を指定しなくてはなりません。
 「cron(0 11 * * ? *)」というのは毎日日本時間20時(GMT11時)にイベント(スクリプト)起動指定を意味しています。設定が終わったら「追加」をクリックします。

スケジュール通りに実行されるか確認

 これで設定は終了です。指定した時刻になるとEC2インスタンスが起動するはずです。EC2インスタンス一覧画面から確認してください。
 また、ログが出力されていますので「CloudWatch」の「ロググループ」から確認できます。/aws/lambda/関数名の表示があるので、すぐに見つかると思います。
 実際に運用する際には、正常に起動したかどうかをステータスで確認したり、色々考えられることはありますが、インスタンスの起動が失敗する可能性は少ないと思われますから、まずはここまでとします。

EC2インスタンスを自動シャットダウンするには

 EC2インスタンスの自動起動を設定しましたが、自動シャットダウンも同様の手順で進めます。別の関数プロジェクトにした方がいいと思いますが、呼び出すAPIをstartInstancesからstopInstancesに変更するだけです。呼び出しパラメータも同じです。
 ただ、バッチ処理のためのサーバー起動などのケースでは、Lambdaを使用してインスタンスのシャットダウン処理をするより、バッチ処理終了後にサーバーが自分自身でシャットダウンしたほうが効率的かもしれません。