Serverless(JAWS) & Slack Slash Commands でお天気通知機能を作ってみた #アドカレ2015

2015.12.15

ども、UIデザイナーの清田です。

弊社ではデザイナー職でもAWSと戯れます(笑)周りにすごいエンジニア軍団が、やさしく?膨大なナレッジを共有してくれるので安心です!

それでは、AWSモバイルアドベントカレンダー15日目の記事になります。

はじめに

今回、アプリケーションフレームワークのServerless(JAWS)と、Slackの面白い機能 Slash Commands を組み合わせて お天気通知機能をSlackに構築してみたいと思います。

大枠の構成

Serverless(JAWS) & Slack Slash Commands

Serverless(JAWS)とは?

Serverless(JAWS) & Slack Slash Commands

以前、弊社 五十嵐が紹介させていただいた。 サーバレスアプリケーションフレームワーク 「JAWS」 が名前を変えて、Serverlessとして再始動した模様です。

Serverless(JAWS)の概要

AWS LambdaとAPI Gatewayを使ったサーバレスなアプリケーションフレームワークです。Lambdaを用いることにより巨大なスケールアウトとコスト削減ができるということです。オープンソースで現在はベータ版として提供されています。

参考記事

Slack Slash Commandとは?

昨今ではいろいろな企業でSlack導入したといったエントリーを拝見するので、Slackについてはご存知の方は多いと思いますので詳細は割愛しますが、 Slackは、チャット機能を主としたコミュニケーションツールです。

自分が所属しているモバイルアプリサービス部でも、各プロジェクトごとにメンバー間のコミュニケーションツールとして重宝しています。

そんなSlackですがいろいろな機能が備わっており、今回は「Slash Commands」についてご紹介します。

Serverless(JAWS) & Slack Slash Commands

Slash Commandsとは、メッセージ入力フィールド内で使用できるショートカットコマンドを指します。
デフォルトで様々機能が用意されているのですが、独自にショートカットを作成し、そのショートカットに**指定したAPIへリクエスト**を送信することができる面白い機構が用意されています。

Serverless(JAWS) & Slack Slash Commands

参考記事

Serverlessの準備

それでは早速Serverlessの環境を準備していきたいと思います。

事前準備

nodeが動く環境と、各AWSリソース(API Gateway、Lambda 等)を操作できるAWS IAMユーザーを準備します。

  • node v4.2.1
  • npm v2.14.7

AWS クレデンシャルの設定

AWS リソースへアクセスするために クレデンシャルの設定を行います。

# ~/.aws/credentials

[serverless-user]
aws_access_key_id = XXXXXXXXXXXX
aws_secret_access_key = XXXXXXXXXXXX

次にServerlessをインストールし、受け口のAPI側を準備をしたいと思います。

npm側インストールします。

$ npm install serverless -g

プロジェクトファイルの作成を行います。ご自身の作業ディレクトリへ移動し、以下コマンドでプロジェクトを作成します。

$ serverless project create

正常にコマンドが通ると各種設定が行えます。

