ボクココ

個人開発に関するテックブログ

Twilio での録音から Amazon Transcribe で話者判断

ども、@kimihom です。

先日の CallConnect リリースで、文字化した話者の判断ができるようになった。文字化を見た瞬間に、どちらが担当者で、どちらが顧客の発言かを、すぐに判断できるようになった。これにより、録音音声をわざわざ聞かなくとも、文字化を読むだけで内容をより正確に把握できるようになった。

ここまでに至るには長いTwilioとの実装やりとり、そしてAmazon Transcribe への操作が必要で、本記事ではその流れについて紹介しようと思う。

録音ファイルの自前管理

まず音声を文字化をする上で、元の音声をどこに置くかを決める必要がある。これから始める場合には、Twilio が今年リリースした Twilio Voiceの通話録音向け外部ストレージ機能 を使うのが一番早い。Twilio 側で AWS S3 の認証情報を貼り付けるだけで、勝手に S3 へ書き出されるようになる。これが出てくる前までは、わざわざ録音をDLして新しく自前S3にアップロードさせる実装が必須であったが、便利になったものである。

Twilio の標準の書き出し先が Amazon S3 である以上、音声の文字化も 同じ AWS にある Transcribe を使うというのは一般的な判断となろう。

もちろん、Amazon Transcribe 以外にも音声を文字化する外部サービスは複数あるので、実際に文字化した時のクオリティに関しては事前に確認しておいた方がいいに違いない。

Transcribe での文字化結果

では Twilio から AWS S3 の録音URL を取ってきて、録音再生までできるようになったとした時、どんな流れで実装していくのかを簡単に示そう。

Amazon Transcribe へリクエスト送信

まずは録音URL を指定して、文字化するリクエストを送る。

const transcribe = new AWS.TranscribeService();

// 新規作成依頼
let name = "一意な名前";
transcribe.startTranscriptionJob({
  "LanguageCode": "ja-JP",
  "Media": { "MediaFileUri": voiceUrl },
  "TranscriptionJobName": name,
  "MediaFormat": "wav",
  "Settings": {
    "ChannelIdentification": true,
    "ShowAlternatives": false
  }
}, function(err, data) {
  console.log(data);
});

// 完了した時に一覧を取得
let jobs = transcribe.listTranscriptionJobs({
  status: "COMPLETED"
})

// 個別に詳細を取得..
jobs.forEach(n => {
  let job = transcribe.getTranscriptionJob({ transcription_job_name: n.transcriptionJobName })
  // job.transcriptionJob.transcript.transcriptFileUri に結果が入っている
})

ここで大事なのは ChannelIdentification を指定し、話者の識別を有効にさせる必要がある。そして、AWS 側では誰が担当者で誰が顧客かの判断は不可能であることがわかるという点にある。

そのため、録音を作った時点で、どっちの発言がどっちであることを識別しておかないといけないことになる。ここが今回のキーポイント。

録音URL を保存する時点で、通話の種類から話者を判断

話者の判断のために、Twilio 側で通話を終了した時点で、どちらが最初に通話の接続をしたのかに関するデータ保存が必要だ。

基本的には「通話に入ってきた順」であることがわかった。つまり、最初に発信した方が channel[0] に入り、後が channel[1] に入るというだけとなる。担当者が発進して顧客が電話に受けた場合、顧客が発進して担当者が受けた場合。これだけの話に思えた。

しかし、実際は 以下の点で channel の入りが変わったりする。

  • 通話が Twilio の Queue を使ったケース
  • 通話が Twilio の Conference を使ったケース
  • 通話を保留をして再開したケース

これら全てのケースを実際に録音通話で確認し、AWS 側では channel[0] か channel[1] かの振り分け結果を判断する必要があった。

研究した結果できるUI

全てが正しく保存できるようになったことが確認でき、実装することで、以下のようなUI を実現できる。

単に"どちらが" 何を言ったかがわかるだけなんだけども、今後の開発の中で "担当者が" 発言した中でのワード分析 など、分離ができることの影響は大きい。

終わりに

まだ音声テキスト変換の改善の序章に過ぎないが、1つ大きな進展ができたリリースをすることができた。

今後、この改善をベースにさらなる通話の分析ができるように、引き続きチャレンジしていこう。

Webサービス障害時にAmazon Connectから通知

ども、@kimihom です。

f:id:cevid_cpp:20190430182438j:plain

Web サービスを運営していれば障害は付き物なので、その対策を検討した結果について記しておこう。

障害検知

きっかけとなったのは、起きている昼間だったら定期的に Slack 通知などをみながら様子を見て、誰かが発見したら共有すれば良い。しかし、夜間で起きた場合にどうするかという問題がある。

これに対して、そもそも夜間は使う人が少ないから、起きた時に見つけたら対応する。というケースでほとんどは問題ないのかもしれない。基本的にこうした障害は結局は外部サービス側の障害解消を待つしかないケースが多いのである。

それでも、自分達でWebサービスを運営しているのであれば、できる限り早くユーザーに向けてその状況を報告しなければならない。そうした責任感が増した時、深夜に障害が発生したら早くお知らせする体制が必要になってくるだろう。

Amazon Connect

Amazon Connect は、いろいろなケースで電話をカスタマイズできるクラウドベースのコンタクトセンターである。

aws.amazon.com

「あれ?いつもはTwilioのことを書いてない?」そう思った読者の皆様にはいつもブログを読んでいただいて感謝である。そう、今回のような障害通知を利用するケースで、Twilio Voice を使うケースがとても多い。

しかし、私自身が Twilioを使ったWebサービスを開発しているため、仮にTwilioで障害が起きてしまったら、そもそも通知さえ届かなくなる事態が発生してしまう。それを防ぐためには、Twilio とは別の環境で電話の通知を出すようにしなければならなかった。

ではAmazon Connectを使って具体的にどう実装をするかについては、以下のWebサイトを参考にさせてもらった。

Amazon ConnectでLambdaから電話をかける | It works for me

こちらの記事の補足として、IAM の設定の記載がなかったので記しておこう。ズバリ、必要なのは普段 AWS Lambda を実行させているIAM権限に connect:StartOutboundVoiceContact を追加すればOKである。

実際の流れは以下となる。

  1. 障害が発生したら、Webhook で Amazon API Gateway へ飛ばす
  2. AWS Lambda を実行させて、その内容をSlack通知
  3. エラーの回数が指定した回数を超えたら、電話で緊急対応通知を出す

電話で緊急対応通知を出すには、AWS Connect の API を呼び出すだけでOKだ。参考までに以下のようなコードとなろう。

const AWS = require('aws-sdk');
const async = require('async');

const CALL_NUMBERS = [
  '+81******', '+81******', '+81******'
];
const connect = new AWS.Connect({apiVersion: '2017-08-08', region:"ap-northeast-1"});
async.each(CALL_NUMBERS, (number, cb) => {
  connect.startOutboundVoiceContact({
    ContactFlowId: 'dc2527a3-****-****-****',
    DestinationPhoneNumber: number,
    SourcePhoneNumber: '+815011113333', // Amazon Connectで取得した電話番号
    InstanceId: '1d8f3ad7-****-****-****',
  }, (err) => {
    console.log(`${number} call done. err: ${err}`);
    cb();
  });
}, callback);

Amazon Connect の感想

こうした特定の電話の処理を、パッとやりたい場合には、ググりながらなんとか対応することが可能。 しかし、実際にコンタクトセンターとして運用をしていく場合には、ものすごい複雑だと思った。その複雑さにより、わかる人にはいろいろな拡張ができる仕組みではある。しかし、これをゼロからいじっていくのは大変なことである。

では、実際にコールセンターを作りたい場合には、何を使えばいいのか?

その答えは、きっと私がここに記さなくても、あなたはわかってくれていると信じている。

終わりに

今回は Amazon Connect を利用した障害通知の方法に関して記した。

夜間の障害をどう捉えるか。Webサービス運営の責任感を感じ始めるようになってきたら、検討してみてはいかがだろうか。

Amazon CloudWatch でのエラー通知

ども、@kimhiom です。そしてあけましておめでとうございます、今年もよろしくお願いします。

f:id:cevid_cpp:20220108142207j:plain

AWS Lambda でエラーが起きた時、どのように通知を受けているだろうか。ものすごい一般的な内容にもかかわらず、AWS 側の設定が多くて面倒だったので、参考サイトの補足として執筆時点で最新の対応について記す。

目標

やりたかったことはものすごくシンプルで、「Lambda 内で例外エラーが発生した際に Slack へ通知する」というもの。 今まで自前で全コードを try で囲って、rescue で Slack 通知させる実装をしていたけど、とても探しづらい問題があった。

具体的にはエラー通知を Cloudwatch の ログストリーム単位(ex: [$LATEST]f76663ac408647ccb336a269aad132aa)でしか場所を特定できず、詳細のエラー内容を見るには Cloudwatch からログストリームのページで対象の時間までスクロールしたりジャンプさせる手間があった。

