EC2 インスタンスを Chat Bot で管理する

こないだ Amazon EC2 でディープラーニングできる GPU インスタンス[^gpgpu]を作ったのだが、趣味で使うにしてはまぁ料金が高いので常時起動させておくのはもったいなく、使わないときはインスタンスを停止させるようにしている。

[^gpgpu]: [趣味でディープラーニングするための GPU 環境を安上がりに作る方法 - Qiita](http://qiita.com/hoto17296/items/16f57b319a0293bd2688)

しかし使い始めるときと使い終わったときにいちいち AWS 管理コンソールにログインしてインスタンスの起動/停止をするのが面倒だったので、我が家の Slack Bot から EC2 インスタンスを管理できるようにしてみた。

最終的にできたものはこういう感じ。

![](https://qiita-image-store.s3.amazonaws.com/0/16369/3fdfa74d-158a-1a62-5e49-84c949b6c377.png)

以下、作り方。

## Chat Bot 用の AWS ユーザを作成する
既存のユーザのアクセスキーを利用してもできるが、セキュリティのために専用のユーザを作成して必要な権限のみを付与するのが好ましい。

![](https://qiita-image-store.s3.amazonaws.com/0/16369/4423906a-348e-cc85-d69a-25563970ec3d.png)

今回は API を叩くためのユーザなので「プログラムによるアクセス」にチェックを入れる。

ユーザを作成すると、API を叩く際に必要なアクセスキー (AccessKeyId, SecretAccessKey) が生成されるので控えておく。

## EC2 を操作できるポリシーを割り当てる
作成したユーザに必要な権限を付与する。

今回やりたいことは

- EC2 インスタンスの一覧とステータスを取得する
- EC2 インスタンスを起動させる
- EC2 インスタンスを停止させる

の3つなので、それに合わせて以下のようなインラインポリシーを割り当てる。

``` json:AllowEC2InstanceManagement
{
    "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 からインストールできる。

``` console
$ npm install aws-sdk
```

### アクセスキーの設定
AWS SDK を読み込んでアクセスキーを設定する。

``` js
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 と名前を関連付けられるようにしておく。

``` js
// 名前から起動/停止できる 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 フレームワーク[^shrike]で動くコードだが、インタフェースを Hubot に似せて作っているので Hubot でもだいたい同じようなノリで書けると思う。

[^shrike]: [Slack 特化 Bot フレームワーク「Shrike」 - Qiita](http://qiita.com/hoto17296/items/dbcc4bc62cc53299edb3)

``` js
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'));
    });
  });

  // インスタンスが起動中の場合は１時間に１回通知する
  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 の３つのコマンドに加えて、停止し忘れを防ぐために「一時間に一回通知する」機能も実装した。

`ec2` オブジェクトのメソッドを使っている箇所が API を叩いている箇所なわけだが、メソッドのオプションなどの詳細は AWS JavaScript SDK のドキュメント[^sdk-doc]を参照して欲しい。

[^sdk-doc]: [Class: AWS.EC2 — AWS SDK for JavaScript](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/EC2.html)

## 動かす
Bot をデプロイしてコマンドを投げてみると、こういう感じ。

![](https://qiita-image-store.s3.amazonaws.com/0/16369/3fdfa74d-158a-1a62-5e49-84c949b6c377.png)

これで AWS の管理コンソールにログインしなくても EC2 インスタンスを操作できるようになった。便利。