Rのつく財団入り口

ITエンジニア関連の様々な話題を書いているはずのブログです。

【感想】『AWSによるサーバーレスアーキテクチャ』:前編

AWSによるサーバーレスアーキテクチャ

 先週は 以下の記事が何故かはてブのホットエントリ入り、多数のアクセスありがとうございました。著者の方からも反応いただいてありがたい限りです。

【感想】『Amazon Web Servicesを使ったサーバーレスアプリケーション開発ガイド』:Lambdaで本格サービス開発まで - Rのつく財団入り口

自分的に勝手にサーバーレス強化月間をやっていたので読んだ本だったのですが、その続きの3冊目、締めで読んだ本がこの『AWSによるサーバーレスアーキテクチャ』。著者は技術カンファレンス「Serverlessconf」の責任者でもあるPeter Sbarskiさん、サーバーレスの第一人者による書籍です。

AWSによるサーバーレスアーキテクチャ

AWSによるサーバーレスアーキテクチャ

  • 作者:Peter Sbarski
  • 発売日: 2018/03/14
  • メディア: 単行本(ソフトカバー)

第1部 導入

第1章 サーバーレスの世界へ

f:id:iwasiman:20210306162532p:plain
AWSによるサーバーレスアーキテクチャ

1.1 ここに至るまでの流れ

 まずはサーバーレスの考え方そのものから。

  • 最初は管理に時間がかかるサーバーに載せたWebアプリケーションが一般的。
  • 次に、背後のインフラの一部を隠蔽してソフトウェアを実行できるPaaSが登場。しかしPaaS側に合わせるために余分な開発がいたりする。
  • そしてコンテナ技術登場。アプリを環境ごと分離できる。だがコンテナの管理はまだ必要。
  • 最後にやってきたのがLambdaのような技術。サーバーに直接アクセスしなくても仕事ができる。サーバーレスの究極の目標は開発者がサーバーやインフラを気にせずコードに全力を注げるようになること。

 よく関連して語られるSOAやマイクロサービスについても、本書では関連を述べています。

  • 特定テクノロジーに縛られず、メッセージのやりとりで通信し、メッセージの作成/交換の方法を定義したスキーマや契約(コントラクト)を持つのがサービス指向アーキテクチャ(SOA: Service Oriented Architecture)
  • マイクロサービスとサーバーレスはこの精神を受け継いでいるがイコールではない。マイクロサービスもサービスごとに多数の技術が混ざり合っていると混乱に陥る。トランザクション管理やエラー処理など難しい場合もある。
  • サーバーレスはマイクロサービスの原則の多くを体現しているが、すべての教義を守る必要はない。どこまで取り入れるかはお好み次第

 それぞれイコールではないけど部分的に重なり合っているわけですね。

また従来の3層アプリケーションでは階層を増やすとどんどん複雑になっていくのを例に取り、サーバーレスのアプローチを正しく使えば複雑さを軽減、変更容易性を保てる…という話で階層の用語の混乱の話が出てきます。

  • ティア(tier): 大きなコンポーネントを分離するモジュール境界。プレゼンテーションティア、アプリケーションティア、データティアの3層が基本。物理的には別インフラの場合もある。(DBサーバーを分けたり)
  • 階層、レイヤー(layer): アプリケーションの中の特定の機能を実行する論理的な断片。1つのティアの中に複数の階層。プレゼンテーション層の中にUIコンポーネント、プレゼンテーションロジック…。アプリケーション層の中に、ドメイン、ビジネスエンティティ、データアクセス層…などなど。

 これは自分もごっちゃにして使っていたかも...! でも設計を扱った書籍やWeb情報などでも同じに扱っていることがあるような...?

1.2 サーバーレスアーキテクチャの原則

原則は5つと述べています。

  1. コンピューティングサービスを使ったオンデマンドのコード実行:カスタムコードは粒度の細かい関数として記述、単独で独立して実行。

  2. 目的が1つでステートレスな関数:単一責任原則(SRP: Single Responsibility Principle) に従って設計する。一つの仕事をする関数はテストしやすく副作用も少ない。またステートレスなので、一時ファイルなどのリソースやプロセスを再利用してはいけない。ステートレス→スケール可能。とても強力。

  3. プッシュベースのイベント駆動パイプライン:イベント駆動で、ある関数が仕事をしたら通知を受けて次の関数やサービスが仕事を…と繋げていく。コストが下がり複雑度が軽減。ただし必ずしも適切出ない場合もあり、Lambdaがポーリングしたりする場合もある。

  4. より厚く強力なフロントエンド:Lambdaは使った時間だけ課金。すべてを一旦バックエンドに飛ばすのでなく、フロントエンドから各種サービスと直接API通信してよい。しかし秘密情報の処理や整合性が重要な処理ではLambdaを使ったほうがよい場合もある。

  5. サ ードパーティサービスの活用:使えるサービスやAPIがあったらどんどん利用して「巨人の肩の上に立つ」。

