misc.tech.notes

主に技術的な雑記的な

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

2017年もServerlessConf Tokyoは最高だった #serverlessconf

去年に引き続き今年もスピーカーとして参加させてもらいました。

marcy.hatenablog.com

会社はスポンサーもやってるんですが、セッションとLTの両方にプロポーザルが通ってしまって余裕がないので同僚に基本任せっぱなしでした(ゴメンナサイ)

会場の雰囲気など

アンダーグラウンドな雰囲気がカッコよかった去年に比べて、今年は立派なカンファレンスになった感じでした。ですが、ベンダーフリーで自由な主張が許される雰囲気は変わっておらず、正当な進化を遂げたという印象でした。去年は食事と最後のソーシャルタイム?のお酒が凄い充実していたけど、代わりに今年はノベルティのTシャツが超カッコ良いので満足しています。

ソーシャルタイムは他の登壇者や普段会えない人達と話せる良い時間だったのですが、それが無くなったのはちょっと残念です。けど、規模的にも時間的にもまあ難しいだろうなという所なので仕方ないですね。個人的にはアツいメンバーと終了後に飲みに行けて良い話を一杯できたので救われたんですけど、もしそれが無かったとしたらちょっと物足りなかったかもしれないなと。

セッション

slideship.com

こちらが今回のメインです。「2年目なので今年はツールとかサービスの外側の話は一切封印して、より具体的な中身の設計とか実装の話をしたい」というのだけはもう初めから決めていて、事例をベースにしながら話すパターンと、大きなテーマだけ掲げてあとはとにかく設計と実装の話をし倒すパターンの2種類のプロポーザルを出した結果、後者を採用していただけました。そして、念のためと思って出したLTも一緒に通ってしまったというw

セッションの冒頭でも話したのですが、去年超有名エンジニアの方にパネルでボコボコにされたのが悔しくて、その時答えられなかったあらゆる質問の答えを持ってきたつもりです。Serverlessでも信頼性の求められるシステムは作れる。(もちろん、その方のことは全く恨んでいません。むしろ去年の自分が如何に甘かったか痛感させてくれてとても感謝してます)

LT

slideship.com

煽り芸に初挑戦してみました。会場でもその後の反応も良くて嬉しかったです。ただ一点ミスがあって「リレーション」は正しくは「リレーションシップ」ですね。普段から理解はしているものの略語みたいな感覚で誤用してしまっているので、名前は正しく表現すべきエンジニアにあるまじきミスなので、気をつけたいと思います。。。

他の方々のセッション

いくつか印象的だったものを(大体時系列順のつもり)

サーバーレスについて語るときに僕の語ること

speakerdeck.com

一応スポンサー企業所属なので、ブースはできなくてもランチスポンサーセッションを放っていくのはどうかってことで、現場では聞けなかったのが残念でした。ですが、基本的には全面的にAgreeです。イベントドリブンこそ本質で、そこの議論をもっと深めて行きたいというのはずっと思っていて「よくぞ言ってくれた!!」という気持ちです。

marcy.hatenablog.com

ただ、Serverlessの良いところの一つが文脈が広く寛容的である所だとも思っていて、入り口としての意味も込めて今までの文脈も残しておいても良いのかなと。隔絶しちゃわない程度に上手く棲み分けしていきたいですね。

アンケートにも書いたのですが、私にとってServerlessとは定義としてはイベントドリブンな文脈が正しいと思っているものの、Serverlessという言葉自体は「ベンダーの垣根を越えて真のクラウドネイティブアーキテクチャを議論するためのネームスペース」って思ったりしてます。そういうコミュニティに成長していってほしいと思うし、微力ながらお手伝いしたいです。

真のサーバレスアーキテクトとサーバレス時代のゲーム開発・運用

speakerdeck.com

おそらく日本のサービス(プラットフォーム)開発者の中で一番本気でServerlessをやっているであろう丹羽さん。さすがの圧倒的知見ですね。立場は違えど、要所要所で同じ答えに辿り着いている感じがして勝手に答え合わせしてもらった気持ちです。現場で聞きたかった。裏番組だったのが本当に悔やまれますw

Open source application and Ecosystem on Serverless Framework

speakerdeck.com