・・・こんな一般的なこと(バグのログを閲覧すること)は AWS 側でなんとかしてほしいなと感じていた。

対応

調べると、それだけのために自前の AWS Lambda メソッドをあげて実行させる方法がたくさん掲載されており、「面倒すぎる・・」と感じた。

それが、"AWS Lambda エラー Cloudwatch" 関連の記事を1年以内に限定して対応方法を調べると、どうやら AWS Chatbot が登場したおかげで、より簡単に設定ができるようだった。参考になったページ

Lambdaの実行時エラーをChatbotでSlackに通知するのが便利すぎる話 | public memo

その中で、自分がはまったところだけ、この記事に記載しておく。

エラー詳細がどうやっても見えない問題

まず IAM を設定し、各 AWS サービスをちょろっと設定して無事に Slack へ通知が届いた!

f:id:cevid_cpp:20220108134056p:plain

ただ、このままだと単にエラー通知が来ただけで、どんなエラーかを把握することができない。一番知りたいのは Show error logs の先のボタンである。

しかし、このボタンをクリックしても Cloudwatch からのエラーが止まらなかった。

❗️ I can't get the logs for you because the role arn:aws:iam::123412341234:role/my-chatbot in account 123412341234 is missing necessary permissions related to commands.

If you want to get logs, go to the AWS Chatbot console and choose or create a role that includes Read-only command permission for this channel.

amazon web services - What minimal IAM permissions are needed by AWS Chatbot so that it can show logs? - DevOps Stack Exchange

上記で全く同じ Issue があったけど、上記のような IAM をセットしても動作せず。なんとかしようと1日くらい色々と設定を変えてみて、ついに問題を発見。AWS Chatbot の設定が以下の表示になっていた。

f:id:cevid_cpp:20220108135247p:plain

ガードレールのポリシー ?? これに関してより詳細を調べたところ、

ガードレールは、AWS 環境全体に継続的なガバナンスを提供する高レベルのルールです。これは、わかりやすい形式で表されます。ガードレールを通じて、AWS Control Tower は予防的または探偵リソースを管理し、AWS アカウントのグループ全体でコンプライアンスをモニタリングするのに役立つコントロールです。

ふ〜むふむ・・、ということで ここのポリシーに IAM 設定を同様にしたところ、無事動かすことができた。

エラー内容が出てこない

さて、これでようやく Show error logs でエラー内容が見れると思ったら、なぜかエラーが表示されない。。

f:id:cevid_cpp:20220108140517p:plain

No logs found。何のために通知してきたのだろうか。

よく見ると、from ** to ** の間が1分間の間であった。この Lambda 処理が 1分以上のものだったため、その一分に含まれていなかったのが原因だった。これは、 Cloudwatch の アラームの設定で期間を 5分に変えたことで、無事エラー内容がが表示されるようになった◎

終わりに

今回の Cloudwatch エラーの Slack 通知の結果、今まではエラー通知のたびに Cloudwatch から対象のエラーを探すことをしていたのが、Slack にあるボタンをワンポチするだけでどこでのエラーかを表示させることができるようになった。

このために新しく使った AWS は AWS Chatbot とAmazon SNS の2つのみ。これから運用をしていく上で詰まるところがあるかもしれないが、現状はとても簡単(?)に設定できたので満足。

私と同じところで無駄に時間を使わないで さっと設定いただけたら幸いである。

さぁ始めよう、より短く素早く問題を解決するために。

Twilio 録音セキュリティの考察と実装

ども、@kimhiom です。

f:id:cevid_cpp:20211221164707j:plain

この記事は Twilio Advent Calendar 2021 23日目の記事となります。他のも是非みていただければと〜。

近年は セキュリティに関して多くの方々が意識するようになり、私にとって Twilio での録音ファイルをどのように管理するのかについて、最適解を考え続けた。そこでの対応や実装について記そう。

録音をしない、保存しない

まず録音セキュリティを話す前に、録音セキュリティをものすごく気にする方には、そもそも録音をしない を絶賛推奨しよう。ことの発端は全て録音をするから始まるのである。さらに、録音をするだけで Twilio ポイントが引かれるし、その録音ファイルを所持するだけで毎月費用が発生してしまう(ある程度までは無料だけど)。

また、録音を一度ダウンロードして、どこかセキュア?な環境へ保存したら、Twilio 側の録音を削除するという運用で、一般公開を防ぐことが可能だ。ただ、手動での対応は手間がかかりすぎるので、TwiML での Twilio Action で指定した URL 側のプログラム実装すれば自動化できる。ただ、そのローカルな環境で保存したところで、誰がその録音にアクセスできるのかなどの課題は残り続ける。

まず前提を考慮した上で、"ネット上で録音を聞きたい。でもセキュリティをなんとかして" という要望に対して、以下の方法を紹介していこう。

Twilio 提供の 録音セキュリティ

まず、Twilio がデフォルトで提供している録音セキュリティに関してまとめてみる。

1. 予測不可能な録音URL

Twilio で 録音を on にして通話をすると、以下のような形式の URL で録音が再生できるようになる。

https://api.twilio.com/2010-04-01/Accounts/AC****************/Recordings/RE*********************

デフォルトの制限なし録音 URL は、その録音 URL を 社内チャットで共有したり、CRM 側でその録音 URL を保存することで、他の担当者も簡単にその録音が再生できるメリットがある。これは、シンプルに HTML で <audio controls src="https://api.twilio.com/2010-04-01/Accounts/AC****************/Recordings/RE*********************.wav"></audio> と記載するだけで再生が可能だ。

録音は上記の形式で、 ******* の部分が予測困難な文字列になっている。そのため、その録音 URL を知っている誰かが意図的に録音 URL を公開しない限りは、意図しない誰かにアクセスされるリスクは低いと言える。

しかし、逆を言えば、誰かが録音 URL を SNS などでシェアしてしまえば、誰でも再生ができてしまうという問題がある。

2. Basic認証

より再生できる人を限定するために Basic 認証が提供されている。ここでの Basic 認証では、 Twilio Account SID, Twilio AuthToken の2つが必要となる。

つまり、再生には https://{AccountSID}:{AuthToken}@api.twilio.com/2010-04-01/Accounts/AC****************/Recordings/RE********************* といった形式が必要となる。

・・・。なんということでしょう。もしこの URL を誰かがシェアしてしまうと、Twilio API の操作がなんでもできる AccountSID と AuthToken が知られてしまう。録音URL をシェアしなかったとしても、録音を聞こうとする人は必ず AccountSID と AuthToken を知ってそれを入力する必要があるのだけど、この2つのトークンは Twilio 情報の中で最も知られてはいけないトークンだ。よくわかってない担当者が、それをコピペして録音を聞くみたいな運用は絶対に避けたほうがいいだろう。

つまり、この Basic 認証だけで使うべきではない。Twilio ドキュメント側にも、"HTTP認証を暗号化と組み合わせて使うことを強くお薦めします。より詳しい情報はベーシック認証とダイジェスト認証や現在お使いのウェブサーバーのドキュメントをご覧ください。" との記載がある。

3. 録音の Encryption

そこで、録音の暗号化の機能が提供されている。Twilio 側では 暗号化されたファイルが保存され、その録音アクセスには特定のパラメータで Twilio へリクエストを送り、それが正しい場合に録音を複合できるようになる。詳細は以下。

Security | Twilio

メリット

  • 録音を聞けるのは特定の暗号文字列を知っている場合のみ。
  • 実装が必要なので、その実装済みの Web サーバーや スクリプトからに限定される。

デメリット

  • 上記の録音再生環境からでしか、録音が聞けない(これはメリットでもある)
  • Twilio に録音を保存し続ける必要がある(コスト)

録音の独自セキュリティ化

さて、Twilio 標準から抜けて、できる限りデメリットを減らす方向を考えていきたくなってきた。どのように実装していったかを記していく。

まず、TwiML 指定の ActionURL へ録音 URL が飛んできたら、以下のような処理を実施していく。

  1. Twilio 録音をダウンロード
  2. S3 へアップロード
  3. ダウンロードした録音を削除
  4. Twilio 録音削除 API で Twilio 録音を削除

これで、自前 S3 へ録音ファイルを保存して、予測できない録音 URL からアクセスできるようにすれば、録音再生ができるような状態になる。これだけで、例えば録音URL を使って音声文字化したり、感情を取得 API を呼んでみたりすることができる。

さて、以降は Twilio とはほとんど関係ない Amazon S3 部分であることに注意してほしい。

最終ゴール: S3 側で付与された特定パラメータ付きの URL からのみアクセスできる。その URL は、指定秒後に期限が切れる ようにする

