AWS Lambdaでスポットインスタンスを作成して、UserDataで環境構築や長い処理を自動で実行させる


「スポットインスタンスを作成 -> 環境構築 -> 長い処理を実行する」なんてことを、「マネージメントコンソールからUIをポチポチ、SSHでコマンド流し込んで」と手作業でやってたんですが。

毎度同じことをやるのは面倒、ということで、Lambdaで自動化してみました。

# Code for Spot Instance creation

長い処理を行うために別途書いたシェルスクリプトを、`s3://mybucket/sh/run.sh`に予め置いてあるという前提で。

```js
console.log('Loading function');

var s3Bucket = 'mybucket';
var shellScriptS3Key = 'sh/run.sh';
var shellScriptS3Path = 's3://' + s3Bucket + '/' + shellScriptS3Key;

var region = 'ap-northeast-1';
var availabilityZone = 'ap-northeast-1a';
var spotPrice = '0.05';
var imageId = 'ami-9a2fb89a';
var instanceType = 'r3.large';

var iamInstanceProfile = 'my_ec2_instance_role';
var securityGroup = 'launch-wizard-1';
var keyName = 'mysshkeypair';

var userData = (function () {/*#!/bin/bash
yum -y update
aws s3 cp %s /home/ec2-user/run.sh
chown ec2-user /home/ec2-user/run.sh
chmod +x /home/ec2-user/run.sh
su - ec2-user /home/ec2-user/run.sh
*/}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1];

exports.handler = function(event, context) {
    var util = require('util');
    var AWS = require('aws-sdk');
    AWS.config.region = region;
    
    var userDataFormatted = util.format(userData, shellScriptS3Path);
    var userDataBase64 = new Buffer(userDataFormatted).toString('base64');

    var ec2LaunchParams = {
        SpotPrice: spotPrice, 
        LaunchSpecification : {
            IamInstanceProfile: {
              Name: iamInstanceProfile
            },
            ImageId: imageId,
            InstanceType: instanceType,
            KeyName: keyName,
            Placement: {
              AvailabilityZone: availabilityZone
            },
            SecurityGroups: [
                securityGroup
            ],
            UserData: userDataBase64
        }
    };
        
    var ec2 = new AWS.EC2();
    ec2.requestSpotInstances(ec2LaunchParams, function(err, data) {
        if (err) {
            console.log(err, err.stack);
            context.fail('[Fail]');
        }
        else {
            console.log(data);
            context.succeed('[Succeed]');
        }
    });
};

```

# Notes

## UserData

Userdataは、ヒアドキュメントでインラインに書いちゃってます。以下を参考にさせてもらいました。

>SafariでもエラーにならないJavascriptのヒアドキュメントの書き方
http://qiita.com/ampersand/items/c6c773ba7ae9115856d0

あと、注意点としては、Base64にエンコードしてやる必要があるってことですかね。

## IAM role for EC2 instance profile required

起動中のEC2内でS3のファイルをダウンロードするには、S3へのアクセス権を振ったIAMロールをインスタンスプロファイルとして設定する必要があります。IAMロールを適当に作って、以下を書き換えてください。

```js
var iamInstanceProfile = 'my_ec2_instance_role';
```

## Security Group

セキュリティグループの指定は必須ではないはずですが、launch-wizard-xxが増えるのもアレなので、既存のを使っちゃうのを推奨しておきます。

```js
var securityGroup = 'launch-wizard-1';
```

## Instance Storage

インスタンスストレージの設定、API documentを見るに、「BlockDeviceMappings」の「VirtualName」で指定と言っているような感じでしょうか？

```js
BlockDeviceMappings: [
    {
         VirtualName: 'ephemeral0'
    }
],
```

> EC2 requestSpotInstances - AWS SDK for JavaScript
http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html#requestSpotInstances-property

まあ、SSD付/EBSのみで切り替えるの面倒で、上記のコードでは試していないわけですが。

#  Terminate Instance after processing - with aws cli

長い処理終わったあと、インスタンスの終了まで自動化したい場合、

0. インスタンスメタデータで自分自身のインスタンスIDを拾う
1. aws cliで止める

ってのが、お手軽ですかね。具体的にはこんな感じで。

```shell-session
response=$(curl http://169.254.169.254/latest/meta-data/instance-id)
export AWS_DEFAULT_REGION=ap-northeast-1
aws ec2 terminate-instances --instance-ids $response
```

EC2のインスタンスプロファイルとして設定するIAMロールに、EC2の操作をする権限の追加が必要にはなりますが。

#  Terminate Instance after processing - with SNS and Lambda

インスタンスの自動終了については、SNSを飛ばしてLambdaで殺す方が便利ってば便利ですね。長い処理の途中でWarningが出てるのを見つけてしまって、EC2を終了させずに調査したいとかいうときに、SNSとLambdaの連携を切ればよくなるので。

適当にSNS Topic作って、instance-idをmessageに含めつつSNS飛ばす。

```shell-session
current_instance_id=$(curl http://169.254.169.254/latest/meta-data/instance-id)
export AWS_DEFAULT_REGION=ap-northeast-1
aws sns publish --topic-arn arn:aws:sns:ap-northeast-1:xxxxxxxxxxxx:KillEc2Request --subject "kill instance request" --message $current_instance_id
```

んで、このSNSメッセージを受けるLambdaは以下。

```js
console.log('Loading function');

exports.handler = function(event, context) {
    var message = event.Records[0].Sns.Message.toString();
    console.log('[Start] EC2 Instance kill: ', message);
    
    var AWS = require('aws-sdk');
    AWS.config.region = 'ap-northeast-1';
    var ec2 = new AWS.EC2();
    var params = {
        InstanceIds: [
            message
        ]
    };
    
    ec2.terminateInstances(params, function(err, data) {
        if (err) {
            console.log(err, err.stack);
            context.fail('[Error] EC2 Instance kill');
        }
        else {     
            console.log(data);
            context.succeed('[Done] EC2 Instance kill');
        }
    });
};

```

リージョンを直で書いちゃってるので東京以外でうごかねーじゃんと自分でも思いましたが、その辺は必要に応じて適当に弄ってください。