misc.tech.notes

主に技術的な雑記的な

災害復旧供給状況マップというサービスを作りました

こんなものを作りました。

supply-map.willy.works

きっかけ

お察しの通り?北海道胆振東部地震がきっかけです。

私自身、北海道の札幌市に住んでいるのですが、幸か不幸か地震発生当時は出張に行っていました。私の家は札幌市内では比較的停電の復旧が遅かった地域で、朝一に家族の安否確認だけはできたのですが、その後奥さんの携帯電話の電池が切れてしまったり長時間の停電で携帯の電波塔も落ちてしまったのか電波の状態が悪くなったりして、しばらく連絡がつかない状況が続きました。札幌の各地の電気が復旧していく中、なかなか連絡がつかない状況はかなり不安にさせられました。

そして、なんとか連絡がつくようになり、幸いにも実家のほうが停電が比較的早く復旧していたので出張から帰ってきてそちらで合流しました。そして、翌朝には家の方も停電が解消していたので家に帰り、長時間の停電によって傷んでしまっている可能性が高いものを破棄して買い物に出たのです。

すると、食料品が全然売ってないのです。そもそも営業していない店も多く、営業している店でも生鮮食品が停電でダメになったため破棄され、残った保存食品に需要が集中して品薄・・・という状況です。

そんな中、色々な店を回ったのですが、店によって全然品揃えが違うんです。そもそも営業していない店、売れるものがなくほぼ開店休業状態の店、余った商品をかき集めて今は売れているけど次の入荷予定が立っていない店、独自の調達網があるのかある程度の生鮮食品を継続的に売れている店などなど・・・

半日を買い物だけに費やしました。それでも、確保できていないものもありましたし、何がどこにあるか分からないので念の為と無駄に買ってしまったものもありました。ずっと車で移動したのでガソリンもなくなってきてしまったものの、スタンドも営業している所が限られているので数時間待ち・・・

それでも私は良い方です。生鮮食品を継続的に売っている店を見つけたのですから(この店はもちろんサービスに登録してありますw)SNSなどを見ると生鮮食品が買えなくて困っている人の投稿も目立ちます。

あの時あれば良かったものを

震災発生直後に感じた無力感、2018年9月8日(土)に半日買い物して感じた徒労感、それらを解消するサービスを作れないだろうかと考えて作り始めました。

簡単にどんなサービスかと言うと、インフラの復旧状況や物資調達の可否などをマップ上で簡単に報告でき、それらを検索することができるサービスです。似たような誰かが作った簡単なサービスのようなものは他にもあったりしますが、特定の災害や特定の情報に特化していて使いにくかったり、日々変化する状況に対応するにはイマイチだったりするので、様々な情報をカテゴリ分けして色で状況が俯瞰的に把握でき、同じ位置に新しい情報が投稿されればそれに合わせて色を変える(古い情報は履歴を残しつつ上書きできる)、そのステータスやコメントの変化を追える、といったあたりを重視して作りました。また、位置情報で検索しながら表示するため、関係のない地域のデータを取得しないので、複数の災害地域で使用されてもクライアントに不要な負荷をかけたりはしない・・・はずです。

その後の週末で作ったので最低限ができて公開したのは翌日の2018年9月9日(日)の夕方です。その時には一部地域を除き全道的に停電も復旧しており、前日は休業した店舗も次第に開店し始めて状況は良い方に向かっていました。だから、きっとこのサービスはもうそれほど需要はないということはなんとなく分かっていました。

でも、まあ良いのです。このサービスはVue.jsとVuetify(最初はBootstrap)で作られ、Firebase Hostingでホストされ、Google Maps JavaScript APIを中核とし、Firebase Authenticationで認証し、データベースとしてFirestoreにクライアントが直接通信して動きます。いわゆる2-TierのServerless SPAです。

jp.vuejs.org

vuetifyjs.com

firebase.google.com

firebase.google.com

developers.google.com

Google Maps JavaScript API, Vuetify, Firestoreあたりは始めて使いました。それ以外も今までお遊び程度にしか使っていませんでした。普通にお勉強になりました。

余談ですが、私の得意なAWSではなくGCPのサービスを使ったのは、Google Maps JavaScript APIがまず必須だったのでそのサインアップをGoogle Cloudで行う必要があったことと、2-Tierで作ろうということは初めから決めていて、位置情報を扱うデータベースとしてDynamoDBを採用するには一工夫居ることを知っていて、それに対してFirestoreがいつの間にか位置情報を扱えるようになっていた(一応チェックだけはしてて、たしか公開時にはなかったはず)ため、開発効率的にもGCPのサービスを使うことにメリットが高そうと判断したからです。

aws.amazon.com

Supported data types  |  Firebase

そして、このサービスを構成するクラウドサービスはすべて従量課金で、使われなければほとんどお金はかかりません。お金がかかっている時は誰かの役に立っている時です。だから、そんなことは起きないでほしいですが、いつか誰かのためになるかもしれないのでこのまま公開し続けます。そして、気が向いた時や時間を見つけて改修します。何かリクエストや不具合を見つけたら教えてもらえると嬉しいです。

今回の地震で被災された皆様に、心よりお見舞い申し上げます。 まだ停電含め予断を許さない状況が続いていますが、一刻も早く平穏な日々が戻ってくることを祈っています。

DynamoDBデータモデリング虎の巻:第参巻 〜実践編〜

前巻のおさらい

前巻はDynamoDBのデータモデリングをするにあたって必要な考え方について、RDBとの対比を交えながら触れてきました。

marcy.hatenablog.com

今回は

この巻では、実際にどのようにデータモデリングしていけば良いのかといった実践的な内容を、いくつかの汎用的と思われるパターンを例にしつつ書き記していきたいと思います。

たぶん、今までの3巻の中で一番今までの巻を理解していないとピンと来ない内容だと思うので、まずは第壱巻第弐巻を読むことをオススメします。

Partition Keyの決め方

まずは、DynamoDBでテーブルを作る際に必ず必要な Partition Key の決め方です。前巻では以下のように書きましたが、じゃあ実際にその識別子はどうやって採番するのか?ということです。

ECサイトなら「ユーザ」「商品」「注文」といった特定の意味のある存在を表す実体です。これはMicroservices的な各サービスが担うべき特定の業務ドメイン領域にも多くの場合に合致します。そして、これら1つの実体は基本的にユーザIDや注文番号、SKUといった識別子を持っているものです。これをPartition Key とするのです。

では、実際にユーザ系の情報を扱うテーブルを例にとって解説していきたいと思います。

一意なIDの決め方

考えられる方法はいくつかありますが、極端に大別すると以下になるかと思います。

  • 連番
  • UUIDのような順序性の無いランダムな値

この2つでまず採用すべきでないものは「連番」です。DynamoDBで連番カウンターを作ることが厳しいことはカウンターアイテムのPartitionが偏ることを考えれば想像に難くないかと思いますが、カウンターを用いないMAX + 1を取る方法はもっと現実的ではありません。 Partition Key はHash Tableなデータ構造なので、B+Treeな Sort Key と違ってフルスキャンすることでしかMAXが取れませんし、MAXした結果が被らないようにロックを取ることもできません。

よって、基本的にUUIDのような衝突する可能性が極小となっているランダム値が良いです。ちなみに「Snowflake」もイマイチです。なぜなら、繰り返しになりますが Partition Key はHash Tableなデータ構造なのでフルスキャンを避けてソートできないからです。「Snowflake」が活きるかもしれない場としては Sort Key のSuffixに付けて生成順にしつつデータを分ける時くらいでしょうか。

Twitter IDs (snowflake) — Twitter Developers

ちなみに今回例にしたユーザのIDについては、Serverlessな実装だと認証とユーザのID管理にCognitoを使うケースも多いと思います。その場合はCognitoが採番するUser IDを使いましょう。Cognito User Poolの情報と紐付けられるのみならず、ユーザが自分の対象アイテムだけにアクセスできるようアクセス制御するのも簡単に実装できます。

docs.aws.amazon.com

Partition KeyだけでSort Keyが要らないパターン

今回の例からはズレますが、シンプルにKeyに対するValueだけが引ければ良いような場合は、プライマリキーを Partition Key のみとしてHash Tableだけでアクセスできるようにした方が高速にアクセスできます(NWレイテンシ等も考えると微々たるものですが)2つの要素の組み合わせで一意となる場合でも、それが常に組み合わせて一意な条件でしか使わないのなら Sort Key と組み合わせたプライマリキーにするよりも結合した値を Partition Key としてしまって良いです。後から別の条件で検索したくなったときに対応が困難になるので、完全に明確であるケースだけにはなりますが。

射影としてのGSIについて

実際に例を出して見ていく前に、GSI の射影について前巻でも触れましたが、もう少し詳しく触れておきたいと思います。

GSI がテーブルから射影する属性は下記のオプションを選択することができます。

  • KEYS_ONLY : テーブルのプライマリキーの値、およびインデックスキーの値のみで構成される
  • INCLUDE : KEYS_ONLY + 選択した非キー属性のみ含まれる
  • ALL : 全ての属性が含まれる

docs.aws.amazon.com

このうち、どれを選択すべきかは、どのようなデータアクセスが多いかによって決まります。まず、GSI に含める属性が増えれば増えるほどインデックスの容量と更新コストが増大します。 GSI の更新は実テーブルの更新に対して自動的に非同期で適用されますが、その際に使用されるキャパシティユニットが増大するということです。もし含めなかったとしても、最低限含まれるプライマリキーによって実テーブルからデータが都度フェッチされるため見かけ上のオペレーションとしての差異はありません

