Help us understand the problem. What is going on with this article?

APIGateway+Lambdaを使って、slackからEC2インスタンス操作する

More than 1 year has passed since last update.

社内でslackから開発機とか起動できたら楽だよね。という会話をしていて実際に作ってみました。

やること

  • slackのOutgoing Webhooksを使って特定のチャンネルからEC2インスタンスの一覧取得、起動、停止を行う。
  • 起動と停止は開発機など一部のインスタンスに限定する。

動作環境

  • Serverless Framework v1.7.0
  • nodejs v4.3.2
  • slack

Outgoing Webhooksの設定

https://your-team.slack.com/apps/manage/custom-integrations
のOutgoing WebhooksからAdd Configurationで作成。

  • Channel:投稿を取得するチャンネル
  • Trigger Word(s):webhooksを呼び出す単語。今回は $server としています
  • URL(s):送信先になるURL。一旦は仮置きで問題ありません。

を設定します。
Tokenは今回作成したIntegrationから送信されたものかを確認するためのものになります。
POSTされるデータは

token=1IA4G7C3ay7CRVdZi0kGGkwO
team_id=T0001
team_domain=example
channel_id=C2147483705
channel_name=test
timestamp=1355517523.000005
user_id=U2147483697
user_name=Steve
text=googlebot: What is the air-speed velocity of an unladen swallow?
trigger_word=googlebot:

といった形式になります。tokenの検証とtextのパースを行えば良さそうです

APIGateway+Lambdaの設定

serverless frameworkを使ってこれらを構築します。

serverless.yml に受信するためのLambdaを記述します。

serverless.yml
functions:
  slack:
    handler: slack/handler.slack
    events:
      - http:
          path: slack
          method: post

今回は /slack というパスでリクエストを受け付けることにします。
handler に記述したように、 slack/handler.slack というファイルを作成します。

slack/handler.js
module.exports.slack = (event, context, cb) => {
  return cb(null, JSON.stringify(event));
}

ここまでで一度デプロイしてendpointを作成します。

% serverless deploy -s dev

このように作成されるので、作成されたURLをOutgoing WebhooksのURL(s)に設定します。

スクリーンショット 2017-02-21 16.31.01.png

Lambdaの実装

aws-sdkを使ってec2インスタンスの一覧取得、起動、停止をする処理を記述していきます。

インスタンス一覧の取得

ec2.js
'use strict'

const aws = require('aws-sdk');
const ec2 = new aws.EC2({region: 'ap-northeast-1'});

module.exports.describeInstances = function*() {
  try {
    const data = yield ec2.describeInstances().promise();
    return data;
  } catch (err) {
    throw err;
  }
}

これでインスタンスの取得ができます。
戻り値そのままだと余計な情報も多いのでインスタンスID、Nameタグ、stateのみを取得することにします。

function parseInstanceObj(data) {
  let instances = [];
  data.Reservations.forEach(reservation => {
    reservation.Instances.forEach(instance => {
      let name = 'unknown server';
      if (instance.Tags.length > 0) {
        name = instance.Tags.reduce((prev, current) => {
          return current.Key === 'Name' ? current.Value : prev;
        }, instance.Tags[0].Value);
      }
      console.log('server name: ', name);
      instances.push({
        instanceId: instance.InstanceId,
        state: instance.State.Name,
        name : name
      });
    });
  });
  return instances;
}

これで、インスタンスID、Nameタグ、インスタンスの状態(running、stopped、pendding)のみを取得できました。

インスタンス一覧を返すレスポンスの作成

インスタンスの情報が取得できたので、slack側へ返すレスポンスを作成します。
Responding に書いてあるように

{
"text": "African or European?"
}

形式のレスポンスを返します。

slack/handler.js
'use strict';

const co = require('co');
const _ = require('lodash');
const ec2 = require('ec2');

module.exports.slack = (event, context, cb) => {

  co(function*() {
    const result = yield ec2.describeInstances();
    let text = '';
    result.forEach(instance => {
      text = `${text}${instance.name}: ${instance.state}\n`;
    });
    const response_body = {
        text: text
    };
    const response = {
      statusCode: 200,
      headers: {
        'Access-Control-Allow-Origin': '*',
        'Content-Type': 'application/json;charset=UTF-8'
      },
      body: JSON.stringify(response_body)
    }
    return cb(null, response);
  });
}

このLambdaをデプロイ後、slack上で $server と発言すると、

dev-web01: running

などとslack上で結果が得られます。

起動、停止の権限制御

一覧が取得できるようになったので、サーバ名ベースで起動や停止を行えるようにします。
ただ、slack上から気軽に本番環境のインスタンスなどを停止されてしまうと困るので、
slack というタグがついているインスタンスのみを起動、停止の対象とします。
以下のようにすることで、Lambdaに制限をかけられます。

serverless.yml
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "ec2:describe*"
      Resource:
        - "*"
    - Effect: "Allow"
      Action:
        - "ec2:RebootInstances"
        - "ec2:StartInstances"
        - "ec2:StopInstances"
      Resource:
        - "arn:aws:ec2:*"
      Condition:
        "ForAnyValue:StringEquals":
          "ec2:ResourceTag/slack": ""

Conditionで ec2:ResourceTag を指定してやることでタグ単位で操作の制御が可能になります。

Nameタグベースでのインスタンス情報取得

インスタンスIDをPOSTすれば処理は楽なのですが、インスタンスIDを覚えるのは大変なのでNameタグベースで起動、停止を行えるようにします。

ec2.js
module.exports.describeInstancesByName = function*(name) {
  if (!name) throw new Error();
  const params = {
    Filters: [
      {
        Name: 'tag-key',
        Values: ['Name']
      },
      {
        Name: 'tag-value',
        Values: [name]
      },
      {
        Name: 'tag-key',
        Values: ['slack']
      }
    ],
    DryRun: false
  }
  try {
    const data = yield ec2.describeInstances(params).promise();
    return parseInstanceObj(data);
  } catch (err) {
    throw err;
  }
}

こうしてやることで、Nameタグに指定された値を持ち、slackというタグがついているインスタンス情報の取得ができます。
あとはこの戻り値を元に startInstancesstopInstances を呼出してあげればいいことになります。

確認

スクリーンショット 2017-02-21 17.14.27.png

slack上で $server status と発言することでインスタンスの一覧を返してくれるようになりました。
あとはこの一覧ベースで $server [server name] start などをしてあげれば任意のサーバの起動などもできます。

コマンドの使い方は
$server (status | help | [server name] start | [server name] stop)
といった風にしてあります。

最後に

かなり飛び足でしたが、slack経由でec2インスタンスを操作する方法を紹介しました。
サーバレスアーキテクチャに触れるための第一歩としてこういったツール群を作ってみても面白いかもしれません。

今回作成したコードはgithubでも公開していますので、何かの参考になればと思います。

github

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away