こないだ Amazon EC2 でディープラーニングできる GPU インスタンス1を作ったのだが、趣味で使うにしてはまぁ料金が高いので常時起動させておくのはもったいなく、使わないときはインスタンスを停止させるようにしている。
しかし使い始めるときと使い終わったときにいちいち AWS 管理コンソールにログインしてインスタンスの起動/停止をするのが面倒だったので、我が家の Slack Bot から EC2 インスタンスを管理できるようにしてみた。
最終的にできたものはこういう感じ。
以下、作り方。
Chat Bot 用の AWS ユーザを作成する
既存のユーザのアクセスキーを利用してもできるが、セキュリティのために専用のユーザを作成して必要な権限のみを付与するのが好ましい。
今回は API を叩くためのユーザなので「プログラムによるアクセス」にチェックを入れる。
ユーザを作成すると、API を叩く際に必要なアクセスキー (AccessKeyId, SecretAccessKey) が生成されるので控えておく。
EC2 を操作できるポリシーを割り当てる
作成したユーザに必要な権限を付与する。
今回やりたいことは
- EC2 インスタンスの一覧とステータスを取得する
- EC2 インスタンスを起動させる
- EC2 インスタンスを停止させる
の3つなので、それに合わせて以下のようなインラインポリシーを割り当てる。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:DescribeInstanceStatus",
"ec2:StartInstances",
"ec2:StopInstances"
],
"Resource": "*"
}
]
}
今回はすべてのインスタンスを操作できるようにするために "Resource": "*"
としたが、ARN を指定することで「特定のインスタンスのみを操作可能」というような制限をすることも出来る。
Chat Bot から AWS API を叩く
我が家の Chat Bot は Node.js で動作しているので、AWS JavaScript SDK を利用して AWS API を叩く。
AWS JavaScript SDK をインストール
npm からインストールできる。
$ npm install aws-sdk
アクセスキーの設定
AWS SDK を読み込んでアクセスキーを設定する。
const AWS = require('aws-sdk');
// アクセスキーは環境変数から読み込む
AWS.config.update({
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: 'ap-northeast-1', // 東京リージョン
});
// EC2 インスタンスを操作するためのオブジェクトを生成
const ec2 = new AWS.EC2();
アクセスキーをハードコートせずに環境変数から読み込んでいるので、Bot を動作させる環境の環境変数 AWS_ACCESS_KEY_ID
/ AWS_SECRET_ACCESS_KEY
にアクセスキーを設定しておく必要がある。
インスタンスの名前をつける
チャットから ec2 gpgpu start
のようにインスタンスの名前を指定して起動/停止できるようにしたいので、インスタンス ID と名前を関連付けられるようにしておく。
// 名前から起動/停止できる EC2 インスタンスの一覧
const instances = [
{ id: "i-01315ab8f6f7015e5", name: "GPGPU" },
];
// 名前からインスタンス ID を返す
function getInstanceId(name) {
const instance = instances.filter(i => i.name.toLowerCase() === name.toLowerCase())[0];
return instance ? instance.id : null;
}
// インスタンス ID から名前を返す
function getInstanceName(id) {
const instance = instances.filter(i => i.id === id)[0];
return instance ? instance.name : null;
}
Bot のアクションを定義する
チャットから Bot にコマンドメッセージが投稿されたときに AWS API を叩くようなアクションを定義する。
以下のコードは自作の Bot フレームワーク2で動くコードだが、インタフェースを Hubot に似せて作っているので Hubot でもだいたい同じようなノリで書けると思う。
module.exports = (bot) => {
// 指定した EC2 インスタンスを起動する
bot.respond(/^ec2 ([\w_]+) start$/i, (msg) => {
const id = getInstanceId(msg.match[1]);
if (!id) return msg.send('知らないインスタンスですね・・・');
ec2.startInstances({ InstanceIds: [id] }, (err) => {
if (err) {
bot.logger.error(err);
msg.send('インスタンスの起動に失敗しました...');
} else {
msg.send('インスタンスを起動しました');
}
});
});
// 指定した EC2 インスタンスを停止する
bot.respond(/^ec2 ([\w_]+) stop$/i, (msg) => {
const id = getInstanceId(msg.match[1]);
if (!id) return msg.send('知らないインスタンスですね・・・');
ec2.stopInstances({ InstanceIds: [id] }, (err) => {
if (err) {
bot.logger.error(err);
msg.send('インスタンスの停止に失敗しました...');
} else {
msg.send('インスタンスを停止しました');
}
});
});
// すべての EC2 インスタンスの起動状況を取得する
bot.respond(/^ec2 status$/i, (msg) => {
const stateIcons = {
running: 'large_blue_circle',
terminated: 'red_circle',
stopped: 'white_circle',
};
ec2.describeInstanceStatus({ IncludeAllInstances: true }, (err, data) => {
if (err) {
bot.logger.error(err);
return msg.send('インスタンス情報の取得に失敗しました...');
}
const text = data.InstanceStatuses.map((instance) => {
const id = instance.InstanceId;
const state = instance.InstanceState.Name;
const icon = stateIcons[state] || 'large_orange_diamond';
const name = getInstanceName(id);
const nameLabel = name ? ` (*${name}*)` : '';
return `:${icon}: \`${id}\`${nameLabel} is ${state}`;
});
msg.send(text.join('\n'));
});
});
// インスタンスが起動中の場合は1時間に1回通知する
bot.jobs.add('0 0 * * * *', () => {
ec2.describeInstanceStatus((err, data) => {
if (err) {
bot.logger.error(err);
return bot.send('インスタンス情報の取得に失敗しました...');
}
const count = data.InstanceStatuses.length;
if (count > 0) bot.send(`${count} 台の EC2 インスタンスが起動中です`);
});
});
};
今回は start / stop / status の3つのコマンドに加えて、停止し忘れを防ぐために「一時間に一回通知する」機能も実装した。
ec2
オブジェクトのメソッドを使っている箇所が API を叩いている箇所なわけだが、メソッドのオプションなどの詳細は AWS JavaScript SDK のドキュメント3を参照して欲しい。
動かす
Bot をデプロイしてコマンドを投げてみると、こういう感じ。
これで AWS の管理コンソールにログインしなくても EC2 インスタンスを操作できるようになった。便利。