JAMStack、それはハイパフォーマンスなウェブフロントを実現するアーキテクチャ

“JAMStack” と呼ばれる新しいウェブの開発手法で「より良いパフォーマンス」「より高いセキュリティ」「より安価で簡単なスケーリング」「より良質な開発者エクスペリエンス」を提供するアーキテクチャが注目を集め始めています。

JAMStack とは何か、どのように使い、どのようなメリットがあるのか、公式サイトの情報をもとに読み解きます。

JAMStack の定義


オフィシャル・サイトhttps://jamstack.orgによると、以下の要素に基づく最新のウェブ開発アーキテクチャとされています。

  • クライアントサイドのJavaScript
  • 再利用可能なWeb API
  • 構築済みの Markup (HTML)

HTML に JavaScript そして Web API なので、ウェブフロントの開発をする場合によく使うであろう技術と言えるでしょう。

ここでは動的な要素をサーバーサイドで組み込んだ HTML などをレスポンスするのではなく、クライアントサイドのJavaScript から Web API を通じて取得/更新し、クライアントサイドでダイナミックに描画する方式、いわゆる REST API などを使ったウェブフロントの作り方を指しています。

そして Markup (HTML) の部分がポイントなのですが“prebuilt at deploy time”、デプロイ時に事前ビルドされているとしています。そして多くの場合、サイト・ジェネレーターなどを使って作ります。(ツールの紹介は後述


The JAM Stack44ページ目のスライドにある、この図が JAMStack のアーキテクチャをシンプルに表現しています。
Git に Push されたコードから、ビルドツール(サイト・ジェネレーターなど)が動作し、Web API などのデータと組み合わせて、構築済みの HTML を生成、CDN へ配置して配信します。

なぜ JAMStack なのか

なぜ JAMStack、とくに構築済みの HTML をデプロイするのか。

公式サイトでは、大きく4つ「より良いパフォーマンス」「より高いセキュリティ」「より安価で簡単なスケーリング」「より良質な開発者エクスペリエンス」のメリットを上げています。

より良いパフォーマンス

最初のページを高速に表示するためには、HTML を事前に構築しておき CDN で配信することが最適です。
構築済みでない場合、高速に配信できる CDN を介することができず、また HTML を生成するための処理を待つことにもなります。

より高いセキュリティ

サーバーサイドの処理をマイクロサービスとして API に抽象化されているため攻撃の対象となる部分を小さくできます。
また、事前に構築済みの HTML をデプロイしているため、HTML を処理するためのプロセスはオフラインにできることもメリットといえるでしょう。

より安価で簡単なスケーリング

デプロイするものは構築済みの HTML となるため CDN に配置することができ、スケーリングすることが容易になります。
CDN が安価かは悩ましい部分はありますが「Web サーバ」と「AP サーバ」をスケールさせるために用意することを考えると、CDN だけでよいので簡単とはいえるでしょう。

より良質な開発者エクスペリエンス

疎結合にすることで、よりターゲットを絞った開発とデバッグを可能とします。
構成を簡素化できることは開発者の精神衛生上よいといえるでしょう。

このようにロジックを Web API として分離し、フロントエンドを構築済みの HTML としておくことで、多くのメリットを享受できます。

ベストプラクティス

JAMStack なプロジェクトを構築する際に、いくつかのベストプラクティスを活用することでスタックのメリットを最大限に発揮できます。

CDN に全部配置されている

JAMStack なプロジェクトは、サーバサイドのコードに依存しないため、単一のサーバではなく分散して配置できます。
CDN から直接サービス提供することでスピードとパフォーマンスを発揮できます。アプリがネットワークにエッジまで配置されることで、より良いユーザー体験になります。

すべてが Git に存在する

JAMStack なプロジェクトは、誰もがgit cloneでき、必要な依存関係をnpm installなどのような標準的な手順でインストールし、プロジェクトをローカルで実行する準備ができているはずです。複製するデータベースも、複雑なインストールもありません。
これによってコントリビューターの負担を減らし、ステージングとテストのワークフローも簡略化されます。

モダンなビルドツール

モダンなビルドツールの世界によるアドバンテージを受けましょう。
それは動きの速い世界でジャングル(無法地帯)を志向するかのようですが、未来のブラウザを待たずに最新のウェブ標準を使うことができるようになります。
それは現時点では Babel、PostCSS、webpack、そしてその仲間です。

自動ビルド

JAMStack のマークアップ(HTML) は、事前ビルドされているため、コンテンツの変更はビルドを実行するまで反映されません。このプロセスを自動化するとフラストレーションを軽減できます。
これを webhook で行うことも、自動的にパブリッシュするプラットフォームなどを使うこともできます。

アトミックなデプロイ

JAMstack なプロジェクトが非常に大きくなるにつれて、新しい変更のために数百ものファイルを再デプロイする必要があるかもしれません。 これらのファイルを一度にアップロードすると、プロセスの完了までの間に不整合を生じる可能性があります。 これを避けるに、すべてのファイルがアップロードされるまで変更が行われない「アトミックデプロイ」を実行できるシステムを使用します。

キャッシュの即時無効化

ビルドからデプロイへのサイクルが定期的に行われるようになったら、デプロイの実行が反映されるようにします。CDN は側にキャッシュを消去できます。

サイト・ジェネレーター

JAMStack のメリットはわかったとして、それでサイトを作るには「構築済みの HTML」が必要となってきます。
この構築済みの HTML は手組みの HTML を用意することではなくて、サイト・ジェネレーターを使うとされています。

主なサイト・ジェネレーターは、こちらStaticGenに列挙されています。

有名どころとしては GitHub Pages でも使われているJekyllなどがあり、HugoHexoといったブログ構築用の静的ジェネレーターやNext,NuxtといったReactVue.jsのアプリ・フレームワークなどもあります。

JAMStack のコンテキストではGatsbyVuePressGridsomeなどを目にすることが多いようにも感じます。

ヘッドレス CMS

これはとくに JAMStack の要件ではないのですが、JAMStack なブログや情報発信サイトを作る際に使われるものです。

WordPress などの CMS はコンテンツを配信するための部分が含まれ、そのままホスティングすると JAMStack なサイトにはならないので、別の仕組みが必要となりますが、WordPress などのように優れたコンテンツを管理するための仕組みが欲しくなります。
そうしたニーズに応えるのが、このヘッドレス CMS です。最低限の UI でコンテンツを管理することができ、作成したコンテンツを API 経由などで提供し、上記サイト・ジェネレーターなどと組み合わせてサイトを作ります。

主なヘッドレス CMS は、こちらheadlesCMSに列挙されています。

前半に Open source なヘッドレス CMS が並び、後半に Closed source があります。
またサービスとして提供されているものは、こちらの Closed source に含まれます。たとえば最近よく目にするContentfulは Closed source になります。

サイト・ジェネレーターによっては、WordPress の REST API からコンテンツをとってきたり、ローカルの Markdown ファイルを使うこともできるので、サイト・ジェネレーターの機能と管理方法に合わせて選択します。

参考情報


実際のところ、Web API ベースなアプリ開発をしていると、意識はしてなくとも JAMStack なウェブフロントを作っているなぁというケースもあったりします。

さっくり言ってしまうと、そんな Web API ベースなアプリでウェブフロントを構築済みの HTML として CDN へ配置するものに、JAMStack の名前がついた感じになります。

名前がついたことで認識され、認識されることで広まる流れになっていくのではないでしょうか。

2018年の振り返り

2018年最後のポスト、1年間の活動を振り返ります。

サマリー

  • 発表: 6回
  • 記事: 3本
  • アプリ開発: 3本
  • ハッカソン: 1回

発表 6回

2018.02 Agile & DevOps 勉強会 -@lulzneko&@lopburny

クローズドな Agile & DevOps 勉強会でスマートデバイス と クラウドサービス を 自動パーソナライズする IoT バックエンド開発チームにおける Agile と DevOps プラクティスの発表をしました。

タイトルが長くでピンとこないのは、オリジナルのタイトルは製品名が入っていたからで、公開に当たって製品名を外す必要があったからになります。

ベースとなる話は昨年の Serverlessconf Tokyo 2017 で発表したava チームが選択した TypeScript による AWS Lambda 開発になり、Java で開発していたプラットフォームを TypeScript で開発しなおす大手術をしたプロジェクトではどのような Agile & DevOps でやっていたのかを話しました。

私たちとしては必要に駆られてのことでしたが、途中でプログラミング言語を切り替えるのは衝撃を受けたほか、そのようなことをできる人たちがいることに驚いたと言われ開発者として誇らしく感じ、そして必要なことであればどのような困難でも真正面から取り組んでいくスタイルはずっとキープしていきたいと改めて感じるイベントでした。

2018.05 JJUG CCC 2018 Spring -@lulzneko

日本最大のJavaコミュニティイベント JJUG CCC でJava から TypeScript へ 切り替えて加速するサーバーレス開発の発表をしました。

Java のコミュニティで「Java をやめて TypeScript を選びました」という主旨の発表になり、かなり緊張しながら入室したことを覚えています。
仕事では Java をずっとやってきて、よく勉強でお世話になっていた会に、このような立場で発表するとは思っていませんでした。

会場は満員御礼で立ち見でも聞いてくださる方も多く、そして熱心に耳を傾けてくださり発表者冥利に尽きる会でした。

内容については賛否両論、手厳しい指摘も上がっていましたが、新たなる可能性についての話につなげられたと思っています。

批判や厳しい指摘を恐れず、発信したいことはしっかり話す。そして議論へつなげ盛り上げることの大切さを実感できた会だと感じました。

発表について1つ反省点が、タイトルは「Java から TypeScript へ切り替えて加速する」としているのに、「Java と TypeScript の親和性」についての話になっていたなと。資料を書いているうちに、どんどんアイデアが膨らんでしまうのに気をつけねば。。

2018.06 SPAJAM 2018 東京D予選 -@lulzneko&@lopburny&@javaponny

日本最高峰のアプリクリエイター競技会 の スマートフォン アプリ ハッカソン SPAJAM。ハッカソンの内容については後述、ここでは発表パートについて。

リアルタイム の 競演 と 参加型観戦 で 音楽を最高に楽しむ🎶 「ラップ、タップ、アップ」で発表しました。

徹夜でアプリ開発をしたあとの発表会。体力的にも集中力的もかなり厳しかった。スライドに何を書くのかとても苦戦しました。

一方で話し始めるとランナーズハイならぬスピーカーズハイとでもいうような高揚感で、今までの発表の中で一番ノッていました。

発表の神様が降りてきてくださった、そんな発表でした。
このような発表を毎回できるように精進しようと心から思いました。

進行より東京D予選は最難関と言われていましたが、どのチームも素晴らしい発表をされており、最高の発表をライバルの立場として体験させてもらえたことに感謝です。

2018.09 Serverlessconf Tokyo 2018 -@lulzneko&@lopburny&@javaponny

サーバーレスアーキテクチャを用いたアプリケーション構築における知見の共有を目的とした、コミュニティ主導の技術カンファレンスで、リアルタイム動画ラップ バトル アプリを 短時間で作り上げた完全サーバーレスな秘技の発表をしました。

Serverlessconf Tokyo は Riotz.works の原点ともいえるイベントで、2017年のイベントで発表させていただいたのが Riotz.works の始まりになります。

SPAJAM で作ったアプリはフル・サーバーレスでできており、サーバーレスで戦ったからこそ、優秀賞を受賞することができたと思っています。
そのサーバーレスの素晴らしさをハッカソンの観点からお伝えできたのではないでしょうか。

今回の会場は東京タワーのスタジオで 25m プール 1面分の超巨大スクリーンでの発表でした。

このような会場ははじめてで緊張したものの、発表する楽しさを強烈に実感できる場所でした。また、このような場所で発表に臨めるよう技術と活動をしっかり行っていこうと思いました。

Vue Fes Japan 2018 Reject Conference -@lulzneko&@lopburny

日本開催初の大規模 Vue.js カンファレンスでVue.js/Nuxt.js で 実現できた PWA の リアルタイム動画ラップ バトル アプリのリジェクトコンで発表しました。

プレイドさんのオフィスで開催され、芝生スペースの素敵な場所でした。
フロント系のイベントははじめての参加で、今まで参加してきたイベントとは雰囲気が違う感じを受けましたが、楽しさはかわらず、技術イベントはいいなぁと再認しました。

懇親会では多くの方々と話をする機会がありとても楽しかったです。

東京 Node 学園祭 2018 -@lulzneko&@lopburny

年に一度、真剣に学んで楽しもう! 日本最大のNode.jsカンファレンス! でVue.js/Nuxt.js で 実現できた PWA の リアルタイム動画ラップ バトル アプリの発表をしました。

TypeScript でサーバーレスを推している身として Node.js の総本山ともいえるイベントでお話しできるのは最高の体験でした。

記事 3本

こちらは全然かけなかったので反省です。
とくに TypeScript のスターター記事を書き始めたのに2回で止まってしまった。

引き続き TypeScript でサーバーレスは推していきたいので、しっかり記事を書いて発信していくとともに、Nuxt.js や Gridsome などを使い始めているので、こちらも情報発信をやっていけるようにしたいです。

活動の仕方を見直して、記事を書く時間をしっかりとるようにしたいです。

アプリ開発 3本

まだ公開できないプロトタイプが1本ありますが、3本作れたのはよかったです。

ラップはハッカソン制作作品ですが、少しずつ機能追加などもやっていて、引き続きいろいろな機能を追加したり、おもしいことを試せたらと思っています。

ウェブサイトは Gridsome を使ってリニューアルしました。 Slides と Articles の引っ越しはできていませんが、リニューアルで念願だった Works と Engineers が追加できました。
Gridsome は JAMStack としておもしいので、ちゃんと使いこなせるようにしつつ情報発信をしていきたいと感がています。

ハッカソン 1回 -@lulzneko&@lopburny&@javaponny

SPAJAM の スマートフォン アプリ ハッカソン に 参加しました。
自分自身も、Riotz の メンバー 全員 で はじめてハッカソンに参加した記念すべきイベントでした。

5人でチーム編成のところ、Riotz は 3人とチャレンジングな参加でしたが、サーバーサイドはサーバーレスの武器を活かすことでチャレンジできる、そしてアプリ側もネイティブではなく敢えて PWA を使うことで持てる技術を最大限に生かせることで戦えると考えていました。

とはいえ日本最高峰のアプリクリエイター競技会を謳っているイベントであり、参加した東京D予選ドワンゴ会場は最難関の予選会場(進行談) とのことで、すごい人、すごいチームばかりでした。

午前のチームを超えて全体でのアイデアソンではポンポンすごいアイデアを出す方、発表の時間では今すぐ公開可能レベルのアプリ、TED 会場かと錯覚するプレゼン、参加者のとてつもないレベルに面食らいました。

当日発表のテーマ「音楽」に対して、メジャーからストリート、Google Home、Spotify に Tシャツ制作サービス連携、スマホのあらゆる機能活用と、想定しきれないありとあらゆるものがそろっていたと思っています。

Riotz.works は幸いにして「優秀賞」をいただけましたが、どのチームがとってもおかしくない、最難関の予選会場(進行談) だったのではないでしょうか。

そのような方々相手にアイデアと技術を競えたことに感謝です。
ハッカソンははじめての参加でしたが、このような最高に高揚し意地を張れる場所に、多く参加できるよう技術の研鑽と戦うマインドをキープしていきたいと思う場でした。


この1年は Riotz.works が発足して、ほぼ1年と半分。

多くの場所でお話しさせていただきました。サーバーレスはアプリ開発にとって最強の武器」を標語に活動してきた自分たちの思いをたくさんをお伝えできたのかなと思っています。

一方で残していけるはずの記事がたくさん作れなかったことは反省です。
話をすること、文章を書くこと、どちらも凄く好きなのは、今文章を書いていて感じていますが、時間を作ることが下手だったのかなと。
この辺は来年に向けて改善をしっかり考えていきます。

よかったところを伸ばし、反省点を見直して次の年へ繋げられるよう頑張ります。

よいお年を。

共有:

2018年の Riotz Works の活動を振り返る

どうも、Riotz Works のうさぎ(@lopburny)です!

昨日、年内予定していた全ての用事が終わり、年末の連休に突入しました。僕は例年クリスマスが終わった時点でそわそわして集中力が持たなくなりますが、今年は色々あって今日からやっと休暇に入ります。

そして Riotz Works も結成から1年が経過し、個人的にはここ数年で最も忙しく過ごした1年で、充実できた1年だったと思います。

一方、反省点も多々あり、ちゃんと振り返って来年の活動に活かしたいという気持ちもあります。この記事は、そんな Riotz Works での活動を振り返り、発表では言えなかったこと、私が感じたことをつらつらと書いていきます。

2018年のうちにやってきた発表

※ 2018/12/29 時点

これ、ちょうど1年前は、たったの1行でした😂

まさかこのページが埋まるとは、想像していませんでしたが、何事もチャレンジしてみるもんですね。Riotz Works の活動は、今のところ仕事というわけではないので、メンバーの 100% ピュアなモチベーションでやってこれたのも効果的だったと思います。

スマートデバイスとクラウドサービスを自動パーソナライズする IoT バックエンド開発チームにおける Agile と DevOps プラクティス

@Agile & DevOps 勉強会、 発表資料はこちら

2月、@lulzneko@lopburnyが発表しました。Riotz Works が結成して 1回目の発表が Serverlessconf Tokyo 2017 という大きくてパブリックな発表だったのに対して、今回(2回目)は Closed な勉強会だったので比較的緊張はしなかったです。

しかし、タイトルにもあるように、勉強会のテーマは Agile や DevOps といった開発プロセスなので、参加者のみなさんが必ずしも Web やクラウドに明るいわけではなく、様々なバックグラウンドを持つ人にどう伝わるか悩ましいところでした。

中盤の自分のパートが AWS に触れたことがない人には分かりにくい内容だったと自覚し、補足説明をたくさん入れるようにしましたが、本番では早口で突っ走るような話し方になっていたかもしれません。

発表後、「刺さる人には刺さった」というコメントをもらったときは嬉しかったです。

Java から TypeScript へ切り替えて加速するサーバーレス開発

@JJUG CCC 2018 Spring、 発表資料はこちら

5月26日。@lulznekoの単独発表です。

AWS Lambda は、リリース当初から現在に至るまで、Java ランタイムを選択すると必然的に Cold Start に悩まされることになります(Cold Start を無視してもいいケースなら問題になりません)。この問題に対するアプローチはいくつかあると思いますが、私たちのチームが身をもって実践してみたことで、「Java 開発者において、TypeScript は良い選択になり得る」というのがポイントになります。

発表会場が開始後すぐ埋まり、立ち見の方もいらっしゃるほど盛況でした。このアプローチに対するフィードバックは賛否両論ありましたが、みなさんにとって興味深い内容だったのではないかと思います。

リアルタイムの共演と参加型観戦で音楽を最高に楽しむ🎶「ラップ、タップ、アップ」

@SPAJAM 2018 東京D予選、 発表資料はこちら

6月16日~17日。今回はカンファレンスではなくハッカソンです。@lulzneko@javaponny@lopburny3人で出場しました。メンバー全員初めてのハッカソンで、Riotz Works としても初です。もともとこの SPAJAM というハッカソンには注目していて、Riotz Works 結成前から「出てみたい」と何度か話したことがありました。そして、思い切って、エントリーしたのです。

1日目の朝、そわそわしながら会場へ向かい、午前のアイディアソンを終えて、ハッカソンのテーマ(音楽)が発表されました。会場近くの築地市場で海鮮丼を食べながら、どうする?何作る?と議論をはじめ、私が思いついたのが、このラップバトルです。言ってみたものの、これどうやってつくるの、やばくね、みたいな感じでした💦

ラップバトルのサービスは世の中にいくつも存在しますが、僕はよりインタラクティブな演出がいいなと思ったのと、ラップバトルの「対戦」と「観戦」を 同時にかつリアルタイムにできる仕組みを作ることで差別化を図りました。いわば Twitch のゲーム実況ストリーマー同士が即座で対戦を始め、その様子をそれぞれのチャンネルで数百〜数千の人たちが観戦しながらコメントやヤジを飛ばす様子。そういうのが作りたかったです。

果たして作りきれるだろうか。。。午後から真面目に調査します。数時間ほどでなんとか SkyWay のサンプルを動かすことができました。夜になって会場を出た時点で、ようやく翌日の発表に向けた最も基本的な機能(リアルタイム動画ストリーミング)ができました。そして、夜も気を抜くことはできません。オールナイトで作業して30分ほど仮眠を取り、発表直前まで開発を進めた結果、なんとかデモができるところまで作れました。

結果は・・・なんと、優秀賞をいただきました!!

今でもあの感動を忘れられません。恐らく今年起きた出来事の中で最強のインパクトでした。。

そして帰りに飲んだビールは間違いなく今年飲んできたビールの中で最高の味でした。😂

残念ながら SPAJAM の本選に出ることはできませんでしたが、次はもっと進みたいと思います!!

リアルタイム動画ラップバトルアプリを短時間で作り上げた完全サーバーレスな被疑

@Serverlessconf Tokyo 2018、 発表資料はこちら

9月29日。@lulzneko@javaponny@lopburny3人で発表しました。Serverlessconf Tokyo での発表は、去年に続き2回目となります。夏のハッカソンの余韻がまだ残っていて、この発表の準備にも気合が入りました。特に、会場スクリーンのアスペクト比が 48:9 という、ものすごく大きくて迫力のある会場だったため、わくわくしつつもかなり緊張していました😂

気合を入れて会場に着き、生でみる会場の広さとスクリーンの迫力にびっくりしながら、デモの準備をしました。ちゃんと動くのを確認してこれはいける!と思ったんですが、本番では開始とともにデモが固まって動かないことに。そういうことにならないよう、開始10分前からアプリを立ち上げた状態で待機したのですが、なぜか開始のタイミングで 僕のスマホが固まってしまったんです。えーーー、なんで。。。ショック。。。とパニックになってしまった僕は、落ち着いて話すことができず、発表中に焦りを出してしまった気がして反省しています。

当時@lulznekoが言っていたように、「デモが動かないことは、よくあることですね」と、一旦落ち着いて進めていき、メンバーが対応してくれることを待つべきだったことを学びました。

幸い、メンバーが対応してくれたおかげて、発表中にデモが復旧し、ハッカソンで作ったアプリを披露することができました。また、サーバーレス関連の色んな方々とお話ができて、他の発表についても、どれもすごく勉強になったので、貴重なイベントでした。

Vue.js/Nuxt.js で実現できた PWA なリアルタイム動画ラップバトルアプリ

@Vue Fes Japan 2018 Reject Conference、 発表資料はこちら

11月10日。@lulzneko@lopburnyが発表しました。前回の Serverlessconf ではバックエンドの内容がメインでしたが、今回はフロントエンドにフォーカスした話です。元々、Vue Fes Japan そのものにエントリーしましたが、枠が非常に限られていたらしく、採用にはならずとも魅力的な内容だということで、 Reject Conference に参加させていただくことになりました。

Riotz Works のメンバーは、全員フロントエンド以外の領域を得意としています。ですが、フロントエンド/バックエンドの領域にこだわらず、思いついたことは自ら形にしていくことが大事だと思っていますし、ハッカソンでものを作っていく中で、人の目にダイレクトに届くUIの重要性と楽しさに気づいてしまったので、もっと極めたいなと思っています。

今回の発表は、今まで数を重ねてきたこともあり自信持ってできた気がして、懇親会でも色んな方々とお話させていただき、個人的に最も楽しくできた発表でした。

Vue.js/Nuxt.js と TypeScript で実現する PWA なリアルタイム動画ラップバトルアプリ〜フル Node.js エコシステムで戦ったモバイルアプリのハッカソン〜

@東京Node学園祭2018、 発表資料はこちら

11月23日。@lulzneko@lopburnyが発表しました。今回も話のネタそのものは変わらないですが、Riotz Works が Node.js を使い始めて、2018年をどう過ごしたかという集合体のような内容です。またサーバーサイド(Node.js)→フロントエンド(Vue.js)→Node.js環境全般 という流れができたのと、Node.js コミュニティでの発表ということで胸が熱くなりました。

発表も比較的慣れてきたところで順調に進みましたが、React 界隈の方々が多く、前回よりは重い空気になることもありました。ですが発表の順番が最後で、たくさんの方々に聞いていただき、たくさんフィードバックをもらえたので、かなり手応えのあったイベントでした。

2018年を振り返って、よかったこと

あえて2点にしぼると、こんな感じです。

  • ハッカソンに出て、アイディアを形にできたこと
  • 次々と発表が決まり、絶え間なくイベントに参加できたこと

2018年を振り返って、反省したいこと

たくさんありますが、これも2点にしぼります。Riotz Works というより個人的な話ですね。

  • タスクの優先順位を考慮した、自分のリソース管理
  • 発表以外のアウトプットができていない点

当たり前な話ですが、何らかの活動を継続して進めていくためには、自分の体力・時間などのリソース管理を上手にできることが重要なポイントかなと思います。

僕は思いつきとその時のモチベーションに左右されやすいタイプで、できないことも意地を張って挑戦し続けたりするので、無駄にした時間が結構ありました。目標やコミットしたことに対して確実に結果を出すためには、もっと冷静に自分を見直して、シビアにならないといけないですね。

あと、もっとブログ書きたかったのですが、上記のリソース管理ができなかったせいか、できていません。書きたい話は結構あります。2019年からばんばん出して行きたいと思います。

終わりに

ちょっと長くなってしまいましたが、いかがだったでしょうか。
Riotz Works は 2019年も楽しく活動していきたいと思います。みなさん良いお年を〜

API Gateway の WebSocket でチャットを実装 w/Serverless Framework

この記事はServerless Advent Calendar 2018の 22日目になります。

AWS re:Invent で発表された API Gateway の WebSocket 対応、ついに利用できるようになりました!
WebSocket がサーバーレスで簡単に利用できるようになるとアプリの幅も広がり、いろいろなことができるようになります。
さっそく API Gateway の WebSocket を試してみます。

※ 今回 Serverless Framework の serverless-websockets-plugin を使いますが 2018年12月現在、暫定の実装になるとのことです。API Gateway の WebSocket 対応が AWS CloudFormation で未サポートのため Serverless Framework 本体には含まれず Plugin になっているとのこと。正式版では構文など変わる可能性があることに注意が必要です。

しかしながら “With all that out of the way, play with our new presents!” ということで、楽しんでみましょう!

環境
本記事の開発環境は以下となります。

  • Windows 10 64bit + WSL Ubuntu 18.04.1 LTS
  • Visual Studio Code
  • Node.js 8.10.0
  • Yarn 1.12.3
  • TypeScript 3.2.2
  • Serverless Framework 1.35.1
  • Serverless Websockets Plugin 1.0.0

参考情報

WebSocket について

HTTP ベースの Web API ではサーバーに対して接続、リクエストを送り、処理結果のレスポンスを受け取り、切断終了の流れになります。
必要に応じて、このリクエスト/レスポンスのやり取りを繰り返す形になります。

それに対して WebSocket はサーバーに接続後、コネクションを維持したまま双方向でメッセージを送ることができます。
これは通常のリクエスト/レスポンスに当たるやり取りのほかに、サーバー側から任意のタイミングでメッセージを送れます。

たとえば私たちがハッカソンで作った「ラップ、タップ、アップ 🎶」では、演奏者へのフィードバックとして「👍」を送ることができ、その数をリアルタイムでカウントする機能があります。

この「👍」をリアルタイムでカウントする部分を HTTP ベースの Web API で作るとすると、アプリから「👍」の数を取得する API を繰り返し「リクエスト/レスポンス」して受け取る必要が出てきます。
仮に数に変化がなかったとしてもアプリ側は知るすべがないので、繰り返し「👍」の数を取得しに行かなければなりません。

WebSocket が使える場合、コネクションは維持したまま必要な時に「👍」の数をサーバー側がアプリへメッセージを送ることができるようになります。
「👍」の数に変化がなかった場合は維持されているコネクションの中にメッセージが流れないだけでアプリはとくに何もする必要はありません。変化があった時だけサーバーが教えてくれるので、それに応じてアプリの表示を変えるだけになります。

これにより不要なリクエスト/レスポンスを減らせるほか、よりリアルタイムに近い状態で変化を受け取ることができるようになります。
HTTP ベースでは「リクエスト/レスポンス」で受け取り、次のリクエストを出すまでの間にあった変化は結果しか受け取れません。5だった「👍」を次のリクエスト/レスポンスでは 10になっているかもしれません。

WebSocket では状態の変化を断続的に返すことも可能になるので、5、6、7… とメッセージを返せます。

これにより、よりスムーズに描画を変えていくことができるようになります。
これはチャットのようなコミュニケーションや金融などの取引の価格情報などに使うことができます。

今回API Gatewayがこの WebSocket に対応したので、その使い方をチャットを実装することで確認します。
(WebSocket でチャットは定番すぎますが、「👍」のカウントだとさみしいですからね)

※「ラップ、タップ、アップ 🎶」では、API Gateway がまだ WebSocket が扱えなかったので、Firebase Realtime Databaseを使って実現しています。

Node.js プロジェクトの準備

Serverless Framework のボイラープレートを使って、TypeScript の Node.js プロジェクトを作成します。

プロジェクトのディレクトリを作成しプロジェクトのひな型を作ります。

1
2
3
4
5
6
7
8
9
username@pc:~$ mkdir samples-apigateway-websocket-chat
username@pc:~$ cd samples-apigateway-websocket-chat
username@pc:~/samples-apigateway-websocket-chat$

username@pc:~/samples-apigateway-websocket-chat$ npx serverless create --template aws-nodejs-typescript
Serverless: Generating boilerplate...
(省略)
Serverless: Successfully generated boilerplate for template: "aws-nodejs-typescript"
Serverless: NOTE: Please update the "service" property in serverless.yml with your service name

続いて Serverless Framework をローカルで追加し、AWS SDK と WebSocket クライアント wscat、必要なパッケージを最新化してインストールします。
Serverless Framework はグローバルに追加して使うようですが、複数人開発でグローバルのを使っているとバージョンの違いなどでトラブルがあったのでpackage.jsonで明示して、それを使うようにしているためです。サクッと試すにはグローバルでもよいでしょう。

1
2
3
username@pc:~/samples-apigateway-websocket-chat$ yarn add -D serverless serverless-websockets-plugin wscat
username@pc:~/samples-apigateway-websocket-chat$ yarn upgrade --latest
username@pc:~/samples-apigateway-websocket-chat$ yarn add aws-sdk

package.jsonをプロジェクトに合わせて修正します。
主にnamedescriptionauthorあたりを合わせるとよいでしょう。
また、作成しているのはアプリケーションなのでモジュール公開の防止のために"private": trueを設定しておくとよいでしょう。private の詳細は、こちらのpackage.json | npm Documentationをご確認ください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"name": "samples-apigateway-websocket-chat",
"version": "1.0.0",
"private": true,
"dependencies": {
"aws-sdk": "^2.382.0"
},
"devDependencies": {
"@types/aws-lambda": "8.10.17",
"@types/node": "^10.12.18",
"serverless": "^1.35.1",
"serverless-webpack": "^5.1.1",
"serverless-websockets-plugin": "^1.0.0",
"source-map-support": "^0.5.6",
"ts-loader": "^5.3.2",
"typescript": "^3.2.2",
"webpack": "^4.5.0",
"wscat": "^2.2.1"
},
"author": "Riotz.works (https://riotz.works)",
"license": "MIT"
}