今回の実装での強みは、何より時間制限があるということである。特別に付与された URL を生成しない限りは、誰もが録音を聞くことができない。仮に録音 URL を意図せずシェアされたとしても、それにアクセスして聞けるのは 指定した秒数のみ。それ以降は誰も聞けなくなる。録音をクリックした瞬間に再生をするようにすれば、もはや30秒でも問題ないだろう。最初の再生さえできれば、残りの音声ロードは問題なく読み込まれるからである。

ではどんな形で実装していくか。S3 の機能として、S3 バケットポリシー というものがあり、これを使っていく。

参考: バケットポリシーの例 - Amazon Simple Storage Service

バケットポリシーを使うことで、"特定のパラメータが正しく追加付与された URL でないと、S3 の特定ファイルにアクセスできない" 設定をする。

このポリシーの書き方はそれぞれ違うと思うので、ぜひ苦戦しながら設定をしてみていただければ幸いだ。ものすごく大雑把に書くと、以下のような形である。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny", "Principal": "*", "Action": "s3:GetObject",
      "Resource": [
        "arn:aws:s3:::my-twilio-s3/records/*"
      ],
      "Condition": {
        "StringNotEquals": { "アクセス拒否する条件をここに記載" }
      }
    }
  ]
}

S3 にアクセス制限をかけたら、そのアクセス制限を回避するための録音 URL を生成する必要がある。Presigned URL というものだ。 この生成には、自前サーバーから 取得したい S3 のバケットとキーを指定し、AWS へリクエスト。そのレスポンスで返ってくる録音 URL を返す流れとなる。

参考 AWS Ruby Class: Aws::S3::Presigner — AWS SDK for Ruby V3

signer = Aws::S3::Presigner.new
url = signer.presigned_url(:get_object,
                           bucket: バケット名,
                           key: キー,
                           expires_in: 30)

すると、例えば以下のような URL が返ってくる。

https://my-s3-domain.s3.ap-northeast-1.amazonaws.com/record.wav?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=%2F20211219%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20211219T073726Z&X-Amz-Expires=60&X-Amz-SignedHeaders=host&X-Amz-Signature=********

この録音 URL でのみ、対象の S3 へアクセスできるようになる。

メリット

  • 時間制限を極端に短くすることで、仮に録音 URL を知られても部外者の再生を不可にさせられる
  • S3 に保存することでコストダウン
  • S3 に録音ファイルを保存することで、AWS の面白い音声処理系 API が使える

デメリット

  • 特定の URL を生成できる環境でのみ再生が可能なため、シェアは難しい
  • 実装やアクセス制限の処理が手間

セキュリティを意識してるのに、シェアできたら聞けるは問題なので、デメリットではないか。しっかりと実装すれば、もはやメリットしかない。

終わりに

今回は Twilio 側で提供されている標準の録音セキュリティ対応と、それをせずに自前で 録音セキュリティを実装する案について記した。

結局は、利用者が 「どこまで公開を許容できるか」にかかってくる。公開を許容することによるメリット。そして公開されたことによるリスク。それぞれをしっかりと理解して、利用者にとって最適な状態を選択できるようにすることが、Twilio 録音管理にとって最も大切になってくる。

セキュリティ、セキュリティ、セキュリティ!その中でインターネットだからこそのシェアの便利さも。

今年もブログを読んでもらってありがとうございます。来年はもっと書けるように、引き続きプログラマーを楽しんでいこう~

Amazon Interactive Video Service との初対面

ども、@kimihom です。

f:id:cevid_cpp:20200731205411j:plain

先日、AWS から新しいサービスがローンチされた。とりわけ音声・動画関連の技術を扱っている私としては大変興味深いものだった。Amazon Interactive Video Service(Amazon IVS) である。

Amazon IVS

私の理解の範囲で説明すると、Amazon IVS は簡単にライブ配信を行うことのできる AWS だ。一般ユーザーであれば YouTube Live を使ったりするのが一般的だが、Amazon IVS を使うことで AWS の配信基盤を使い、自作した 視聴画面から視聴者はその配信動画を見ることができる。つまり、YouTube にあるような広告や他の動画リンクなどが不要でシンプルな動画配信のサービスを作ることが可能ってわけだ。

とりわけ配信で問題になりがちな、動画視聴の快適さにこだわりがあるようで、視聴しやすい動画を簡単に配信できるモノとなっているようだ。

どうやって配信するか

ドキュメントの Set Up Streaming Software にその方法が記されていた。

OBS Studio での配信

まず一つ目の方法として、OSS ソフトウェアである OBS Studio を使っての配信方法があるようだ。OBS Studio は日本でもかなりの方が使っているメジャーな配信ソフトのようで、日本語でも多くの情報が出回っていた。Windows や Mac にインストールして使う、PC ソフトウェアとなってる。

obsproject.com

FFmpeg を使った既にあるビデオを配信

既にあるビデオファイルを配信させる方法として、FFmpeg を使った方法もあるようだ。FFmpeg も動画関連に詳しい人ならほぼ全員入れてるくらいの有名なソフトウェアで、コマンド一つであらゆる動画の操作をすることのできる大変便利なソフトウェアである。rtmps 形式でのネットワークやりとりで、リアルタイムの配信をするようだ。

ffmpeg -re -stream_loop -1 -i $VIDEO_FILEPATH -r 30 -c:v libx264 -pix_fmt yuv420p -profile:v main -preset veryfast -x264opts "nal-hrd=cbr:no-scenecut" -minrate 3000 -maxrate 3000 -g 60 -c:a aac -b:a 160k -ac 2 -ar 44100 -f flv rtmps://$INGEST_ENDPOINT:443/app/$STREAM_KEY

視聴ページの実装

Amazon IVS の提供する Video.js を使って自ら視聴ページを実装することができるようになっているようだ。配信動画の再生や、現在の経過時間などの情報が JavaScript で取ってこれる模様。

Amazon Interactive Video Service Player: Video.js Integration - Amazon Interactive Video Service

私の望むこと

現状の Amazon IVS では、先述した2つの方法でのみ配信ができるようになっている。確かに配信するってだけなら、ローカル環境に入れた配信用のソフトウェから配信するだけでいい。しかし、私としては やはり配信も Web 上からやりたいところである。そう、 WebRTC でね。

配信サーバーの技術は専門外なので、実現可能かどうかわからないけど、ゆくゆく Amazon IVS が WebRTC での配信をサポートしてくれたら熱いなと思う。配信者が WebRTC で接続し、視聴者側は変換エンコードされた動画を見れるようにしてほしい。そうすれば、視聴者側は WebRTC でない、単に動画が見れる環境であれば配信を視聴することができる。

終わりに

このご時世ということもあり、ビデオサービスを簡単に作れる この Amazon IVS は注目に値するサービスだ。

配信は WebRTC ではなく Amazon IVS と OBS Studio で配信する前提にして、視聴者用の Web UIだけ作るっていう流れも悪くないかもしれない。引き続き試行錯誤していこうと思う。

Rails でのシンプルな S3 ダイレクトアップロード実装

ども、@kimihom です。

f:id:cevid_cpp:20200312211753j:plain

今回はシンプルに S3 にファイルを上げる方法を案内しよう。

Rails 側でファイルアップロードを受ける課題

おそらく多くの Rails デベロッパーは、 画像のサイズ縮小や変換をするプログラムを実行したいがために、一度 Rails 側でファイルを受け取る実装をしたことがあることだろう。carrierwave などはまさにそのための Gem だ。

Webサイトからファイルアップロード => Rails アプリ => 画像等の変換 => S3

しかし時代は流れ、S3 にアップロードされたら、それをトリガーとして AWS Lambda 側でプログラムを実行させることが簡単にできるようになった。AWS Lambda 側でアップロードされた画像を取得・加工し、バックグラウンドジョブ的な形で変換されたファイルを S3 へ置きにいくことができる。その方が、明らかに Rails サーバー的には嬉しい。なぜなら、carrierwave などを使った画像の加工や生成には非常に多くのメモリや処理実行時間がかかり、サーバー負荷の代表的な要因であるからである。

Rails アプリ側では、そもそもアップロードされたファイルを受け取らない。ダイレクトアップロードを実装するべき価値はここにある。

Webサイトからファイルアップロード => S3 => AWS Lambda の実行

ということで、本記事では最もシンプルな Rails を使った S3 ファイルダイレクトアップロードの方法を解説する。

Presigned URL

今回の記事でキーワードとなるのが、S3 の Presigned という概念だ。

AWS 側で短期間有効な URL を生成し、その URL に対して ファイル付きの Ajax POST をすることでアップロードができる。この Presigned URL を生成するのに、Rails サーバー側で AWS SDK を利用する必要がある。以下に Presigned URL を生成するサンプルコードを示す。固定文字でのサンプルコードなので、適宜 動的なプログラムに改善して欲しい。

def presigned_post
    bucket = Aws::S3::Resource.new.bucket("my-bucket")
    post = bucket.presigned_post(
      key: "images/sample.jpg",
      acl: 'public-read',
      content_type: "image/jpg"
    )
    render json: { status: "ok", url: post.url, fields: post.fields }