では、どのような場合にインデックスに含める属性を増やすかというと、そのインデックスに対してクエリを行う際に頻繁に取得する属性がある場合です。インデックスに含まれている属性は実テーブルからデータをフェッチする必要がなくなり、特定のPartition内だけで読み込みが完了するため読み込み効率が上がります。いわゆる、MySQLで言う所の Covering IndexPostgreSQLで言う所の Index Only Scan です。そのようなクエリが効果的かつ頻繁に想定される場合、更新コストや容量の効率および課金と天秤にかけて KEYS_ONLY 以外を選択します。

ちなみにここから先は実際のデータを模した二次元表が出てきますが、 GSI には暗黙的にプライマリキーが含まれていると思って見てください。

Sort Keyの決め方と汎用的なGSI

前巻で書いた「できる限りテーブルを少なくする」というものは、同じように GSI にも適用されます。 前巻でも述べたように、 GSI は元となるテーブルに対する射影として実体を持つ別のテーブルと言えるものであり、独立したキャパシティの管理が必要となります。つまり、多ければ多いほど管理コストが増します。そして何より、 GSI1テーブルあたり5つまでという制限を超えないよう気を使わなくてはならず、これは「できる限りテーブルを少なくする」という方針と対立します。だから、 GSI の使い方はとても重要なのです。

Partition Keyある実体に対する一意な識別子と比較的シンプルに決めることができますが、 Sort Key についてはその実体に対するアクセスパターンを十分に踏まえる必要があります。とはいえ、ある程度順序立てて決めていくことが可能です。

まず、前巻で述べたようにある実体に対して必要なデータを列挙して一つの Sort Key に押し込めた状態から考えます。

f:id:FumblePerson:20180804020600p:plain

そして、前巻であった、この中で更新頻度やタイミングが明らかに異なる状態系のデータなどを分離するとした場合にはこのようになります(実際に全部でこれしか属性が無かったら別に分けるほどでもないんですけどねw)

f:id:FumblePerson:20180804020730p:plain

次に、この中で name で検索をしたい要求があったとしましょう。素直に name をキーとする GSI とすることもできますが、そのように次々と要求が増える度に GSI を定義していけばあっという間に増えて5つという制限を使い切ります。そこで、以下のようにデータの種別を表すプライマリの Sort Keyname という値を持つアイテムを追加します。そして、プライマリの Sort Key である keyPartition Key とし、その検索対象となる値を入れるための valueSort Key とする GSI を作成します。

f:id:FumblePerson:20180804230628p:plain

key = "name" AND value = "Terui" のように検索できる GSI になりました。そして、ついでに status でも検索できるようになっています。つまり、同様に email でも検索する要求もあるのであれば、同じようにアイテムを作ることで対応可能となるのです。

f:id:FumblePerson:20180804230731p:plain

Sort Key とすることで、検索だけではなく「登録日時」のように最新(降順ソートの先頭)や範囲検索した一覧を取りたいような場面でも使えます。

f:id:FumblePerson:20180804230827p:plain

GSIのシャーディング

一つのテーブルに対して汎用的に検索できる GSI が出来ましたが、この GSI には弱点があります。それは、検索項目毎にPartitionが偏ってしまっていることです。この GSI に対する検索トラフィック1Partitionの上限を超える可能性がある場合、 Sharding を検討する必要があります。

ちなみに1Partitionの物理的な上限は 3000RCU, 1000WCU, Index Size 10GB です。これを超えるユニットやサイズを確保した場合はPartitionが分割されます。その場合は分割されたPartitionに対して確保したユニットが均等に割り振られることに注意が必要です。特定のPartitionにアクセスが集中してスロットリングが発生した場合に、物理的な上限を超えない範囲で他のPartitionに割り振られた余剰分を回す機能(Adaptive Capacity)も持っていますが、スロットリングが実際に発生しないと発動しないので、基本的に安全を見積もっておく必要があります。現在のPartition数を計算する計算式は下記資料のp21, 22に書いています。

www.slideshare.net

この Sharding をどうやるかというと、計算可能なSuffixを付与する方法があります。例えば、「Unicodeのコードポイント化して積算した数値を200で割った余りに1を足す」などです。

Pythonで雑に書くとこんな感じの計算

import functools

functools.reduce(lambda x,y:x*y, [ord(x) for x in 'Terui']) % 200 + 1 # => 161

あえて剰余を取っているのは Partition Key の数を固定するためです。 Partition Key の数が不定になる計算だと分散度合いは高くなりますが、範囲検索や集計を取りたいような場合にテーブル全体をフルスキャンする必要が出てしまうからです。数が分かっていればその数だけのインデックスをスキャンするだけに収めることができるので、それだけでもコストは大幅に違います。逆に言うと範囲検索や集計を取ったりインデックス全体に対するオペレーションが必要が全く無ければ不定でも良いです。

それでも現実の名前の分布が均一ではない*1以上ある程度の偏りは想定されますが、一般的なシステムではその程度分散してくれれば十分です。むしろ、その程度の計算で済まないレベルで分散が必要なら、範囲検索や集計を捨てられるなら値そのもの(より大きなサイズを取り得る属性ならハッシュ値)を Partition Key に含めた別の GSI を用意するか、別のテーブルに DynamoDB Streams から連携するべきでしょう。

f:id:FumblePerson:20180804230945p:plain

docs.aws.amazon.com

本筋とはズレますが公式の日本語翻訳の "注文 ID の文字の UTF-8 コードポイント値を乗算して、それを 200 で割って + 1 するなど" は間違ってますね。剰余を取らないと固定数Shardingできない・・・(英語はちゃんと"modulo 200"って書いてるので合ってます。Feedback送っておきましたw)あと、いくらユニークな別の属性と併せて頻繁にクエリすることがあると言っても別の属性を使って計算するのは他の場面で全く使えないので制約強すぎて厳しいと思うんですよね・・・たしかにその方が偏らないけど・・・

nameemail のように、一つ実体につき一つである属性を意図的に分散させるケースとは違い、そもそも複数の要素を持ち得る属性であるため分散させざるをえない場合もあります。例えば「好きな食べ物 (favorite_foods)」のような任意に複数個設定できるものです。これを検索可能とする場合は少々工夫が必要で、計算可能な固定範囲のSuffixをプライマリの Sort Key および GSIPartition Key としてしまうと、プライマリの Sort Key衝突する可能性があります。そのため、範囲検索や集計を捨てられるなら、値そのものや大きなサイズを取り得る属性ならハッシュ値なSuffixを付けるのが無難です。もし、範囲検索や集計が必要なら別途取り得る値の範囲が固定で計算可能なSuffixを持つ属性を別途追加してそれを Partition Key とした GSI を持たせます。

f:id:FumblePerson:20180804234442p:plain

次巻に向けて

今回は例を出しながら実際にテーブル設計を行う際の基本的なアプローチやポイントに触れてきました。書いていると「あ、あのパターンも必要かな」とかけっこうあったけど汎用性の低いものは今回は触れないようにしたので、次巻はもう少しピンポイントな問題を解決するアドバンスドなパターンの中から、わりとよく使うものを紹介する感じになるかもしれません。その前にもうちょっと触れておいた方が良いこともあるような気もするので、そのあたり思いついたらそっちになるかも。

*1:日本は佐藤さん、鈴木さんが多い的な

DynamoDBデータモデリング虎の巻:第弐巻 〜考え方編〜

前巻のおさらい

前巻はDynamoDBのデータモデリングをする前に知っておいた方が良いDynamoDB自体の仕組みやデータ構造のお話でした。

marcy.hatenablog.com

今回は

今回はデータモデリングを行う際に必要なマインドセット、つまり「考え方」について書き記したいと思います。非常によく聞かれる「RDBとの考え方の違い」といった切り口で進めていきたいと思います。

RDBとはアプローチが真逆

RDBのデータモデリングをする場合、まず正規化されたデータのスキーマを決めることから始めると思います。慣れてくると律儀に第一正規化から始めずにいきなり第三正規形あたりから設計しだすことも多いと思います(私もそうです)

そして、データのスキーマが決まってからそれに対してどのようにアクセスするか(=SQL)をアプリケーションを設計する際に考えていくというのがRDBでの一般的なアプローチではないかと思います。

これ、DynamoDBでは全く逆になります

DynamoDBのデータモデリングでは、まず先にデータにどのようにアクセスするかを考えます。そして、それに合わせたデータモデルを作るのです。つまり、データモデルを設計してからアプリケーションの設計を行うことはDynamoDBではしません。アプリケーションとデータモデルは同時に設計するのです。

これは、 スキーマレス なDynamoDBだからこそできることです。アプリケーションの仕様が変わってもスキーマを変更するコストは極小で済みます。というか、スキーマが無いんだから変更コストも何も無いと言えばそうなんですがw

かといって、既存のデータを変換する必要があるような変更をしてしまうとそれは大きなコストがかかります。それを避けるために大枠として抑えておくべきポイントもあるので、そのあたりにも触れていきたいと思います。

テーブルの数を少なくする

RDBでは正しく正規化していくとテーブルの数は基本的に増えていきます。同じ実体に対して論理的に分かれたテーブル間は リレーションシップ という概念とそれに付随した 外部キー制約 という整合性を守る仕組みによって整合性が取られます。 外部キー制約 使ってますか?使わないとRDBでも整合性は守られないですからね??(煽り芸)

しかし、DynamoDBには リレーションシップ という概念はありません。だから、極論テーブルを分けても別に良いことなんて無いのです。

テーブルが少なくなると何が良いかというと、キャパシティの最適化ができます。アクセス頻度が少ないテーブルと多いテーブルに分かれているとそれぞれのキャパシティの管理が難しくなります。特にAutoScaleはキャパシティが少ないと効果的に動作しません。*1テーブルを少なく保つことでキャパシティを合計で考えることができ、AutoScaleも効果的に使えるようになります。

テーブルが少ないと負荷の集中が気になりますか?前巻の内容をよく思い出してみてください。Partitionが分散さえすればテーブルを分けなくても負荷は分散するのです。