福岡のサーバレスおじさん、さすがの実装力です。いつもawspecのコンセプトデザイナーだと私のことを立ててくれるんですが、私はたまたま先に同じことを思いついて途中で投げ出しただけ(ちょうどaws-sdkのメジャーバージョンアップと重なって一旦凍結したという言い訳はあるけど、その後再開しなかったのでホントただの言い訳)なのです。今回も圧倒的実装力で楽しい世界を見せてくれました。自分ももっとコード書かねばと思わせてくれました。

OpenSource with Serverless World

speakerdeck.com

日本人初どころか、Serverless Inc.社外では初のメンテナの座を日々の努力で勝ち取ったアツい気持ちは本当に脱帽します。

Serverless Frameworkについては1.0が出たタイミングでそれ以前から比べると圧倒的に洗練された内部構造になったことに関心していて、その後もLamveryにあってServerless Frameworkに無い機能の中でどうしても欲しいものはPRを出したりプラグインで解決できるようになったので、日本人コミッター(しかも知り合い)が居る安心感も合わせてもうデプロイの所はServerless Frameworkに任せたい気持ちです。

その他、現状資料未公開(と思われる)もの

A Cloud Guruの人とAWSのServerless系サービスのProduct Managerが話していたServerless Architecture Patternは超実戦的で必見な感じでした。資料公開に期待したい。Googleの浦底さんもLT準備でチラチラ覗くことしかできなかったので、趣味でGCPをやっている身からは資料公開に期待したい(けど、始まる前に個人の主張であることを強調してたので難しいのかな)

あと、nekoruriさんの「アウトプットしないのは知的な便秘」は的確すぎて刺さりました。慢性的にちょっとずつ出しても便秘は解消しないですもんねw栄養の吸収も悪くなるしwほんと的確過ぎるwwwスッキリ出すの大事。

おまけの与太話

ワークショップは今年は参加できなかった。。。PyCon JPもそうだったんですが、チケットが早く売り切れてしまうと地方勢としてはちょっとツラい所があります。飛行機やホテルがあって行けることが確定できないと色々無駄になってしまう可能性がある・基本泊まりになるのでスケジュールの調整難度が高いので、理想を言うと2週間前くらいまではジワジワ追加販売とかしてもらえると嬉しいです。>カンファレンス主催者各位

個人的には行きたいカンファレンスはCFPも出す率が高くて(通れば無理を通してでも100%行くので)CFP落選後に落ちても行きたいか考えてから買えるタイミングがあると嬉しいけど、これはかなり特殊な例だと思うので配慮しなくて良いですwServerlessConfについては落ちてもほぼ間違いなく行ってただろうから買っておけば良かったんですけどね。怠慢。。。

でもまあ、圧倒的少数派に配慮して多数派の満足度を損なうのもどうかって話もあると思うんで、特に文句があるわけじゃなくただの願望です。地方でMeetupやった時に大盛り上がりとかしてれば自然と配慮してもらえるだろうし、コチラの問題ではって気もします。

(カンファレンスで数百人を沸かせることができても、地元では20人集めるのにも苦労してるので悲しくなる)

最後に

スピーカーで2年連続はクラウドベンダー勢を除くと私と丹羽さんだけのようですね。丹羽さんはさっきも言ったようにサービス開発者の中では一番だと思っているので、自分はインテグレーターの中では一番だという気持ちで、来年もあの場に立たせてもらえるように一年間精進していきたいと思います。

主催の吉田さん、スタッフの皆様、今年も最高のカンファレンスをありがとうございました!!

DynamoDBをキャッシュストアっぽく使うPyPIライブラリを作りました

仕事は一応納まったけど、リモートワークなので忘年会とかは特に無くて、奥さんも体調が良くないので年末年始に備えて寝てしまったので、一人寂しくブログ書いてます(;´Д`)

何はともあれソースなど

github.com

pypi.python.org

背景や動機

Lambdaでアプリケーションを作っていると、外部のSaaSAPIを叩いたりする時にrate limitやレイテンシが気になったり、DynamoDBに重めのクエリを投げると性能だったりキャパシティが気になって結果をキャッシュしておきたかったり、キャッシュストアが欲しくなることがままあります。

なんですが、ElastiCacheはVPC内で使うとENI生成時の遅延が気になるし、せっかく楽がしたくてServerlessなのにマネージドとはいえインスタンスを気にしないといけないのも微妙なところです。となると、Lambdaから手頃に使えるデータストアは結局DynamoDBしかないわけです。(AppEngineならMemcacheがあるから、もうAppEngineで良いんじゃないか?とか思ってしまうw)

じゃあ、もうDynamoDBをキャッシュストアの代わりに使うしかないよねってことで、それっぽいライブラリを探したんですが、見つからなかったので作りました。

インストール

Pythonおよびpipのインストールは省略すると、PyPIなので、これだけです。

pip install ddbc

余談ですが、APIが変わったらしく、PyPIのパッケージのアップロード方法がいつの間にか変わってたんですね。前からあるやつは手元のvirtualenv環境が古いままだからか気が付かなった。。。

準備

IAM権限

以下のようなPolicyを持つIAM Role/Userを使用します。

<cache-table> の所は実際に使用するキャッシュ用のDynamoDBテーブル名が入ります。
<region>:<account-id> の所はお使いの環境に合わせてください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
              "dynamodb:CreateTable",
              "dynamodb:DeleteItem",
              "dynamodb:GetItem",
              "dynamodb:PutItem",
              "dynamodb:DescribeTable"
            ],
            "Resource": "arn:aws:dynamodb:<region>:<account-id>:table/<cache-table>"
        }
    ]
}

キャッシュ用のテーブルを作成

上記の権限を割り当てた環境で以下のようなコードを実行します。既に存在する場合は何もしないので、2回以上実行してしまっても無問題です。

#!/usr/bin/env python

import ddbc.utils

ddbc.utils.create_table(
    table_name='cache_table',
    region='us-east-1', # optional
    read_units=10,      # default: 5
    write_units=10      # default: 5
)

使用方法

キャッシュクライアント生成

import ddbc.cache
import time

cache = ddbc.cache.Client(
    table_name='cache_table',
    region='us-east-1', # optional
    default_ttl=100,    # default: -1 (Infinity)
    report_error=True   # default: False
)

default_ttl がキャッシュデータをセットする際にデフォルトで適用するデータのTTLです。未指定時は無限になります。
report_error はDynamoDB APIのコール時に発生した例外を報告(再スロー)するかどうかです。キャッシュストアなので、一時的なエラーやスロットリングなどの想定されるエラーは無視しても良い場面が多いので、未指定時は例外は握りつぶし、読み込み時は失敗時に None (変更可能)、書き込み系の操作なら結果を True (成功) / False (失敗) で返します。

キャッシュデータ操作

ざっとこんな感じです。pylibmc をなんとなく意識した感じで dict っぽく使えます。

cache['foo'] = 'bar'
print(cache['foo']) # => 'bar'

time.sleep(100)
print(cache['foo']) # => None

cache.set('foo', 'bar', 1000)
time.sleep(100)
print(cache['foo']) # => 'bar'

del cache['foo']
print(cache.get('foo')) # => None
print(cache.get('foo', 'buz')) # => 'buz'

補足

ちなみに、DynamoDBへのリクエストも減らすためにメモリ上にもキャッシュを持ちます。書き込みは毎回リクエストが発生しますが、読み込みはメモリ上のキャッシュが有効な間はそこからデータを取得します。なので、Lambdaもコンテナの再利用がされている場合はメモリにキャッシュ済みの有効期限内のデータを使うケースではDynamoDBへのリクエストは飛びません。

また、キー範囲が偏ると性能が出ないDynamoDBに配慮して一応キーはハッシュ化して分散するようにしています。

現状、有効期限が切れたデータは削除する際のスループット消費の方が気になるので、消されずにそのまま放置されます。

あと、1つのキーに対するデータは pickleシリアライズした文字列長でDynamoDBの制限である400KBまでしか入りません。

docs.aws.amazon.com

最後に

Lambda上でのオンライン処理で使えるデータストアが実質DynamoDBくらいしかない現状では、それなりに便利かと思うので良かったら使ってみてください。

AWSさんには、Serverlessなキャッシュストア的な新サービスと、SimpleDBの再興を切に願うばかりです。来年は是非おなしゃす。

それでは、良いお年を。

AppEngineで作るComputeEngineの定期バックアップシステム

この記事は、Google Cloud Platform Advent Calenderの21日目の記事です。遅れに遅れてしまい大変申し訳ありません。。。

エントリー時は「Cloud Functionsでなにか」と書いていたのですが、イマイチ目新しい機能も出ておらず良いネタがでなかったので、過去にAppEngineで作ったComputeEngineの定期バックアップシステムの紹介をしたいと思います。

前置きと概要

AWS方面ではわりと早い段階からAPIとタグを駆使したEC2の定期バックアップ手法は有名でした。EC2にバックアップ世代数のタグを設定し、そのタグがついているインスタンスを対象にバックアップを行うバッチを定期で起動します。

blog.suz-lab.com

最近だと、CloudWatch Eventsを利用してEC2要らずで行うことができます。

qiita.com

GCEでの例を見かけなかったのですが、基本的にはGCPスケールメリットを活かすにはステートレスなアプリケーションが前提となるので、そうするとデータを基本的にマネージドなデータストアに入れておけば良い話なので、そういったことは必要ないんだろうなと。

ただ、東京リージョンもオープンしたことですし、これからは社内システムやステートレスではない言ってしまえばレガシーなアプリケーションを移行するようなケースも増えてくるだろうと思いますし、そうなると必要になってくるんだろうなと。

ということで、AppEngineで作ったのですが、これがなかなか良い感じにできました。

EC2における手法の問題点

既知のEC2の手法には問題がいくつかありますが、これらがAppEngineを使うと良い感じに解決できます。

インスタンス数の増大による処理時間の増加

一般的にはそうそう問題とはならないでしょうが、大量のインスタンスを利用する規模の大きい企業や、GCPで代行ビジネスを行ういわゆるMSPのような立場だと、インスタンス数が増えてくると全部深夜のうちに終わらせたいのに終わらない可能性が出てきます。

APIエラー時のリトライ

もちろん確率としては低いですが、失敗することはあります。これも台数が多くなると顕著になってきます。APIのエラーハンドリングをキチンとやればという話ではあるのですが、エラー時にWaitなんかを入れだすとまた上記の話で処理時間が増えていって想定時間内に終わらない可能性が出てきます。

AppEngineでの構成

このような構成になります。公式アイコンが出たのでさっそく使ってみました。

f:id:FumblePerson:20161225004509p:plain

cloudplatform.googleblog.com

解説

ポイントを解説していきます。

定期実行

AppEngineにはタイマー機能があるので、これを使用します。バッチ処理の実行はキューなどを使っていくらでも冗長性を持たせられますが、発火元の冗長化は自力でやるととてもツラい領域なので、お任せできるのは嬉しいですね。

Scheduling Tasks With Cron for Python  |  App Engine standard environment for Python  |  Google Cloud Platform

分散処理

対象のリストアップとバックアップの実行を分けて、バックアップの実行をTaskQueueに詰めて分散で実行させます。これによって、単一のインスタンスで処理する際の処理時間の問題が解決できます。また、PushQueueを使用するとバックエンドのインスタンスをポーリングさせたり余裕を持って起動しておく必要がなくなるのがとても楽で良いですね。よくあるキューシステムではできない形なので、AppEngineのお気に入り機能の一つです。

自動再実行

TaskQueue (PushQueue)は処理に失敗すると間を置いて再実行をしてくれます。連続で失敗すると再実行間隔も調整しながら実行してくれるので、自力でExponential backoffのような実装をしなくて良いのでとても楽です。

yoshidashingo.hatenablog.com

権限管理が楽

AppEngineというかGCP自体の良さという感じではあるのですが、アカウントを跨いだ権限を与えて複数のアカウントのバックアップを一括で管理したい場合に、AppEngineのアプリケーションに自動で付与されるサービスアカウントのメールアドレスを該当のプロジェクトに招待して権限を付与してあげるだけなのが楽ですし、APIキーの共有などの煩わしさからも開放されるのでとても良い感じです。

まとめ

このような、AppEngineとTaskQueueでタスクの列挙と実行を分散で行う手法は、色々と応用が効きそうです。また、お任せできる領域が多いので、実装や管理が非常に楽になるのも大きなメリットです。AppEngineはWebアプリケーションを乗せるPaaSに留まらない便利なサービスなので、今後も積極的に使っていきたいです。

このシステムは既に私の手を離れており、手元にソースコードが無いのが残念ですが、暇を見つけてまた実装して公開したいなと思います。