1.3 サーバーからサーバーレスへの乗り換え

少しづつ移行できるのはサーバーレスの長所。プロトタイプを作って試すと良い。妥協を強いられる場合もあり、ゆっくりと慎重に進むべき…と述べています。

1.4 サーバーレスの長所と短所

 短所が以下。

  • 万能ではない:ミッションクリティカルなケース(銀行や病院)ではLambdaで作るべきではないときもある。
  • サービスレベル:AWSSLAを用意しているものとしていないものがある。
  • カスタマイズ:Lambdaの場合、メモリ割り当ては変えられるがOSのカスタマイズはできない。他のサーバーレス系サービスもカスタマイズの柔軟性はまちまち。
  • ベンダーロックイン:アーキテクチャがそのプラットフォームに密結合する危険性はある。
  • 脱中央集権化:モノリシックからサーバーレスの分散アーキテクチャに移行しても、もともとのシステムの複雑さが自動的に軽減されるわけではない。リモート呼び出し特有のエラー処理の複雑さやレイテンシー増加などの固有の問題はある。

 長所が以下。

  • サーバー不要:管理、運用のコストを有能な外部の会社に任せられる。
  • 多くの用途がある:ステートレスでスケーラビリティが高いので、並列処理が効果的である様々な問題解決に使える。適切なテクノロジーを組み合わせれば開発速度も圧倒的に速くなり、速いペースで前進したい企業に特に有効。
  • 低コスト:負荷のピークが予想しづらい状況でも無駄なコストが掛りにくい。
  • コード量の削減:フロントエンドの仕事を増やしてサービスと直接通信する構成で、大きくて階層がたくさんあるバックエンドのコードが減る。
  • スケーラブルで柔軟:バックエンドをサーバーレスに置き換えたくない場合はそれでもよいし、並列化のメリットがある箇所をLambdaにしてもよいし、自由。スケーリングも簡単。

作者のピーター・スバースキさんはServerlessconfのオーガナイザーでもあり企業のVice President、コンピューターサイエンスの博士号取得。同じく大学の博士が序文、翻訳の方も日本のServerlessコミュニティの方...と錚々たる顔ぶれだけあって、文章は非常に読みやすく学びになります。

serverlessconf.io tokyo.serverlessconf.io

第2章 アーキテクチャとパターン

 どのような場面でどんな設計をしていくのがよいかの章。

2.1 ユースケース

以下のような場面で役に立つとしています。

  • アプリケーションのバックエンド:Webアプリ、モバイル、そしてIoTアプリにも向いている。IoT向けサービスのAWS IoT Coreもあり。
  • データ処理:CSVJSONXMLなどの変換、動画のトランスコードなども。S3+Lambdaで実現できる。
  • リアルタイム分析:Kinesis Data Streams+Lambdaが強力。
  • レガシーAPIプロキシ:API GatewayのRESTfulなインターフェースを読んで、中でリクエストを変換して古いサービスを呼んだりも可能。
  • スケジューリングされたサービス:定期実行される処理もLambdaで実現できる。
  • ボットとスキル:Slack, Skype, Facebook, Amazon Echoなど。

aws.amazon.com

2.2 アーキテクチャ

 アーキテクチャの大きな分け方としてはバックエンドとグルー(glue, のり, ワークフロー実行のパイプライン)があるとし、組み合わせて使っていくと述べています。
本書の作者さんらによるサービスのA Cloud Guru のグールー(Guru)は「導師」のような尊称なのでちょい注意ですね。

バックエンド:
一般的なアプローチ。原則の「分厚いフロントエンド」と「サードーパーティの活用」が重要になる。フロントから直接APIを通して認証や検索のサービス、データベースとやりとりしてもよい。フロントでやってはいけない仕事はバックエンドのLambdaにする。Lambda関数の粒度を適度に分けるのも大事。

acloudguru.com unless.com

レガシーAPIプロキシ:
API Gatewayの中でリクエストを変換して他のサービスを呼んだりできる。複雑な変換はできないので一度Lambdaを介するケースもある。Node.jsには入り口が古いSOAP形式のサービスとも変換できるライブラリがあったりする。

ハイブリッドシステム:
二者択一ではないので、レガシーなシステムの一部にサーバーレステクノロジーを導入することもできる。一部の機能をAPI Gatewayを介してLambdaで行うなど。ゆっくりとサーバーレスに移行することも可能。

GraphQL
Facebook製、RESTに変わるものとして設計されたデータクエリ言語。ひとつのLambda関数がGraphQLを担当して複数のデータソースにクエリを送ることができる。2017−2018より登場したモバイル用のAWS AppSyncも検索にGraphQLを使っている。