Serverless Framework の設定

serverless.ymlを編集し Serverless Framework の設定を行います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
service:
name: samples-apigateway-websocket-chat

plugins:
- serverless-webpack
- serverless-websockets-plugin

provider:
name: aws
stage: ${ opt:stage, 'dev' }
region: ${ opt:region, 'us-west-2' }
runtime: nodejs8.10
iamRoleStatements:
- Effect: Allow
Action:
- execute-api:ManageConnections
Resource:
- arn:aws:execute-api:*:*:**/@connections/*
- Effect: Allow
Action:
- dynamodb:Scan
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource:
- Fn::GetAtt: [ ChatConnectionsTable, Arn ]
environment:
DYNAMODB_CONNECTIONS:
Ref: ChatConnectionsTable
websocketApiName: ${self:service.name}-${self:provider.stage}
websocketApiRouteSelectionExpression: $request.body.action

functions:
connect:
handler: handler.connect
events:
- websocket:
routeKey: $connect
disconnect:
handler: handler.disconnect
events:
- websocket:
routeKey: $disconnect
defaultMessage:
handler: handler.defaultMessage
events:
- websocket:
routeKey: $default
sendMessage:
handler: handler.sendMessage
events:
- websocket:
routeKey: sendMessage

resources:
Resources:
ChatConnectionsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:service.name}-connections-${self:provider.stage}
AttributeDefinitions:
- AttributeName: ConnectionId
AttributeType: S
KeySchema:
- AttributeName: ConnectionId
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
SSESpecification:
SSEEnabled: True
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES

pluginsに API Gateway WebSocket を使うためのserverless-websockets-pluginを追加します。

iamRoleStatementsでは、API Gateway の WebSocket API を呼び出すためのexecute-api:ManageConnectionsに許可を与えます。
また WebSocket に接続しているクライアントのコネクションを管理するための DynamoDB が必要なので、DynamoDB に関する許可も与えます。

30行目、31行目のwebsocketApiNamewebsocketApiRouteSelectionExpressionは、serverless-websockets-plugin の設定になります。websocketApiNameは管理用にわかりやすい名前を設定しておくとよいでしょう。

functionsは WebSocket のイベントとアクションに合わせて定義を行います。
eventswebsocketが追加でき、websocketApiRouteSelectionExpressionに設定された Key の値とrouteKeyの文字列のマッチングによって起動する Lambda を振り分けています。
$connect$disconnectは WebSocket の接続と切断に対応します。また$defaultはマッチするアクションがない場合に呼び出されます。
最後のsendMessageが WebSocket で受け取るメッセージのキーになります。具体的には{ "action":"sendMessage", "data":"hello world" }のような JSON のactionに入る文字列のマッチングになります。

resourcesは WebSocket のコネクション管理用 DynamoDB を定義しています。
今回は最低限のConnectionIdだけを管理します。名前など無しの完全に匿名のメッセージだけのチャットになります。(WebSocket の動きと実装を試すってことで💦)

AWS Lambda の実装

handler.tsに処理を実装します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import { APIGatewayEventRequestContext, APIGatewayProxyEvent, APIGatewayProxyHandler, APIGatewayProxyResult } from 'aws-lambda';
import { ApiGatewayManagementApi, DynamoDB } from 'aws-sdk';

interface APIGatewayProxyEventWithWebSocket extends APIGatewayProxyEvent {
requestContext: APIGatewayEventRequestContextWithWebSocket
}
interface APIGatewayEventRequestContextWithWebSocket extends APIGatewayEventRequestContext {
domainName: string,
connectionId: string
}


const apigateway = ({ domainName, stage }): ApiGatewayManagementApi => new ApiGatewayManagementApi({ endpoint: `${domainName}/${stage}` });
const dynamodb = new DynamoDB.DocumentClient();

export const connect: APIGatewayProxyHandler = async (event: APIGatewayProxyEventWithWebSocket): Promise<Result> => {
console.debug('Starting Lambda handler: event=%s', JSON.stringify(event));
await dynamodb.put({ TableName: process.env.DYNAMODB_CONNECTIONS, Item: { ConnectionId: event.requestContext.connectionId }}).promise();
return new Result('Connected');
};

export const disconnect: APIGatewayProxyHandler = async (event: APIGatewayProxyEventWithWebSocket): Promise<Result> => {
console.debug('Starting Lambda handler: event=%s', JSON.stringify(event));
await dynamodb.delete({ TableName: process.env.DYNAMODB_CONNECTIONS, Key: { ConnectionId: event.requestContext.connectionId }}).promise();
return new Result('Disconnected');
};

export const defaultMessage: APIGatewayProxyHandler = async (event: APIGatewayProxyEventWithWebSocket): Promise<Result> => {
console.debug('Starting Lambda handler: event=%s', JSON.stringify(event));
const params: ApiGatewayManagementApi.Types.PostToConnectionRequest = {
ConnectionId: event.requestContext.connectionId,
Data: 'Error: Invalid action type'
};
await apigateway(event.requestContext).postToConnection(params).promise();
return new Result('Error: Invalid action type', 500);
}

export const sendMessage: APIGatewayProxyHandler = async (event: APIGatewayProxyEventWithWebSocket, context, callback): Promise<Result> => {
console.debug('Starting Lambda handler: event=%s', JSON.stringify(event));
const connections = await dynamodb.scan({ TableName: process.env.DYNAMODB_CONNECTIONS, ProjectionExpression: 'ConnectionId' }).promise();
await Promise.all(connections.Items.map(async ({ ConnectionId }) => {
try {
const params: ApiGatewayManagementApi.Types.PostToConnectionRequest = {
ConnectionId: ConnectionId,
Data: JSON.parse(event.body).data
};
await apigateway(event.requestContext).postToConnection(params).promise();
} catch (e) {
if (e.statusCode === 410) {
await dynamodb.delete({ TableName: process.env.DYNAMODB_CONNECTIONS, Key: { ConnectionId: ConnectionId }}).promise();
} else {
throw e;
}
}
}));
return new Result('OK');
}


class Result implements APIGatewayProxyResult {
public constructor(public body: string, public statusCode: number = 200) {}
}

4行目~10行目の interface 宣言ですが、こちらは API Gateway の WebSocket 対応の TypeScript 型定義がまだ入っていないので補完するために宣言しています。requestContextに入ってくるdomainNameconnectionIdが必要なため追加しています。型定義が更新されたら不要となります。

13行目ApiGatewayManagementApiのインスタンスを取得する関数ですが、API Gateway から WebSocket のメッセージを返す際にendpointが必要となり、これがdomainNamestageから決まってきます。固定値の場合はよいのですが、今回のように動的に決まってくる場合はrequestContextから受け取ったdomainNamestageが必要なため毎回インスタンスを生成しています。

16行目~58行目でserverless.ymlに定義した Lambda のエントリーポイントと実装があります。

connectdisconnectは WebSocket の「接続/切断」の際に呼び出されます。
接続時にconnectionIdを DynamoDB へ保存し、切断時に削除しています。切断は呼び出されないこともあるので、注意が必要です。

defaultMessageは WebSocket で受け取ったメッセージに対応するアクションがなかった場合に呼び出されます。ここでは “Error: Invalid action type” をクライアントに返しています。
WebSocket でメッセージを返すにはApiGatewayManagementApipostToConnection()を呼び出します。その際にクライアントのconnectionIdが必要となります。
この関数のようにメッセージを送ってきたクライアントだけにメッセージを送る場合はrequestContextconnectionIdが使えるので、それを使ってエラーメッセージを返します。

最後のsendMessageは、チャットのコメントを送る処理になります。リアルタイムに複数クライアントへメッセージを送る WebSocket を使う処理の肝となる部分です。
複数クライアントにメッセージを返すには、メッセージを送るべきクライアントすべてのconnectionIdが必要となり、それぞれにApiGatewayManagementApi#postToConnection()を行う必要があります。
つまり API Gateway が接続中のクライアントを知ってくれてるわけではないので、自前で管理する必要があり、そのために DynamoDB を用意しているものになります。

今回は全員参加の簡単な実装なので、DynamoDB のコネクション管理テーブルをスキャンし、全件に対してApiGatewayManagementApi#postToConnection()を呼び出しています。(なんか、もっとスマートな実装できないかなぁ)

ApiGatewayManagementApi#postToConnection()を呼び出す際に、コネクションが切断されているクライアントがありえます。先ほど「切断は呼び出されないこともあるので、注意が必要です。」と書きました通り、クライアントが切断されてもイベントが呼び出されないケースもあります。そのため DynamoDB への削除されずconnectionIdが残るケースもありえます。その場合statusCode410のエラーが投げられるので、そのエラーが投げられた場合は DynamoDB から該当するconnectionIdを削除しておき再発防止しておきます。

デプロイ & 実行!

デプロイは Serverless Framework がしっかりやってくれるので、以下のコマンドで行えます。
あらかじめ AWS の Access Key の用意とプロファイルを設定しておきます。(デフォルト・プロファイルでない場合は--aws-profile [your profile name]と利用するプロファイル名を--aws-profileで指定します)

1
2
3
4
5
6
username@pc:~/samples-apigateway-websocket-chat$ yarn serverless deploy
(省略)
Serverless: Deploying Websockets API named "samples-apigateway-websocket-chat-dev"...
Serverless: Websockets API named "samples-apigateway-websocket-chat-dev" with ID "6ovkt8XX" has been deployed.
Serverless: Websocket URL: wss://6ovkt8XX.execute-api.us-west-2.amazonaws.com/dev/
Done in 137.90s.

デプロイが完了すると、コマンドの実行結果に WebSocket の URL が出力されます。
その URL に対してwscatで接続し API Gateway WebSocket の動きを確認します。

1
2
3
username@pc:~/samples-apigateway-websocket-chat$ yarn wscat -c wss://6ovkt8XX.execute-api.us-west-2.amazonaws.com/dev/
connected (press CTRL+C to quit)
>

接続できると>で入力待ちになります。{ "action":"sendMessage", "data":"hello world" }とメッセージを送ると、チャットのコメントが帰ってきます。

1
2
3
4
5
username@pc:~/samples-apigateway-websocket-chat$ yarn wscat -c wss://6ovkt8XX.execute-api.us-west-2.amazonaws.com/dev/
connected (press CTRL+C to quit)
> { "action":"sendMessage", "data":"hello world" }
< hello world
>

複数のコンソールから接続すると、送ったコメントがすべてのコンソールに流れてきます。
WebSocket でリアルタイムに複数クライアントへメッセージが遅れていることが確認できます。

チャットとは名ばかりのコメントだけを送りあうだけの実装(しかも wscat によるコマンドライン)でしたが、API Gateway WebSocket の実装がつかめました。

Serverless Framework は今後の AWS CloudFormation 対応によって変わってくる部分がありますが、こんなに簡単に設定できるのでとても助かります。

WebSocket が簡単に利用できるようになったので、いろいろなアプリづくりに生かせそうで楽しみです。


では、ハッピー・サーバーレス・ライフ! ハッピー・ホリデー!!

Serverless Framework と TypeScript で Hello & Version の Web API を実装

前回サーバーレスなアプリケーションのひな形を作りました。
今回は、ひな形で作られたAWS Lambdaのソースを Hello World にし、また新しくアプリケーションのバージョンを返す Version Web API を追加します。

シリーズの記事

環境
本記事の開発環境は以下となります。

  • Windows 10 64bit
  • Visual Studio Code
  • Node.js 9
  • TypeScript 2.7
  • Serverless Framework 1.26.0

主な実行環境について、また全部稼働まではいきませんが以下を想定しています。
※ Amazon API Gateway などは、Serverless Framework が自動設定するので省略、明示的に使うものとしてリストになります。

  • Amazon S3
  • AWS Lambda

ベースとなるソースコード

前回の続きとなりますので、こちらhttps://github.com/riotz-works/samples-hello-serverless/tree/0.0.1をもとに追加開発します。

本記事と一緒に作成する場合は前回のソースもしくはタグを指定してチェックアウトしたソースをお使いください。

1
2
3
c:\Temp\nlog>git clone --depth 1 -b 0.0.1 https://github.com/riotz-works/samples-hello-serverless.git
Cloning into 'samples-hello-serverless'...
(省略)

Hello Web API の改修

Serverless Frameworkのボイラープレートで生成した Lambda のプログラムはhandler.jsで以下のようになっています。
これを Hello World の Web API へ改修するにあたり、このままでresponsebodyを変えるだけでよさそうです。今回はmessage'Hello Serverless !!'にし、inputは削除することにします。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { APIGatewayEvent, Callback, Context, Handler } from 'aws-lambda';

export const hello: Handler = (event: APIGatewayEvent, context: Context, cb: Callback) => {
const response = {
statusCode: 200,
body: JSON.stringify({
message: 'Go Serverless Webpack (Typescript) v1.0! Your function executed successfully!',
input: event,
}),
};

cb(null, response);
}

そしてプロジェクトのディレクトリ直下で汎用的な名前のhandler.jsだと、このあと追加する Version Web API の配置などで困ります。
まずはディレクトリ配置を見直し、Lambda のハンドラー・ファンクションのソースが入っていることがわかるようにsrc/handler配下に置おきます。
続いて Hello Web API のハンドラーに対応したファンクションのファイル名としてgreetings.tsとすることにします。

ファイル名と配置を変えたので Lambda の定義をしているserverless.ymlを修正します。
helloファンクションのハンドラーはhandler.helloと定義されていましたが、greetingsファクションsrc/handler/greetings.helloハンドラーに変更します。

Hello Web API ができました。
この時点でserverless deployコマンドを使いデプロイできます。(–aws-profile [profilename] を忘れずに)

Version Web API の作成

Hello Web API が作れましたがボイラープレートのままだったので、新しく Web API を追加します。
とりあえず、アプリケーションのバージョンを返す Version Web API を作ることにします。
仕様は「HTTP GET /versionpackage.jsonversionを返す」にします。

まずは、Lambda のハンドラーのソースを配置するsrc/handlersystems.tsを作ります。
他にシステム関連の Web API の想定はありませんが、システム関連 Web API のハンドラーを配置するsystems.tsversionのハンドラーを追加するイメージにしました。

コードは以下のようにします。(このコードを使う場合は、1行目の Copyright は書き換えてください)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* Copyright 2018 Riotz.works. */
import { APIGatewayEvent, Callback, Context, Handler } from 'aws-lambda';
import { version } from '../../package.json';