end

まずダイレクトアップロード前に、この presigned_post アクションを Ajax 経由で呼び出そう。うまく実行されれば urlfilelds がそれぞれレスポンスとして返ってくる。この情報を用いて、フロントエンドでダイレクトアップロードを実行する。

var formdata = new FormData();
for (field in data.fields) {
  formdata.append(field, data.fields[field]);
}
formdata.append("file", file);
$.ajax({
  url: data.url,
  data: formdata,
  processData: false,
  contentType: false,
  method: "POST",
}).done(function() {
  console.log("uploaded");
}).fail(error);

presigned_post アクションで返ってきたデータに加えて、実際にアップロードしたいファイルを formdatafile として追加している。

なんということでしょう、これだけで S3 にダイレクトアップロードを実装できる!しかもフロントエンドで AWS-SDK の JavaScript を入れる必要もなく、単なる Ajax 呼び出しだけで完結してしまう。とても便利なものである。

S3 からファイルのダウンロード

パブリックとして公開していない S3 からファイルをダウンロードするのも同様に簡単に実装できるので紹介しておく。この場合、一時的な presigned_url を Rails 側で生成するだけでよくなる。

def presigned_get
    signer = Aws::S3::Presigner.new
    url = signer.presigned_url(:get_object, bucket: "my-bucket", key: "images/sample.jpg")
    render json: {url: url}
end

返ってきた URL に対して アクセスが可能になる。

$.ajax({
  url: data.url,
  processData: false,
  contentType: false
}).done(function(data) {
  console.log(data);
}).fail(error);

CORS の設定をお忘れなく

S3 側で アクセス権限 > CORS の設定をしよう。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>https://www.yourdomain.com</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

終わりに

今回は S3 ダイレクトアップロード と おまけのダウンロードをご紹介した。

Rails ファイルアップロードによる高負荷処理から卒業しよう。これからはそうした処理は AWS Lambda に任せる。役割分担をしっかりとして、より効率的なサーバー運用を実現しよう。

Amazon Transcribe による音声の文字化

ども、@kimihom です。

f:id:cevid_cpp:20200112163824j:plain

去年の11月末に、ついに Amazon Transcribe が日本語対応したということで調査を続けている。現時点での調査まとめ的な形で記事にしておこう。

Amazon Transcribe とは

Amazon Transcribe(音声をテキストに変換する機能を簡単に追加)| AWS

Amazon Transcribe は「音声をテキストに変換する機能」を提供してくれる AWS サービスの一つだ。会話を自動で文字として書き起こししてくれるので、例えば議事録を文字として起こしたり、何かのスピーチをサッと文字に書き起こしたりすることが簡単にできる。

この分野で言うと Google Cloud Speech-to-Text のほうが日本語対応が早かった。だから日本語を文字に起こす技術としては最新技術というわけではない。

Cloud Speech-to-Text - 音声認識  |  Cloud Speech-to-Text  |  Google Cloud

それでも Amazon Transcribe の日本語サポートが熱いのはなぜか。それはずばり、「Amazon S3 にある音声ファイルを文字に書き起こすことができる」という点に尽きる。Amazon S3 に音声ファイルを置いている企業にとっては、Amazon Transcribe の利用は最適な選択となる。

「どうせ音声文字化の品質はよくないでしょ」という反応は誰もが持つことだろう。実際に使って文字化してみた感想としては、もちろん現時点では人間が文字化した方が圧倒的に精度は高いのは事実である。だが、音声を聞くのではなく、ざっと概要だけ知れれば OK というケースにおいては実用に耐えうる精度だと言えるのではないだろうか。

Amazon Transcribe を使ってみる

Amazon Transcribe はとてもシンプルな設計になっている。S3 に置かれた音声ファイルを指定し、start_transcription_job を 実行をするとバックグラウンド処理として文字化処理が始まる。API で音声文字化処理のステータスを確認することができる。以下のステータスが存在する。

  • "QUEUED"
  • "IN_PROGRESS"
  • "FAILED"
  • "COMPLETED"

とりわけ大事なのが、 Transcribe を始めるときに指定するオプションだ。ここで 言語音声ファイルの場所音声ファイルには何人の人が存在するか とか チャネル数専門用語の登録 などを指定できる。

{
  transcription_job_name: "TranscriptionJobName", # required
  language_code: "en-US", # required, accepts en-US, es-US, en-AU, fr-CA, en-GB, de-DE, pt-BR, fr-FR, it-IT, ko-KR, es-ES, en-IN, hi-IN, ar-SA, ru-RU, zh-CN, nl-NL, id-ID, ta-IN, fa-IR, en-IE, en-AB, en-WL, pt-PT, te-IN, tr-TR, de-CH, he-IL, ms-MY, ja-JP, ar-AE
  media_sample_rate_hertz: 1,
  media_format: "mp3", # accepts mp3, mp4, wav, flac
  media: { # required
    media_file_uri: "Uri",
  },
  output_bucket_name: "OutputBucketName",
  output_encryption_kms_key_id: "KMSKeyId",
  settings: {
    vocabulary_name: "VocabularyName",
    show_speaker_labels: false,
    max_speaker_labels: 1,
    channel_identification: false,
    show_alternatives: false,
    max_alternatives: 1,
    vocabulary_filter_name: "VocabularyFilterName",
    vocabulary_filter_method: "remove", # accepts remove, mask
  },
  job_execution_settings: {
    allow_deferred_execution: false,
    data_access_role_arn: "DataAccessRoleArn",
  },
}

実際に文字化処理の実行が終わると、get_transcription_jobresp.transcription_job.transcript.transcript_file_uri から JSON ファイルを取得することができる。以下にサンプルのレスポンスを掲載する。channels の方には、AさんBさんといたときに、Aさんだけのセリフがずらっと出てくる形となり、items の方には時系列で言ったセリフ順にデータが入っている。 実際に UI に落とし込むには 会話として Aさん Bさんのセリフを時系列として出すだろうから、このJSON の解析が必要となるだろう。

