• 加筆
  • 2018.01.28
  • 修正
九 月
11
月曜日

ClojureScriptでLambd@Edge

『打刻システムの構築(前編)』 『打刻システムの構築(後編)』 では打刻APIを作成し、APIが受け取った打刻イベントをSNSのトピックにパブリッシュ。トピックのリスナーであるLambda関数 dakoku-google-sheets-writerGoogle Sheets へ打刻イベントを記録する仕組みを構築した。

記録される時刻はdakoku-google-sheets-writer内で取得した現在日時を利用しているが、この方法ではトピックに新たなリスナーを追加した場合、同一打刻イベントに対して各リスナーがそれぞれに現在時刻を取得する事になり塩梅が悪い。

各リスナー毎に時刻を取得するのではなくAmazon SNSイベントのTimestampプロパティを参照すれば良いのだが、これとは別に以前から

を使う機会を窺っていたのでこれを機に利用する。

内容としては打刻APIの前面、CloudFrontにClojureScriptで書いたLambda@Edgeを配置。
APIの呼び出し時にLamda@Edge側で現在時刻を取得し HTTPヘッダ X-Dakoku-Timestamp にセット、API Gatewayで同ヘッダの値をSNSのURLクエリ文字列パラメータSubjectに載せ替え打刻トピックへパブリッシュ。 後続のリスナーはパラメータSubjectから打刻された時刻を取得するように変更する。

少々というか、かなり回りくどい事になっているがLambda@EdgeのHello Worldのようなサンプルを作るより実際に自分が日々利用する処理に適用した方がモチベーションもあがるので良しとしよう。


Lambda@Edgeについて

CloudFrontイベントをエッジロケーションで処理することができるLambda関数。通常のLambda関数と比べて厳しい制約がある。

制約

特に意識する必要がある制約は以下。

  • Node.js 6.10のランタイムのみ
  • メモリは最大128MBまで
  • Origin (request / response) eventの処理は3秒以内で完了
  • Viewer (request / response) eventの処理は1秒以内で完了
  • Viewer (request / response) eventの処理内でS3/DynamoDB等のネットワーク呼び出しは不可
  • Lambda関数のzipパッケージは1MB以内
  • 環境変数は利用不可

全ての制約についての詳細は CloudFront開発者ガイド を参照のこと。

リージョン

CloudFrontにはリージョンの概念は無いが管理上米国東部(バージニア北部)に属するためLambda関数とアップロード用のS3バケットについても米国東部(バージニア北部)リージョンである必要がある。

Lambda関数を作成

dakoku-request-rewriterを作成。

先の制約にあったようにLambda@Edgeのランタイム環境はNode.js(6.10)のみではあるが、 ClojureScriptはデフォルトでES3のコードを出力するため問題なく実行することができる。

コード

入力となるCloudFrontイベントを一度clojureのデータ構造に変換したのち、ヘッダ X-Dakoku-Timestamp を追加。その後にJSオブジェクトに戻し処理を続行している。

(ns jp.nijohando.dakoku.request-rewriter.lambda
  (:require [cljs.nodejs :as nodejs]))

(defn- now
  []
  (.now js/Date))

(defn- rewrite
  [event]
  (-> (get-in event [:Records 0 :cf :request])
      (update-in [:headers :x-dakoku-timestamp] concat [{:key "X-Dakoku-Timestamp" :value (str (now))}])))

(defn handler
  [event context callback]
  (->> (js->clj event :keywordize-keys true)
       (rewrite)
       (clj->js)
       (callback nil)))

(nodejs/enable-util-print!)
(aset js/exports "handler" handler)

関数handlerを同名でexportしている。
ClojureScriptのビルド結果ファイルがindex.jsであればLambdaを登録する際のハンドラ名はindex.handlerとなる。

ビルド&パッケージ

lein-cljsbuildを使わずにcljs.build.api を直接使ってビルドしている。
またパッケージングは自前でZipアーカイブを作成している。
今回はnpmエコシステムに依存していないが、依存する場合はnode_moudles配下を含める必要がある。