/**
* System Web API's AWS Lambda handler function.
*
* @param event – event data.
* @param context – runtime information of the Lambda function that is executing.
* @param callback – optional callback to return information to the caller, otherwise return value is null.
* @see http://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html
*/
export const handle: Handler = (event: APIGatewayEvent, context: Context, cb?: Callback): void => {

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

if (cb !== undefined) {
cb(null, response); // tslint:disable-line
} else {
context.succeed(response);
}
};

主なキー・ポイントは以下になります。 先の Hello Web API と若干異なる部分 (context.succeed()とか) がありますが詳細は後日の記事として、基本的には変わりありません。

  • 3行目で、バージョンのもとになるpackage.jsonsystems.tsの相対パスで指定しversion要素をインポートします
  • 17行目で、インポートしたversion属性の値をレスポンス・ボディとしてセットします

Visual Studio Code の環境によってはエラーが出ないかもしれませんが、3行目のインポート文でエラーにあります。

何も表示されていなくてもserverless packageを実行するとエラーが出力されます。

1
2
3
4
5
C:\Temp\samples-hello-serverless> serverless package
Serverless: Bundling with Webpack...
(省略)
ERROR in C:\Develop\repos\riotz\samples-hello-serverless\src\handler\systems.ts
(3,25): error TS2307: Cannot find module '../../package.json'.

