【AWS】Amazon Lexのカスタムボットできんにくフレンズを作ろう[Part1]

タダです。

Amazon Lex(以下、Lex)と戯れてみたレポートを何回か書いてみたいと思います。
今回は、定型文の会話を行うカスタムボットを作ってみた時の紹介記事です。
社内のLT大会で話してみたので、その模様は以下からどうぞ。

社内ブログとか動画

[contents]

Lexの概要

さて、改めてLexの概要をまとめてみると、次のような特徴があります

  • Lexはアプリケーションの対話型インタフェースを作るのをサポートするサービス

    • チャットボット開発に最適
  • Lambda,Cognito,Polly,MobileHub 等とも連携可能
  • チャットのプラットフォームとして、Facebook Messanger,Twilio SMS,Slackと連携可能
  • 従量課金制

Lexの専門用語

Lexで扱う専門用語は次のものです

  • Bot : ボットそのもの
  • Intents : ユーザが入力した自然言語に応答する、ボットのアクションの固まり
  • Utterances : Intentsを発動する口頭もしくは入力されるフレーズ
  • Slot : Intentsを満たすための要求される入力データ
  • Prompt : Slotを引き出すためのフレーズ
  • Fulfillment : Intentsを実現するビジネスロジック

公式資料

やってみたこと

  • Slackにきんにくフレンズを召喚

    • 普段から使っているし、馴染みがある
  • きんにくフレンズBot(@musclechang)に話しかけて自分のトレーニングメニューを管理するものを作ってみたい
    • トレーニングしたいと話しかけると、トレーニングメニュー、回数、セット数を聞いてくれて、最後は励ますようなボットです

実現するためにやったこと

AWS側

Utterancesと、Slots、Promptはこんな感じです

Screen Shot 2017-05-21 at 10.25.05 PM.png

Slots typesは3つ作りました

  • Set(トレーニングのセット数)
  • Training Menu(トレーニングメニュー)
  • Count(何回やるかのカウント数)

Fulfillmentは、Lambdaでやらないので、「Return parameters to client」を選択しています
Screen Shot 2017-05-21 at 10.27.11 PM.png

Slack側

Slackの連携部分は、以下のドキュメントを参考にしました

結果

こんな感じの対話ができるフレンズができました!

Screen Shot 2017-05-21 at 10.06.30 PM.png

まとめ

  • ノンプラグラミングでボットを作れました

    • 定型文しか返せないので、AIぽく入力した情報を貯めて、より良いトレーニングメニューを提案するような感じにしていきたい
    • 次回に他のAWSリソースと連携してみたいです(Lambda,Pollyとの連携とか)

続きを読む

AWS ECSにてカスタムしたredmineのdockerイメージを動かすまでのメモ(その1)

redmineの構築、プラグインの導入をふつーにやると面倒くさい。
あと、一旦構築した後redmineのバージョンをあげるのもやっぱり面倒くさい。

→ので、dockerにてプラグインのインストールやらなにやらを手順をコード化して簡単にRedmineの導入が
できるようにしました。
なお動作環境はAWSのECS(Elastic Container Service)を使います。

大きな流れ

1.Dockerfileを用意
2.AWSにてElastic Container Service(ECS)のタスクを定義
3.ECSのインスタンスを用意

今回はまず1を用意します。

1.Dockerfileを用意

redmineの公式イメージがdockerhubにあるのでこれをもとにpluginを導入する手順を
dockerfile化していきます。

ポイントは2つです。
1.ベースのイメージはredmine:x.x.x-passengerを利用する
2.DBのマイグレーションが必要ないものはpluginsフォルダに配置し、マイグレーションが必要なものは別フォルダ(install_plugins)に配置。
→コンテナ起動時にマイグレーションを行うようdocker-entrypoint.shに記載しておきます。

インストールするプラグイン一覧

独断と偏見で入れたプラグインです。

No プラグインの名前 概要
1 gitmike githubの雰囲気のデザインテーマ
2 backlogs スクラム開発でおなじみ。ストーリーボード
3 redmine_github_hook redmineとgitを連動
4 redmine Information Plugin redmineの情報を表示可能
5 redmine Good Job plugin チケット完了したら「Good Job」が表示
6 redmine_local_avatars アイコンのアバター
7 redmine_startpage plugin 初期ページをカスタマイズ
8 clipboard_image_paste クリップボードから画像を添付できる 
9 Google Analytics Plugin 閲覧PV測定用
10 redmine_absolute_dates Plugin 日付を「XX日前」 ではなくyyyy/mm/ddで表示してくれる
11 sidebar_hide Plugin サイドバーを隠せる
12 redmine_pivot_table ピボットテーブルできる画面追加
13 redmine-slack 指定したslack channnelに通知可能
14 redmine Issue Templates チケットのテンプレート
15 redmine Default Custom Query 一覧表示時のデフォルト絞り込みが可能に
16 redmine Lightbox Plugin 2 添付画像をプレビューできる
17 redmine_banner Plugin 画面上にお知らせを出せます
18 redmine_dmsf Plugin フォルダで文書管理できる
19 redmine_omniauth_google Plugin googleアカウントで認証可能
20 redmine view customize Plugin 画面カスタマイズがコードで可能