{
  "jobName": "ChannelIdentification",
  "accountId": "123456789011111",
  "results": {
    "transcripts": [
      {
        "transcript": "こんにちは うん"
      }
    ],
    "channel_labels": {
      "channels": [
        {
          "channel_label": "ch_0",
          "items": [
            {
              "start_time": "0.04",
              "end_time": "0.75",
              "alternatives": [
                {
                  "confidence": "0.9935",
                  "content": "こんにちは"
                }
              ],
              "type": "pronunciation"
            }
          ]
        },
        {
          "channel_label": "ch_1",
          "items": [
            {
              "start_time": "1.04",
              "end_time": "1.25",
              "alternatives": [
                {
                  "confidence": "0.657",
                  "content": "うん"
                }
              ],
              "type": "pronunciation"
            },
          ]
        }
      ],
      "number_of_channels": 2
    },
    "items": [
      {
        "start_time": "0.04",
        "end_time": "0.75",
        "alternatives": [
          {
            "confidence": "0.9935",
            "content": "こんにちは"
          }
        ],
        "type": "pronunciation"
      },
      {
        "start_time": "0.04",
        "end_time": "0.75",
        "alternatives": [
          {
            "confidence": "0.9935",
            "content": "うん"
          }
      }
    ]
  },
  "status": "COMPLETED"
}

料金

Amazon Transcribe の料金 を見てみると、実際に文字化を行った分数だけのシンプルな料金体系のようだ。なので文字化されたものを API で取ってくる部分はお金がかからない(おそらく)。

90分の文字化でも $6.75 ってことで、そこまで大きな負担にはならなそうだ。

実装のポイント

文字化のプロセスにおいて、リクエストを送ったらサッと文字のレスポンスが返ってくるという形ではない点に注意しよう。

  1. Amazon S3 に音声ファイルを置く
  2. Amazon Transcribe に S3 パスとオプションを指定してリクエストを送る
  3. 2 で指定した transcription_job_name で現在のステータスを確認
  4. 現在のステータスが "COMPLETED" なら、get_transcription_job で文字化の結果を取得

注意しなければならないのはこれくらいで、だいぶ使いやすい印象を受ける。

もしリクエストを送ってレスポンスで文字化されたものが欲しい!という場合には、Google Cloud Speech-to-Text だと実はできる。ただ、この場合は制約があって1分未満の音声ファイルでないといけない。また、Google Cloud Speech-to-Text だとストリームによるリアルタイムの文字化も可能である。ただストリーミング入力も制限があって、およそ5分までとなっている。また、ストリーミングはgRPC経由のみとなっている。

終わりに

Amazon Transcribe は だいぶシンプルで使いやすい印象を受ける。去年11月に使えるようになったので、今年来年あたりは音声文字化の機能が至る所でリリースされていくのではないだろうか。

書記担当者がいなくなって、より快適に仕事ができるようになる未来がすぐそこまで来ていると言える。そして文字化が一般化された世界において、「音声を文字にする AI で〜」 と むやみやたらに "AI" という言葉は使わない方が身のためであろう。

外部サービスの API で取ってきたデータを一括でDB登録する

ども、@kimihom です。

f:id:cevid_cpp:20191024173445j:plain

最近はいろいろな企業が API を提供してくれるようになってきた。外部サービスの API で、例えば顧客情報などを一括で取り込みたいというケースはよくあることだろう。私はこうして実装したということで記事として起こしてみる。

バックグラウンド処理

こうした外部サービスとのやりとりは、外部サービス側のレスポンス速度などで実行時間が大きく変動するため、バックグラウンドで呼び出す流れが基本となるだろう。私はバックグラウンド処理は基本的に AWS Lambda 側に渡して実行させるようにしている。AWS Lambda 側で処理をさせると開発やテストなどが個人的には簡単でやりやすくて気に入っている。

サーバー => AWS Lambda => 外部サービスAPI 呼び出し => サーバー => データベース

AWS Lambda で外部API を呼んで一気にデータを取ってきた後、データを整えて JSON 形式にする。その JSON を AWS Lambda から自前のサーバーへ HTTP リクエストを送って、そのリクエストを判定してデータベースに保存していくという流れだ。

実装の中で、AWS Lambda 側の処理とサーバーでデータを受け取る部分の実装に関して記す。

データ取得における注意点

まずは AWS Lambda 側でデータ取得する際の注意点を記しておく。

一括取得の API はデータ数と API 側の制約に注意しよう

一括取得の API は、想像の通りそれだけでそれなりにサーバーに負荷がかかる。そのため、一部の API では秒間のリクエスト制限を設けているところもある。何も考えずに全データ一気に取ってくるぜ!ってことやっちゃうと、途中でデータ取得に失敗してうまくいかないことがよくある。ほとんどの API ドキュメントには、このような制限が記載されているので確認しよう。API からデータをリスト取得する際の ページごとの取得件数や上限なども確認しておこう。

テスト段階では、外部サービスの API で取ってくるデータ量ってのはテストデータ程度なので問題になることはない。ただ実際に使っているユーザーの中には想像を超えるような大量のデータを外部サービス側で保存していることがある。なので仮にデータが何万件あっても API で全部とってこれちゃうような場合でも、実装の中に最大の取得上限は設けておいたほうがいい。外部サービス側で万・億並のデータがあったら、それだけで自分たちのデータベースがパンクしてしまう恐れがある。全データ取ってこなきゃ意味がない!っていう API 連携を予定している場合には、それでも期間などで絞り込みをして全データ取得って手段は控えるべきだと思う。ここまで書いてそれでも全部取るってなら私は止めはしない。

用件に満たないデータは予め整備しよう

外部サービス側の API では データがない(NULL)って場合も多くある。そうしたデータのないものも とにかく API で全部とってきてしまうと、それだけで無駄なリクエストとなってしまう。例えば自分たちのサービスではメールアドレスが必須だけど外部サービス API 側ではメールアドレスが必須ではない場合、メールアドレスの存在するデータだけを API のパラメータで指定できるなら指定しよう。メールアドレスありだけっていう API パラメータが指定できない場合は、自分たちのサーバーに送る前のデータ整備の段階で、メールアドレスのない情報は予め削除しておこう。これだけで多くのリクエストの無駄を解消することができる。全部自前のサーバーでやればいいやって判断は後々痛い目を見ることになる。

一括でデータを送ることは控えよう

例えば外部サービス API で 4,000件のデータを取ってこれたとしよう。そのデータを一括で 自前のサーバーにリクエストを送ることは決してしないように。それだけで自前サーバーがパンクする恐れがある。

100件ごとに データを区切って、AWS Lambda => 自前サーバー のレスポンスがちゃんと返ってきたら、次のデータを送るって流れを取ろう。最近の AWS Lambda は10分以上も実行させ続けられるので、数千件程度であれば問題になることはないはずだ。

データ保存における注意点

では次に AWS Lambda から自前のサーバーに渡ってきたときのサーバー側の注意点を上げよう。

リクエストの認証をしよう

AWS Lambda から送られてきた HTTP リクエストのみをデータ保存するように実装しよう。仮にもし 一括登録の URL が外部に漏れて、データを一括で送信された場合、意図しないデータ登録が発生してしまうリスクがある。どこまで厳密にするかは実装次第ではあるけど、最低限リクエストのパラメータを確認することはちゃんとしよう。

同一データの扱いに気をつけよう

例えば自分たちのデータでは メールアドレスはユニークでなければならないケースなどだ。外部サービス側では当然そんな制約がないことも多くあるので、その場合のデータ登録について実装を考慮する必要がある。私の場合、メールアドレスが登録されていない場合にはデータ挿入、メールアドレスが既に存在する場合にはデータ更新 というところで実装を分けている。サーバーに送られてきた JSON を一つ一つチェックし、データ一括挿入枠と一件ずつ更新するデータ更新枠の2つに分ける。

データ一括挿入枠に入ったデータは、activerecord-import を使って一括登録する。データ更新枠に入ったデータは、一件ずつ Update をしていく。アップデートだけだったとしても、1リクエストにつき 100件なので、そこまで負荷がかかる処理でもない。

終わりに

今後、SaaS のビジネスがますます賑わっていくにつれ、こうした API による連携ってのはどんどん増えていくことだろう。

利用ユーザーさんがより便利に活用できるように、Cool な実装で問題を解決していこう。

Rails コールバックによる S3 事故と対策

ども、@kimihom です。

f:id:cevid_cpp:20190601165719j:plain

Rails でコードを書いていたら、きっと一度は書いたことがあるだろう before_destroy で起こりうるリスクと対策について記す。

S3 のドメインが変わるお知らせ

先日、S3 の URL が変更されるとのお知らせが届いた。

dev.classmethod.jp

これを読んで、「そのうち対応が必要なら早いうちにやっちゃおう。"s3-ap-northeast-1.amazonaws.com" ってリージョンも長ったるしいし。」ってことでさっと対応してみよう。

User の Image URL の更新

User モデルがあったとして、その User に image_url で画像URLを保存するデータがある。そこで、画像を保存している S3 URL をアップデートする処理を書く。

そもそも、ユーザー画像に S3 のドメインから登録させるってのは、あまりすべきことではないだろう。ただ、Twitter ログインなどから画像を登録したり、CRM の画像を参照する可能性もあったことから、画像URL をドメインから DB に保存するようにするケースもあるだろう。

てことで、以下のようなコードを実行することになる。

prev_s3 = "s3-ap-northeast-1.amazonaws.com/myapp"
after_s3 = "myapp.s3.amazonaws.com"
User.where("image_url is not null").each do |c|
  c.update(image_url: c.image_url.sub(prev_s3, after_s3))
end; ""

画像の自動削除

ユーザーが画像を再度アップロードした時は、前に使っていた画像はもう使わないので削除するような処理を実装することがあるだろう。これをしないと使わない画像がどんどん溜まっていってしまうからである。具体的には以下のような形である。

class User < ApplicationRecord

  before_save :delete_image_if_image_changed

  def delete_image_if_image_changed
    return if self.attribute_was("image_url").blank? || !self.changed.include?("image_url")
    remove_s3_image(self.attribute_was("image_url"))
  end

end

これでユーザーが画像を繰り返しアップロードするたびに、前の画像は無事削除されるようになり、無駄に S3 の画像が残らず、費用の節約にもなる。

さて、このコールバックが定義された状態で、先ほどのコードを実行したらどうなるか、もうお分かりだろう。そう、画像URL だけが変わり、画像の実態が削除されてしまうのである。

S3 のバージョニングを有効にしておこう

画像が一括削除されてしまうような悲劇が起きたとしても、なんとか対応できるようにするために、S3 のバージョニングを有効にすることをお勧めする。S3 の対象のバケットに行って "プロパティ" -> "バージョニング" で 有効にすれば OK である。

「バージョニングを有効にするくらいならそもそも消さなければよくないか」という考え方もあるだろうが、使われている画像/使われていない画像が全てバケット内にあるようなカオスな状態にはしたくないケースでバージョニングが便利に使える。

これで、削除された画像もバージョン表示をすれば、取得が可能だ。S3 費用の節約よりも、安全に運用できる道を選ぶべきだろう。

そもそも、update はコールバックが呼ばれてしまうので、今回の場合は update_column を呼び出すのが正解ということになる。

終わりに

今回の根本である「Rails のコールバックがいけない」とは私は思わない。使い方次第ではコールバックは便利に使えるし、コードを短くできる。

本記事がより安全にサービス運用をするために考える きっかけになれば幸いである。

API Gateway 用の Swagger JSON を生成する方法

ども、@kimihom です。

久々の AWS ネタは Amazon API Gateway のお話。

Amazon API Gateway を使えば、 Swagger 形式のドキュメントをアップロードするだけで、APIの "側" を作ってくれる。この "側" を API Gateway で作っておけば、キャッシュを有効化できたり、大量APIリクエスト防止のスロットリング(リクエスト数の制限)ができたり、外部呼び出し用の iOS/Android/JavaScript SDK を生成できたり、CloudWatch にログを吐き出したりすることができる。

今回は、Swagger の話は細かくは書かない。興味があれば以前の記事を参照していただきたい。

さて、上記の記事は1年前なのでちょっと古かったりするため、今回はより完璧に "API Gateway 用の Swagger JSON を生成する方法" をご紹介しよう。

swagger.json の生成

まず swagger.json を書き出す。より詳細のコマンドは以下の Github を参照していただきたい。

https://github.com/Gild/ruby-swagger

# Generate a swagger 2.0-compatible documentation from the metadata stored into doc/swagger
bundle exec rake swagger:grape:generate_doc\[API::Root\]
# Build all the API clients
bundle exec rake swagger:compile_doc

これで書き出された swagger.json を Amazon API Gateway でインポートすれば、確かに API の定義がされた状態まで形作ってくれる。

しかし、この状態のままでは、統合リクエストや統合レスポンスの定義がされていないままアップロードされるため、結局一個ずつ定義していかなければならないという痛すぎる問題が起きる。せっかくなら swagger.json をアップロードしただけで、スパッとデプロイできるようにしたいよね!?

てことで今回はその方法をご紹介。

まずは面倒でも手動でセットせよ!

例えば CORS を有効にしたいといった場合、swagger 形式で上げた後に先ほどのメニューより CORS の有効化を押せば勝手に設定を変えてくれる。これはこれで手軽で便利である。その他諸々 統合リクエスト/統合レスポンスの設定をまずは完璧にセットしてデプロイし、動作を確認しよう。

んでここがキモなんだけど、これでOK!となったら、[ステージ] -> [エクスポート] -> [Swagger + API Gateway 拡張でエクスポート] を選択しよう。ここでエクスポートした json が、あなたにとって理想の JSON 形式のファイルとなる。

そこには、例えば x-amazon-apigateway-integration といったキーの JSON が追加で埋め込まれている!

つまり、書き出した swagger.json を、x-amazon-apigateway-integration などが入った JSON に再度 データを突っ込めば、API Gateway でインポートした時に全部が定義された完璧な状態を一発でアップロードできるようになるのである!

てことでここは単純な Ruby プログラムの時間である。 Rake タスクとして定義しておくのが良いだろう。

desc 'Swagger ドキュメントに AWS 関連情報を付加'
namespace :swagger do
  task :compile_aws_doc do |task, args|

    json_data = open(json_file_path) do |io|
      JSON.load(io)
    end

    json_data['paths'].each do |path, methods|
      methods.each do |method, content|
        # API Gateway settings
        content['x-amazon-apigateway-integration'] = {}
        #....
     end
  end
end

これがうまくできれば、ポチッと rake タスクを実行すれば良いことになる。

rake swagger:compile_aws_doc

一発で完璧な API を生成できた時の気持ち良さはたまらんね。

終わりに

API Gateway のメリットを知った上で、サクッとSwagger 定義に則った API Gateway の API を作ってみてはいかがだろうか。

Swagger 形式にまで出力できれば、 API Gateway に載せることはそこまで難しくはない。ぜひ挑戦してみよう。

AWS Lambda をローカルで実行できる環境を作る方法

ども、@kimihom です。

AWS のサービスの中でダントツに気に入ってる AWS Lambda 。コードの実行自体はテストで実行する回数分なんてたいしたことないからリモートでやってもいいんだけど、毎回Zipにして固めてアップロードするのがめんどくさいってのがある。

てな訳でローカルで実行できる環境を用意しておくと、開発時には何かと便利。今回は Node.js の場合のコードを記す。

index.handler を呼び出す

結局は シンプルな JavaScript ファイルを実行するだけなので、ローカルで実行するのも非常に簡単だ。

test.js とかを index.js と同じディレクトリ内に入れて、こんな感じのコードを書けばいいだけだ。

var event = {
    "Records": [ { "s3": { "bucket": { "name": "mybucket" }, "object": { "key": "test.json" } } } ]
};

var context = {};
var callback = function(err, data) {
    return;
};

var myLambda = require('./index');
myLambda.handler(event, context, callback);

event ってのは例えば S3 をトリガにして起動するタイプだと、Records[{s3: ~}] みたいなデータが渡ってくるので、それをローカルのJSONで書いてあげればOKだ。これに関しては最初に一回実行してみて console.log で確認するのが一番早い。

context はあんまり使わないかもだけど、例えば Lambda ないでエラーが起きた時に、CloudWatch ログの名前をメールなり Slack なりに通知する際に context.functionNamecontext.logStreamName は使えるだろう。

そんなこんなで ローカルで node test.js を打てば実行できる環境ができあがった。

AWS Lambda 内で手動実行する方法

ローカルだと当然実行する環境が違うので、テストできないという場合も出てくるだろう。そんな場合は AWS Lambda コンソールないから手動で実行ができる。これも割と便利でよく使う機能だ。

まずは Actions -> Configure test event を選択し、テスト実行する際の JSON を定義する。これが通常の AWS Lambda に渡ってくる event の JSON そのものだ。

f:id:cevid_cpp:20160901214837p:plain

これで Save して Test すれば、AWS Lambda の環境で作ったコードを実行することができる。S3トリガの AWS Lambda ファンクションであっても、それの event を test event として定義すればいいことになる。

Zip デプロイはシェルにでも書いておこう

んでいよいよ完成で Lambda に反映だ!ってなった時は Zip 化して aws コマンドを呼ぶことになる。簡単なシェルを書いておけばコマンド一発でデプロイできる。

aws lambda update-function-code --function-name myFunc --zip-file fileb://zipped.zip

zip で固める処理と、アップデート終わった後に zip を削除するシェルも書いておけばより楽になるだろう。

終わりに

AWS Lambda では以下の記事でも注意点など紹介している。AWS Lambda は本当に便利なので、まだ触ったことがない方がいればぜひチェックしておきたいところ。 Azure でも GCP , Bluemixでも同等の技術があるらしいので、それぞれのプラットフォームで調べてみるといいだろう。やはり便利だからどこのクラウドサービスでも提供しているということだろう。

www.bokukoko.info

Enjoy Coding!

Rails を学んだ後に学びたいオススメ技術

ども、@kimihom です。

たまには Rails 初心者向けのコンテンツでも書こうと思い立った。 Rails を本なりなんなりで勉強した後、どういった技術を学ぶべきなのか。私のオススメする順番でご紹介していこう。

対象者

主にWeb アプリを作りたいと思っている Rails エンジニア。今回は スマホアプリとか フロントエンドの話はしないことにする。Bundler を扱えて、Rails の AssetPipeline, ActiveRecord 周りはそれなりに理解したけど、そのあとどうするのっていうくらいな方。

Heroku

Rails ならこれというくらいの定番。 最近では Rails の本にでも Heroku が登場することが多くなってきた。 Heroku を使えば、ローカルで開発した Rails アプリを公開して運用することができる。せっかく Rails でアプリ作れるようになったら、そのグレートなサービスをみんなに見てもらいたいところ。Heroku を使えば 5分もかからずに公開することができてしまう。 Heroku は Rubyist を Hero にしてくれるのだ。

メールをどうする? ログやアラートの管理は?HTTPS にするには? そういった Web の共通の課題は全て Heroku アドオンが解決してくれる。私たちはポチッと使うアドオンをクリックするだけで便利な機能を利用することが可能だ。 Chef? Ansible? Capistrano? そんなこと全く知らなくて OK。そんな所にコードを書く時間があったら、サービスの改善に時間を使おう。Heroku と その周辺のアドオンがインフラのあらゆる悩みを解決してくれるのだ。

Heroku はアメリカにサーバーがあるからレイテンシが〜。そんな声は無視しよう。実際に使えばわかるが、Heroku を使って遅いと感じることはほとんどない。Rails 初学者に必要なのは、自分たちの作りたいサービスがどんどん出来ていくその過程と、その実現のしやすさだ。それを実感していく中でどうやったら Heroku x Rails で速くするかは自ずと学んでいくことだろう。

AWS の一部

AWS は最近どんどん新しいサービスを出してきているが、Heroku を使っていれば知らなければならないことはそんなに多くない。私がオススメする以下の AWS を最低限マスターしておくと良いだろう。

  • S3。画像などのファイルのアップロードや閲覧などの静的ファイルを置く場所として有用。
  • Route 53。ドメイン周りの管理をしてくれる。独自ドメインを運用していきたい時に有用。
  • Lambda。 Rails の一部のコードを外に切り出してレスポンスを早めたり、定期的に処理をさせたい時に有用。
  • Cognito。 Web や スマホアプリからAWSサービスにアクセスする際に有用。
  • IAM。 AWS の権限周りの管理。

このくらいだ。他は類似した Heroku のアドオンを使ったほうが手っ取り早いし安上がりに済む場合が多い。他の AWS サービスは概要を知っておくっくらいで、後に回して良いだろう。

Redis

Redis を"キャッシュストアだ"と思うだけでは Redis の本当の素晴らしさを分かっていない。Redis は非常に柔軟な KVS でいてとても高速だ。Redis は PostgreSQL などの RDB では実現が難しいような痒い所に手が届く素晴らしい技術である。

例えば、一時的にデータを持っておきたいんだけど一定時間後に削除していいようなデータがあったとしよう。RDBでは定期的に削除するバッチ処理などを実装しなければならないが、Redis であればタイムアウトを設定することで勝手に消えてくれる。Key に自由な値を設けることで、RDB さながらの実装を Redis だけで実現できてしまう。この詳細については Redis の本を学ぶとイメージがつきやすいだろう。

Redis入門 インメモリKVSによる高速データ管理 : Josiah L. Carlson, 長尾高弘 : 本 : Amazon.co.jp

Heroku Redis として標準のアドオンとして提供されているので、 KVS であれば Redis という選択は賢い選択と言えるだろう。

Elasticsearch

たいていのサービスでは"検索"機能を実現したいということになるだろう。 Heroku Postgres では日本語の全文検索をサポートしていないので、 Elasitcsearch を使う決断をすることにいずれなる。Rails と Elasitcsearch を簡単に扱えるような Gem があるので、概要をさらっとドキュメントを読んで Sense を使いながらデータの出し入れと検索ができるようになればひとまずは OK 。日本語のドキュメントが全然なくて最初は苦戦するかもしれないが、Elasticsearch に限って言えば実際にインストールして使ってみながら改善していくスタイルでいいと思う。 Elasticsearch 周りの話はこのブログでもたまに取り扱ってるので気になる方はタグの Elasticsearch からざっと眺めてもらえれば幸いだ。

Elasticsearch 自体は Heroku のアドオンとして豊富に取り扱っているので好きに選べば良い。AWS でもいいけど。

終わりに

一気に書いていったが、まず一番大事なのは基本であると改めて書いておきたい。Rails より HTML/CSS/JavaScript のフロントエンド。フロントエンドよりも HTTP やCookie、セッションなどの基本的な概念だ。というのも Web の性質 をしっかりと理解しないと Rails コードにおいて簡単にレールを踏み外して、最高にカオスなコードを書き散らかす初学者が後を絶たないからだ。Rails を学んだらもう一度 HTTPの基本から始まり、 Rails の ActiveRecord, ActionView, ActionController などをしっかりと理解しておきたいところ。テストコードが自然に書けるようになれれば、 Rails エンジニアと名乗ることができよう。

今回紹介した技術はその基礎があって初めて応用できる分野とも言える。基礎を吹っ飛ばしていきなりこういう技術に手を出すのもいいけど、結局 Rails の基礎が足りないといつか気づいて後戻りするだけだから、ルートとしてはどちらでもいいのかもしれない。

エンジニアを成長させる一番の材料は「好奇心」だ。あの技術はどれを使ったら実現できるのだろう?というアンテナを常にめぐらせ、それに飛び込む。そうした思いっ切りの良さこそがいいエンジニアになるための条件である。是非とも好奇心のあるエンジニアの仲間になって世界を変えるサービスを作って欲しい。同じ思いを持つ私からの僅かながらのメッセージである。

AWS Lambda の Node.js で連続で外部APIを叩く作法

ども、@kimihom です。

今回は AWS Lambda における Node.js のコードの書き方について。

実装したいこと

例えば、id を複数持った配列があるとして、その配列を 一個一個 HTTP リクエストで叩きたい、ということがあるだろう。id単位でしかリソースを削除できないような API があった場合などは必ずそんな場面に出くわす。

これを AWS Lambda で実装するには、どうすれば良いだろうか。ここに AWS Lambda の落とし穴が潜んでいる。何も考えずに書くとこんな感じになるだろう。

var request = require('request');

exports.handler = function(event, context) {
  var host = "https://api.awesomeapp.com";
  var ids = [1,2,3,4];
  ids.forEach(function(val) {
    request.delete({
      uri: host + "/records/" + id,
      json: true
    }, function(err, response) {
      context.done(); //?
    });
  });
};

これだと当然、期待した動作にならないのはわかるだろう。 AWS Lambda は、最後まで処理を終えた時点で context.done() を呼ばなければならず、これだと処理の途中で呼ばれてしまう。じゃあと言って、コールバック地獄の Node.js コードを書いてしまったら負けだ。非同期前提の JavaScript で頭を悩ますことになる。さて、どうすべきか。

Async を使う

AWS Lambda のドキュメントにもあるが、Async を使ってこの問題を解消させよう。最初は特殊な書き方に手こずるかもしれないが、慣れればなんとかなる。

今回は、 async.mapSeries を利用してこの問題を解決する。

var request = require('request');
var async = require('async');

exports.handler = function(event, context) {
  var host = "https://api.awesomeapp.com";
  var ids = [1,2,3,4];

  async.mapSeries(ids, function(id, callback) {
    request.delete({
      uri: host + "/records/" + id,
      json: true
    }, callback);
  }, function(err, res) {
    context.done();
  });
};

mapSeriesは第一引数に連続処理させたいアイテムを指定する。そんで第二引数の function の id にそれぞれが渡るようになっている。 function の中で callback を非同期に呼ぶような処理を書き、すべての ids の配列ループが終わったら、 第三引数の function が呼ばれる。

このように書けば、すっきりと連続した 外部 API のリクエスト処理を AWS Lambda で動かすことができる!

あと たいていの Node.js ライブラリは、 コールバック引数に (error, response) の順番で入ってくることを意識しよう。この慣習を守ることで、 Async の処理をより簡潔に記述することができる。上記コードの callback もその仕組みを利用して省略して書くことができている。より冗長的に書くと、以下のように書くことも可能である。

    request.delete({
      uri: host + "/records/" + id,
      json: true
    },  function(err, res) {
        callback(err, res);
    });

async には、mapSeries の他に、シンプルなコールバック地獄を防ぐための waterfall も用意されている。 AWS Lambda を Node.js で扱うには、これらのメソッドを使いこなし、すべての非同期処理が完全に終わったタイミングで、 context.done() を呼ばなければならない。Asyncにはその他様々なコールバック操作の仕組みが用意されている。詳しくはドキュメントを参照していただきたい。

終わりに

普段 Node.js をあまり書かない人にとって AWS Lambda を使いこなすには、 Async のような非同期処理のライブラリの知識が必須とも言える。私も普段は Ruby でプログラムを書いているし、Webフロントエンドの JavaScript ではあまり複雑に非同期処理を書いたりしないため、なかなか慣れずに苦労した。本記事が AWS Lambda で Node.js を扱う方にとって有益な記事になれば幸いだ。

Amazon API Gateway Importer を使って Rails x Grape から API を生成する

ども、@kimihom です。割とマニアックな記事。

以前書いた Grape Swagger で Amazon API Gateway 連携 の記事では、 grape-swagger から Amazon API Gateway に乗せるまでの手順を書いた。

しかし、このままだと 結局 Amazon API Gateway 側で Integration Request, Integration Response を1つずつ定義しなければならず、開発中にAPIが変わるたびにデプロイするのが大変だった。そこで、今回は Amazon API Gateway Importer の中にある機能をフル活用して、API Gateway 側でほとんど設定することなしにデプロイできる環境を作ろうと思う。

前準備

さて、API Gateway 側の Integaration~ 系の設定方法のサンプルはaws-apigateway-importer/tst/resources/swagger/apigateway.jsonにある。これを参考にして設定していこう。

すでに ruby-swagger を使って、doc/swagger/swagger.json にファイルがある前提だ。

このswagger.json をプログラムで読み直して、API Gateway 情報を付与した新しい swagger.json を作ろう。そうすれば、API Gateway の Web コンソール上での設定はほとんどしなくて済むようになる。 lib/tasksの rake ファイルでタスクをちょろっと追加してJSONをいじるサンプルが書く。

desc 'Swagger ドキュメントに AWS 関連情報を付加'
namespace :swagger do
  task :compile_aws_doc => :environment do

    json_file_path = 'doc/swagger/swagger.json'

    json_data = open(json_file_path) do |io|
      JSON.load(io)
    end
    json_data['paths'].each do |path, methods|
      methods.each do |method, content|
        #header settings
        # response settings
        %w{200 201 400 401 403 404 500}.each do |rc|
          next unless content['responses'][rc]
          content['responses'][rc]['headers'] = {}
          content['responses'][rc]['headers']['Access-Control-Allow-Origin'] = {}
          content['responses'][rc]['headers']['Access-Control-Allow-Origin']['type'] = 'string'
        end
        # API Gateway settings
        content['x-amazon-apigateway-integration'] = {}
        content['x-amazon-apigateway-integration']['type'] = 'http'
        content['x-amazon-apigateway-integration']['uri'] = "https://#{host}/#{json_data['info']['version']}#{path}"
        content['x-amazon-apigateway-integration']['httpMethod'] = method.upcase
        content['x-amazon-apigateway-integration']['responses'] = {}
        #...
      end
    end

    open(json_file_path, 'w') do |io|
      JSON.dump(json_data, io)
    end
  end
end

これはもうサンプルを見ながらだらだらと書いていけばいいだけだ。残りは省略するが、適宜 Amazon API Gateway 内で設定が必要な項目を書いていけばOK。基本的にWeb 上で設定する情報は全てコード化することができる。

作成した rake タスクを実行すれば、 Amazon API Gateway で設定する情報を付与した swagger.json ファイルが出来上がる。これを例の Amazon API Gateway Importer を使って API Gateway にインポートしよう。

CORS の設定

Swagger UI Rails を使っていると、API Gateway 連携したAPIとやりとりしてサンプルを動かしたくなる。ここで Swagger UI は Ajax を使っているので、基本的には同じドメインのパスからでしかAjaxで取得することはできない。しかしCORSをしっかりと設定してあげれば、Amazon API Gateway に移した URL も Ajax で操作することができるようになる。

CORSを設定するにはどうしても Web 上で設定する必要があった。Resources の左のツリーのメソッドではなくリソースをクリックすると、Enable CORS というボタンが上部に出現する。これをクリックして、 OPTIONリクエストを生成する。するとエラーになる。んで、OPTIONリクエストのMethod Response に 200 を追加してあげて(面倒...) から再度 CORSを設定すると、腫れてAPI Gateway の CORS 設定が完了となる。

ちなみに全てのレスポンスステータスのCORSを有効にするには、各Method Resopnse のResponse HeadersにAccess-Control-Allow-Originを追加し、さらにIntegration Response の Header Mappings の Access-Control-Allow-Origin に '*' を設定してあげる必要がある。いうまでもなく、これら全てをWeb上で設定すると日が暮れてしまうので、これも先ほどの rake タスクで自動で入力された状態になるよう、swagger.json を作ってからインポートさせよう。

CORS を設定すると嬉しい事

CORS を設定してあげると、先ほどの Swagger UI にある Try it! を API Gateway で生成した URL に対して行う事ができるようになる。もちろん、どんなWebサイト上からでも Ajax で通信が可能になる事を意味する。

さらに、 Amazon API Gateway で生成した JavaScript SDK も同時に利用する事ができるようになる。ちょっとだけいじってみたが、割と簡単に API から情報を取ってこれた。

<html>
  <head>
    <script type="text/javascript" src="lib/axios/dist/axios.standalone.js"></script>
    <script type="text/javascript" src="lib/CryptoJS/rollups/hmac-sha256.js"></script>
    <script type="text/javascript" src="lib/CryptoJS/rollups/sha256.js"></script>
    <script type="text/javascript" src="lib/CryptoJS/components/hmac.js"></script>
    <script type="text/javascript" src="lib/CryptoJS/components/enc-base64.js"></script>
    <script type="text/javascript" src="lib/moment/moment.js"></script>
    <script type="text/javascript" src="lib/url-template/url-template.js"></script>
    <script type="text/javascript" src="lib/apiGatewayCore/sigV4Client.js"></script>
    <script type="text/javascript" src="lib/apiGatewayCore/apiGatewayClient.js"></script>
    <script type="text/javascript" src="lib/apiGatewayCore/simpleHttpClient.js"></script>
    <script type="text/javascript" src="lib/apiGatewayCore/utils.js"></script>
    <script type="text/javascript" src="apigClient.js"></script>

<script>

var apigClient = apigClientFactory.newClient();
var params = {
  'X-API-Token': 'your token',
  'per_page': 50,
  'page': 1
};
var body = { };
var additionalParams = {  };
apigClient.contactsGet(params, body, additionalParams)
    .then(function(result){
        console.log(result);
    }).catch( function(result){
        console.log(result);
    });
</script>
</head>
<body>
    Hello.
</body>
</html>

script タグ多すぎぃ って感じだが、実際のJSはほんの数行で JSON でデータを取ってこれる。まぁこれは jQueryなり何なりでせっせと書いてもいいけども。

同様に iOS, Android SDK も生成してくれるので、これらを利用して連携するアプリが簡単に実装できそうだ。

所感

自分で作った API を Amazon API Gateway に乗せるのはそれなりに苦労があるけども、それをやることで得られるメリット(SDK生成、アクセス解析、キャッシュなど) があるので、頑張って設定する価値はあると思う。

API Gateway で最も問題になるのは、Web上での設定のめんどくささである。 これを Amazon API Gateway Importer を利用して各 API を自動で設定できるよう、ちゃんとした Swagger JSON を書いていこう。

Grape Swagger で Amazon API Gateway 連携

ども、 @kimihomです。

前回は、Grape on Rails での開発初めまでを紹介した。 API のお話もこれで最後。Grape Swagger で作った Swagger ドキュメントを Amazon API Gateway 上で動くように動作させる。

swagger.json の取得

f:id:cevid_cpp:20151223004122p:plain

まず、今回は作成した Swagger 形式のドキュメントを、 JSON 形式で出力する必要がある。その出力した JSON ファイルを、aws-apigateway-importer を用いて Amazon API Gateway に取り込むようにするのである。

てことで調べてみると、 grape-swagger は 現状では Swagger 1.2 のみの対応で、Swagger 2.0 対応の予定は当分なさそうな雰囲気だった。そのため、Swagger 2.0 形式で出力してくれる、Ruby Swaggerを利用しよう。

bundle exec rake swagger:grape:generate_doc\[API::Root\]
bundle exec rake swagger:compile_doc

この2つを実行すると、doc/swagger.json ファイルを出力してくれる。

swagger.json の修正

このまま API Gateway Importer を使っても、うまく動かない。以下のことをする必要がある。

  • V1:: などのネームスペースで区切った::は使えないので削除する
  • 配列を返すSuccessful~ の項目をそれぞれユニークな名前にセット ~ArrayEntity。こうしないとAPI Gateway 側で任意の名前になってしまう。
  • swagger.json内にあるcontent ~ の項目をそれぞれユニークな名前にセット ~Param。同様に名前付けしないとAPI Gateway 側で変な名前がついてしまう

aws-apigateway-importer の利用

これでいよいよ API Gateway に swagger ファイルをインポートさせることができるようになる。

セットアップ
$ brew install Caskroom/cask/java
$ brew install maven
$ git clone https://github.com/awslabs/aws-apigateway-swagger-importer.git
$ cd aws-apigateway-swagger-importer/
$ mvn assembly:assembly

$ ./aws-api-import.sh --create /path/to/swagger.json

こうすると、見事 API Gateway 側で定義された メソッド一覧とモデル一覧が生成される。

API Gateway 側での設定

f:id:cevid_cpp:20160102184805p:plain

ここからは一個ずつ面倒な設定。。 ここら辺、 aws-apigateway-swagger-importer にもやり方はあるようなんだけどあまりにもドキュメントが少ないのでそれに手間暇かけるよりかは一個一個設定した方がいいと判断。

API Gateway のそれぞれのメソッドにアクセスすると、まずはendpoint url を設定するように言われる。全て入力していこう。

そうすると、 API Gateway のいつもの画面 "Method Request", "Integration Request", "Integration Response", "Method Response" の設定が始まる。この4つ最初はなんで"Method Request", "Integration Request みたいに分かれているのだろう?と疑問に思ったので、まず自分なりの解釈を説明する。

"意味不明なパラメータや REST じゃない、酷いAPIを自社サービスで持っている"ということを前提として考えていただきたい。この場合、まずAPI Gateway 側でセットした ちゃんとした受け取りパラメータの入り口 を用意する必要がある。これが"Method Request" である。そしてそこで受け取った綺麗なパラメータを酷いAPI 側で認識できるように変換してあげる必要がある。これが "Integration Request" である。 Response もそれと同様で、変換作業と綺麗なレスポンスを分けるために2つがある。

そんで、aws-apigateway-swagger-importer 側でやってくれるのは、"Method Request"と"Method Response"の2つのみ。あとは仕方ないので一個一個設定してあげる。特にURLやPATHパラメータ、Headerトークンを "Integration Request" の方にも設定してあげなければならない。そうしないとendpoint url にパラメータが飛んでこない。

諸々設定が完了したらとは Deploy API で公開しよう。見事 API の完成だ。それができれば、 API Gateway でできる他のことも見てみたりすると、Amazon API Gateway の便利さを知ることになるだろう。