Amazon lexで音声チャットボットを作成してみた

天気の良い週末にやることがなく、昼の12時ごろ、趣味の長時間歯磨きをしながらtechCrunchを見ていたら、amazonのlexの記事が乗っていた。

http://jp.techcrunch.com/2017/04/21/20170420amazon-lex-the-technology-behind-alexa-opens-up-to-developers/

Amazonの仮想アシスタントAlexaを支えているテクノロジーであるAmazon Lexが、今朝(米国時間20日)のロイターの記事によれば、プレビュー段階を終了したということだ。

↓↓↓
https://aws.amazon.com/jp/lex/

Amazon Lex は、音声やテキストを使用した会話型インターフェイスをさまざまなアプリケーションに構築するためのサービスです。
Lex では、自動音声認識 (ASR) という音声をテキストに変換するための高度な深層学習機能と、
テキストの意図を理解するための自然言語理解 (NLU) を利用できます。
これにより、非常に魅力的なサービスと生き生きとした音声対話を実現するアプリケーションを構築できます。
Amazon Lex を使うと、すべての開発者が Amazon Alexa に採用されている深層学習技術と同じ技術を利用し、
自然言語での高度な対話ボット (チャットボット) を短時間で簡単に構築できるようになります。

このページにある動画は去年の11月末のものだ。
その頃、某ゲームの開発と並行して走る運営の現場の両方が修羅場で、毎日トラブルと戦っていた。さらにトラブルは現場だけとは限らず、外野がキナ臭くなり、モヒカンと肩パッドの荒くれ供から平和を守るために北斗百烈拳を繰り出していた。明日を見失って、CV千葉繁になっていた記憶がうっすらとある。
当然re:InventのKeynoteなど見る暇はなかった。

Try a sample

まずはこちらのページでサンプルを作ってみる。
https://console.aws.amazon.com/lex/home?region=us-east-1#bot-create:

cb-1.png
BookTripをクリック。

cb-2.png

IAM:自動的に作ってくれるんはありがたい。念のため後でデタッチされた権限を見ることにするけどな。
Child-Directed?:子供専用ではないからNo。

Createを押しやす。


マイクを許可しますかのプロンプトが出るので、許可にする。
このmacには内臓マイクがないので、iPhoneのヘッドホンを繋いでおく。

cb-3.png

しかしこの後、何をどうしたらいいのか、説明がないからさっぱりわからない。。。。。。
困った。

tutorialの動画をみながらもう一度sample chat botをつくる

AWS Lex Demo Tutorial (Jan 2017)
https://www.youtube.com/watch?v=7uG9cuxNo5k
「花の注文をするチャットボット」を10分くらいで作る様子。

4ヶ月ほど前に親切な人が投稿してくれたチュートリアル動画で、
やさしい英語をえらんで説明してくれているが、
英語のききとりがやっとの自分はYoutobeの字幕機能で補いながらみた。
この通りやってみると、なんとなくわかる。

なおこの動画の中では、まだ「Channels」がFacebookしかないが、
3ヶ月後の現在は、3つに増えている。

  • Facebook
  • Twilio SMS ←増えた
  • Slack ←増えた

花屋のサンプルをそのまま真似して、以下のように作ってみた。

name:OrderFlowersSitopp
Description:Bot to order flowers on the behalf of a user
Output voice:Joey

cb-8.png