これはpackage.jsonのモジュール定義が存在しないためになります。
このように型定義などを確認してくれるのが TypeScript のありがたいところです。
一方で今回のは若干無茶な実装でした。標準モジュールには存在しませんし、適切なモジュール定義をとってくることも難しいでしょう。さりとてpackage.jsonと何か別ファイルでversionのダブルメンテはしたくないです。。。 ということで、モジュール定義を追加することにします。
ソースディレクトリのsrctypes.d.tsを追加します。.d.tsファイルでしたらとくに命名規則はなくtyping.d.tsもよく見かけます。

コードは以下のようにします。(1行目の Copyright は書き換えてください)

1
2
3
4
5
6
7
8
9
10
/* Copyright 2018 Riotz.works. */

/**
* Module declaration for JSON file.
*/
declare module '*.json' {

/** version element of package.json using Version API. */
const version: string | undefined;
}

*.jsonなので、モジュール定義が見つからなかった JSON ファイルの型定義になります。
その中にversionが文字列として存在することを定義しますが、これはすべてての JSON ファイルに当てはまることではないので、型定義はstring | undefinedとしドキュメントで注釈しました。
頑張るとpackage.json固有のモジュール定義もできたのですがversionを取得するだけにしては複雑になりすぎたので、このあたりを落としどころとしました。