Dockerfile

上記のプラグインをインストール済みとするためのDockerfileです。
なお、ベースイメージは最新(2017/05/19時点)の3.3.3を使用しています。

Dockerfile
FROM redmine:3.3.3-passenger
MAINTAINER xxxxxxxxxxxxxxxxx

#必要コマンドのインストール
RUN apt-get update -y 
 && apt-get install -y curl unzip ruby ruby-dev cpp gcc libxml2 libxml2-dev 
  libxslt1-dev g++ git make xz-utils xapian-omega libxapian-dev xpdf 
  xpdf-utils antiword catdoc libwpd-tools libwps-tools gzip unrtf 
  catdvi djview djview3 uuid uuid-dev 
 && apt-get clean

#timezoneの変更(日本時間)
RUN cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime
ENV RAILS_ENV production

#gitmakeテーマのインストール
RUN cd /usr/src/redmine/public/themes 
 && git clone https://github.com/makotokw/redmine-theme-gitmike.git gitmike 
 && chown -R redmine.redmine /usr/src/redmine/public/themes/gitmike



#redmine_github_hookのインストール
RUN cd /usr/src/redmine/plugins 
 && git clone https://github.com/koppen/redmine_github_hook.git

#Redmine Information Pluginのインストール
RUN curl http://iij.dl.osdn.jp/rp-information/57155/rp-information-1.0.2.zip > /usr/src/redmine/plugins/rp-information-1.0.2.zip 
 && unzip /usr/src/redmine/plugins/rp-information-1.0.2.zip -d /usr/src/redmine/plugins/ 
 && rm -f /usr/src/redmine/plugins/rp-information-1.0.2.zip

#Redmine Good Job pluginのインストール
RUN curl -L https://bitbucket.org/changeworld/redmine_good_job/downloads/redmine_good_job-0.0.1.1.zip > /usr/src/redmine/plugins/redmine_good_job-0.0.1.1.zip 
 && unzip /usr/src/redmine/plugins/redmine_good_job-0.0.1.1.zip -d /usr/src/redmine/plugins/redmine_good_job 
 && rm -rf /usr/src/redmine/plugins/redmine_good_job-0.0.1.1.zip

#redmine_startpage pluginのインストール
RUN cd /usr/src/redmine/plugins 
 && git clone https://github.com/txinto/redmine_startpage.git

#Redmine Lightbox Plugin 2 Pluginのインストール
RUN cd /usr/src/redmine/plugins 
 && git clone https://github.com/peclik/clipboard_image_paste.git

#Google Analytics Pluginのインストール
RUN cd /usr/src/redmine/plugins 
 && git clone https://github.com/paginagmbh/redmine-google-analytics-plugin.git google_analytics_plugin

#redmine_absolute_dates Pluginのインストール
RUN cd /usr/src/redmine/plugins 
 && git clone https://github.com/suer/redmine_absolute_dates

#sidebar_hide Pluginのインストール
RUN cd /usr/src/redmine/plugins 
 && git clone https://github.com/bdemirkir/sidebar_hide.git

#redmine_pivot_tableのインストール
RUN cd /usr/src/redmine/plugins 
 && git clone https://github.com/deecay/redmine_pivot_table.git

#redmine-slackのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/sciyoshi/redmine-slack.git redmine_slack

#Redmine Issue Templates Pluginのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/akiko-pusu/redmine_issue_templates.git redmine_issue_templates

#Redmine Default Custom Query Pluginのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/hidakatsuya/redmine_default_custom_query.git redmine_default_custom_query

#Redmine Lightbox Plugin 2 Pluginのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/paginagmbh/redmine_lightbox2.git redmine_lightbox2

#redmine_banner Pluginのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/akiko-pusu/redmine_banner.git redmine_banner

#redmine_dmsf Pluginのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/danmunn/redmine_dmsf.git redmine_dmsf

#redmine_omniauth_google Pluginのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/yamamanx/redmine_omniauth_google.git redmine_omniauth_google

#redmine_omniauth_google Pluginのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/onozaty/redmine-view-customize.git view_customize

#redmine_local_avatars用モジュールを用意(インストールはredmine起動時に実施)
RUN cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/ncoders/redmine_local_avatars.git

#backlogsのインストール用モジュールを用意(インストールはredmine起動時に実施)
RUN mkdir /usr/src/redmine/install_plugins 
 && cd /usr/src/redmine/install_plugins 
 && git clone https://github.com/AlexDAlexeev/redmine_backlogs.git 
 && cd /usr/src/redmine/install_plugins/redmine_backlogs/ 
 && sed -i -e '11,17d' Gemfile