グルー:
「Compute-as-glueアーキテクチャと呼ぶ。サービスとサービスの間の糊(グルー)をLamda関数が受け持ってイベント駆動型のパイプラインが作れる。S3のファイル操作、SNSの通知、Lambda自力のポーリング…など各イベントごとに小さなLambdaがそれぞれの仕事をして、組み合わさって複雑なタスクも比較的簡単に実現できる。

リアルタイム処理:
ログ、イベント、トランザクションソーシャルメディアからの入力など膨大なデータをKinesis Data Streamsga処理できる。API Gatewayの後ろにおけるし、クライアント側やLambda関数からもデータ追加可能。

2.3 パターン

本書では「ソフトウェア設計の問題に対するアーキテクチャ上の解決策」だとして、サーバーレス以前からあるもの含めていくつか紹介しています。

Command パターン:
リクエストの違いに基づいてクライアントをパラメータ化したり、キューイング、ロギング、取り消し操作をサポートするためにリクエストをオブジェクトにカプセル化すること。GoFデザインパターンにもある。
サーバーレス的にはAPI Gatewayを介して1つのLambdaにリクエストが飛んでくるとその中のオブジェクトに命令が入っており、命令に応じてそれぞれ別のLambdaを呼び出したり…と分岐できる。Restful URIをいちいち作らなくてもよくなる。ただし検索などで使うと複数のLambdaが入る分、レイテンシーが増す恐れもある。

qiita.com

Messaging パターン:
関数やサービスの直接の相互依存関係をなくし、イベントやレコード、リクエストをキューに格納するパターン。分散システムやマイクロサービスでおなじみ。密結合が減って将来の変更も楽になる。
複数のデータソースからやりたいことがSQSKinesis Data Streamsのキューにどんどん貯まり、ディスパッチ用のLambda関数が定期実行でキューを見にい行き、それぞれの命令によって別のLambda関数を呼び出して処理。SQSSNSトピックにもサブスクライブできるので、SQSにキュー追加→SNSトピックに通知→見ているすべてのLambda関数が一斉起動…という展開もできる。
ここでLambda関数がまた別のキューに登録→別のディスパッチ用のLambdaが定期的に見に行って…という設計もできる。

Priority queue パターン:
処理の優先度をAWS任せでなく自分で決めたいとき。すぐ処理したいものはコストが高いサービスに渡して優先順位1のSNS/SQSへ、そうでないものは優先順位2の別のSNS/SQSへ…などと分けていく。有料ユーザーと無料ユーザーで処理方法を分けたりするときに使える。複雑度が増すので控えめにしたほうがよい。

Fanout パターン:
S3にオブジェクト登録のイベント発生→Lambda起動 だとひとつしか起動しない。CommandパターンでこのLambda関数が別のLambda群を呼んでいくこともできるがコードが必要になる。
こんな時はイベント発生→SNSのトピックにメッセージ追加→サブスクライブしているLambda関数群が一斉起動、と処理できる。扇形に広がっていくので「ファンアウト」と呼ぶ。並列に実行していくイベント駆動アーキテクチャで効果的。

Pipes and filters パターン:
データの変換を目的とするコンポーネントがフィルタ、フィルタとフィルタの間で受け渡すのがパイプ。粒度の細かいLambda関数をフィルタとして順番に処理していくことで、複雑なタスクを分解してそれぞれを別のサービスとして整理して扱うことができる。全体がひとつの関数のようなイメージ。
Lambda関数は単一責任原則、冪等に注意して作っていく。このパターンがアーキテクチャとして大きくなったのがCompute-as-glueアーキテクチャ

 図も多く、実際のサービスの事例もあり分かりやすく読むことができました。

第3章 サーバーレスアプリケーションの構築

 今度は具体的に、YouTubeライクな動画投稿サービス「24-Hour Video」の構築を通してサーバーレスを体感していく章。

3.1 24-Hour Video
  • 画面が投稿されるとS3バケット
  • 登録をトリガにして第1のLambda関数が起動、Amazon Elastic Transcoderというサービスを呼び出して動画を変換、別のS3バケット
  • この登録をトリガにしてSNSトピックに通知。同時にメール配信。
  • SNSをトリガにして第2のLambda関数が起動、S3バケットに上がった動画のアクセス権限変更
  • 同じくSNSをトリガにして第3のLambda関数が一緒に起動、動画のメタデータを算出して別のS3バケットにファイルとして保存

という流れです。Elastic Transcoderは若干料金がかかるが他のは無料枠に収まるということでうまく考えてあります。

Lambda関数は本書ではJSで、第1の関数が以下のような感じ。