最後にserverless.ymlへ Version Web API の Lambda 定義を追加します。
functions: greetings:の下に追加します。

1
2
3
4
5
6
systems:
handler: src/handler/systems.handle
events:
- http:
method: get
path: version

Version Web API ができました。

デプロイ

AWS 環境へデプロイします。前回のデプロイ と 稼働テスト同様に、AWS のプロファイを参照してserverless deploy --aws-profile [profilename]を実行します。

endpointshelloversionの URL が出力されるので、それぞれブラウザでアクセスすると、プログラムした JSON が返ってきます。

ソースコード

今回作成した部分までのソースを GitHub へアップしました。
https://github.com/riotz-works/samples-hello-serverless/tree/0.0.2


Hello Web API はボイラープレートのレスポンスを改修しただけだったのでとくに大きな変化はありませんでした。一方で Version Web API は新しく追加となりましたし、両方のプログラムを配置するためにプロジェクトの構造も変化させました。

Hello と Version で実装レベルが異なっている部分はありますが、これについては次の記事で確認しながら合わせこみをしていきます。できれば TypeScript のコンパイラ設定や Lint にまでふれたいです。

Serverless Framework と TypeScript でサーバレス開発事始め

前回の記事から時間が空いてしまいましたが、ちょうど新しいプロダクトの開発を始めるところなのでServerless FrameworkTypeScriptで開発する際の事始めについてまとめます。
例によって Hello World になりますが、こんな感じで作り始めをお伝えします。