そして、DynamoDBは スキーマレス です。 スキーマレス であるということは、データにListやMapといったデータ構造を持たせられることでも、後から属性を自由に追加・変更・削除できるということでもありません。プライマリキーの型さえ合っていれば同じテーブルになんでも突っ込めるのです。

DynamoDBの公式ドキュメントにあるベストプラクティス集にはこう書いてあります。

DynamoDB アプリケーションではできるだけ少ないテーブルを維持する必要があります。設計が優れたアプリケーションでは、必要なテーブルは 1 つのみです。

docs.aws.amazon.com

さすがにアプリケーションにつき1つはプライマリキーの型を合わせないといけないことも考えるとやりすぎ感もあるし(全部文字列にしちゃえばできないことはないんですが)、Serverlessである程度の規模以上のシステムを設計していくとMicroservices的なサービス分割は切っても切れない関係になるので、サービス間で同じテーブルを共有するのはなんとも気持ちが悪い部分があります。

そこで、私は少なくとも1つの実体について1つのテーブルに収めるようにしています。この「1つの実体」というのはECサイトなら「ユーザ」「商品」「注文」といった特定の意味のある存在を表す実体です。これはMicroservices的な各サービスが担うべき特定の業務ドメイン領域にも多くの場合に合致します。そして、これら1つの実体は基本的にユーザIDや注文番号、SKUといった識別子を持っているものです。これをPartition Key とするのです。

トランザクションが無くても1アイテムの更新はアトミック

トランザクションとは何のためにあるのでしょうか?RDBトランザクションが守るのは主にデータの 原子性(Atomicity)独立性(Isolation) です。*2それが問題になるのは複数のレコードを更新する一連の処理における話です。単一のレコード(DynamoDB的にはアイテム)を更新する処理についてはDynamoDBでも 原子性 が保証されています。また、1アイテムしか更新しない処理は当然独立しています。

つまり、ListもMapも扱えるDynamoDBではある実体に関連するデータを全て一つのアイテムに押し込めることができ、それを更新している限りは原子性と独立性が崩れることはないということです。さらに言えば、一貫性(Consistency) もデータ間の関連性がそのアイテムに適切に詰め込まれていれば 条件付き書き込み によって担保することができます。400KBという1アイテムのサイズ制限に気を使う必要はありますが、そうそう超えるものじゃありません。

docs.aws.amazon.com

docs.aws.amazon.com

とはいえ完全に一つに突っ込んでしまうとキャパシティの無駄遣いや性能面での懸念もあります。それをどう分けるかという面でもまたデータアクセスが重要となってくるのです。例えば、あるユーザの基本的な「名前」や「住所」といった「情報」と、「ログイン中」などといった「状態」は明らかに更新タイミングが違うはずです。こういったデータは分割することでキャパシティ効率がよくなります。更新タイミングが違うのであれば1回の操作につき1つの更新になるため以下同文です。

そして、同じ実体に対する Partition Key のデータを分割するならどうするか?そこで Sort Key の出番というわけです。

user_id (PK) data_key (SK) attrs
aaa info 〜〜〜
aaa status 〜〜〜
bbb info 〜〜〜
bbb status 〜〜〜

それでも整合性の担保ができないものもある

それは、トランザクションの代表例とも言える口座間取引のような実体として複数に跨って整合性を取る必要のある処理です。全く別の実体である複数の口座を一つのアイテムにすることは現実的ではありません。この場合は、諦めてRDBを使う、まず必ず担保すべき「移動元の口座の残高が0を下回らないこと」だけを条件付き書き込みで担保してそれ以外は結果整合性を受け入れる、頑張ってトランザクションを再実装する、といったちょっとつらめの対策になります。

f:id:FumblePerson:20180802001414p:plain

www.slideshare.net

ただし、私はServerlessでもRDBを使うことは全く問題ないと考えています。Serverless(Lambda)とRDBの相性が悪いのはコネクションモデルと現状避けられないVPC内で起動するLambda FunctionのENI生成オーバヘッドによるものです。つまり、オンラインで使おうとするから悪いのです。これらは、トランザクションが必要な処理をKinesisを挟んだバックグラウンド処理に置き換えることで、コネクション数をコントロールすると共に一定数が常時起動となることによってオーバヘッドも避けることが可能です。そして、RDBによるトランザクションの成功を以てDynamoDBのデータも更新して、オンラインの参照はそちらを見るようにすれば良いのです。結果整合さえ受け入れられればやり方はいくらでもあります。

このように、DynamoDBでも限定的ではありますがACIDを守る方法はあります。「DynamoDBはBASEで結果整合性だからACIDは捨てなきゃいけない」とか言っちゃうなんちゃってアーキテクトが如何に厚顔無恥か分かりますね!(煽り芸)

書き込むデータと読み込むデータは別で良い

RDBは正規化することで特定の事実を示すレコードを一箇所とし、データの整合性を守ります。これは、 リレーションシップ外部キー制約 という整合性を守る仕組みと、必要なら JOIN をすることでその一箇所にしかない事実を的確に取得できる仕組みに裏打ちされたものです。そして、DynamoDBは JOIN ができません。また、前述したようにテーブルは極力少なくしなくてはなりません。

そんな リレーションシップJOIN も無いDynamoDBで、どのように整合性を守りつつ読み込みの効率化をすれば良いのでしょうか?その答えが、読み込みと書き込みの分離です。

CQRS

CQRS (Command Query Responsibility Segregation) という考え方があります。Command(更新操作)とQuery(データ読み込み)を責任分界を行い、データそのものも分けてしまおうという考え方です。 書き込みに都合が良いデータ読み込みに都合が良いデータ は異なります。両者を分離し、前者を更新するイベントをイベントソースに積み上げて、それを以て後者を更新するのが CQRS の一つの形です。

martinfowler.com

postd.cc

書き込みに都合が良いデータ というのは前述したように1度のオペレーションで更新されるべきデータが一つのアイテムに詰まったものです。対して、 読み込みに都合が良いデータ はそうではありません。様々な属性が詰まったアイテムに対して検索に使用する全ての属性に GSI を張っていてはあっという間に GSI の5つという制限を使い切ってしまいます。そこで、 書き込みに都合が良いデータ が更新されたことをトリガーとして、 読み込みに都合が良いデータ を生成するというわけです。

非常に都合の良いことに、DynamoDBにはそれを実現するためにとてもマッチする DynamoDB Streams という仕組みを持っています。DynamoDBのあるテーブルに対する更新操作が流れてくるStreamです。ここに流れてきた更新情報を元に、 読み取りに都合が良いデータ を生成するコマンドを発行していくというわけです。

f:id:FumblePerson:20180801234414p:plain

データに対する整合性は属性の詰まったアイテムを更新することで書き込み時に担保されているので、それを元に更新している限りは整合性が崩れることはありません。もちろん、非同期更新となるので結果整合にはなりますが。

また、オンラインの書き込みのオペレーションとしては一度で済むためとても処理がシンプルになります。読み込み用のデータを生成する処理は一つ一つが独立したバックグラウンドで行われる処理となるため、仮に失敗してもリトライすれば良い話です。もしこれがオンラインで全て同期でやるとしたらハンドリングしたくないですが、適切に分解することで一つ一つの処理はシンプルに保つことができます。

そして、読み込みのためのデータを生成する処理が分かれていることにより、要求の変化によってデータ構造を変えないといけないといったことを避けることができます。今あるデータや処理を変えずに新しい要求に合わせたデータを生成する処理を追加するだけで良いのです。

次巻に向けて

次巻は「じゃあ、 読み込みに都合の良いデータ ってなんだ」ということで、次こそ具体的なデータ構造とインデックスの使い方に入っていきたいと思います。

次巻

書きました

marcy.hatenablog.com

*1:極端な例を出すと1000RCUの90%閾値でAutoScaleにすると900でスケールしてくれますが、10だと9になるまでスケールしません。残り1しかない。。。

*2:独立性についてはRDBもPhantom Readを妥協する等していますが

DynamoDBデータモデリング虎の巻:第壱巻 〜前提知識編〜

動機など

最近、Serverlessの文脈からDynamoDBのテーブル設計の相談を受けることが多くなってきていて、Podcastでも話したけどけっこう図とかが無いと説明しづらい領域なので、まとまった資料がほしいなということでまとめてみる。

cloudinfra.audio

どう考えても長編大作エントリ不可避なので気力が続けば第二巻以降に続きます…!(フィードバックが多いと頑張れるかも…!)

本巻の対象と前提知識

本巻はDynamoDBのデータモデリングにスコープを絞っています。DynamoDBおよびデータベースの一般用語などについての説明は省きます。 前提知識としては以下のようなものになるかと思います。

  • DynamoDBのサービスとしての概要や用語( WCU , RCU , GSI , LSI など)を知っている
  • Hash TableやB-Tree(B+Tree)といったデータ構造がどんなものか分かる
  • RDBで第3以上の正規化されたデータモデリングができる
  • ACIDがそれぞれ何か分かっている

DynamoDBの概要と少し踏み込んだユースケースなどを知るにはこちらのAWS Black Belt Online Seminarの資料がオススメです。

www.slideshare.net

正規化やACIDについてはおググりください。

本巻を読む必要がない方

最近(2018年の5月頃だったかな?)DynamoDBのデータモデリングをする上でとても有益なドキュメントが公式に公開されています。これを全て(かなりのボリュームがありますが)何の疑問もなく読めて理解できてしまう方はきっとこの虎の巻を読む必要はありません。本巻でもかなり引用させてもらう予定です。

docs.aws.amazon.com

DynamoDBはどんなデータベースか

実際にDynamoDBのデータモデリングの話に入る前に、DynamoDBが サービスではなくデータベースとして どのようなものであるかを知っておくと色々な場面で理解の助けになります。