#database.ymlファイルを置くフォルダを用意
RUN mkdir /config
COPY docker-entrypoint.sh /
RUN chmod +x /docker-entrypoint.sh

ENTRYPOINT ["/docker-entrypoint.sh"]

EXPOSE 3000
CMD ["passenger", "start"]

docker-entrypoint.sh

次にdocker-entrypoint.shファイルです。
githubに公開されているファイル
(https://github.com/docker-library/redmine/blob/41c44367d9c1996a587e2bcc9462e4794f533c15/3.3/docker-entrypoint.sh)
を元にプラグインのインストールを行うコードを記載していきます。

docker-entrypoint.sh
#!/bin/bash
set -e

case "$1" in
    rails|rake|passenger)
        if [ -e '/config/database.yml' ]; then
                    if [ ! -f './config/database.yml' ]; then
                echo "use external database.uml file"
                ln -s /config/database.yml /usr/src/redmine/config/database.yml
            fi
        fi
                if [ -e '/config/configuration.yml' ]; then
                        if [ ! -f './config/configuration.yml' ]; then
                                echo "use external configuration.uml file"
                                ln -s /config/configuration.yml /usr/src/redmine/config/configuration.yml
                        fi
                fi
        if [ ! -f './config/database.yml' ]; then
            if [ "$MYSQL_PORT_3306_TCP" ]; then
                adapter='mysql2'
                host='mysql'
                port="${MYSQL_PORT_3306_TCP_PORT:-3306}"
                username="${MYSQL_ENV_MYSQL_USER:-root}"
                password="${MYSQL_ENV_MYSQL_PASSWORD:-$MYSQL_ENV_MYSQL_ROOT_PASSWORD}"
                database="${MYSQL_ENV_MYSQL_DATABASE:-${MYSQL_ENV_MYSQL_USER:-redmine}}"
                encoding=
            elif [ "$POSTGRES_PORT_5432_TCP" ]; then
                adapter='postgresql'
                host='postgres'
                port="${POSTGRES_PORT_5432_TCP_PORT:-5432}"
                username="${POSTGRES_ENV_POSTGRES_USER:-postgres}"
                password="${POSTGRES_ENV_POSTGRES_PASSWORD}"
                database="${POSTGRES_ENV_POSTGRES_DB:-$username}"
                encoding=utf8
            else
                echo >&2 'warning: missing MYSQL_PORT_3306_TCP or POSTGRES_PORT_5432_TCP environment variables'
                echo >&2 '  Did you forget to --link some_mysql_container:mysql or some-postgres:postgres?'
                echo >&2
                echo >&2 '*** Using sqlite3 as fallback. ***'

                adapter='sqlite3'
                host='localhost'
                username='redmine'
                database='sqlite/redmine.db'
                encoding=utf8

                mkdir -p "$(dirname "$database")"
                chown -R redmine:redmine "$(dirname "$database")"
            fi

            cat > './config/database.yml' <<-YML
                $RAILS_ENV:
                  adapter: $adapter
                  database: $database
                  host: $host
                  username: $username
                  password: "$password"
                  encoding: $encoding
                  port: $port
            YML
        fi

        # ensure the right database adapter is active in the Gemfile.lock
        bundle install --without development test
        if [ ! -s config/secrets.yml ]; then
            if [ "$REDMINE_SECRET_KEY_BASE" ]; then
                cat > 'config/secrets.yml' <<-YML
                    $RAILS_ENV:
                      secret_key_base: "$REDMINE_SECRET_KEY_BASE"
                YML
            elif [ ! -f /usr/src/redmine/config/initializers/secret_token.rb ]; then
                rake generate_secret_token
            fi
        fi
        if [ "$1" != 'rake' -a -z "$REDMINE_NO_DB_MIGRATE" ]; then
            gosu redmine rake db:migrate
        fi

        chown -R redmine:redmine files log public/plugin_assets

        if [ "$1" = 'passenger' ]; then
            # Don't fear the reaper.
            set -- tini -- "$@"
        fi
                if [ -e /usr/src/redmine/install_plugins/redmine_backlogs ]; then
                        mv -f /usr/src/redmine/install_plugins/redmine_backlogs /usr/src/redmine/plugins/
                        bundle update nokogiri
                        bundle install
                        bundle exec rake db:migrate
                        bundle exec rake tmp:cache:clear
                        bundle exec rake tmp:sessions:clear
            set +e
                        bundle exec rake redmine:backlogs:install RAILS_ENV="production"
                        if [ $? -eq 0 ]; then
                echo "installed backlogs"
                                touch /usr/src/redmine/plugins/redmine_backlogs/installed
            else
                echo "can't install backlogs"
                        fi
            set -e
            touch /usr/src/redmine/plugins/redmine_backlogs/installed
        fi
                if [ -e /usr/src/redmine/install_plugins/redmine_local_avatars ]; then
                        mv -f /usr/src/redmine/install_plugins/redmine_local_avatars /usr/src/redmine/plugins/
            bundle install --without development test
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
                fi
        if [ -e /usr/src/redmine/install_plugins/redmine_slack ]; then
            mv -f /usr/src/redmine/install_plugins/redmine_slack /usr/src/redmine/plugins/
            bundle install --without development test
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
        fi
        if [ -e /usr/src/redmine/install_plugins/redmine_issue_templates ]; then
            mv -f /usr/src/redmine/install_plugins/redmine_issue_templates /usr/src/redmine/plugins/
            bundle install --without development test
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
        fi
        if [ -e /usr/src/redmine/install_plugins/redmine_default_custom_query ]; then
            mv -f /usr/src/redmine/install_plugins/redmine_default_custom_query /usr/src/redmine/plugins/
            bundle install --without development test
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
        fi
        if [ -e /usr/src/redmine/install_plugins/redmine_lightbox2 ]; then
            mv -f /usr/src/redmine/install_plugins/redmine_lightbox2 /usr/src/redmine/plugins/
            bundle install --without development test
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
        fi
        if [ -e /usr/src/redmine/install_plugins/redmine_banner ]; then
            mv -f /usr/src/redmine/install_plugins/redmine_banner /usr/src/redmine/plugins/
            bundle install --without development test
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
        fi
        if [ -e /usr/src/redmine/install_plugins/redmine_dmsf ]; then
            mv -f /usr/src/redmine/install_plugins/redmine_dmsf /usr/src/redmine/plugins/
            bundle install --without development test xapian
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
        fi
        if [ -e /usr/src/redmine/install_plugins/redmine_omniauth_google ]; then
            mv -f /usr/src/redmine/install_plugins/redmine_omniauth_google /usr/src/redmine/plugins/
            bundle install --without development test
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
        fi
        if [ -e /usr/src/redmine/install_plugins/view_customize ]; then
            mv -f /usr/src/redmine/install_plugins/view_customize /usr/src/redmine/plugins/
            bundle install --without development test
            bundle exec rake redmine:plugins:migrate RAILS_ENV=production
        fi
        if [ ! -f '/usr/src/redmine/plugins/redmine_backlogs/installed' ]; then
            set +e
            bundle exec rake redmine:backlogs:install RAILS_ENV="production"
            if [ $? -eq 0 ]; then
                echo "installed backlogs"
                touch /usr/src/redmine/plugins/redmine_backlogs/installed
            else
                echo "can't install backlogs"
            fi
            set -e
        fi
        set -- gosu redmine "$@"
                ;;