シリーズの記事

環境
本記事の開発環境は以下となります。

  • Windows 10 64bit
  • Visual Studio Code
  • Node.js 9
  • TypeScript 2.7
  • Serverless Framework 1.26.0

主な実行環境について、今回は準備なので全部稼働まではいきませんが以下を想定しています。
※ Amazon API Gateway などは、Serverless Framework が自動設定するので省略、明示的に使うものとしてリストになります。

  • Amazon S3
  • AWS Lambda

想定アプリケーション

ウェブサイトで、アクセス時に JavaScript で Web API を呼び出し “Hello World” の挨拶を取得します。 まずは動作させるところを目標にシンプルな形で進めます。

ウェブサイトはAmazon S3に HTML を配置し静的ウェブサイトのホスティングを使います。
本格稼働するにはAmazon CloudFrontAmazon Route 53などが必要となりますが、まずは稼働を目指し S3 だけで進めます。

Web API はサーバーレスで作りたいのでAWS Lambdaを使います。
インスタンスやコンテナーの管理が不要で、純粋にアプリケーションの開発やメンテに集中できるのサーバーレスにこだわりたいです。
こちらも本格稼働にあたってはAmazon API Gatewayにカスタム・ドメインを割り当てたりしますが、まずは稼働を目指し自動割り当てで進めます。

