「スポットインスタンスを作成 -> 環境構築 -> 長い処理を実行する」なんてことを、「マネージメントコンソールからUIをポチポチ、SSHでコマンド流し込んで」と手作業でやってたんですが。
毎度同じことをやるのは面倒、ということで、Lambdaで自動化してみました。
Code for Spot Instance creation
長い処理を行うために別途書いたシェルスクリプトを、s3://mybucket/sh/run.sh
に予め置いてあるという前提で。
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ロールを適当に作って、以下を書き換えてください。
var iamInstanceProfile = 'my_ec2_instance_role';
Security Group
セキュリティグループの指定は必須ではないはずですが、launch-wizard-xxが増えるのもアレなので、既存のを使っちゃうのを推奨しておきます。
var securityGroup = 'launch-wizard-1';
Instance Storage
インスタンスストレージの設定、API documentを見るに、「BlockDeviceMappings」の「VirtualName」で指定と言っているような感じでしょうか?
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
長い処理終わったあと、インスタンスの終了まで自動化したい場合、
- インスタンスメタデータで自分自身のインスタンスIDを拾う
- aws cliで止める
ってのが、お手軽ですかね。具体的にはこんな感じで。
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飛ばす。
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は以下。
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');
}
});
};
リージョンを直で書いちゃってるので東京以外でうごかねーじゃんと自分でも思いましたが、その辺は必要に応じて適当に弄ってください。