Buildをするとtest Botでtext入力できるようになる。
Sample Utterancesの1個めをコピペして、
I would like to pick up flowers
と記入してエンターしてみるとすぐ答えが返ってきた。ほうほう。
cb-7.png
あっ、これもうできちゃったのか。早いなぁ。(΄◉◞౪◟◉`)

音声入力してみる

AWSのLexのコンソールに行く
https://console.aws.amazon.com/lex/home?region=us-east-1#bots:

さっき作った「name:OrderFlowersSitopp」を選ぶcb-15.png

右側にチャットボットのタブが現れるので、
入力欄のマイク部分をクリックする。そうすると音声入力モードになった。

cb-13.png


Macに繋いだiPhone用のヘッドフォンのマイクのあたりに話しかけると、テキスト化して、チャットにしてくれます。

上の例だと、私の発音が悪いので、rosesがlosesとかclosesになっている。ひどい(笑)
しかもボットはそのまま「Ok your closes will be ready 」と予約終了してしまった。
いったいなんの花が届くのか。。笑

もちろん、これはサンプルだから何も届かないけど、
ちゃんとカスタムスロットに無いものはエラーにしないと、そのまま注文を受け取ったことになってしまうから、
注意が必要ということで。

sampleを作ってわかったこと

  • サンプルの3種類は以下の通り。主たる目的はこういうものなのかな。

    • 旅行の予約
    • 花屋のオーダー
    • 歯医者の予約

考えられる用途

  • 保険の見積もり
  • 通販サイトのパスワード忘れた
  • 美容院の予約
  • ゲームの進行不具合の通報
  • ゲームのチート疑惑の通報
  • ゲーム掲示板の炎上の通報
  • ゲームの..いかん、つい仕事が

これがFacebookのお店のページに備わってたら、確かに気楽に呼び出しやすい。
でも受け取る側はどうなんだろう? botが応答して解決しない場合もあるだろうし、ちゃんと過去の履歴を呼び出せるのかな?

大変そうな点

Sample UtterancesとSlot typesの用意が大変そう。

Sample Utterancesとは、ユーザーがこういったらこの部分をパラメタとして受け取るという設定。花屋の予約sampleでは2個しかなかったけど、こんなんで済むはずがない。

  • I would like to pick up flowers
  • I would like to order some flowers

様々な言い方を網羅しておかないと、「おっしゃることがわかりません」を連発するポンコツbotになってしまう。
(参考動画 Virgin Flight – Saturday Night Live

Slot typesは、ユーザーの返答の中から受け付けるキーワードの種類で、花屋の予約sampleでは花の種類3つが該当する。だがこんな少なくて済むはずがない。3種類しか売ってない花屋っていうのだったら別だけど。
– roses
– lilies
– tulips

Sample utterancesもSlot TypesもAlexa Skillsにも同様の設定項目があり、これらをいかにきめ細かに定義できるかでbotの優秀さが決まると思う。

まとめ

  • AWSのlexを使ってchatbotが作れる。
  • 連携先は、Facebook、Twilio SMS、slackが選べる。ただし音声入力は、声の入力装置があるプラットフォームじゃないとダメ。(Twilioは電話と連携してるから、これが候補か。)
  • Utterancesとslot typesをちゃんと定義しないと、ポンコツbotになってしまう。人工知能というより人工無能。これはAlexa skillsも同じ。
  • しゃべった言葉を分解してテキストにするところが人工知能。ここはブラックボックス。
  • サンプルでは必要なかったが、ちゃんとbotを作るときにはLambda関数も書く必要がある。(http://docs.aws.amazon.com/ja_jp/lex/latest/dg/getting-started-ex2.html)
  • botの会話ログを見るのってどうするんだろ?

以上!

続きを読む

AWS X-RayでLambda→Athenaのアクセスを可視化してみた

以前こんなものを作りましたが、これをAWS X-Rayで可視化してみたら、何がわかるのか、実験してみました。

Amazon AthenaをAWS Lambdaから操作できるようにしてみた

AWS X-Ray デーモンの実行

AWS X-Ray SDK は、AWS X-Ray に Trace データを直接送信しないらしいので、送付用のEC2インスタンスを作成します。ユーザデータとして以下を登録してインスタンスを生成するだけなので、簡単です。

#!/bin/bash
curl https://s3.dualstack.us-east-1.amazonaws.com/aws-xray-assets.us-east-1/xray-daemon/aws-xray-daemon-2.x.rpm -o /home/ec2-user/xray.rpm
yum install -y /home/ec2-user/xray.rpm

システムログにxrayのインストールログが出力されていたのでOKでしょう。

Examining /home/ec2-user/xray.rpm: xray-2.0.0-1.x86_64
Marking /home/ec2-user/xray.rpm to be installed
Resolving Dependencies
--> Running transaction check
---> Package xray.x86_64 0:2.0.0-1 will be installed
--> Finished Dependency Resolution

Dependencies Resolved

================================================================================
 Package         Arch              Version               Repository        Size
================================================================================
Installing:
 xray            x86_64            2.0.0-1               /xray            6.6 M

Transaction Summary
================================================================================
Install  1 Package

Total size: 6.6 M
Installed size: 6.6 M
Downloading packages:
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  Installing : xray-2.0.0-1.x86_64                                          1/1 
xray start/running, process 2576
  Verifying  : xray-2.0.0-1.x86_64                                          1/1 

Installed:
  xray.x86_64 0:2.0.0-1                                                         

Complete!

Lambdaアプリ側の準備

今回Javaアプリケーションを動かすわけですが、LambdaアプリケーションをX-Rayで監視したい場合は、Lambdaアプリケーションの「設定」タブの中で以下のチェックボックスをONにするだけで良いようです。

スクリーンショット 2017-04-23 22.21.44.png

参考:http://docs.aws.amazon.com/ja_jp/xray/latest/devguide/xray-services.html

またX-Rayを操作するための権限をIAMで設定する必要もあります。今回は試験的な運用だったため「AWSXrayFullAccess」をつけてしまいましたが、実際の運用に合わせてこの辺りは慎重に選びたいですね。

アプリを起動して可視化してみる

ここまでできれば、普通にLambdaアプリを動かしてみてX-Rayでどのように見えるのか確認ができます。今回Lambdaアプリケーションには以下のJSONをインプットとして与えるようにしました。以前の記事でサンプルとしてAthenaのテーブルからデータを取得するようにした際の入力値です。

{
  "region": "us-east-1",
  "s3Path": "s3://ishida-athena-staging-dir/",
  "sql": "SELECT elbname, requestip,  requestport, backendip, backendport, requestprocessingtime, backendprocessingtime, timestamp FROM sampledb.elb_logs order by timestamp desc limit 10",
  "columnListStr": "elbname, requestip,  requestport, backendip, backendport, requestprocessingtime, backendprocessingtime,  timestamp"
}

実行後1分ほど待つと、以下のような表示がX-Rayで確認できました。無事可視化ができたようです。

スクリーンショット 2017-04-23 22.56.40.png

X-Rayの中身を確認してみる

表示されたService Mapの右側のオブジェクトをクリックすると以下のような表示がされました。
スクリーンショット 2017-04-23 22.56.51.png

それぞれの処理にどの程度時間がかかってレスポンスとして何を返しているのかが一覧でわかります。
表示されているIDをクリックすると、そのTraceの詳細が確認できました。

スクリーンショット 2017-04-23 22.56.58.png

これをみる限り、Lambdaアプリの初期化に230ms程度、実際のAthena接続部分に約3秒程度かかっている、という風にみればいいんですかね。この処理全体としては4.6秒かかっているので、実際にAthenaにアクセスするため以外に1.5秒ほどは時間が取られている、と理解すればいいんでしょうか。この辺はもっと勉強が必要だ(^^;

ちなみにエラーが出ている場合は、その例外の中身も確認することができるようです。

まとめ

それぞれの処理がどの程度時間にかかっていて、さらに呼び出し関係までこれほど簡単にセットアップしつつ可視化ができるのは強力ですね。これからMicroservicesなどで分散して処理をさせることが当たり前になることを考えると、必須の技術と言えると思います。Springで言えばZipkinとSleuthをAWS上で実現しているような感じですね。

続きを読む

AtlassianのLocalStackを使ってみてなんとなく理解するまでのお話

Atlassianが「LocalStack」なんてとても便利そうなものを出していたけど、なかなか使い方を解説しているページが見つからなかったので、とりあえず使いながらなんとなく中身を理解するまでのお話。

https://github.com/atlassian/localstack
スクリーンショット 2017-04-23 17.53.59.png

起動

いくつかGithubで利用方法が紹介されていますが、今回はdockerでの利用をしてみます。

$ docker run -it -p 4567-4578:4567-4578 -p 8080:8080 atlassianlabs/localstack
2017-04-23 08:50:15,876 INFO supervisord started with pid 1
2017-04-23 08:50:16,879 INFO spawned: 'dashboard' with pid 7
2017-04-23 08:50:16,885 INFO spawned: 'infra' with pid 8
(. .venv/bin/activate; bin/localstack web --port=8080)
. .venv/bin/activate; exec localstack/mock/infra.py
Starting local dev environment. CTRL-C to quit.
 * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
 * Restarting with stat
Starting local Elasticsearch (port 4571)...
Starting mock ES service (port 4578)...
Starting mock S3 server (port 4572)...
Starting mock SNS server (port 4575)...
Starting mock SQS server (port 4576)...
Starting mock API Gateway (port 4567)...
Starting mock DynamoDB (port 4569)...
Starting mock DynamoDB Streams (port 4570)...
Starting mock Firehose (port 4573)...
Starting mock Lambda (port 4574)...
Starting mock Kinesis (port 4568)...
Starting mock Redshift server (port 4577)...
 * Debugger is active!
2017-04-23 08:50:18,537 INFO success: dashboard entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
2017-04-23 08:50:18,538 INFO success: infra entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
 * Debugger PIN: 844-652-544
Ready.

とりあえず起動はしたみたい。で、http://localhost:8080/にアクセスしてみたけど、こんな感じで何も表示されず。

スクリーンショット 2017-04-23 17.59.04.png

使ってみてわかりましたが、要は本当に各種サービスが以下のアドレスで利用できるようにしてくれたもののようです。

全部試すのもアレなので、とりあえず馴染み深いDynamoDBとS3を使ってみる。

DynamoDBのテーブル作成

全然関係ないですが、http://localhost:4569/にアクセスすると以下のレスポンスをもらえます。デフォルトはus-east-1で動いている想定のようで。(Githubページにも書いてあった。バインドすればいくつか設定を変更できるようですね。)

healthy: dynamodb.us-east-1.amazonaws.com 

では早速テーブルをCLIから作ってみます。一応作成前にlist-tablesをしてみますが、もちろん何も登録されていません。

$ aws --endpoint-url=http://localhost:4569 dynamodb list-tables
{
    "TableNames": []
}

こちらを参考にさせていただいて、create-tableコマンドを発行します。

$ aws --endpoint-url=http://localhost:4569 dynamodb create-table --table-name test --attribute-definitions AttributeName=testId,AttributeType=S --key-schema AttributeName=testId,KeyType=HASH --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1
{
    "TableDescription": {
        "TableArn": "arn:aws:dynamodb:us-east-1:000000000000:table/test", 
        "AttributeDefinitions": [
            {
                "AttributeName": "testId", 
                "AttributeType": "S"
            }
        ], 
        "ProvisionedThroughput": {
            "NumberOfDecreasesToday": 0, 
            "WriteCapacityUnits": 1, 
            "ReadCapacityUnits": 1
        }, 
        "TableSizeBytes": 0, 
        "TableName": "test", 
        "TableStatus": "CREATING", 
        "KeySchema": [
            {
                "KeyType": "HASH", 
                "AttributeName": "testId"
            }
        ], 
        "ItemCount": 0, 
        "CreationDateTime": 1492937089.534
    }
}

なんか作られたっぽいですね。もう一度list-tablesをしてみます。

$ aws --endpoint-url=http://localhost:4569 dynamodb list-tables
{
    "TableNames": [
        "test"
    ]
}

出来上がってますね。なるほど、本当にAWS上でやる操作をEndpointURLを変更するだけで操作できてしまうようです。これは思っていたよりも便利かも。

S3のバケットを作成してみる

S3のバケットも作成できるのか試してみます。まずバケットのリストを見てみますが、当然何もありません。

$ aws --endpoint-url=http://localhost:4572 s3 ls
(何も表示されず)

では作成してみます。

$ aws --endpoint-url=http://localhost:4572 s3 mb s3://kojiisd-test/
make_bucket: s3://kojiisd-test/
$ aws --endpoint-url=http://localhost:4572 s3 ls
2006-02-04 01:45:09 kojiisd-test

まじか、これは便利。ただdockerでサービス起動したら停止時に中身が消えてしまうから、できれば作成したものが残るような起動方法の方が色々試そうとしたら適していそうですね。
(作成時間がはちゃめちゃですが、とりあえずそこまで問題にはならないかな)

ちなみにDynamoDBのテーブルとS3のバケットを作成してから気づきましたが、http://localhost:8080/にアクセスしたら、作成したものが表示されていました。なるほど、そのためのDashBoardだったのか。素敵。

スクリーンショット 2017-04-23 18.20.02.png

まとめ

どれくらいどこまで何ができるのかは気になりますが、一般的なことであればだいたいローカルでできるような気がします。しかもEndpointURLを変更するだけで良さそうなので、これはかなり便利かも。今度作成したアプリを全てLocalStackで動かしてみるとかやってみようかな。

とりあえず、もっとこれは知られるべきと、思いました。

続きを読む

Amazon Lex でサンプルbotを試した

Amazon Lex

Lexがついに一般公開されました。
Alexa Voice Serviceで色々と頑張らなくても手軽にAlexaを使えそうです。

サンプルbotのモデル

公式ドキュメントのExercise2を通して、Lexの使い方を学んでいきたいと思います。

Pizza Ordering Bot

早速作っていきましょう。
※Lexはus-east-1(バージニア北部)のみで利用可能です。

1. lambda関数の用意

1-1: IAMロールの作成

マネジメントコンソールからIAMを選択し、サイドバーのロールから新しいロールの作成に進みましょう。
ロールタイプはAWSサービスロールのlambdaを選んでください。
ポリシーのアタッチ画面で検索窓に lex と入力し、
AmazonLexRunBotsOnlyを選択してロールを作成します。

スクリーンショット 2017-04-23 9.31.06.png

1-2: 関数の作成

ここを参考に作れば問題ないです。

ざっくり言うと lambda→ブランク関数→Node.js v4.3 を選択し、ドキュメントのコードをそのまま貼り付ければ問題ないです。
ロールについては、既存のロールを選択より先程作成したものをアタッチしてください。

2. Lex botの作成

コンソールの LexBotsCreate からbotを作成します。
名前は適当に(e.g. PizzaOrderingBot)、好きな声を選んで、Session timeout は5分で。Child-DirectedNo で大丈夫です。

3. SlotTypeとIntentの設定

3-1: Slotの追加

EditorタブのサイドバーからSlot typesにある+ボタンより新しいスロットを追加。
今回は以下のように追加します

  • Crusts というslotを作成し、valueに thickthin
  • Sizes というslotを作成し、valueに small, medium, large
  • PizzaKind というslotを作成し、valueに cheeseveg

※↓ではPizzaSizeになってますが、SizesでもPizzaSizeでも好きな名前をつけてください
スクリーンショット 2017-04-23 8.58.28.png

※ Slotとは

Alexaの質問に対して答えるべき単語のまとまり、といった感じです。
(Alexaが回答として期待している単語のまとまりと言うべきか。)
主要な都市や日時、空港や俳優などAmazonが用意しているものも利用可能です。
Amazonが用意したBuilt-in SlotはAMAZON.xxxで定義されており、インストールなどの必要もありません。

3-2: Intentの追加

EditorタブのサイドバーからIntentにある+ボタン、Create New Intentより作成しましょう。

3-2-1: Sample utterancesの編集

ここに入れた文章でAlexaに呼びかけると、反応して処理を開始します。
以下のような文章を追加します。

  • I want to order pizza please
  • I want to order a pizza
  • I want to order a {pizzaKind} pizza
  • I want to order a {size} {pizzaKind} pizza
  • I want a {size} {crust} crust {pizzaKind} pizza
  • Can I get a pizza please
  • Can I get a {pizzaKind} pizza
  • Can I get a {size} {pizzaKind} pizza
3-2-2: Slotsの編集

作成したslotを追加していきます。
Nameで登録した名前は、上のutterancesの文章や、lambdaの処理で利用する際のkeyとなります。
Slot typeは先程作成したものを選択、PromptにはAlexaから投げられる質問を入れます。
また、Requiredにはすべてチェックを入れましょう。

※ Promptの例
– pizzaKind: Do you want a cheese or veg pizza?
– size: What size pizza?
– crust: Thick or thin crust?

スクリーンショット 2017-04-23 9.37.54.png

3-3: その他設定

FulfillmentでAWS Lambda functionを選択し、先程作ったlambda関数を選択します。
Lambda initialization and validationConfirmation promptはとりあえずそのまま空白で問題ありません。

※ Intentとは

utterancesに追加した文章で我々が呼びかけるとAlexaが質問を投げかけてくるので、それに対してslotに入ってる単語で回答。また、promptでAlexaは別の質問を行いデータをさらに集める。それらのデータを用いてAlexaが回答、またはlambdaに処理が投げられる。
このような一連の流れをIntentとしてまとめています。

4. テスト

早速テストしましょう。画面上部の Build ボタンよりビルド完了後、画面右下のチャット欄からチェックできます。

スクリーンショット 2017-04-23 9.49.23.png

ビルド後はIntentが保存されて編集できなくなるのですが、画面上部でversionをLatestに変更すると再び編集が可能になります。

スクリーンショット 2017-04-23 9.37.39.png

5. おつかれさまでした😌

lambda関数の動きについては触れていませんが、今後何か書くかもしれません。
あとは音声入力などについても調べたいと思います。
(Alexa Voice ServiceのSkill Kitに追加できないのかしら…)

続きを読む

簡単に仮想のS3を作成してAWSLambdaとS3サービスの連携をローカル環境でテストする

事前準備

実行する前にEclise用のAWSツールキットを導入しておいてください。導入手順は以下のリンクにご参考をお願いします。

ー>AWS Toolkit導入手順

導入完成したらAWSプロジェクトがプロジェクト新規画面で出てくるはずです。

mavenは導入済みの前提です。

Screenshot from 2017-04-20 17-13-55.png

Lambdaファンクションを書く

  1. まずはAWS Lambda Java ファンクションのプロジェクト作成する.

Screenshot from 2017-04-20 17-19-41.png
- プロジェクト名 :S3EventTutorial
- パッケージ名:com.amazonaws.lambda.s3tutorial
以上のように必須な情報をいれておきまましょう。「完了」を押したらプロジェクトは作成されて一般的なプロジェクトフォルダは以下のようにみれます。
Screenshot from 2017-04-20 17-25-12.png

  1. S3をモックするように「s3mock_2.11」というライブラリをMavenで導入する。pomファイルに依存ライブラリを定義するだけでいいですので下のpomファイルを参考にして自分が作成したプロジェクトのpomを作成してみてください。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.amazonaws.lambda</groupId>
    <artifactId>s3tutorial</artifactId>
    <version>4.0.0</version>
    <dependencies>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-core</artifactId>
            <version>1.1.0</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-lambda-java-events</artifactId>
            <version>1.3.0</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.amazonaws</groupId>
            <artifactId>aws-java-sdk</artifactId>
            <version>1.11.119</version>
            <scope>compile</scope>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.typesafe.akka/akka-http-experimental_2.11 -->
        <dependency>
            <groupId>com.typesafe.akka</groupId>
            <artifactId>akka-http-experimental_2.11</artifactId>
            <version>2.4.11.1</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.typesafe.scala-logging/scala-logging_2.11 -->
        <dependency>
            <groupId>com.typesafe.scala-logging</groupId>
            <artifactId>scala-logging_2.11</artifactId>
            <version>3.5.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/io.findify/s3mock_2.11 -->
        <dependency>
            <groupId>io.findify</groupId>
            <artifactId>s3mock_2.11</artifactId>
            <version>0.1.10</version>
            <scope>test</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>2.7.22</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.github.tomakehurst/wiremock -->
        <dependency>
            <groupId>com.github.tomakehurst</groupId>
            <artifactId>wiremock</artifactId>
            <version>2.6.0</version>
        </dependency>


    </dependencies>
</project>

ロカールにおいてあるmavenのリポジトリーにない依存ライブラリがあるかもしれないので一応プロジェクトのrootフォルダで「mvn package」をコマンドラインとして実行してみましう。そしてmavenはpomに定義されていたdependencyをダウンロードしてくれます。

  1. Lambdaファンクションのロジック
    作成してもらったLambdaFunctionHandler.javaを開いてロジックをかいてみましょう。アイデアは凄っく簡単です。

S3からファイルがアップロードされたというイベントがこられたら、イベントの内容を見てアップロードされたファイルをゲットしてコンソールでそのファイルを書き出すという作業です。コードみてみたらすぐ分かると思いますので説明しないですむ。


public class LambdaFunctionHandler implements RequestHandler<S3Event, Object> {

    private AmazonS3 s3Client;

    public LambdaFunctionHandler(AmazonS3 s3Client){
        this.s3Client = s3Client;
    }
    public LambdaFunctionHandler(){
        this.s3Client =  new AmazonS3Client(new ProfileCredentialsProvider());
    }

    private static void storeObject(InputStream input) throws IOException {
        // Read one text line at a time and display.
        BufferedReader reader = new BufferedReader(new InputStreamReader(input));
        while (true) {
            String line = reader.readLine();
            if (line == null)
                break;
            System.out.println("    " + line);
        }
        System.out.println();
    }

    @Override
    public Object handleRequest(S3Event input, Context context) {
        context.getLogger().log("Input: " + input);

        // Simply return the name of the bucket in request
        LambdaLogger lambdaLogger = context.getLogger();
        S3EventNotificationRecord record = input.getRecords().get(0);
        lambdaLogger.log(record.getEventName()); // イベント名

        String bucketName = record.getS3().getBucket().getName();
        String key = record.getS3().getObject().getKey();
        /*
         * Get file to do further operation
         */
        try {
            lambdaLogger.log("Downloading an object");

            S3Object s3object = s3Client.getObject(new GetObjectRequest(bucketName, key));

            lambdaLogger.log("Content-Type: " + s3object.getObjectMetadata().getContentType());

            storeObject(s3object.getObjectContent());

            // Get a range of bytes from an object.

            GetObjectRequest rangeObjectRequest = new GetObjectRequest(bucketName, key);
            rangeObjectRequest.setRange(0, 10);
            S3Object objectPortion = s3Client.getObject(rangeObjectRequest);

            System.out.println("Printing bytes retrieved.");
            storeObject(objectPortion.getObjectContent());

        } catch (AmazonServiceException ase) {
            System.out.println("Caught an AmazonServiceException, which" + " means your request made it "
                    + "to Amazon S3, but was rejected with an error response" + " for some reason.");
            System.out.println("Error Message:    " + ase.getMessage());
            System.out.println("HTTP Status Code: " + ase.getStatusCode());
            System.out.println("AWS Error Code:   " + ase.getErrorCode());
            System.out.println("Error Type:       " + ase.getErrorType());
            System.out.println("Request ID:       " + ase.getRequestId());
        } catch (AmazonClientException ace) {
            System.out.println("Caught an AmazonClientException, which means" + " the client encountered "
                    + "an internal error while trying to " + "communicate with S3, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message: " + ace.getMessage());
        }catch (IOException ioe){
            System.out.println("Caught an IOException, which means" + " the client encountered "
                    + "an internal error while trying to " + "save S3 object, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message: " + ioe.getMessage());
        }
        return record.getS3().getObject().getKey();
    }

}


書いたコードに対してのテストケースを作成しましょう

今回は実装したLambdaコードを注目しますのでLambdaFunctionHandlerTestを開いてテストケース作成します。まずはテストケースのコードに目を通してみましょう。


    private static S3Event input;
    private static AmazonS3Client client;

    @BeforeClass
    public static void createInput() throws IOException {
        input = TestUtils.parse("s3-event.put.json", S3Event.class);

        S3Mock api = S3Mock.create(8999, "/tmp/s3");
        api.start();

        client = new AmazonS3Client(new AnonymousAWSCredentials());
        client.setRegion(Region.getRegion(Regions.AP_NORTHEAST_1));

        // use IP endpoint to override DNS-based bucket addressing
        client.setEndpoint("http://127.0.0.1:8999");

    }

    private Context createContext() {
        TestContext ctx = new TestContext();

        // TODO: customize your context here if needed.
        ctx.setFunctionName("Your Function Name");

        return ctx;
    }

    @Test
    public void testLambdaFunctionHandlerShouldReturnObjectKey() {

        client.createBucket(new CreateBucketRequest("newbucket", "ap-northeast-1"));
        ClassLoader classLoader = this.getClass().getClassLoader();
        File file = new File(classLoader.getResource("file/test.xml").getFile());
        client.putObject(new PutObjectRequest(
                                 "newbucket", "file/name", file));

        LambdaFunctionHandler handler = new LambdaFunctionHandler(client);
        Context ctx = createContext();

        Object output = handler.handleRequest(input, ctx);

        if (output != null) {
            assertEquals("file/name", output.toString());
            System.out.println(output.toString());
        }
    }

テストのため、createInput関数でS3Mockのインスタンスを作成して起動します。このインスタンスはローカル環境の8999番ポートにバイドしてリクエストを待ちます。それに「/temp/s3」というフォルダを作成しておいてS3サービスのストレージを真似する。

一番大事なのはtestLambdaFunctionHandlerShouldReturnObjectKeyという関数の内容です。見るの通り、以下の作業を実装します。
– 「testbucket」を作成する。注意:Regionを指定するのは必須です(Regionの内容は別になでもいいですがなかったらjava.lang.NoSuchMethodError: com.amazonaws.regions.RegionUtils.getRegionByEndpoint(Ljava/lang/String;)Lcom/amazonaws/regions/Region;というErrorが出てきます。これはAWSのバグです)
– プロジェクトのしたにあるresourceフォルダに作成したfile/test.xmlを仮ストレージにアップロードする
– アップロードしたファイルを仮S3からダウンロードして内容をチェックする。

トリガーは「s3-event.put.json」で定義されているイベントの内容なので「s3-event.put.json」の内容にアップロードされたファイルの情報を反映しなければなりません


{
  "Records": [
    {
      "eventVersion": "2.0",
      "eventSource": "aws:s3",
      "awsRegion": "us-east-1",
      "eventTime": "1970-01-01T00:00:00.000Z",
      "eventName": "ObjectCreated:Put",
      "userIdentity": {
        "principalId": "EXAMPLE"
      },
      "requestParameters": {
        "sourceIPAddress": "127.0.0.1"
      },
      "responseElements": {
        "x-amz-request-id": "C3D13FE58DE4C810",
        "x-amz-id-2": "FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"
      },
      "s3": {
        "s3SchemaVersion": "1.0",
        "configurationId": "testConfigRule",
        "bucket": {
          "name": "testbucket",
          "ownerIdentity": {
            "principalId": "EXAMPLE"
          },
          "arn": "arn:aws:s3:::mybucket"
        },
        "object": {
          "key": "file/name",
          "size": 1024,
          "eTag": "d41d8cd98f00b204e9800998ecf8427e"
        }
      }
    }
  ]
}

注意:bucket名とobjectのキーは一番大事です。見た内容の通りファイルはtestbuckにfile/nameというキーでアップロードされましたので応じてjsonの内容はそ言うことを表現される。

#終わり

ドラフトに説明しましたが不明なところがありましたらご相談をお願いします

続きを読む