esac

exec "$@"

次はこのイメージをAWSのECSにて動作させます。
(次回に続く)

続きを読む

RaspberryPIを使って、Amazon Rekognitionの画像認識を試してみた

概要

偶然、RaspberryPI Model 3が手に入ったので、転がっていたUSBカメラを利用して、
外出中のハムスター達の画像をSlackに投稿するシステムを作ってみました。

https://github.com/n-someya/rekog-hamster

構成

aws-rok-rasp.png

USBカメラから画像取得

raspberry piなのでカメラモジュールかと思いきや、普通のUSBカメラを使います。

デバイス認識

特に何も考えず、USBを接続するだけで認識されるはずです。
認識されたかを確認するには、lsusbコマンドを実行します。
下は、私の環境の例です。
1行めでカメラデバイスが認識されてます。(Logitechのカメラを使っているのでLogitechと出力されています)

$ sudo lsusb
Bus 001 Device 004: ID 046d:0826 Logitech, Inc.
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

Pythonプログラムからの画像(動画)取得

色々方法はあると思いますが、私はOpenCVを使いました。
(画像処理も簡単にできるし)

特にバージョンにこだわりがないなら、以下のコマンドでOpenCVがインストールされPythonから利用できます。
入るバージョンは、2.4です。

sudo apt-get install libopencv-dev python-opencv

これで、以下のようにPythonからUSBカメラの画像を取得できるようになります。

# -*- coding: utf-8 -*-
import cv2
# OpenCVを利用してWebカメラからのキャプチャ準備
cap = cv2.VideoCapture(0)
# Webカメラから画像読み込み
ret, frame = cap.read()

Amazon Rekognitionを使った画像認識

:warning: Amazon Rekognitionの利用には無料枠がありますが、利用の仕方によっては、利用料が発生するケースがあります。
AWSのページなどから利用料金をしっかり確認しましょう。

AWS PythonSDKのインストールと利用準備

AWSのPythonSDKをインストールします。

sudo pip install boto3

~/.aws/credentialsというファイルを用意し、
AWSのクレデンシャルを記載します。

[default]
aws_access_key_id = ****
aws_secret_access_key = ****
region = us-west-2

これで、PythonSDKからAWSにアクセス可能となります。

AWS Rekognitionの利用

boto3からAWS RekognitionのDetect Labels APIを呼び出します。

Detect Labels APIは画像が入力されると、
画像に含まれるオブジェクトの名前(=Label)一覧と、
そのオブジェクトが本当に画像に含まれているかの確度が返却されます。

注意点としては、AWS側は画像データをbytearray型で受け取るので、
OpenCV側でndarray型となっているデータを変換する必要がある点でしょうか。

import boto3

client = boto3.client('rekognition')

# awsに送信するためndarrayとなっている画像をpngのbytearrayに変換
ret, data = cv2.imencode('.png', frame)
byte_data = bytearray(data)

# Amazon rekognitionを呼び出し
response = client.detect_labels(
    Image={
        'Bytes': byte_data
    },
    MaxLabels=50
)
print(response)

レスポンス例:

{u'Labels': [
{u'Confidence': 66.29442596435547, u'Name': u'Animal'}, 
{u'Confidence': 66.29442596435547, u'Name': u'Hamster'},
{u'Confidence': 66.29442596435547, u'Name': u'Mammal'},
{u'Confidence': 66.29442596435547, u'Name': u'Rodent'}],
'ResponseMetadata': {'RetryAttempts': 0, 'HTTPStatusCode': 200, 'RequestId': '***', 
'HTTPHeaders': {'date': 'Fri, 12 May 2017 06:20:06 GMT', 'x-amzn-requestid': '***', 'content-length': '244', 'content-type': 'application/x-amz-json-1.1', 'connection': 'keep-alive'}}, 
u'OrientationCorrection': u'ROTATE_0'}

あとはレスポンスデータのLabelsを判定すれば、物体認識が実現できます。
私は、以下のようにHamsterラベルが存在するかをチェックするようにしています。

def exists_hamster(labels):
    '''
    From amazon rekognition detect label response,
    Detect exisiting hamster
    @return confidencial
    '''
    for label in labels:
        if label['Name'] == 'Hamster':
            return label['Confidence']
    return 0

動作結果

家のハムスター達に実験台となってもらいました。

撮影画像:

hamster20170512152007_ok.jpg

実行結果:

hamster-slack.png

みごと、Slackにハムスター画像が通知されました。

続きを読む

AWS LambdaとAPI GatewayでSlackBot「ShuzoBot」を作ってみた

はじめに

AWSド初心者のわたなゔぇです。
流れでSlackBotを作ることになって、流れでサーバーレスにしてみようって話になって、
流れで試作してできたのがShuzoBotです(雑)

機能として
・あらかじめ設定したマイナスな言葉を打つと対応したShuzoのアツい言葉が届く

以上です。

また、今回はこちらの記事を参考にしました。

AWS Lambda を使って Slack ボット (命名: Lambot [ランボー]) を低予算で作ろうじゃないか

AWSコンソールの画面配置が変わっておりちょっと混乱したので
覚え書きも兼ねて。

AWS側の設定

Lambda

  • Lambda関数の作成

1.png

  • 設計図選択

「microservice-http-endpoint」を選択。

2.png

  • トリガー設定

API名:ShuzoBot
デプロイされるステージ:prod
セキュリティ:オープン

にそれぞれ指定します。なんか警告出るけど無視で今回はおk。

3.png

  • 命名

次の画面で名前をつけます。説明の欄は多分ちゃんと書いておいた方がいいけどめんどくさかったので今回はこのままやっちゃいました。

4.png

  • Lambda関数のコード設定

「コードをインラインで編集」モードにしてから、

5.png

exports.handler = function(event, context) {
    var text = '';

    switch(event.text) {
        case '帰りたい':
            text = '富士山のように、日本一になるって言っただろ! お前、昔を思い出せよ!! 今日からお前は富士山だ!!!';
            break;
        case 'もう無理':
            text = 'もっと熱くなれよ…!!熱い血燃やしてけよ…!!人間熱くなったときがホントの自分に出会えるんだ!';
    }

    if(text !== '') {
        context.done(null, {text: text});   
    }
};

これを貼っつけます。

  • ハンドラとロールの設定

ハンドラ:index.handler
ロール:既存のロールを選択
既存のロール:lambda_basic_execution

6.png

  • 詳細設定

画面したのタブを開いて、
メモリ:128
タイムアウト:3秒 に設定。

7.png

API Gatewayの設定

  • API Gateway設定画面へ

https:~~~~/ShuzoBotのURLは、どこかに控えておいてください。
あとで使います。

画像の「ANY」をクリック。

8.png

  • POSTメソッドの追加

/ShuzoBotを選択した状態で アクションをクリック。

9.png

続いて、「メソッドの作成」をクリック。

10.png

こんなやつが出てくるので、POSTを選択してチェックマークをポチ。

11.png

  • ANYの削除

ANYを選択した状態で、アクションからメソッド削除。

12.png

  • POSTメソッドのセットアップ

POSTを選択して、
関数名を「ShuzoBot」に設定、保存。

13.png

  • マッピングテンプレートの設定

するとこんな画面になるので、
「統合リクエスト」をクリック。

14.png