開発環境の構築

コーディングにためのエディターを用意します。こちらは手慣れたもので大丈夫です。今回は Visual Studio Code を使いました。
こちらhttps://code.visualstudio.comから取得できます。

続いて Node.js をインストールします。こちらhttps://nodejs.orgからダウンロードしてインストールします。今回は 9.5.0 Current をインストールしました。

プロジェクトの作成

Node.js がインストールできたら、プロジェクトのディレクトリを作成します。
今回はC:\Temp\hello-serverlessをプロジェクトのディレクトリとしました。

続いて Serverless Framework を 導入します。今回は Visual Studio Code の ターミナルを使っていますが、コマンド プロンプト からも同様のコマンドになります。
今回はプロジェクトごとに Serverless Framework を 導入したいので、-gなしでnpm install serverlessとしました。

1
2
3
4
C:\Temp\hello-serverless> npm install serverless
(省略)
+ serverless@1.26.0
added 302 packages in 44.148s

Serverless のモジュールが導入され、node_modulespackage-lock.jsonが作られています。

Serverless の AWS/TypeScript ボイラープレートからプロジェクトのひな形を作ります。
※ 今回はプロジェクト・ローカルにServerless Framework を導入しているためnode_modules\.bin\serverlessコマンドになります。node_modules\.binにパスを通しておくか、めんどくさい場合はnpm install -g serverless-gをつけてグローバルに入れてしまうのも手です。

1
2
3
4
C:\Temp\hello-serverless> node_modules\.bin\serverless create --template aws-nodejs-typescript
(省略)
Serverless: Successfully generated boilerplate for template: "aws-nodejs-typescript"
Serverless: NOTE: Please update the "service" property in serverless.yml with your service name

Node.js/npm プロジェクト関連のファイルが追加されます。

package.jsondevDependenciesに Serverless Framework が入っていないので、もう一度--save-devを付けてnpm install --save-dev serverlessを実行します。
※ 最初から--save-devしないのはpackage.jsonが作られているとボイラープレートが展開できないためです。

他のモジュールもインストールするため、npm installを実行します。

プロジェクトの設定

自動生成された設定が入っているので、プロジェクトに合わせた設定を行います。

package.jsonは、最小限namedescriptionauthorあたりを変更しておきます。
また、作成しているのはアプリケーションなのでモジュール公開の防止のために"private": trueを設定しておくとよいでしょう。privateの詳細はこちらpackage.json | npm Documentationをご確認ください。

serverless.ymlは、service: name:の値を変更しておきます。

tsconfig.jsonlibesnextを追加します。
2018年2月現在、これを追加しておかないとビルド時に参照しているモジュールで以下のようなエラーが発生するためです。

1
2
3
4
5
6
7
C:\Temp\hello-serverless> node_modules\.bin\serverless package

ERROR in C:\Temp\hello-serverless\node_modules\@types\graphql\subscription\subscribe.d.ts
(17,4): error TS2304: Cannot find name 'AsyncIterator'.

ERROR in C:\Temp\hello-serverless\node_modules\@types\graphql\subscription\subscribe.d.ts
(29,4): error TS2304: Cannot find name 'AsyncIterable'.

プロジェクトのビルド

serverless packageコマンドでビルドします。

1
2
3
4
5
6
7
8
9
C:\Temp\hello-serverless> node_modules\.bin\serverless package
Serverless: Bundling with Webpack...
ts-loader: Using typescript@2.6.2 and C:\Temp\hello-serverless\tsconfig.json
Time: 2085ms
Asset Size Chunks Chunk Names
handler.js 2.94 kB 0 [emitted] handler
handler.js.map 3.18 kB 0 [emitted] handler
[0] ./handler.ts 350 bytes {0} [built]
Serverless: Packaging service...

TypeScript がコンパイルされ、JavaScript とソースマップが作られ、.serverlessディレクトリに AWS CloudFormation のテンプレートと Lambda のデプロイ Zip が作られます。

デプロイと稼働テスト

ビルドに成功したらserverless deployコマンドで AWS 環境へデプロイします。
デプロイにあたっては AWS IAM ユーザー のアクセス・キーが必要となります。あらかじめ作成アクセス・キーを取得し、%USERPROFILE%\.aws\credentials(e.g.C:\Users\username\.aws\credentials) に記述しておきます。
IAM のアクセス権はPowerUserAccessでは足りず IAM 周りの権限追加が必要です。まだ絞り込みきれておらず結局AdministratorAccessを使ってしまっています。頑張らないと。。。

1
2
3
4
[hello]
region = us-west-2
aws_access_key_id = ZDEJREC4D1GM3DXXXX
aws_secret_access_key = ce4FH4Dcyh5DGh2dDGG4hDk6DYfhDAjrFhXXXX

デプロイを実行します。
serverless deploy --aws-profile hello--aws-profilecredentials[]で指定したプロファイル名を指定します。

1
2
3
4
5
6
C:\Temp\hello-serverless> node_modules\.bin\serverless deploy --aws-profile hello
(省略)
endpoints:
GET - https://ca3kg0hrXX.execute-api.us-east-1.amazonaws.com/dev/hello
functions:
hello: hello-serverless-dev-hello

無事にデプロイできると自動生成された API Gateway のエンドポイントが出力されます。

ブラウザでアクセスすると、handler.tsの実装の通り「Go Serverless webpack (TypeScript) v1.0! Your function executed successfully!」のメッセージと HTTP リクエストの内容が表示されます。

お掃除

serverless removeで AWS 環境をクリーンアップできます。

1
2
3
4
5
6
7
C:\Temp\hello-serverless> node_modules\.bin\serverless remove --aws-profile hello
Serverless: Getting all objects in S3 bucket...
Serverless: Removing objects in S3 bucket...
Serverless: Removing Stack...
Serverless: Checking Stack removal progress...
..........
Serverless: Stack removal finished...