_______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                          serverless.com, v.0.0.12
`-------'
Serverless: Enter a project name:  (serverlessNyANeVIHe) weather-slack
Serverless: Enter a project domain (used for serverless regional bucket names):  (myapp.com) xxxxxxxx
Serverless: Enter an email to use for AWS alarms:  (me@myapp.com) xxxxxxxx
Serverless: Select a region for your project:
    us-east-1
    us-west-2
    eu-west-1
  > ap-northeast-1 # 東京リージョンが選択できるので選択
Serverless: Select an AWS profile for your project:
    Alfa-user
    Bravo-user
    Charlie-user
  > serverless-user # 設定したクレデンシャルを選択
Serverless: Creating a project region bucket on S3: serverless.apnortheast1.weather.slack.com...
Serverless: Creating CloudFormation Stack for your new project (~5 mins)...
Serverless: Successfully created project: weather-slack

serverless moduleの作成

プロジェクトファイルが作成できたので、今度はモジュールファイルの作成を行います。 早速プロジェクトディレクトリへ移動します。

$ cd project_dir

モジュールとファンクションの作成

プロパティ項目について
  • -m : モジュール名
  • -f : ファンクション名
$ serverless module create -m serverless-slack -f weather

作成が成功すると以下のメッセージが表示されます。

$ Serverless: Successfully created new serverless module "serverless-slack" with its first function "weather"

これで各種設定ファイル、lambda のソースコードの雛形の準備ができました。

serverlessのデプロイ

JAWS フレームワークの際はjaws dashコマンドでデプロイがエンドポイント側、ファンクション側両方へデプロイを行えたのですが 今回から別れたみたいです。

ファクション側(Lambda)のデプロイコマンド

$ serverless function deploy

エンドポイント側(API Gateway)のデプロイコマンド

$ serverless endpoint deploy

AWSコンソールで確認

AWSコンソールにログインし、API Gateway の管理画面へ遷移すると作成したプロジェクト名が表示されています。 serverless側でのデプロイの成功です。

先ほど指定したプロジェクト名が表示されています。

Serverless(JAWS) & Slack Slash Commands

プロジェクト名のリンクをクリックしプロジェクトのページへ遷移すると 作成した、モジュール名、ファンクション名も表示されています。反映されていることが確認できました。

Serverless(JAWS) & Slack Slash Commands

作成したエンドポイントの確認

管理画面のメニューの「Resources > Dashboard」をクリックします。

Serverless(JAWS) & Slack Slash Commands

今回作成されたエンドポイントがこちらで確認することができます。

Serverless(JAWS) & Slack Slash Commands

Slack Slash Commandsの準備

それではSlack Slash Commandsの方を指定していきます。事前準備として、Slackの管理アカウントをご用意ください

Serverless(JAWS) & Slack Slash Commands

Slackの左上部にあるメニューからConfigure Integrationsへ遷移します。

Serverless(JAWS) & Slack Slash Commands

IntegrationsページのAll Servicesタブを選択し、下部にあるSlash Comands項目のViewを選択し、設定画面へ遷移します。

Serverless(JAWS) & Slack Slash Commands

Choose a Command エリアに独自で設定するコマンド名を入力し、 Add Slash Command Integration で追加を行います。

Serverless(JAWS) & Slack Slash Commands

作成が完了すると、詳細の設定画面が表示されます。今回設定する項目は2点あります。

  • URL
  • Method

URL(エンドポイント)の設定

API Gateway で作成したURLをこちらに入力します。

入力するURLは、URL + モジュール名 + メソッド名を連結した形式になります。

https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/xxxxxxxx/serverless-slack/weather

Methodの設定

今回の機能では、Getメソッドでも大丈夫かなと思いますが、POSTを指定しておきます。

詳細設定について

上記の2項目以外にも、入力補完時のヘルプテキストやアイコン等設定できる項目がありますが、割愛させていただきます。

Serverless(JAWS) & Slack Slash Commands

各設定項目入力し、Save Integrationをクリックして設定を反映し、Slack側の設定は完了です。

serverlessのプロジェクト設定を修正

JAWSからserverlessになった際に、ディレクトリ構成も多少変更になりました。以下がひながらの構成になります。

Project_Dir
├── back/
├── cloudformation/
├── plugins/
├── admin.env
├── README.md
└── s-project.json


疎通ができるようにする

現在ではSlack側よりリクエストを送っても、メソッド等のフォーマットが合っていないのでエラーが返ってきてしまっているので疎通ができるまで調整してみたいと思います。

メソッドをPOSTに変更

# Project_Dir/back/modules/serverless-slack/weather/s-function.json

{
...

        "serverless-slack/weather": {
          "method": "POST",
          "authorizationType": "none",
          "apiKeyRequired": false,
          "requestParameters": {},
          "requestTemplates": {
            "application/x-www-form-urlencoded": "{\n  \"postBody\" : $input.json(\"$\")\n}"
          },
...
}

GETメソッドをPOSTへ変更しています。

"method": "POST", 

Integration Requestの修正

POSTメソッドで受け取れるとこまで来ましたが、API GatewayからLambdaへリクエストを渡す際、 Lambda側が受け取れる形式に変換する対応(マッピング)が必要です。

requestTemplatesにて指定しているapplication/jsonをapplication/x-www-form-urlencodedに変更し、 マッピングのフォーマットは以下を記述します。

"application/x-www-form-urlencoded": "{\n  \"postBody\" : $input.json(\"$\")\n}"

参考サイト

デプロイしてみる。

以下コマンドでAPI Gatewat側へデプロイを行います。

$ serverless endpoint deploy

Serverless: Deploying endpoints in "development" to the following regions: ap-northeast-1
Serverless: Successfully deployed endpoints in "development" to the following regions:
Serverless: ap-northeast-1 ------------------------
Serverless: POST - https://xxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/xxxxxxxx/serverless-slack/weather

本当に反映されたのかをAWSコンソール画面で確認してみます。

Serverless(JAWS) & Slack Slash Commands

  • methodPOSTに変更されている
  • Content-Typeapplication/x-www-form-urlencodedに変更されている
  • Mapping templateが指定した記述が反映されている

正常に反映されていることが確認できました。

動作検証

Slack の入力フィールドから /weather と入力すると入力補完が表示されるようになります。

Serverless(JAWS) & Slack Slash Commands

コマンドを実際に叩いてみると、以下のようなレスポンスが返ってきます。

Serverless(JAWS) & Slack Slash Commands

おお!ちゃんと返ってきてますね!疎通成功です。

天気予報通知機能を作成してみます。

やっと本題の機能の作成になります。長かった。。。。

OpenWeatherMapというサービスが無料でAPIを提供しているのでこちらを使いたいと思います。

以前までは、アプリケーションIDが必要なかったみたいですが、2015年10月から仕様変更があり必要になったみたいです。

Serverless(JAWS) & Slack Slash Commands

上記サイトアカウントを作成してみたいと思います。

Serverless(JAWS) & Slack Slash Commands

アカウントの作成が完了すると、API Keyが発行されるのでこちらをAPIでリクエスト投げる際に必要になります。

プロジェクトの修正

package.jsonの修正

外部APIへのアクセスを用意にするため、requestライブラリをインストールします。

package.jsonに追記します。

# Project_Dir/back/modules/serverless-slack/package.json

{
  "dependencies": {
    "serverless-helpers-js": "~0.0.3",
    "request": "^2.67.0"
  }
}

上記、package.jsonと同じ階層で以下コマンドでインストールします。

# Project_Dir/back/modules/serverless-slack/

$ npm install request --save

event.jsonの修正

Lambda側へ値を渡すリクエストパラメータをネストするような?(調査中)記述があったので、 ネストしないように修正します。{}だけの記述に変更します。

# Project_Dir/back/modules/serverless-slack/weather/event.json

{}

index.jsの修正

処理のロジック部分を記述するのが、lib配下のindex.jsみたいです。 ざっくり処理の概要をご説明しますと、以下の項目になります。

  • appIdにOpenWeatherMapで取得したアプリケーションIDを入力します
  • スラック側から受け取った、リクエストから都市名を抽出
  • requestモジュールを使って、外部APIへリクエストを送信
  • レスポンスをSlack側のフォーマットに合わせ返却
# Project_Dir/back/modules/serverless-slack/weather/lib/index.js

/**
 * Lib
 */

var request = require('request');

module.exports.respond = function(event, cb) {

    var webApiUrl = "http://api.openweathermap.org/data/2.5/weather?q=",
        country = "jp",
        city,
        appId = "XXXXXXXXXXXXXXXX",
        params = [],
        response,
        hash,
        hashes = event.postBody.split("&");

        for(var i = 0; i < hashes.length; i++) {
            hash = hashes[i].split('=');
            params.push(hash[0]);
            params[hash[0]] = hash[1];
        }

        city = params.text;

    //オプションを定義
    var options = {
      url: webApiUrl + city + "," + country + "&appid=" + appId,
      method: 'GET',
      headers: { 'Content-Type':'application/json'},
      json: true
    };

    //リクエスト送信
    request(options, function (error, response, body) {
        if (!error && response.statusCode == 200) {

            //成功時
            response = {
                "text": body.name  + "の天気予報",
                "attachments": [
                    {
                        "color": "#bdc2c6",
                        "image_url": "http://openweathermap.org/img/w/" + body.weather.icon + ".png",
                        "fields": [
                            {
                            "title": "天気",
                            "value": body.weather.main,
                            "short": false
                            }
                        ]
                    },
                    {
                        "color": "#00cdc0",
                        "fields": [
                            {
                            "title": "気温",
                            "value": Math.round( body.main.temp - 273.15),
                            "short": false
                            }
                        ]
                    },
                    {
                        "color": "#ff6d6f",
                        "fields": [
                            {
                            "title": "最高気温",
                            "value": Math.round(body.main.temp_max - 273.15),
                            "short": false
                            }
                        ]
                    },
                    {
                        "color": "#2095db",
                        "fields": [
                            {
                            "title": "最低気温",
                            "value": Math.round(body.main.temp_min - 273.15),
                            "short": false
                            }
                        ]
                    },
                ]
            };

            return cb(null, response );

        } else {

			  // 失敗時
            response = { text : 'error: '+ response.statusCode };

            return cb(null, response );
        }
    });
};

修正項目をデプロイする

プロジェクト内の修正項目をLambda側へデプロイします。

$ serverless function deploy

Serverless: Deploying functions in "XXXXXXXX" to the following regions: ap-northeast-1
Serverless: Successfully deployed functions in "development" to the following regions: ap-northeast-1

Slack側からリクエストをなげてみる

実際にSlackのメッセージエリアからリクエストしてみると以下の結果が返ってきました!

Serverless(JAWS) & Slack Slash Commands

まとめ

アプリケーションフレームワークのServerless(JAWS)と、Slackの Slash Commands を組み合わせて、ちょっとした機能を作成してみました。

最後に

アイディア次第で、SlackをトリガーとしてLambda経由でいろいろな操作ができるのではないでしょうか?例えばSlackからデプロイコマンドを実行するなどなど。 Serverless(JAWS)は、まだまだプロダクション環境での使用は厳しい印象でしたが、今後の機能追加や発展に大いに期待でいるのではないでしょうか。

次回の予告

明日(12月16日)は、五十嵐 良輔さんの AWS Lambdaについての記事です。お楽しみに!