DynamoDBはNoSQLの中で強いて分類するなら分散KVSという種類のデータベースになります。Keyによってデータが分散配置されるデータベースです。KVSと一口に言っても本当にKeyによる完全一致で1つずつしかデータを更新・取得できないものもあれば、内部的にインデックスを構築することで範囲検索や前方一致検索、全文検索などができるものもあります。

DynamoDBにおけるデータの物理構成

DynamoDBの物理的なデータ配置に関する有名なモデルとしてはクォーラムなどの仕組みを利用した分散同期と合意アルゴリズムが挙げられますが、これは内部的な話であり読み込みクォーラムを増やすことで強い整合性のある読み込みができる場合とできない場合があることを抑えておけば良いです。より深く知りたい方は論文を読んでみることをオススメします。

Amazon's Dynamo - All Things Distributed

それよりもデータモデリングにおいて大事なのが、データの物理的な分散配置を決める Partition Key による分散方式です。この Partition Keyハッシュ関数にかけた結果によってデータの配置が決まります。逆に言うと、この Partition Key がなければデータがどのPartition(イメージとしてはDynamoDBのクラスタを構成するデータノード)にあるかと特定することができません。そのため、DynamoDBにおいて Partition Key はテーブルに必須のものであり、フルスキャンを除いた検索クエリにおいては必ず Partition Key を指定する必要があるのです。

f:id:FumblePerson:20180731185233p:plain

docs.aws.amazon.com

そして、DynamoDBテーブルの定義には前述の Partition Key と共に Sort Key というキーとの組み合わせをプライマリキー(主キー)とすることができます。この Sort KeyPartition Key によって配置されたPartitionの中にB+Treeインデックス *1 として構築されます。DynamoDBで Sort Key だけのインデックス探索ができないのは、Partition毎にインデックスが構成されているためなのです。

B+木 - Wikipedia

では、何故わざわざSort Keyだけで検索できた方が便利であるはずなのにPartition毎に構成する必要があったのでしょうか?それは(おそらく)性能を優先した結果です。 Sort Key を共有のインデックス空間に配置してしまうとあらゆるデータのインデックスがそこに構築されてしまい、RDBでは扱いにくい膨大なデータ量やハイトラフィックを扱うためのデータベースとしては致命的なボトルネックになりえます。また、 Partition Key で配置が決まるということは、Partitionの偏りを許容して Partition Key を全て同じ値にしてしまえば一つのインデックスとして構築できるわけで、分散を基本とすることは合理的であると言えます。

DynamoDBにおけるインデックス「GSI」と「LSI

DynamoDBをよく分からずにテーブル設計をすると、KVSというイメージからか一意となるメインのキーだけに焦点を当てがちになる印象があります。しかし、DynamoDBを真に使いこなすためにはこの GSI (Global Secondary Index) と LSI (Local Secondary Index)特に GSI の使い方こそが肝であると言えます。

LSI

LSI は、プライマリキーと Partition Key が同じで Sort Key を別の属性としたキーです。Partition Key が同じためプライマリキーと同じ物理配置となり、 WCURCUGSI のように独立せずテーブル自体のキャパシティユニットを消費します。また、物理配置が同じであるため LSI の更新はデータの更新に対して同期的に行われ、LSI に対するクエリは 強い整合性 を選ぶことができます *2。この LSI はプライマリキーと同様にテーブル作成時にしか指定できず、後から追加や変更することはできません。そのため、 LSI の使い道は比較的限られています。

Partition Key が同じ、後から変更不可ということで、RDBで言う所の主キーと第2キーだけが異なるような一部の候補キーのようなものがあればそれに付けることができる他、 プライマリキーとは違い重複可能であることを利用して、例えば掲示板やチャットシステムであるようなスレッドIDを Partition Key 、その中のコメントIDを Sort Key とするような明確な親子関係を表現したテーブルにおいて子属性であるコメントの作成・更新日時などといった重複する可能性があるがソートに使用するようなキーとして使用できます。

thread_id (Main-PK, LSI-PK) comment_id (Main-SK) created_at (LSI-SK)
aaa ccc 2018-07-30 00:00:00
aaa ddd 2018-07-31 00:00:00
bbb eee 2018-07-30 10:00:00

GSI

GSI の使い方こそがDynamoDBの真髄です。GSILSI と違い、プライマリキーのようにユニークである必要もなければ、 LSI のように Partition Key を同じくする必要もありません。その代り、 GSI はメインテーブルに対する射影として実体を持つ別のテーブルであるということを念頭に入れる必要があります。射影って言われてピンときますか?射影ってRDBでも大事な概念である集合論における重要要素ですからね??(煽り芸)

射影 (集合論) - Wikipedia

slideship.com

GSI は超ざっくり言うとテーブルを別の角度から切り出したソート済みのデータの部分複製です。

https://image.slidesharecdn.com/mysql-160125081954/95/mysql-62-638.jpg?cb=1454483169

雑なMySQLパフォーマンスチューニング from yoku0825

つまり、射影として実体を持つ別のテーブルであるため、通常のテーブルと同じように独立したキャパシティを持ち Partition Key で分散した中で Sort Key によってインデックスが構築されています。セカンダリインデックスではありますが、その実体も分散しているのです。

また、 GSI は別のテーブルとして実体を持つため、本体のテーブルへの更新は非同期で反映されます。つまり、 GSI に対するクエリは結果整合性しか選ぶことができません。 結果整合性 と聞くと「ウッ」とか「大丈夫かな?」ってなる方が多い印象がありますが、MySQLのリードレプリカだって結果整合性なわけで、検索用である GSI の主な用途を考えるとほとんどがMySQLならリードレプリカに投げるようなクエリになるはずで、多くの場面で問題は無いといえるはずです。

分散したインデックスにおいて考慮すべきこと

(プライマリキーも含む)インデックスがPartitionによって分割されているということは、別々のPartitionに対してアクセスが分散されれば高い並列処理性能が出せるということです。これをまず念頭に置きましょう。それと同時に、あえて偏りを許容することで検索に都合の良いインデックスを構成するために、Partition当たりの処理性能を踏まえる必要があります。Partition当たりの処理性能は2018-07-31現在、 3000RCU および 1000WCU です。これを超えるとスロットリングが発生して処理がエラーとなります。逆に言えば、これを超えないならあえて偏りを許容することもできるのです。

docs.aws.amazon.com

次巻に向けて

SQLのような柔軟なクエリ言語を持たず、「フルスキャンしたら負け」と言われるDynamoDBにおいてテーブルの検索を効率的に行うにはこれらのインデックスの構成が非常に重要です。次回は実際のデータモデリングを行う上での考え方から入り、インデックスの設計まで踏み込めるといいなあ(願望)

次巻

書きました

marcy.hatenablog.com

*1:B-TreeではなくB+Treeであるという記述はDynamoDBの論文などでも見つけることはできません。でもSort Keyとか別名Range KeyとかいうくらいだからSortとRangeに強いB+Treeじゃないと説得力無いよね?

*2:あくまでPartition内では同期であるということなので、Partitionが複製されるDynamoDBにおいては読み込みクォーラムの数を増やさないと強い整合性を担保できないためオプションです

2018年の抱負とか

31歳になりました。30台になると、自分がおっさんであること、もうどうひっくり返っても若手とは言い難いことを自覚しますね。それ以外には特に何かが変わるわけでもないけど、自分の周りの活躍してる人達っていうのは大体30半ばくらいからの方が多くて、きっとその年代はあっという間なので、去年みたいに年始に受けた精神的なダメージを年の半ばまで引きずってウジウジしてると後悔することになるので、今年も同じダメージを既に受けかけてますがもうそれはそれとして自分の中で切り離してやっていける心の強さは身についた気がするので、今年はスタートから勢い良くいきたいと思います。

2017年の振り返りと2018年の抱負

仕事

そんな感じで、去年は年間通しても良いパフォーマンスは出せていなかった。さすがに仕事で迷惑をかけたりはしなかったけれど、もっとできることは全然一杯あったと思うので今年はもっと色々結果で示していければと。とりあえず、今やってるやつを春先くらいに上手いこと世に送り出す。色々面倒なこともあるけども、界隈の力関係をひっくり返したい気持ちで頑張りたい。

技術

去年はあまり新しい領域へチャレンジできなかった。反面、メインの領域であるクラウドとかその辺については案件の多い会社でいくつか濃いやつに関わったりそれ以外も近い位置で見られたこと、サービス開発では自分の思うように開発をさせてもらったことで、今まで微妙に繋がりきっていなかった部分が繋がって自分の中で上手く理論が組み上がった感じがしていて、元々得意だったWeb系やServerlessとかその辺については大体何が来ても大丈夫と思える程度には仕上がったと思う。この領域では「インテグレーターの中では一番」と言い張れるように引き続き頑張る。

marcy.hatenablog.com

で、それはそれとしてプライベートで新しい領域にチャレンジすることは全然できていないので、そっちは反省して頑張らないといけない。去年Rustをちょっと書いたらクソコードを生産したり想像以上に苦労してしまったりしたので、ゆるふわなスクリプト言語以外で一つまともに書けるようになりたい。それこそRustとか。ダメ人間なので特に具体的な目的もなくとにかく勉強するということができないので、Rustとかで書くことが妥当であると思われるような何かほしいものを見つけて一個ちゃんと完成させたい感じ。ミドルウェアとか作ってみたいけど、インテグレーターだとそれを実戦投入するの無理ゲーなんだよなぁ・・・なんか良いのないだろうか。

あと、両極端なんだけどミドルウェアと同じように自分で作れていなくてカバーしきれてないフロントエンドの方もNode.jsには抵抗なくなったので、自分でちゃんと何か作りたい。これは仕事で作った方が良いよなと思っている所があるので勉強してサクッと作れるようになって実戦投入したい。したいというか、これはMustでこれすらもできないようならホントダメだと思うのでやる。

まとめると得意領域が上手く組み上がった実感があるので今年は得意じゃない領域をしっかりやっていきたい。