パッケージのサイズは最適化レベル :advanced で9kバイト弱となった。
Lambda@Edgeの制約としてパッケージサイズを1MB以内に納める必要があるが、サイズについては本体コードのサイズ、最適化レベル等よりも依存するnpmパッケージ量が大きな要因となりそうだ。

REPL

今回課題だったのがREPL環境。
REPL自体はcljs.repl/repl*を直接使いNode.js環境のREPLを問題無く利用する事ができたのだが、 肝心のREPL駆動開発時におけるコードのリロード方法のセオリーが分からなかった。
(tools.namespaceFigwheelのような)

ということで今回は致し方なく素朴にrequireをreloadフラグ付きで利用した。

2017/01/28 追記
『ドアの動き検知システムの構築3』にてClojureScriptでLambdaを書く際にFigwheelを試した。

S3バケットの作成

Lambda@EdgeをアップロードするためのS3バケットをに米国東部(バージニア北部)に作成する。
AWSマネージメントコンソールから S3 を開き、バケットの作成

バケット名
dakoku-edge.nijohando.jp
リージョン
米国東部(バージニア北部)

作成 でバケットを作成。

Lambdaの登録

AWSマネージメントコンソールから Lambda を開く。
リージョンを 米国東部(バージニア北部) に切り替え、関数の作成 から 一から作成

トリガーの追加 で何も指定せずに 次へ

名前
dakoku-request-rewriter
ランタイム
Node.js 6.10
コード エントリ タイプ
Amazon S3からのファイルのアップロード
S3リンクのURL
https://s3.amazonaws.com/dakoku-edge.nijohando.jp/dakoku-request-rewriter.zip
ハンドラ
index.handler
ロール
テンプレートから新しいロールを作成
ロール名
dakoku-request-rewriter
ポリシーテンプレート
基本的なエッジLambdaのアクセス権限

次へ 関数の作成

関数を登録したら アクション -> 新しいバージョンは発行 で現在のLambda関数のバージョンを固定する。
Lambda@EdgeではCloudFront側からLambdaを参照する際に$LATESTやエイリアスによる参照ができないためバージョン付きのARNで参照する必要がある。

バージョン1が発行されるのでこのバージョンのARNをメモしておく。
(次のCloudFront側からのLambda関数の参照時に利用する)

CloudFrontイベントとLambda関数を紐付ける

AWSマネージメントコンソールから CloudFront 、打刻用ディストリビューションを選択。
Behaviors から events-ifttt-<任意の英数字の組み合わせ>Default (*) の各Behaviorに対して、

Edit

Lambda Function AssociationsEvent Type
Origin Request
Event Type
Origin Request
Lambda Function ARN
先ほど作成したLambda関数dakoku-request-rewriterのバージョン1のARN

Yes, Edit で保存。

API Gateway

API GatewayにてX-Dakoku-Timestampヘッダの追加とURLクエリ文字列パラメータSubjectへのマッピングを行う。

HTTPヘッダの追加

AWS マネージメントコンソールから API Gateway を開き、API dakokuevents-ifttt リソース、POSTメソッドの メソッドリクエスト を選択。
HTTPリクエストヘッダ から ヘッダの追加

名前
X-Dakoku-Timestamp
必須

HTTPヘッダをクエリ文字列にマッピング

events-ifttt リソース、POSTメソッドの 統合リクエスト を選択。
URL クエリ文字列パラメータ から クエリ文字列の追加

名前
Subject
マッピング元
method.request.header.X-Dakoku-Timestamp

で適用。

APIのデプロイ

API Gatewayの変更を本番環境へ反映するためにデプロイを行う。

API dakoku を選択し、Action から APIのデプロイ を実施。

デプロイされるステージ
prod

デプロイ で反映。

最後に

以上で打刻API呼び出し時にLamdadakoku-request-rewriterが実行され打刻時刻が後続の打刻トピックリスナーへ伝播するようになった。
次回の『打刻情報をKinesis FirehoseでS3へ保存』ではこの仕組みに乗っかり打刻情報をKinessis Firehoseを経由してS3に蓄積する打刻トピックリスナーを追加する。