画面下の方のタブを開いて、

15.png

Content-Typeに
「application/x-www-form-urlencoded」を入力して横のチェックマークをぽち。
「マッピングテンプレートの追加」をクリック。

16.png

出てきたやつに入力。
(これについての解説は参考にしたページをご覧ください)

19.png

## convert HTML POST data or HTTP GET query string to JSON

## get the raw post data from the AWS built-in variable and give it a nicer name
#if ($context.httpMethod == "POST")
 #set($rawAPIData = $input.path('$'))
#elseif ($context.httpMethod == "GET")
 #set($rawAPIData = $input.params().querystring)
 #set($rawAPIData = $rawAPIData.toString())
 #set($rawAPIDataLength = $rawAPIData.length() - 1)
 #set($rawAPIData = $rawAPIData.substring(1, $rawAPIDataLength))
 #set($rawAPIData = $rawAPIData.replace(", ", "&"))
#else
 #set($rawAPIData = "")
#end

## first we get the number of "&" in the string, this tells us if there is more than one key value pair
#set($countAmpersands = $rawAPIData.length() - $rawAPIData.replace("&", "").length())

## if there are no "&" at all then we have only one key value pair.
## we append an ampersand to the string so that we can tokenise it the same way as multiple kv pairs.
## the "empty" kv pair to the right of the ampersand will be ignored anyway.
#if ($countAmpersands == 0)
 #set($rawPostData = $rawAPIData + "&")
#end

## now we tokenise using the ampersand(s)
#set($tokenisedAmpersand = $rawAPIData.split("&"))

## we set up a variable to hold the valid key value pairs
#set($tokenisedEquals = [])

## now we set up a loop to find the valid key value pairs, which must contain only one "="
#foreach( $kvPair in $tokenisedAmpersand )
 #set($countEquals = $kvPair.length() - $kvPair.replace("=", "").length())
 #if ($countEquals == 1)
  #set($kvTokenised = $kvPair.split("="))
  #if ($kvTokenised[0].length() > 0)
   ## we found a valid key value pair. add it to the list.
   #set($devNull = $tokenisedEquals.add($kvPair))
  #end
 #end
#end

## next we set up our loop inside the output structure "{" and "}"
{
#foreach( $kvPair in $tokenisedEquals )
  ## finally we output the JSON for this pair and append a comma if this isn't the last pair
  #set($kvTokenised = $kvPair.split("="))
 "$util.urlDecode($kvTokenised[0])" : #if($kvTokenised[1].length() > 0)"$util.urlDecode($kvTokenised[1])"#{else}""#end#if( $foreach.hasNext ),#end
#end
}
  • デプロイ

保存したら、
アクション>APIのデプロイをクリック。

17.png

デプロイされるステージを「prod」に設定して、デプロイ。

18.png

これでAWS側は完成です!

Slack側の設定

  • Outgoing WebHooksのintegrationを追加

20.png

  • URLを設定

修造を呼びたいチャンネルと、トリガーワード(任意)、
URLには先ほど控えておいたものをペースト。

21.png

  • Bot名設定

お好みでカスタム。
できたらsave settingsをクリック。

22.png

完成!

sample.png

続きを読む

serverless frameworkを使ってec2を起動 / 停止するスケジュールを組む

作った理由

ec2で動かしているバッチサーバーの節約のため

最近、serverless framework勉強したから
awsの各サービスの画面ポチポチをコードで管理できて素敵ですよね。。

使い方

  1. custom.dev.yml をcustom.ymlに編集し
    必要なパラメータを打ち込む

  2. serverless.ymlのfunctionsにスケジューリングを記載する

  3. あとは、devなりprodなりデプロイをするだけ

$ serverless deploy -v --stage=dev

Github

https://github.com/KoheiMisu/serverless-ec2-invoke

Todo

slackに通知するようにしたい

参考

http://dev.classmethod.jp/cloud/aws/lambda-scheduled-event-updown/

続きを読む

lambda入門(Node)③ – API Gatewayを使ってslackからのリクエストをlambdaで受けられるようにする

第3回になりました。

過去のはこちら。

今回は、ようやくやりたいことに近づいて来まして
slackからのリクエストをlambdaで受けられるようにしたいと思います。

何か調べていくと、どうもAPI Gatewayを使うと良い感じぽい。
まずはAPI Gatewayについて予習を。

API Gateway

どんな役割をしてくれるのか

APIのエンドポイントとして待ち構える玄関として使える
現在はhoge/*のようなパスを/hoge/{proxy+}として設定できる
プロキシリソースなるものらしい
post, putの振り分けなどが簡単になった

課金体系

受信した API 呼び出しと、送出したデータ量に対して発生

serverless frameworkでの設定

serverless.yml
functions:
  bookStore:
    handler: books/store.store
    events:
      - http:
          path: books
          method: get
          cors: true

これだけ。
デプロイ実行すると

$ serverless deploy -v --stage dev
Serverless: Packaging service...
・
・
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Deployment - ApiGatewayDeployment1490662361327
CloudFormation - CREATE_IN_PROGRESS - AWS::ApiGateway::Deployment - ApiGatewayDeployment1490662361327
CloudFormation - CREATE_COMPLETE - AWS::ApiGateway::Deployment - ApiGatewayDeployment1490662361327
・
・
Service Information
service: testProject
stage: dev
region: ap-northeast-1
api keys:
  None
endpoints:
  GET - https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/books
functions:
  bookStore: testProject-dev-bookStore

エンドポイントが作られた。。!

レスポンスを送れるか試してみる

$ curl https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/books
{"message":"ok",...........}

うん、okそう、すごい。ymlに書くだけで何でも設定してくれちゃう。

Slackから応答させるようにする

ここで今回の本題。
作成したエンドポイントに対してスラックがリクエストを送って
slack上にメッセージが返ってくるようにする。

全体像

スクリーンショット 2017-04-29 15.17.57.png

やることとしては、リクエストパラメータを解析してデータを保存。
保存した結果を返す

book.js

'use strict';

const slackAuthorizer = require('../authorizer/slackAuthorizer');
const parser = require('../service/queryParser');

const bookSave = require('../useCase/book/save.js');

module.exports.book = (event, context, callback) => {
  const queryParser = new parser(event.body);
  const authorizer = new slackAuthorizer(queryParser.parseToken());

  /**
   * 認証
   */
  if (!authorizer.authorize()) {
    context.done('Unauthorized');
  }

  /**
   * @Todo ここで lambda function の振り分けを行いたい
   */
  bookSave(event, (error, result) => {});

  const response = {
    statusCode: 200,
    body: JSON.stringify({
      message: 'ok',
    }),
  };

  callback(null, response);
};

同期で処理を行いたいので asyncを使って処理をブロックごとに実行する

save.js

'use strict';

const AWS = require('aws-sdk');
const dynamoDB = new AWS.DynamoDB.DocumentClient();
const bookTable = process.env.bookTable;

const uuidV1 = require('uuid/v1');

const webhookUrl = process.env.slack_webhook_url;
const request = require('request');

const dateTime = require('node-datetime');
const dt = dateTime.create();
const insertDate = dt.format('Y-m-d H:M:S');

const async = require('async');

const parser = require('../../service/queryParser');

module.exports = (event, callback) => {

  const queryParser = new parser(event.body);

  const key = uuidV1();

  async.series([
    function(callback) {

      /**
       * データを保存
       */
      dynamoDB.put({
        'TableName': bookTable,
        'Item': {
          'id': key,
          'title': queryParser.parseText(),
          'insert_date': insertDate,
        },
      }, function(err, data) {
        callback(null, "saved");
      });
    },
    function(callback) {

      /**
       * 保存したものを取り出して
       * 結果を返す
       */
      dynamoDB.get({
        TableName: bookTable,
        Key: {
          id: key,
        },
      }, function(err, data) {
        if (!err) {
          const response = {
            text: ``${data.Item.title}` is Saved !!`,
          };
        
          /**
           * webhook でチャンネルにメッセージを返す
           */
          request.post(webhookUrl, {
            form: {
              payload: JSON.stringify(response),
            },
          }, (err, response, body) => {
            callback(null, 'getData');
          });
        }
      });
    },
  ], function(err, results) {
    if (err) {
      throw err;
    }
  });
};

slash commandから送られてきたパラメータをパースするために
query-stringモジュールを使い、ラップした

queryParser.js
'use strict';

const queryStringParser = require('query-string');

module.exports = class queryParser {

  constructor (queryString) {
    this.queryString = queryString;
  }

  parseToken () {
    return queryStringParser.parse(this.queryString).token;
  }

  parseText () {
    return queryStringParser.parse(this.queryString).text;
  }
};

slashコマンドの設定は省略します。
最初の api gatewayで設定されたエンドポイントのURLを
設定してあげればいいので

実際に動かすとこんな感じです。

4b4b9687608f58545ef2b4536a7166c2.gif

ソースは汚いかもしれませんが、saverlessフレームワークで
エンドポイント作成やdb、外部サービスの連携が簡単に出来ました

もっとキレイに書けるようにjsの筋力をつけていかねば。。笑

続きを読む

AWS Lambdaをつかって、CloudWatchが監視した値を超えたら、slackに通知する仕組みをつくろう on Python

はじめに

社内で「らむだが〜らむだが」と話していて、元C#の僕からするとプログラミング言語の何かかな?と思っていましたが、違いましたね。(C#にはラムダ式というのがある。)

社内では、AWS Lambda上にnode.jsランタイムを配置して、slack通知していましたが、nodeのversionが古くてそろそろサポートやめるよ、と言われたので、Pythonに切り替えることにしました。(nodeより、Pythonのほうが知っている人が社内にいたので)