その他

丸一年リモートワークして色々課題(基本的にダメ人間である私個人の問題)が見えてきていて、とりあえずは対策の一つとしてずっと自宅に居ることが良くないので今年はコワーキングとか契約して外で仕事する時間を増やす。

あと、会社で受けるストレスがなくても外での刺激が必要だと分かったので、来年も東京のカンファレンスで話せるように引き続き頑張りつつ、それだけではもたないので、相変わらず札幌で面白そうな集まりは全然ないので、去年は全然できなかった自分でやる活動を頑張る。

去年は社費でre:Inventに行かせてもらって、ハッカソンで技術的にはある程度頑張れるけど英語が壊滅的と分かったので、英語もやらないと。あと、今年もre:Invent行きたいし、他のカンファレンスとかも行きたいけど、会社だけに期待するのも違うと思うので自費で行ける程度に稼ぎたいのでお仕事ください。 (一応、個人事業もまだやってます。有名無実じゃなく一応仕事は完全に無くなってはないんだけど、普段東京に居ないので話があっても次に会うのがいつか分からないから中々上手く仕事に繋がらないというのも去年の学び・・・)

AWS LambdaのOS情報を色々見てみる 〜Lambdaのインスタンスガチャを検証する〜

この記事は、Serverless Advent Calendar 20日目の記事です。

qiita.com

もっと他のネタを書く予定だったのですが、年内タスクが沢山積んでてヤバいので去年わりと好評だったLambdaのリバースエンジニアリング?をまたやってみたいと思います。

marcy.hatenablog.com

OS情報の集め方

Lambdaの実体がコンテナであることは周知の事実なので、OS情報というには語弊がある部分もあるのですが、コンテナからもホストOSの情報が部分的には見られるので他に良い呼び方も思いつかなかったんで、とりあえずOS情報と呼称します。

OSの情報を集めるのは、ChefのOhaiしかりServerspec(Specinfra)のHost Inventoryしかり、古今東西泥臭くコマンドを叩いて集めると相場は決まっています。余談ですが、ホントこういうのをやってくれるライブラリは偉大ですよね・・・!

なので、こんなFunctionをServerless Frameworkでデプロイします。

import subprocess


def osdata(event, context):
    return dict([(cmd, subprocess.getoutput(cmd).split('\n')) for cmd in event])

serverless.yml はこんな感じ。簡単ですね!

service: lambda-osdata

provider:
  name: aws
  runtime: python3.6

functions:
  osdata:
    handler: handler.osdata

これをこんな感じで思いついたコマンドを列挙するyamlを用意して

- env
- uptime
- uname -a
- df -aTh
- cat /etc/system-release
- cat /proc/cpuinfo
- cat /proc/meminfo
- ps auxf
- id
- cat /etc/passwd
- ulimit -a
- /sbin/ip a
- /sbin/ip r
- netstat -av

このように叩くと結果がJSON Objectで得られます。

$ sls invoke -f osdata -p data.yml

結果①

{
    "env": [
        "AWS_LAMBDA_FUNCTION_VERSION=$LATEST",
        "AWS_SESSION_TOKEN=xxxxxxx",
        "AWS_LAMBDA_LOG_GROUP_NAME=/aws/lambda/lambda-metadata-dev-metadata",
        "LAMBDA_TASK_ROOT=/var/task",
        "LD_LIBRARY_PATH=/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib",
        "AWS_LAMBDA_LOG_STREAM_NAME=2017/12/20/[$LATEST]0a28fe46a1de4e5c8258fe00ee63cd9c",
        "AWS_EXECUTION_ENV=AWS_Lambda_python3.6",
        "AWS_XRAY_DAEMON_ADDRESS=169.254.79.2:2000",
        "AWS_LAMBDA_FUNCTION_NAME=lambda-metadata-dev-metadata",
        "PATH=/var/lang/bin:/usr/local/bin:/usr/bin/:/bin",
        "AWS_DEFAULT_REGION=us-east-1",
        "PWD=/var/task",
        "AWS_SECRET_ACCESS_KEY=xxxxxxxx",
        "LAMBDA_RUNTIME_DIR=/var/runtime",
        "LANG=en_US.UTF-8",
        "AWS_REGION=us-east-1",
        "TZ=:UTC",
        "AWS_ACCESS_KEY_ID=xxxxxxx",
        "SHLVL=1",
        "_AWS_XRAY_DAEMON_ADDRESS=169.254.79.2",
        "_AWS_XRAY_DAEMON_PORT=2000",
        "PYTHONPATH=/var/runtime",
        "_X_AMZN_TRACE_ID=Root=1-5a3a99f7-0b93ba80006cbb7149758be4;Parent=556e4185655d4a61;Sampled=0",
        "AWS_SECURITY_TOKEN=xxxxxxx",
        "AWS_XRAY_CONTEXT_MISSING=LOG_ERROR",
        "_HANDLER=handler.metadata",
        "AWS_LAMBDA_FUNCTION_MEMORY_SIZE=1024",
        "_=/usr/bin/env"
    ],
    "uptime": [
        " 17:12:23 up  2:04,  0 users,  load average: 0.00, 0.00, 0.00"
    ],
    "uname -a": [
        "Linux ip-10-39-53-71 4.9.62-21.56.amzn1.x86_64 #1 SMP Thu Nov 16 05:37:08 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux"
    ],
    "df -aTh": [
        "Filesystem     Type  Size  Used Avail Use% Mounted on",
        "/dev/xvda1     ext4   30G  3.1G   27G  11% /",
        "/dev/xvda1     ext4   30G  3.1G   27G  11% /var/task",
        "/dev/xvda1     ext4   30G  3.1G   27G  11% /dev",
        "/dev/loop2     ext4  526M  440K  514M   1% /tmp",
        "none           proc     0     0     0    - /proc",
        "/dev/xvda1     ext4   30G  3.1G   27G  11% /proc/sys/kernel/random/boot_id",
        "/dev/xvda1     ext4   30G  3.1G   27G  11% /var/runtime",
        "/dev/xvda1     ext4   30G  3.1G   27G  11% /var/lang"
    ],
    "cat /etc/system-release": [
        "Amazon Linux AMI release 2017.03"
    ],
    "cat /proc/cpuinfo": [
        "processor\t: 0",
        "vendor_id\t: GenuineIntel",
        "cpu family\t: 6",
        "model\t\t: 63",
        "model name\t: Intel(R) Xeon(R) CPU E5-2666 v3 @ 2.90GHz",
        "stepping\t: 2",
        "microcode\t: 0x3b",
        "cpu MHz\t\t: 2899.875",
        "cache size\t: 25600 KB",
        "physical id\t: 0",
        "siblings\t: 2",
        "core id\t\t: 0",
        "cpu cores\t: 1",
        "apicid\t\t: 0",
        "initial apicid\t: 0",
        "fpu\t\t: yes",
        "fpu_exception\t: yes",
        "cpuid level\t: 13",
        "wp\t\t: yes",
        "flags\t\t: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology eagerfpu pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm fsgsbase bmi1 avx2 smep bmi2 erms invpcid xsaveopt",
        "bugs\t\t:",
        "bogomips\t: 5800.07",
        "clflush size\t: 64",
        "cache_alignment\t: 64",
        "address sizes\t: 46 bits physical, 48 bits virtual",
        "power management:",
        "",
        "processor\t: 1",
        "vendor_id\t: GenuineIntel",
        "cpu family\t: 6",
        "model\t\t: 63",
        "model name\t: Intel(R) Xeon(R) CPU E5-2666 v3 @ 2.90GHz",
        "stepping\t: 2",
        "microcode\t: 0x3b",
        "cpu MHz\t\t: 2899.875",
        "cache size\t: 25600 KB",
        "physical id\t: 0",
        "siblings\t: 2",
        "core id\t\t: 0",
        "cpu cores\t: 1",
        "apicid\t\t: 1",
        "initial apicid\t: 1",
        "fpu\t\t: yes",
        "fpu_exception\t: yes",
        "cpuid level\t: 13",
        "wp\t\t: yes",
        "flags\t\t: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx pdpe1gb rdtscp lm constant_tsc rep_good nopl xtopology eagerfpu pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand hypervisor lahf_lm abm fsgsbase bmi1 avx2 smep bmi2 erms invpcid xsaveopt",
        "bugs\t\t:",
        "bogomips\t: 5800.07",
        "clflush size\t: 64",
        "cache_alignment\t: 64",
        "address sizes\t: 46 bits physical, 48 bits virtual",
        "power management:",
        ""
    ],
    "cat /proc/meminfo": [
        "MemTotal:        3855844 kB",
        "MemFree:         3328804 kB",
        "MemAvailable:    3558848 kB",
        "Buffers:           27780 kB",
        "Cached:           322744 kB",
        "SwapCached:            0 kB",
        "Active:           238224 kB",
        "Inactive:         188640 kB",
        "Active(anon):      76260 kB",
        "Inactive(anon):      128 kB",
        "Active(file):     161964 kB",
        "Inactive(file):   188512 kB",
        "Unevictable:           0 kB",
        "Mlocked:               0 kB",
        "SwapTotal:             0 kB",
        "SwapFree:              0 kB",
        "Dirty:                 0 kB",
        "Writeback:             0 kB",
        "AnonPages:         76180 kB",
        "Mapped:            30912 kB",
        "Shmem:               140 kB",
        "Slab:              72332 kB",
        "SReclaimable:      36756 kB",
        "SUnreclaim:        35576 kB",
        "KernelStack:        2284 kB",
        "PageTables:         2904 kB",
        "NFS_Unstable:          0 kB",
        "Bounce:                0 kB",
        "WritebackTmp:          0 kB",
        "CommitLimit:     1927920 kB",
        "Committed_AS:     413576 kB",
        "VmallocTotal:   34359738367 kB",
        "VmallocUsed:           0 kB",
        "VmallocChunk:          0 kB",
        "AnonHugePages:         0 kB",
        "ShmemHugePages:        0 kB",
        "ShmemPmdMapped:        0 kB",
        "HugePages_Total:       0",
        "HugePages_Free:        0",
        "HugePages_Rsvd:        0",
        "HugePages_Surp:        0",
        "Hugepagesize:       2048 kB",
        "DirectMap4k:       47104 kB",
        "DirectMap2M:     1787904 kB",
        "DirectMap1G:     2097152 kB"
    ],
    "ps auxf": [
        "USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND",
        "475          1  0.0  0.4 170508 18996 ?        Ss   16:02   0:00 /var/lang/bin/python3.6 /var/runtime/awslambda/bootstrap.py",
        "475        597  0.0  0.0 117184  2352 ?        R    17:12   0:00 ps auxf"
    ],
    "id": [
        "uid=475(sbx_user1072) gid=474 groups=474"
    ],
    "cat /etc/passwd": [
        "root:x:0:0:root:/root:/bin/bash",
        "bin:x:1:1:bin:/bin:/sbin/nologin",
        "daemon:x:2:2:daemon:/sbin:/sbin/nologin",
        "adm:x:3:4:adm:/var/adm:/sbin/nologin",
        "lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin",
        "sync:x:5:0:sync:/sbin:/bin/sync",
        "shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown",
        "halt:x:7:0:halt:/sbin:/sbin/halt",
        "mail:x:8:12:mail:/var/spool/mail:/sbin/nologin",
        "uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin",
        "operator:x:11:0:operator:/root:/sbin/nologin",
        "games:x:12:100:games:/usr/games:/sbin/nologin",
        "gopher:x:13:30:gopher:/var/gopher:/sbin/nologin",
        "ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin",
        "nobody:x:99:99:Nobody:/:/sbin/nologin",
        "rpc:x:32:32:Rpcbind Daemon:/var/cache/rpcbind:/sbin/nologin",
        "ntp:x:38:38::/etc/ntp:/sbin/nologin",
        "saslauth:x:499:76:\"Saslauthd user\":/var/empty/saslauth:/sbin/nologin",
        "mailnull:x:47:47::/var/spool/mqueue:/sbin/nologin",
        "smmsp:x:51:51::/var/spool/mqueue:/sbin/nologin",
        "rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin",
        "nfsnobody:x:65534:65534:Anonymous NFS User:/var/lib/nfs:/sbin/nologin",
        "sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin",
        "dbus:x:81:81:System message bus:/:/sbin/nologin",
        "ec2-user:x:500:500:EC2 Default User:/home/ec2-user:/bin/bash",
        "slicer:x:498:497::/tmp:/sbin/nologin",
        "sb_logger:x:497:496::/tmp:/sbin/nologin",
        "sbx_user1051:x:496:495::/home/sbx_user1051:/sbin/nologin",
        "sbx_user1052:x:495:494::/home/sbx_user1052:/sbin/nologin",
        "sbx_user1053:x:494:493::/home/sbx_user1053:/sbin/nologin",
        "sbx_user1054:x:493:492::/home/sbx_user1054:/sbin/nologin",
        "sbx_user1055:x:492:491::/home/sbx_user1055:/sbin/nologin",
        "sbx_user1056:x:491:490::/home/sbx_user1056:/sbin/nologin",
        "sbx_user1057:x:490:489::/home/sbx_user1057:/sbin/nologin",
        "sbx_user1058:x:489:488::/home/sbx_user1058:/sbin/nologin",
        "sbx_user1059:x:488:487::/home/sbx_user1059:/sbin/nologin",
        "sbx_user1060:x:487:486::/home/sbx_user1060:/sbin/nologin",
        "sbx_user1061:x:486:485::/home/sbx_user1061:/sbin/nologin",
        "sbx_user1062:x:485:484::/home/sbx_user1062:/sbin/nologin",
        "sbx_user1063:x:484:483::/home/sbx_user1063:/sbin/nologin",
        "sbx_user1064:x:483:482::/home/sbx_user1064:/sbin/nologin",
        "sbx_user1065:x:482:481::/home/sbx_user1065:/sbin/nologin",
        "sbx_user1066:x:481:480::/home/sbx_user1066:/sbin/nologin",
        "sbx_user1067:x:480:479::/home/sbx_user1067:/sbin/nologin",
        "sbx_user1068:x:479:478::/home/sbx_user1068:/sbin/nologin",
        "sbx_user1069:x:478:477::/home/sbx_user1069:/sbin/nologin",
        "sbx_user1070:x:477:476::/home/sbx_user1070:/sbin/nologin",
        "sbx_user1071:x:476:475::/home/sbx_user1071:/sbin/nologin",
        "sbx_user1072:x:475:474::/home/sbx_user1072:/sbin/nologin",
        "sbx_user1073:x:474:473::/home/sbx_user1073:/sbin/nologin",
        "sbx_user1074:x:473:472::/home/sbx_user1074:/sbin/nologin",
        "sbx_user1075:x:472:471::/home/sbx_user1075:/sbin/nologin",
        "sbx_user1076:x:471:470::/home/sbx_user1076:/sbin/nologin",
        "sbx_user1077:x:470:469::/home/sbx_user1077:/sbin/nologin",
        "sbx_user1078:x:469:468::/home/sbx_user1078:/sbin/nologin",
        "sbx_user1079:x:468:467::/home/sbx_user1079:/sbin/nologin",
        "sbx_user1080:x:467:466::/home/sbx_user1080:/sbin/nologin",
        "sbx_user1081:x:466:465::/home/sbx_user1081:/sbin/nologin",
        "sbx_user1082:x:465:464::/home/sbx_user1082:/sbin/nologin",
        "sbx_user1083:x:464:463::/home/sbx_user1083:/sbin/nologin",
        "sbx_user1084:x:463:462::/home/sbx_user1084:/sbin/nologin",
        "sbx_user1085:x:462:461::/home/sbx_user1085:/sbin/nologin",
        "sbx_user1086:x:461:460::/home/sbx_user1086:/sbin/nologin",
        "sbx_user1087:x:460:459::/home/sbx_user1087:/sbin/nologin",
        "sbx_user1088:x:459:458::/home/sbx_user1088:/sbin/nologin",
        "sbx_user1089:x:458:457::/home/sbx_user1089:/sbin/nologin",
        "sbx_user1090:x:457:456::/home/sbx_user1090:/sbin/nologin",
        "sbx_user1091:x:456:455::/home/sbx_user1091:/sbin/nologin",
        "sbx_user1092:x:455:454::/home/sbx_user1092:/sbin/nologin",
        "sbx_user1093:x:454:453::/home/sbx_user1093:/sbin/nologin",
        "sbx_user1094:x:453:452::/home/sbx_user1094:/sbin/nologin",
        "sbx_user1095:x:452:451::/home/sbx_user1095:/sbin/nologin",
        "sbx_user1096:x:451:450::/home/sbx_user1096:/sbin/nologin",
        "sbx_user1097:x:450:449::/home/sbx_user1097:/sbin/nologin",
        "sbx_user1098:x:449:448::/home/sbx_user1098:/sbin/nologin",
        "sbx_user1099:x:448:447::/home/sbx_user1099:/sbin/nologin",
        "sbx_user1100:x:447:446::/home/sbx_user1100:/sbin/nologin",
        "sbx_user1101:x:446:445::/home/sbx_user1101:/sbin/nologin",
        "sbx_user1102:x:445:444::/home/sbx_user1102:/sbin/nologin",
        "sbx_user1103:x:444:443::/home/sbx_user1103:/sbin/nologin",
        "sbx_user1104:x:443:442::/home/sbx_user1104:/sbin/nologin",
        "sbx_user1105:x:442:441::/home/sbx_user1105:/sbin/nologin",
        "sbx_user1106:x:441:440::/home/sbx_user1106:/sbin/nologin",
        "sbx_user1107:x:440:439::/home/sbx_user1107:/sbin/nologin",
        "sbx_user1108:x:439:438::/home/sbx_user1108:/sbin/nologin",
        "sbx_user1109:x:438:437::/home/sbx_user1109:/sbin/nologin",
        "sbx_user1110:x:437:436::/home/sbx_user1110:/sbin/nologin",
        "sbx_user1111:x:436:435::/home/sbx_user1111:/sbin/nologin",
        "sbx_user1112:x:435:434::/home/sbx_user1112:/sbin/nologin",
        "sbx_user1113:x:434:433::/home/sbx_user1113:/sbin/nologin",
        "sbx_user1114:x:433:432::/home/sbx_user1114:/sbin/nologin",
        "sbx_user1115:x:432:431::/home/sbx_user1115:/sbin/nologin",
        "sbx_user1116:x:431:430::/home/sbx_user1116:/sbin/nologin",
        "sbx_user1117:x:430:429::/home/sbx_user1117:/sbin/nologin",
        "sbx_user1118:x:429:428::/home/sbx_user1118:/sbin/nologin",
        "sbx_user1119:x:428:427::/home/sbx_user1119:/sbin/nologin",
        "sbx_user1120:x:427:426::/home/sbx_user1120:/sbin/nologin",
        "sbx_user1121:x:426:425::/home/sbx_user1121:/sbin/nologin",
        "sbx_user1122:x:425:424::/home/sbx_user1122:/sbin/nologin",
        "sbx_user1123:x:424:423::/home/sbx_user1123:/sbin/nologin",
        "sbx_user1124:x:423:422::/home/sbx_user1124:/sbin/nologin",
        "sbx_user1125:x:422:421::/home/sbx_user1125:/sbin/nologin",
        "sbx_user1126:x:421:420::/home/sbx_user1126:/sbin/nologin",
        "sbx_user1127:x:420:419::/home/sbx_user1127:/sbin/nologin",
        "sbx_user1128:x:419:418::/home/sbx_user1128:/sbin/nologin",
        "sbx_user1129:x:418:417::/home/sbx_user1129:/sbin/nologin",
        "sbx_user1130:x:417:416::/home/sbx_user1130:/sbin/nologin",
        "sbx_user1131:x:416:415::/home/sbx_user1131:/sbin/nologin",
        "sbx_user1132:x:415:414::/home/sbx_user1132:/sbin/nologin",
        "sbx_user1133:x:414:413::/home/sbx_user1133:/sbin/nologin",
        "sbx_user1134:x:413:412::/home/sbx_user1134:/sbin/nologin",
        "sbx_user1135:x:412:411::/home/sbx_user1135:/sbin/nologin",
        "sbx_user1136:x:411:410::/home/sbx_user1136:/sbin/nologin",
        "sbx_user1137:x:410:409::/home/sbx_user1137:/sbin/nologin",
        "sbx_user1138:x:409:408::/home/sbx_user1138:/sbin/nologin",
        "sbx_user1139:x:408:407::/home/sbx_user1139:/sbin/nologin",
        "sbx_user1140:x:407:406::/home/sbx_user1140:/sbin/nologin",
        "sbx_user1141:x:406:405::/home/sbx_user1141:/sbin/nologin",
        "sbx_user1142:x:405:404::/home/sbx_user1142:/sbin/nologin",
        "sbx_user1143:x:404:403::/home/sbx_user1143:/sbin/nologin",
        "sbx_user1144:x:403:402::/home/sbx_user1144:/sbin/nologin",
        "sbx_user1145:x:402:401::/home/sbx_user1145:/sbin/nologin",
        "sbx_user1146:x:401:400::/home/sbx_user1146:/sbin/nologin",
        "sbx_user1147:x:400:399::/home/sbx_user1147:/sbin/nologin",
        "sbx_user1148:x:399:398::/home/sbx_user1148:/sbin/nologin",
        "sbx_user1149:x:398:397::/home/sbx_user1149:/sbin/nologin",
        "sbx_user1150:x:397:396::/home/sbx_user1150:/sbin/nologin",
        "sbx_user1151:x:396:395::/home/sbx_user1151:/sbin/nologin",
        "sbx_user1152:x:395:394::/home/sbx_user1152:/sbin/nologin",
        "sbx_user1153:x:394:393::/home/sbx_user1153:/sbin/nologin",
        "sbx_user1154:x:393:392::/home/sbx_user1154:/sbin/nologin",
        "sbx_user1155:x:392:391::/home/sbx_user1155:/sbin/nologin",
        "sbx_user1156:x:391:390::/home/sbx_user1156:/sbin/nologin",
        "sbx_user1157:x:390:389::/home/sbx_user1157:/sbin/nologin",
        "sbx_user1158:x:389:388::/home/sbx_user1158:/sbin/nologin",
        "sbx_user1159:x:388:387::/home/sbx_user1159:/sbin/nologin",
        "sbx_user1160:x:387:386::/home/sbx_user1160:/sbin/nologin",
        "sbx_user1161:x:386:385::/home/sbx_user1161:/sbin/nologin",
        "sbx_user1162:x:385:384::/home/sbx_user1162:/sbin/nologin",
        "sbx_user1163:x:384:383::/home/sbx_user1163:/sbin/nologin",
        "sbx_user1164:x:383:382::/home/sbx_user1164:/sbin/nologin",
        "sbx_user1165:x:382:381::/home/sbx_user1165:/sbin/nologin",
        "sbx_user1166:x:381:380::/home/sbx_user1166:/sbin/nologin",
        "sbx_user1167:x:380:379::/home/sbx_user1167:/sbin/nologin",
        "sbx_user1168:x:379:378::/home/sbx_user1168:/sbin/nologin",
        "sbx_user1169:x:378:377::/home/sbx_user1169:/sbin/nologin",
        "sbx_user1170:x:377:376::/home/sbx_user1170:/sbin/nologin",
        "sbx_user1171:x:376:375::/home/sbx_user1171:/sbin/nologin",
        "sbx_user1172:x:375:374::/home/sbx_user1172:/sbin/nologin",
        "sbx_user1173:x:374:373::/home/sbx_user1173:/sbin/nologin",
        "sbx_user1174:x:373:372::/home/sbx_user1174:/sbin/nologin",
        "sbx_user1175:x:372:371::/home/sbx_user1175:/sbin/nologin",
        "sbx_user1176:x:371:370::/home/sbx_user1176:/sbin/nologin"
    ],
    "ulimit -a": [
        "core file size          (blocks, -c) unlimited",
        "data seg size           (kbytes, -d) unlimited",
        "scheduling priority             (-e) 0",
        "file size               (blocks, -f) unlimited",
        "pending signals                 (-i) 14992",
        "max locked memory       (kbytes, -l) 64",
        "max memory size         (kbytes, -m) unlimited",
        "open files                      (-n) 1024",
        "pipe size            (512 bytes, -p) 8",
        "POSIX message queues     (bytes, -q) 819200",
        "real-time priority              (-r) 0",
        "stack size              (kbytes, -s) 8192",
        "cpu time               (seconds, -t) unlimited",
        "max user processes              (-u) 1024",
        "virtual memory          (kbytes, -v) unlimited",
        "file locks                      (-x) unlimited"
    ],
    "/sbin/ip a": [
        "1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1",
        "    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00",
        "    inet 127.0.0.1/8 scope host lo",
        "       valid_lft forever preferred_lft forever",
        "33: vinternal_11@if34: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000",
        "    link/ether ca:6e:10:ca:95:3c brd ff:ff:ff:ff:ff:ff link-netnsid 0",
        "    inet 169.254.76.21/23 scope global vinternal_11",
        "       valid_lft forever preferred_lft forever",
        "36: vtarget_6@if35: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000",
        "    link/ether 0e:97:c0:54:7b:9d brd ff:ff:ff:ff:ff:ff link-netnsid 1",
        "    inet 169.254.79.1/32 scope global vtarget_6",
        "       valid_lft forever preferred_lft forever"
    ],
    "/sbin/ip r": [
        "default via 169.254.76.22 dev vinternal_11 ",
        "169.254.76.0/23 dev vinternal_11  proto kernel  scope link  src 169.254.76.21 ",
        "169.254.76.22 dev vinternal_11  scope link ",
        "169.254.79.2 dev vtarget_6  scope link "
    ],
    "netstat -av": [
        "netstat: no support for `AF INET (sctp)' on this system.",
        "netstat: no support for `AF INET (sctp)' on this system.",
        "netstat: no support for `AF IPX' on this system.",
        "netstat: no support for `AF AX25' on this system.",
        "netstat: no support for `AF X25' on this system.",
        "netstat: no support for `AF NETROM' on this system.",
        "Active Internet connections (servers and established)",
        "Proto Recv-Q Send-Q Local Address               Foreign Address             State      ",
        "udp        0      0 169.254.79.1:58860          169.254.79.2:sieve-filter   ESTABLISHED ",
        "Active UNIX domain sockets (servers and established)",
        "Proto RefCnt Flags       Type       State         I-Node Path"
    ]
}

雑感

NW周りが興味深いですね!

env の結果で以下のようなものがあるんですが、そこだけルーティングが別になっていたり。ちなみに同じ 169.254 から始まるEC2のmetadataを取る 169.254.169.254IPアドレスへの接続は通らないようになってるんですよね。

        "_AWS_XRAY_DAEMON_ADDRESS=169.254.79.2",
        "_AWS_XRAY_DAEMON_PORT=2000",
    "/sbin/ip r": [
        "default via 169.254.76.22 dev vinternal_11 ",
        "169.254.76.0/23 dev vinternal_11  proto kernel  scope link  src 169.254.76.21 ",
        "169.254.76.22 dev vinternal_11  scope link ",
        "169.254.79.2 dev vtarget_6  scope link "
    ],

netstat -a で得られる接続先がこれしかなかったり、 sieve-filter はググったら Dovecot と組み合わせて使うメールフィルタのようです。

udp        0      0 169.254.79.1:58860          169.254.79.2:sieve-filter   ESTABLISHED 

あとは、 df -a で見られるファイルシステムがDockerコンテナより圧倒的に少ないなーとか(Dockerも起動方法次第なのかもですが)、Lambdaの最大メモリが3GBになって、2vCPUあたるようになったからかそれに合わせたVMに載ってるなーとか、プロセスの起動ユーザが sbx_userXXXX なのは知っていたのですが、なんか一杯作ってあるなーとか、仕事に役立つかは不明ですが色々興味深くはあります。

追試:Lambdaのインスタンスガチャを検証する

さて、ではみんな気になる?Lambdaのインスタンスガチャを検証してみます。

serverless.yml でLambdaのメモリ割り当てを増やしてインスタンスが別れるように仕向けます。

provider:
  name: aws
  runtime: python3.6
  memorySize: 3008

そして、こんなコードをデプロイ。

import subprocess
import time


def osdata(event, context):
    time.sleep(1)
    return subprocess.getoutput('cat /proc/cpuinfo | grep "model name" | uniq')

こんな感じの手抜きワンライナーで実行します。

for i in $(seq 1 10); do sls invoke -f osdata >> result.txt & done; wait; cat result.txt | sort | uniq -c

結果②

   8 "model name\t: Intel(R) Xeon(R) CPU E5-2666 v3 @ 2.90GHz"
   2 "model name\t: Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz"

ガチャだ・・・!(とはいえ、どちらも良いCPUですが) Lambdaの場合はコンテナなので cgroups あたりで調整してたり・・・はしないかなw

世代的にはC系の最新から1〜2世代前って感じですかね。極端に古くはなくて安心しましたw

ちなみに本検証はServerless Frameworkがデフォルトで利用する us-east-1 で検証してます。

パリみたいに最初からC5系しか居ないリージョンなら、割り当てリソースが同じでもファンクションの単体性能は変わりそうですね

こんなコメントも貰ったので、リージョンによってまた結果は変わりそうですね。

こちらからは以上です

元々書く予定だったのは近いうちに書くはず!(と言って去年は書かなかったことがあったけど今年は絶対書くつもり)

Alexa Skillの開発をServerless Frameworkだけで完結するための「Serverless Alexa Skills Plugin」の紹介

この記事は、Serverless Advent Calendar 13日目の記事です。

qiita.com

今回は拙作ですがオススメのServerless FrameworkのPluginを紹介したいと思います。

概要

Serverless Alexa Skills Pluginは、Serverless FrameworkでAlexa Skill用のLambda Functionを開発しながら、スキル周辺の設定も serverless.yml および serverless(sls) コマンドから管理できるようにすることで、Alexa Skillの開発をServerless Frameworkで統合管理するためのPluginです。

github.com

経緯など

Alexa Skillの開発にはLambda Functionの開発がセット

Alexaのカスタムスキルを作る場合、基本的にはLambda Functionの開発がセットになります。もちろん、Lambdaを使わずに特定のHTTP Endpointとやり取りすることで実行することもできますが、HTTPサーバを作って運用したりする手間とコストを考えるとLambdaを使う方が大抵の場合楽ですし、コストメリットも高いです。

developer.amazon.com

Lambda Functionの開発はServerless Frameworkでやりたい

最近は専らLambda Functionの開発はServerless Frameworkでやっています。以前は独自のデプロイツールを作って使っていたりもしましたが、v1.0からFrameworkの内部構造がそれ以前より段違いで良くなり、足りない機能はpull requestを送ったりpluginを作ったりして補えるようになったため、フルタイムで会社まで作ってメンテしていて、良い感じにエコシステムが確立しつつある製品に対抗してもしょうがないので乗っかる方針でやっています。

Alexa Skillの設定管理がめんどくさい

対して、Alexa Skillの設定はAlexa Skill Kitのコンソール画面から行うか、ack-cliを使う方法が公式で提唱されています。このack-cliがまた設定をJSONで書かないといけなかったり、公式ツールなのでしょうがないのですが、APIに忠実に作られているので必要なコマンド数が多かったりするわけです。

developer.amazon.com

Serverless Frameworkで全部良い感じにやりたい

そんな思いから生まれたのがServerless Alexa Skills Pluginというわけです。

使い方

インストール

インストールはnpmにリリースされてるのでこれだけです。

$ npm install -g serverless
$ serverless plugin install -n serverless-alexa-skills

認証情報の取得

利用するためにはLambda FunctionをデプロイするためのAWSの認証情報もそうですが、Alexa Skills Kit APIを実行するための認証情報も必要になります。AlexaはAWSではなくAmazon本体の管轄であり、Login with Amazon というAmazon本体アカウントを使ったOAuth2.0によるシングルサインオンで認証を受ける必要が有るため、認証情報の扱いが異なるのでそこを説明したいと思います。AWSの認証情報の取得方法についてはここでは触れません。

まず、Amazon Developer Consoleへログインします。

developer.amazon.com

APPS&SERVICES タブから Login with Amazon へ移動し、 Create a New Security Profile からセキュリティプロファイルを作成します。

f:id:FumblePerson:20171213225925p:plain

各入力項目は適当で良いです。

f:id:FumblePerson:20171213230336p:plain

作成したら、出来上がったセキュリティプロファイルの Web Settings を設定します。

f:id:FumblePerson:20171213230629p:plain

Allowed Origins は空で良いです。 Allowed Return URLshttp://localhost:3000 を入力します。このポート番号は設定(serverless.yml)で変更可能なので、変えたい場合は変えても良いです。

f:id:FumblePerson:20171213230810p:plain

ここまでできたら、できあがったセキュリティプロファイルの Client IDClient Secret を控えておきましょう。併せてもう一つ、 Vendor ID というものも必要になるので、Developer Consoleへログインした状態でコチラへアクセスして控えておきましょう。

ちょっと面倒ですが、この作業は初回だけです。同じアカウントを使う限りは今後いくつスキルを作ろうと同じ情報が使えます(漏洩などして入れ替えが必要にならなければw)

面倒な画面ポチポチはここまでです!!ここから先はみんな大好きの黒い画面をメインに進みます!!

認証情報をセット

取得した認証情報を serverless.yml へ書き込みます。直接書き込むのに抵抗がある場合は下記のように環境変数を利用すると良いでしょう。また、 Allowed Return URLs でポート番号を買えた場合は localServerPort という設定を追加してポート番号を書き込んでください。

provider:
  name: aws
  runtime: nodejs6.10

plugins:
  - serverless-alexa-skills

custom:
  alexa:
    vendorId: ${env:YOUR_AMAZON_VENDOR_ID}
    clientId: ${env:YOUR_AMAZON_CLIENT_ID}
    clientSecret: ${env:YOUR_AMAZON_CLIENT_SECRET}

そして、以下のコマンドを実行します。

$ serverless alexa auth

このコマンドを実行するとブラウザが開かれAmazonへのログイン画面が出てきます。そこでログインに成功すると localhost:3000 へリダイレクトされ、認証に成功していれば Thank you for using Serverless Alexa Skills Plugin!! とデザイン皆無の味も素っ気もないメッセージが表示されているはずです!w

ちなみにこれによって取得した認証トークンの有効期限は1時間となっています。今後、トークンの自動リフレッシュなど実装予定ですが、現状では認証エラーが出たら再度上記コマンドの実行をお願いします。

スキルの作成

それではスキルを作ってみましょう。以下のコマンドを実行します。

$ serverless alexa create --name $YOUR_SKILL_NAME --locale $YOUR_SKILL_LOCALE --type $YOUR_SKILL_TYPE

それぞれオプションは以下の通りです。

  • name(n): スキルの名前
  • locale(l): スキルのロケール。日本語なら ja-JP, 英語なら en-US
  • type(t): スキルのタイプ。 custom or smartHome or video

スキルマニフェストの更新

スキルを作ったら、スキルの基本的な設定を表すスキルマニフェストを設定する必要があります。今作成したスキルに初期設定されているマニフェストは以下のコマンドで確認できます。

$ severless alexa manifests

Serverless: 
----------------
[Skill ID] amzn1.ask.skill.xxxxxx-xxxxxx-xxxxx
[Stage] development
[Skill Manifest]
skillManifest:
  publishingInformation:
    locales:
      ja-JP:
        name: sample
  apis:
    custom: {}
  manifestVersion: '1.0'

実行すると上記のような出力が出るので、 [Skill ID][Skill Manifest] をコピーして serverless.yml へ以下のように貼り付けます。

custom:
  alexa:
    vendorId: ${env:AMAZON_VENDOR_ID}
    clientId: ${env:AMAZON_CLIENT_ID}
    clientSecret: ${env:AMAZON_CLIENT_SECRET}
    skills:
      - id: ${env:ALEXA_SKILL_ID}
        skillManifest:
          publishingInformation:
            locales:
              ja-JP:
                name: sample
          apis:
            custom: {}
          manifestVersion: '1.0'

この serverless.yml の設定を更新後、下記のコマンドを実行するとマニフェストが更新されます。ちなみに恒例?の --dryRun/-d オプションで実際に更新は行わずに更新内容だけ確認できます。

$ serverless alexa update

マニフェストの設定構文についてはAPIに準じているので、こちらで確認できます。

developer.amazon.com

対話モデルの構築

Alexaスキルを動かすためには、対話モデルの構築が必要です。これもマニフェストとほぼ同じ流れでできますが、作成しただけのスキルは対話モデルを持っていないのでまず先に serverless.yml に対話モデルの設定を書き込む所から始まります。

custom:
  alexa:
    vendorId: ${env:AMAZON_VENDOR_ID}
    clientId: ${env:AMAZON_CLIENT_ID}
    clientSecret: ${env:AMAZON_CLIENT_SECRET}
    skills:
      - id: ${env:ALEXA_SKILL_ID}
        skillManifest:
          publishingInformation:
            locales:
              ja-JP:
                name: sample
          apis:
            custom: {}
          manifestVersion: '1.0'
        models:
          ja-JP:
            interactionModel:
              languageModel:
                invocationName: PPAP
                intents:
                  - name: PineAppleIntent
                    slots:
                    - name: Fisrt
                      type: AMAZON.Food
                    - name: Second
                      type: AMAZON.Food

このような形で models.ロケール.interactionModel という構造で下記に準じた対話モデルを設定します。

developer.amazon.com

そして、下記のコマンドで対話モデルを更新し、構築します。ここでも --dryRun が利用可能です。

$ serverless alexa build

構築した対話モデルは下記のコマンドで確認可能です。

$ serverless alexa models

Serverless: 
-------------------
[Skill ID] amzn1.ask.skill.xxxx-xxxx-xxxxx
[Locale] ja-JP
[Interaction Model]
interactionModel:
  languageModel:
    invocationName: PPAP
    intents:
      - name: PineAppleIntent
        slots:
        - name: Fisrt
          type: AMAZON.Food
        - name: Second
          type: AMAZON.Food

現段階では以上です

ここから実際にスキルを公開するまではもう少しステップがありますが、一番変更が多く試行錯誤が必要でバージョン管理しておきたいマニフェストと対話モデルが統合管理できるのは大きいのではないかと思います。今後Alexa Skills Kitを通したテストや公開まで統合できるよう開発は進めていきたいと思っています。

また、ここでは触れなかったServerless FrameworkでAlexa Skill用のLambda Functionを実装する方法についてはこちらをご確認ください。

serverless.com

良かったら今後Alexaスキルを作る際にご利用いただき、フィードバックやStarをいただけると励みになりますので、よろしくお願いします。

github.com