exports.handler = function(event, context, callback) {
  // いろいろ省略
  let params = {
   PipelineId: {使用開始したElastic TranscoderのID}, 
   OutputKeyPrefix: {出力フォルダ},
   Input: { Key: {入力ファイル名} },
   Outputs: [
     {出力のプリセット群}
   ]
  };
  elasticTranscoder.createJob(params, function(error, data) {
    if (error) {
      callback(error);
  });
};

 2018年の本なので原文のコードだと変数宣言がvarなのが若干気になりますが、コールバック関数がある以外はPythonとそれほど変わらずでした。
 またJavaScriptではおなじみのnpmには run-local-lambda というモジュールがあり、package.json の中に実行用のスクリプトを書いたり入力引数のeventの中になる値をJSONで書いておいて実行させたり、AWSへの関数デプロイもコマンドで出来たりするそうです。これは便利そうです。
S3側でのObjectCreatedのイベントソースでのこの関数を設定していくのは、画面からいつもどおり。

 Elastic TranscoderのIDを取得するところで入力のS3バケットが一意になってLambdaコード内では不要なのは分かりましたが、出力先のS3バケットがどこで決まるのかがわかりませんでした。これもElastic Transcoderで自動的に決まるのかな?

3.2 Amazon SNSの設定
  • SNSでトピックを作り、条件でトランスコード済み動画が置かれるS3バケットのARNを設定。
  • S3バケットのプロパティのイベントで、ObjectCreateのイベントが起こったら上のSNSトピックに飛ぶよう設定。
  • SNSの設定でメールも飛ぶようにする。
3.3 動画ファイルのアクセス権限の設定

アクセス権限を変える第2のLambda関数が以下のような感じ。

  // いろいろ省略
  let params = {
    Bucket: {バケット名},
    Key: {キーのファイル名},
    ACL: 'public-read'
  };
  s3.putObjectAcl(params, function(err, data) {
    if (err) {
      callback(err);
    }
  });

 アクセスコントロールリストの変更をコードから行うのも、分かると難しくないです。

 そして3.2で作った通知用のSNSトピックの設定でサブスクリプションを作成し、届ける先をこの第2のLambda関数にします。

3.4 メタデータの生成

今度は第3のLambda関数で、動画からメタデータを取得するのにffprobeというコマンドラインユーティリティを使います。

// exports.handlerの内部で呼ぶ内部関数で、
// S3バケット内の動画をローカルの/tmpに展開
let file = fs.createWriteStream((/tmp' + {ローカル上のファイル名} );
let stream= s3.getObject({
  Bucket: {バケット名},
  Key: {キーのファイル名}
  }).createReadStream().pipe(file);

steam.on('error', function(error) {
  callback(error);
});
stream.on('close', function() {
  // 展開し終わったら次の内部関数へ
});

// ffprobe ユーティリティを使ってメタデータを抽出
let cmd = 'bin/ffprobe -.....{オプションやファイル名など}';
// exec関数でコマンド実行!
exec(cmd, function(error, stdout, stderr) {
  if (error === null) {
    // エラーがなかったら変数stdoutの中が出力のメタデータなので
    // メタデータ用の別のS3バケットに保存
  } else {
    console.log(stderr);
    callback(error);
  }
});

 使うffprobeユーティリティは予め入手、bin/ ディレクトリに置いてLambda関数と一緒にZIPにして登録するというやり方です。
/tmpに動画を展開しているので、512MBより大きいと失敗することになります。(よく試験に出るやつ...)
あとは3.2で作ったSNSサブスクリプションをもうひとつ作って、送信先をこの第3のLambda関数にします。
全部AWSのサービスでやらなければいけない訳ではなくて、こんな風にふつうのユーティリティを同梱して利用するやり方もあるのですね。

3.5 仕上げ

こうして動画投稿アプリケーションが一式できてしまいました。個々のLambdaコードも短いし、繋がりを理解するとこんなにすぐできちゃうのか…と思います。本の方ではテスト方法もサポートしてあります。

 そして本書で面白いのが章末に「演習問題」があること。読んでいて「このコードは仕事レベルだったら入力チェックもっと厳しくやるよな…」とか「サンプルだから動画の削除機能はないのかな?」とか思うのですが、そうしたところや機能の拡張の話が、正解のない演習問題として記載されています。
学校の勉強だったら時間が余った人はチャレンジしてみましょう…的な位置づけです。読者に自分で考えさせながら学ばせるこのやり方はよいですね。

第4章 クラウドの設定

 AWSに的を絞ってセキュリティ、ログ、アラートなどをおさらいしていく章。

4.1 セキュリティモデルとID管理

ここはお馴染みIAMの話。

  • 人間と結びつくのが内部でARNを持っているIAMユーザー。MFA(多要素認証)でセキュリティを強くしておくのがオススメ。
  • IAMユーザーが集まったものがIAMグループ。
  • ユーザー、アプリケーション、サービスに一定期間アクセス権限が与えられるのがIAMロール。
  • アクセス権限はIDベースもののと特定のS3バケットなどリソースベースの2種類がある。
4.2 ログとアラート
  • CloudWatch のログの使い方。
  • S3はこれとは別にログを出せる。
  • CloudWatchアラームは一定の閾値を超えると警告のアクションを行う。
  • APIの実行結果を追跡するのがCloudTrail
4.3 料金
  • サードパーティ製のCloudCheckrが使いやすい。
  • AWS Trusted Adviserは無料だと機能制限がある。
  • Cost Explorerを有効化するとしばらく時間が掛かった後、向こう3ヶ月の料金が予測できる。
  • AWSサイトにあるSimple Monthly Calculatorも使える。
  • サーバーレスでは多くの場合従来型より料金は安くなる。AWS Lambdaは最初の100万リクエスト無料、その後は100万リクエストごとに料金。実行時間は100ms単位。

cloudcheckr.com

本書は2018年なので100ms単位と書いてありますが、Lambda関数の実行時間の課金は2020年12月頃より、より細かく1ms単位に更新されていますね。

dev.classmethod.jp

この章はAWSを使っていたり学んでいたりする人ならだいたい知っている話でした。

第2部 コア機能

第5章 認証と認可

Amazon CognitoAuth0を使いながらサーバーレスの認証周りを述べていく章。

5.1 サーバーレス環境における認証
  • CognitoAuth0で認証を楽に実装できるる。
  • ユーザ情報交換のトークンには本書はJWT(JSON Web Token)を使用。
  • API GatewayやLambdaで認証もできる。可能な場合はクライアント側のフロントエンドから直接認証サービスとやりとりするのがよい。
  • 業界で指示されている認証・認可の手段を使う。独自で作りこむのは避ける。

Amazon Cognito

  • クライアントの画面からまずサードパーティのIDプロバイダーにリダイレクト、認証が成功したら委任トークンを返す。
  • クライアントがCognitoを通して委任トークンを使って書き込み。
  • CognitoSTS(Security Token Service)と通信してAWS認証情報が認められたら、そのAWS認証情報をがクライアントに返す。
  • クライアントはこの認証情報を用いて各種アクセス。
  • 足りない機能はあるので、その際はAuth0を使う。

Auth0

  • 多要素認証(MFA)やTouch IDもサポートした万能IDプラットフォーム。
  • AWSともうまく統合する。
5.2 24-Hour Videoへの認証の追加

3章で作ったYouTubeライクな動画投稿サービス「24-Hour Video」に認証機能を追加していきます。以下のような処理の流れです。

  • サービスの画面にサインイン/サインアウト/プロフィールボタンを追加、このアプリケーションをAuth0に追加する。
  • サインインするとAuth0と繋がって認証、成功するとJWTとユーザープロフィールを返す。これはJavaScriptからlocalStorageに保存。
  • サービスの画面からAPI Gatewayと通信する際、このJWTを一緒に送る。
  • API GatewayにはこのJWTの正当性チェックのためにカスタムオーソライザー(≒今の「Lambdaオーソライザー」)として専用のLambda関数を追加。
  • 通過すると本来のLambda関数に来て、飛んできたJWTを使ってAuth0と通信、ユーザ情報を返す。ここの中身は先の章で実装。

  • 実はクライアントにAWS SDKをダウンロードすれば、Lambda関数を直接呼ぶことも一応できる。しかし密結合になってしまい様々な付加機能が使えないので、RESTfulなURLからAPI Gatewayを経由するのがよい。

  • 本質に集中するため、本書のサンプルはバニラな素のJavaScriptjQuery、デザインはBootstrapを使用。
  • まずAuth0で新しいアカウント登録。設定でモバイル/ネイティブアプリ、SPA、Webアプリを選べる。無料使用ではソーシャルIDプロバイダーから選べるのは2つまで。

index.html の画面を修正していきます。

  • 画面にボタンなどを追加。
  • Auth0側のjsファイルをCDNから読み込み追加。
  • 独自のJSとしてconfig.js追加、中に設定としてAuth0の「ドメイン」と「クライアントID」を固定値として保存。
  • 独自のJSとしてuser-controller.jsがそのまま使えるよう本書で提供。userControllerというJSオブジェクトが一式を処理。画面のログインボタンを押すとAuth0側の処理を呼んで認証、成功するとlocalStorageにJWTを保存。以降認証にこの値を使う。
  • 画面側の初期処理で、このuserControllerオブジェクトの初期処理を一緒に呼ぶように追加。
5.3 AWSとの統合

フロントエンドメインの画面を動かすだけなら上の5.2で完結ですが、次にクライアント→API Gateway→Lambdaと繋げていきます。 下がLambdaオーソライザーを通過した後の、ユーザ情報問合せ用のuser-profileのLambda関数のイメージ。

// jsonwebtokenとrequestモジュールをインポートしておく

exprots.handler = function(event, context, callback) {
  if (!event.authToken) {
    // トークンがない場合の処理
  }
  // Authorization: Bearer XXXと飛んでくるのでXXXを抽出
  let token = event.authToken.split(' ')[1];
  let secretBuffer = new Buffer({環境変数からAuth0シークレット取得});
  jwt.verify(token, secretBuffer, function(err, decoded) {
      if (err) {
        // エラー処理でcallback('失敗!!'); を呼ぶ
      } else {
        let body = {'id_token': token};
      }
      // Auth0のドメインにある/tokeninfo というリソースを指定。
      let options = {いろいろ};
      request(options, function(error, response, body) {
        if (!error && response.statusCode === 200) {
          callback(null, body); // 本来の通信先と通信して正常時
        } else {
          callback('失敗!');
        }
      });

    }
  })
};

 画面側の実装ではAuth0の「ドメイン」と「クライアントID」という固定値を使いますが、Lambda関数では「ドメイン」と「Auth0シークレット」の2つの固定値なので注意ですね。
jwtモジュール側の関数の呼び方は一緒なので慣れればだいたい同じでしょうか。単純に慣れの話ですが、JavaScriptの方がカッコが多くてcallback関数があるので、ぱっと見はPythonで書いた方が分かりやすいなあと感じました。

  • 管理コンソールのAPI GatewayAPI一つ作成、リソースにuser-profile追加。
  • 繋ぐ先を上のuser-profile関数にして、CORSを有効に。
  • 本書では練習の為、クライアントから飛んでくるJWTを採る部分はAPI Gatewayの「マッピングテンプレート」機能を使う。以下の設定をしておくとハンドラ関数内で引数から event.authToken として取れる。「Lambdaプロキシ統合」を使っていればこれはいらない。
{ "authToken": "$input.params('Authorization')" }
  • ステージを選んでデプロイすると、そのAPI Gatewayが有効になる。

そして上のuser-profile関数が動く手前に配置する「カスタムオーソライザー」のやり方。

  • クライアントはトークンを持ってAPI Gatewayに飛んでくる。
  • ここでまずカスタムオーソライザーのLambda関数が起動。入力にトークンがあり、評価して正常なら戻り値でIAMポリシーを返す。
  • API Gatewayに戻ってきて、カスタムオーソライザーの評価が正常ならリクエスト続行、本来のLambda関数へ。

名前を custom-authorizerとしたLambda関数が以下のような感じです。

export.handler = function(event, context, callback) {
  if (!event.authorizationToken) {
    callback('トークンがない!');
    return;
  }
  // Authorization: Bearer XXX のXXXを取得
  let token = event.authorizationToken.split(' ')[1];
  let secretBuffer = new Buffer({環境変数のAuth0シークレット});
  jwt.verify(token, secretBuffer, function(err, decoded) {
    if (err) {
      callback('JWTで認証失敗!');
    } else {
      // 下のIAMポリシーがAPI Gatewayに返される。内部関数で生成したり。
      callback(null, {JSオブジェクト形式で作られたIAMポリシー});
    }
  });
};

 入力のトークンを使ってこの関数内で自由に認証をカスタマイズできるが、この例だと認証自体はjsonwebtokenモジュールに任せているので呼ぶだけ、ということですね。戻り値でIAMポリシーを作るところはJavaScriptならJavaScriptオブジェクト、Pythonなら辞書型でまあ似たようなものです。
 その後の流れがこちら。

  • 作成済みのAPI Gatewayについて、新しくオーソライザーを作成。上のLambda関数を指定する。
  • この時画面のトークンのソースに「method.request.header.Authorization」を指定。これで上の関数で引数 event.authorizationToken から取れるようになる。
  • API Gatewayの作成済みリソースについて、認証のところでこの新しく作ったオーソライザーを指定。HTTPメソッド全てにひとつのオーソライザーで大抵間に合う。

 この「カスタムオーソライザー」は2016年提供開始、その後2018年?ごろからAPI Gatewayの中の機能「Lambdaオーソライザー」として名前が変わっています。柔軟にいろいろ認証を工夫できそうです。

docs.aws.amazon.com

dev.classmethod.jp dev.classmethod.jp qiita.com

以前読んだ『Amazon Web Servicesを使ったサーバーレスアプリケーション開発ガイド』では認証に全面的にCognitoを使った実装例が詳しく載っていたので、本書でAuth0とちょうど両方分かって良かったです。カスタマイズの余地もありつつ、なるべく独自実装部分は少なくして認証の仕組み側に任せるという事ですね。

iwasiman.hatenablog.com

第6章 オーケストレーターとしてのAWS Lambda

 サーバーレスアーキテクチャの心臓だと表しながら、Lambdaを深堀りしていく章。

6.1 AWS Lambdaの内部

 本書ではサーバーレスとLambdaは同義語ではなく、強力なパターンとアーキテクチャを使った新しいアプローチがサーバーレスアーキテクチャであり、その一部がLambdaFaaS(Function as a Service)サーバーレスの一部だとしています。
 Lambdaが呼べるイベントモデルは以下。

  • AWSで発生したイベント (イベント駆動型)
  • API Gatewayから届くHTTPリクエスト (要求/応答型)
  • AWS SDKを使ったAPI呼び出し (イベント駆動型と要求/応答型の両方)
  • AWS コンソールからの手動の呼び出し (イベント駆動型と要求/応答型の両方)

イベント駆動型は「イベントドリブン」と表現することも多いでしょうか、その中に2種類。

  • プッシュモデル:例えばS3のファイル登録など。Lambdaにイベントをパブリッシュし、関数を直接実行。
  • プルモデル:LambdaがKinesisストリームを定期的に自力でポーリングし、新しいレコードがあったら関数で処理。

諸元が以下。

  • 同時実行数は上限1000。Kinesisのようなストリームベースではアクティブなシャードの数。
  • ストリームベースでないイベントソースでは、同時実行数=1秒あたりのイベント×関数の実行時間。
  • 内部で新しいコンテナが初期化されて関数がロードされて動くのが「コールド状態」。動くまで時間がかかる。
  • コンテナを再利用する際は初期化プロセスがスキップ、「ウォーム状態」ですぐ動く。
  • 新しいコンテナが作られるかどうかはLambda内部の仕組み次第で、アプリケーションからは制御できない。

 コールドスタートをなるべく避けてパフォーマンスを上げる方法は以下を上げています。

  • スケジュールイベントで定期的に関数を実行し、ウォーム状態を保つ。
  • 初期化処理のコードをイベントハンドラ関数の外に出す。ウォーム状態なら実行されない。(AWS認定の問題に出るやつ...)
  • Lambdaへの割当メモリを増やす。
  • コードのサイズはできるだけ小さい方がよい。不要なモジュールも減らす。
  • コールドスタートが最も長いのはJavaで、別の言語を試す手もある。

 開発者はコードしか触れませんから、Javaコードで書くとコンテナ初期化時に内部で一式ビルドされてそれからやっと動くのでタイムラグあり。ウォーム状態だとコンパイル済みの状態で再利用されるので軽快に動く……という理屈でしょうか。  JavaScriptPythonより初期化に掛かりそうなのは分かりますがますが、それだと同じくコンパイルが必要なC#はどうなのかな?と思いました。

 なおLambda本体と一緒にJavaScriptならモジュールを一緒にアップロードする方法は、基本はZipで展開後に250MBが上限。その後「Lambdaレイヤー」の機能が追加されて複数の関数で共有が可能に。そして2020年12月から、最大10GBのコンテナイメージの形でもデプロイできるようになりました。

aws.amazon.com aws.amazon.com dev.classmethod.jp

6.2 プログラミングモデル
  • 関数ハンドラ:最初に呼び出される関数。JSでは、 exports.handler = function(event, context, callback) {}
  • イベントオブジェクト:JSON形式でイベントとイベントソースの情報が入っている。
  • コンテキストオブジェクト:Lambdaランタイムの環境情報。
  • コールバック関数:JSの場合だけの第3引数がある。
    callback(Error error, Object result)形式。
    callback(null, '成功したよ'); が正常終了時。
    callback('失敗したよ'); が異常終了時。
    callback();callback(null)と同じで正常終了。
  • ログ:JSでは console.log("ロギングしよう") とするとCloudWatchに渡る。.error(), .info(), .warn() も実質差なし。ログ用のしっかりしたライブラリを使うのを推奨。
6.3 バージョニング、エイリアス環境変数
  • バージョニング:バージョンをつけるとAPI Gatewayから呼ぶ際にARNの最後に :3 のように指定できる。最新版は$LATEST
  • エイリアス特定のバージョンの関数に例えば dev, staging, production とつけるなど。イベントソースからはバージョンでなくエイリアスを指定すると便利。
  • 環境変数キーと値の組を画面から定義。暗号化もできる。JSコードからは process.env.ENV_VARIABLE_SAMPLE のようにして指定。

 画面のスクショはすべてちゃんと日本語版になっていてありがたいです。特に古くなってるようなところは見当たりませんでした。

6.4 CLIの使い方

aws lambda create-function {引数でZIPファイルの場所などなど}
のようにコマンドからも操作できる。

6.5 AWS Lambdaのパターン

 JavaScriptでLambda関数を書く際は非同期のコールバック関数が多くなってコールバック地獄に陥りやすいので、シーケンシャルに記述できる「非同期ウォーターフォール」を推奨しています。Asyncモジュールを使って
async.waterfall([関数, 関数, 関数...], 最後のコールバック関数);
と書くと見た目もわかりやすくなるというもの。

 名前が似ていてややこしいですが、このAsyncモジュールは古い時代の話ですね。非同期処理にはその後ES6(ES2015)でPromiseが登場し、ES8(ES2017)でasync/awaitが登場しています。Lambdaランタイムで古いNode.jsを使うのでなければおそらく出番はないでしょう。
 例題に登場する24−Hour Videoアプリケーション用に、S3にアクセスして動画一覧を取得するLambda関数、SESを使ってメール送信するLambda関数を中で機能ごとに内部関数に分割し、このAsyncモジュールを使ってシーケンシャルにやっていく例のコードが載っています。

www.npmjs.com

6.6 Lambda関数のテスト
  • ここでの例はテストフレームワークMocha、TDDのアサーションライブラリのchaiユニットテスト用のrewireを使ってローカル上でテスト。
  • AWS上では、Lambdaの設計図で lambda-test-harnessを選んで作っていくと、他の関数をテストして結果をDynamoDBに格納できる関数が作れる。

aws.amazon.com

 本書では簡単に触れられているだけですが、Lambda関数の開発時にローカル/AWS上でどう上手くテストしていくかのテーマも、いろいろ奥が深そうです。

第7章 Amazon API Gateway

7.1 インターフェイスとしてのAmazon API Gateway

バックエンドの様々なAWSサービスとフロントエンドのクライアントアプリを結び付けるインターフェスがAPI Gatewayだとして、深掘りしていきます。 サービスとの統合が4種類。

  • Lambda統合:後ろのLambda関数を呼び出す。
  • HTTPプロキシ:他のHTTPエンドポイントにリクエストを転送できる。
  • AWSサービスプロキシ:実はLambda関数を介さずとも直接AWSサービスが呼べる。こちらの方が早く作れ、基本的なユースケースでは有用。高度な使い方をしたりロジックが必要な時はLambdaを介する。
  • モック統合:ダミーのJSONを返したり。

  • 負荷を下げるキャッシング、アルゴリズムを使ってAPI呼び出しの回数を押さえるスロットリングの機能。ロギングはCloudWatchが対応。

  • ステージング、バージョニングの機能あり。
  • Swaggerを使ってAPIを定義できる。

swagger.io

7.2 Amazon API Gatewayの操作

スクショを元に実際のAPIを作っていく様子が解説されています。

  • リソース作成時に「プロキシリソースとして設定」オプションがあるが、特定のユースケース用。
  • リソース作成時の「API Gateway CORSを有効にする」が全体用で、個別にマッピングするにはメソッドを作ってから[アクション]-[CORSの有効化]がいる。
  • 「Lambdaプロキシ統合の使用」をチェックすると、すべての情報がJSONになってLambda関数の引数eventに渡るので楽、通常こちら。一方、リクエストが巨大で一部しか使わない場合は手動の方が良いケースもある。
  • APIのリソースとLambda関数を紐づける所では、エイリアスを使った方がよい。
  • Lambda関数は決められた形式でレスポンスを返さなければならない。JavaScriptの場合はcallback関数になる。ここでレスポンス形式が間違っていると、API Gateway->クライアント に502 Bad Gatewayを返す。

後半では 24-Hour Videoサービスの例として、動画リストを取得するLambda関数、そして画面側のHTMLとJavaScriptの実装例が載っています。
jQueryベースなので、出来上がったAPI Gatewayと通信して得られたデータを特定div要素の中に、動画1件用のdiv要素をどんどん複製して表示していく…という当たり前だけど直球の実装でした。

7.3 ゲートウェイの最適化
  • ロットリング画面から有効化すると使える。レートが1秒間のメソッド呼び出しを認める平均回数、バーストがメソッド呼び出しの上限回数。for文でガンガンHTTPリクエストを発行するLambda関数を作り、疑似DOS攻撃(!)して効果を確かめる手順を解説。
  • ロギング:CloudWatchのログとメトリクスは常に有効化したほうがよい。適切なIAMロールの設定が必要。
  • キャッシング:画面から有効化。金がかかる。キャパシティの容量と有効期限は、試行錯誤が必要。かなり細かく設定できる。

 Lambda関数でHTTPリクエストを発行しまくる処理を書いても行けるんですね。AWSの中からAWSの別の場所を攻撃するので、自分自身を標的にしているようで面白いです。

7.4 ステージとバージョン
  • ステージ:APIごとにステージは最大10個。dev/stage/prodなど。ここでステージ変数というものが定義でき、ステージごとに別の値を採れる。APIの各リソースと紐づいたLamda関数を指定する時に変数が使えるので、ステージごとに動かす関数を別に定義したりできる。
  • バージョン:ステージエディタから履歴が辿れ、過去のデプロイ状態に戻れる。

 ちょうど本書を読んでいた頃にAPI Gatewayの実物を触って最初はハマったり試したりしていたので、この章は割とすんなり読めました。スクショも全て日本語化されています。