AWS Lambdaでは、よくあるような処理はテンプレートとして用意されているので、結構簡単にできます。手順をまとめてみます。

設定手順

おおまかに下記の手順でやります。

  1. Lambda関数作成
  2. SNSの作成とLambdaとの連携
  3. EC2とSNSの連携
  4. コーディング
  5. テスト

構成図

最終的にこんな感じになります。

aws.png

※ 構成図はdraw.ioを使って書いてみました

手順

1. Lambda関数の作成

今回のメインとなるLambda関数を作成しておきます。指示に従っていけば簡単に作れます。
作り直しや削除も簡単にできるのでご安心を。

サービス一覧から、Lambdaを選択。

①.png

Lambda関数の作成を開始します。

②.png

フィルターに、slackと入力するとslackの雛形ができますが、今回はブランクで作成します。

③.png

トリガーはあとで設定するので、からのままにしておきます。

④.png

2. SNSの作成とLambdaとの連携

SNS = Simple Notification Service。何らかの処理や状態変更を受取、メッセージにして通知してくれるサービスです。
メールや今回のようなLamdaへメッセージを通知できます。
サービス自体は、2017/04/06現在翻訳はされていませんが、簡単な英語なので問題ないと思います。

SNSを作成し、subscriptionをくっつけます。

Subscriptionsを作成する。

AWS_SNS.png

先程作成したLambda関数を選択します

AWS_SNS.png

3. EC2(Cloud Watch)とSNSの連携

EC2の画面から、アラームを設定します。

EC2_Management_Console.png

4. コーディング

各種設定を行います

設定するものは下記です。
* Lambda関数名
* コード
* 環境変数
* 関数ハンドラーとロール
etc

今回は下記のように設定してみました。

Lambda_Management_Console.png

@コード

lambda_handler.py
from __future__ import print_function

import boto3
import json
import logging
import os

from urllib2 import Request, urlopen, URLError, HTTPError

# slackの設定
SLACK_CHANNEL = os.environ['SLACK_CHANNEL']
HOOK_URL      = os.environ['HOOK_URL']

logger = logging.getLogger()
logger.setLevel(logging.INFO)


def lambda_handler(event, context):
    logger.info("Event: " + str(event))
    message = json.loads(event['Records'][0]['Sns']['Message'])
    logger.info("Message: " + str(message))

    alarm_name  = message['AlarmName']
    description = message['AlarmDescription']
    new_state   = message['NewStateValue']
    reason      = message['NewStateReason']

    if new_state == 'OK':
        emoji = ":+1:"
    elif new_state == 'ALARM':
        emoji = ":exclamation:"

    slack_message = {
        'channel': SLACK_CHANNEL,
        'text': "*%s %s: %s*n %sn %s" % (emoji, new_state, alarm_name, description, reason)
    }

    req = Request(HOOK_URL, json.dumps(slack_message))
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted to %s", slack_message['channel'])
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)

@環境変数+あるふぁ

Lambda_Management_Console_1.png

5. テスト

Lambdaの画面から、下記を実施します。

  1. アクション→テストイベントの設定→テストイベント入力
  2. 保存してテスト

手前味噌ですが、テストデータはこちらを参考にしていただければと。
* 参考:AWS Lambdaをつかった、CloudWatch監視 -> slack通知の際のテストデータ

実行結果

sandboxチャンネルに無事通知が飛びました。絵文字も入っていますね。

Slack_-_eversense.png

おわりに

簡単なプログラムでslack通知が実現できました。適切にメトリクス設定して、快適なAWSライフを。(通知しても、見をとしてしまったりしたら意味ないけれど…)

なお、Lambdaからは同一リージョンで作成したSNSしか設定できないようなので、リージョンをまたぐ場合は各リージョンのSNSをからLambdaを選択してあげます。

当初、Lambdaの環境変数を利用しようと思いましたが、base64周りでエラーが出てしまったので使えていません。これ使えると、Lambda関数が簡単に使いまわせるので次の機会にはきちんと対応しようと思います。

続きを読む

Gmailに添付されたAWS請求書をSlackに転送

Gmailに送られてくるAWSの請求書をSlackに共有

毎月Gmailに送られてくるAWSの請求書をSlackに転送する

方法

泥臭くGmailの件名から抽出して、Slackに通知するようにしています

以下でこちらのソースコードをベースに説明します

  1. Tokenの取得
  2. 通知させたいGoogleユーザでログイン
  3. 新規Google App Scriptを作成
  4. プロジェクト名は任意の名前を入力
  5. コードには、Main.gsの内容を張り付け
  6. 以下の箇所を修正してください

    var apiToken = '<token>';       // https://api.slack.com/custom-integrations/legacy-tokensでtokenを発行してください
    var postChannel = "<channel>";  // 通知したいチャンネル名
    
  7. スケジュール実行の設定
    毎月3~4日に送られてくるので、5日あたりに自動実行を設定しとくと安全でしょう

リファレンス

続きを読む