ソースコード

今回作成した部分までのソースをGitHub へアップしました。
https://github.com/riotz-works/samples-hello-serverless/tree/0.0.1


初期設定に多少の作業がありましたが、Serverless Framework を使うことで簡単にデプロイとクリーンアップができました。

アプリケーションとしては Lambda でメッセージを返すレベルではありますが、インフラ面を考えると、これをしべて AWS マネジメントコンソールではかなりの手間がかかります。それを Serverless Framework が補ってくれるので、サーバレス開発のインフラ運用が削減できるメリットに加えて、インフラ構築も格段と楽になるのではないでしょうか。

また Try & Error を繰り返すうちに AWS 環境がついつい散かってしまい、よくわからないリソースを放置されるケースが出てきたりします。開発環境とはいえ Serverless Framework で環境を作るようにすると片付けも簡単ですね。

引き続き、このプロジェクトに設定やコードを加えてもう少し発展させていきます。

CSV ファイルのバッチ連携も AWS Lambda でサーバーレス

この記事はServerless Advent Calendar 2017の 7日目になります。

Serverlessconf Tokyo 2017で発表させていただく機会をいただき、Java チームが選択した TypeScript による AWS Lambda 開発のタイトルでお話をさせていただきました。

今回はそのシステムの裏手側、CSV ファイルを連携するバッチをサーバーレスで実現したアーキテクチャについてご紹介します。

システム全体の概略図

ざっくりと下図のようなシステムで AWS 上に作られています。
スマートフォンなどから IoT 機器を操作するようなクラウド・サービスがあり、そのバックグラウンド・プラットフォームになります。認証や機器のデータ管理を行うシステムです。
このシステムは、図のAmazon API GatewayAWS Lambdaが、ペアになっているアイコン毎が1つ1つのマイクロサービスとして作られています。つまり 10を超えるマイクロ・サービスの集合体になります!

今回の話は、この図の右上部分 “Traditional DC”、企業の伝統的なデータセンターからデータを受ける部分です。
この部分は伝統とシキタリにのっとり CSV ファイルを受け取り、処理する仕組みが必要となります。

※ CSV の C は Comma と信じてましたが、Character や Column 説 (e.g.Wikipedia) があり広義には Comma に限らず特定の文字でデータを区切るファイルらしい。どうでもいいけど。。。

なぜ AWS Lambda ?

このようなシステムを AWS で実装するには、AWS BatchAWS Glueなどを使います。しかしながら、今回は Lambda で実装することにしました。

背景としては、以下のような点があります。

  • 少人数の DevOps チームであり、AWS のマネージド度合いが高いほど嬉しかった
  • データ量や処理量から想定してコスパが優れていた
  • 1回に連携するデータ量が Lambda の処理時間におさまる可能性が持てた

とくに2番目のコストについては、Glue に対して圧倒的によかったのが大きかったです。今回の処理は ETL に近いとはいえ、DWH 連携ではなく、DB のマスター・データ登録だったので Glue を活かしきれない点がありました。

なお Lambda の実行ランタイムは Java になります。
(Serverlessconf Tokyo 2017 の発表資料の通り、Java チームなので)

CSV ファイル連携のアーキテクチャ概要図


“Traditional DC” からは、Amazon S3に CSV を置いてもらうことにします。FTP/SFTP とのリクエストがありましたが、サーバーレス厨とは言わずとも、ここは S3 へのアップロードへお願いしたいところ、何としてでも承諾いただきます。(いただきました)

S3 からはAmazon SNSへイベント通知をし、Lambda を起動します。その Lambda で CSV をパースして DynamoDB へデータを登録します。

複雑なデータの加工や外部の API/データを参照するような場合は、DynamoDB Streams から Lambda を呼び出して後続の処理として実装します。このようにすることで、データの登録に特化させて処理数を稼ぐことができ、また DynamoDB Streams の後続処理は、登録されたデータ1件ごとの処理になるので時間的な余裕が生まれます。

ここでのポイントは S3 ⇒ SNS からの Lambda は DynamoDB への登録だけに特化し、他の処理をしないことです。このようにすることで、なるべく CSV ファイルの対応に処理を振り分けるようにし、時間内で処理できる量を増やすようにしています。

処理能力とアーキテクチャの改良

このような形にすることで、CSV を Lambda で処理できるようになります。
このアーキテクチャの処理能力は、CSV ファイル 1行のデータ量や、カラム数によって変わってくる部分がありますが、今回のケースでは 1ファイルで約 800行のレコードが処理できました!
て、まったく処理できてません。。。

これでは役に立たないので対応が必要となります。
Lambda は設定するメモリの使用量によって処理速度が変わるという話もあり、最大に設定してみましたが設定による差はとくにありませんでした。ファイルを読み込みながらの順次処理なので、ここでの処理性能はあまり変わらないのでしょう。また DynamoDB へアクセスするオーバーヘッドも処理性能では大きく変わらないはずです。

では、どうするのか。


S3 に置かれたファイルを DynamoDB へ登録する前に、ファイル分割する処理をはさみます。
1回の Lambda で DynamoDB へ登録できるのは余裕を見て 500件までとして、CSV ファイルを 500行ごとのファイルに分割して S3 へ再アップロードします。その 500行ごとのファイルを Lambda で DynamoDB へ登録する形にしました。

これにより、30,000行まで処理できるようになり現実的なラインまで持っていけるようになりました。S3 からの Lambda に変わりはないので、サーバーレスの形態も維持できます。

まとめ

サーバーレスでシステムを作ろうとしても、往々にして発生してしまう既存システムとのバッチ連携。そのために FTP/SFTP やら、CSV パースするための処理やらとどうしてもサーバ・インスタンスが欲しくなってしまい、せっかくのサーバーレスがサーバー有り(サーバーレスの反対語って何だろう)になってしまいます。

Glue に見合う処理であればよいのですが、今回のように小規模なデータ登録となるとオーバースペック気味にもなります。そんな時には S3 からの Lambda で処理することで、サーバーレスでバッチ連携をするのも手ではないでしょうか。

ポイントとしては以下になります。

  • ファイルは S3 に置いてもらう
  • S3 ⇒ SNS から Lambda を起動し DynamoDB へデータ登録に特化する
  • 1回の Lambda で処理できるデータ量は少ない、必要に応じてファイル分割の Lambda をはさむ

Serverless Advent Calendar 2017、楽しく、そして勉強になる記事がたくさんです。

昨日6日目は@RyoheiMorimotoさんのAWS LambdaをTest Runnerとして使ってみるでした。 ユーザ管理のシステムをサーバーレスで 34歳タローさんがハナコさんになれるかをテストしています。サーバーレス・テスト、おもしろいですね!

明日8日目は@narikeiさんです。何の記事か楽しみです!


では、ハッピー・サーバーレス・ライフ! ハッピー・ホリデー!!

Hello World !!

こんにちは、世界!
こんにちは、みなさん!!

わたしたちは Riotz.works です。

“a cheerful engineering team” を標榜する、わたしたちは
IT を中心に技術的なことやさまざまな挑戦が大好きで陽気で愉快なエンジニアの集団です。

これまで学んできたこと今挑戦していること、先々やってみたいことなど、をおもしろおかしく、まるでお祭り騒ぎのようにアウトプットしていきたい、そんな思いから『Articles | Riotz.works』の場を作りました。

またエンジニアにとっての最初の挨拶は、何といっても “Hello World !” ですね。
ということで最初のポストのタイトルも “Hello World !!” にしました。

この記念すべき Hello World はプログラム言語でいうと、どんな感じだろうかと「Hello worldプログラムの一覧 - Wikipedia」を眺めていたら、これかな!

1
import __hello__

Python のコードで、”プログラマーに必要な機能をすべて標準ライブラリーとして提供する「バッテリー同梱 (batteries included)」の思想を具現化するジョーク -Wikipedia」なのだとか。

さすがに「すべて」とまではいかないですが、エンジニアにとって必要なものがたくさんアウトプットされている場にしていきたいと考えています。

わたしたちのアウトプットがわずかでもお役に立てられれば、勉強させていただいているコミュニティへ少しでも還元できれば、そして新たな出会いと挑戦につながればと考えております。

どうぞ、よろしくお願いいたします!!

共有: