Service Mesh and Cookpad

こんにちは、開発基盤の Taiki です。今回は、マイクロサービスで必須のコンポーネントとなりつつあるサービスメッシュについて、クックパッドで構築・運用して得られた知見についてご紹介できればと思います。

サービスメッシュそのものについては以下の記事や発表、チュートリアルで全体感をつかめると思います:

目的

クックパッドでは主に障害対応やキャパシティプランニング、Fault Isolation に関わる設定値の管理といった、運用面での課題を解決すべくサービスメッシュを導入しました。具体的には:

  1. サービス群の管理コストの削減
  2. Observability*1*2の向上
  3. より良い Fault Isolation 機構の構築

1つ目については、どのサービスとどのサービスが通信していて、あるサービスの障害がどこに伝播するのか、ということを規模の拡大とともに把握しづらくなってるという問題がありました。どことどこが繋がっているかの情報を一元管理することでこの問題は解決できるはず、と考えました。

2つ目については (1) をさらに掘ったもので、あるサービスと別のサービスの通信の状況がわからないという課題でした。例えば RPS やレスポンスタイム、成功・失敗ステータスの数、タイムアウトやサーキットブレーカーの発動状況などなど。あるバックエンドサービスを2つ以上のサービスが参照しているケースでは、アクセス元のサービス毎のメトリクスではないため、バックエンドサービス側のプロキシーやロードバランサーのメトリクスでは解像度が不十分でした。

3つ目については、「Fault Isolation がうまく設定できていない」という課題でした。当時はそれぞれのアプリケーションでライブラリを利用して、タイムアウト・リトライ・サーキットブレーカーの設定を行っていましたが、どんな設定になっているかはアプリケーションコードを別個に見る必要があり、一覧性がなく状況把握や改善がしづらい状況でした。また Fault Isolation に関する設定は継続的に改善していくものなので、テスト可能であったほうが良く、そのような基盤を求めていました。

さらにもっと進んだ課題解決として、gRPC インフラの構築、分散トレーシング周りの処理の委譲、トラフィックコントロールによるデプロイ手法の多様化、認証認可ゲートウェイなどのような機能もスコープに入れて構築しています。このあたりについては後述します。

現状

クックパッドでのサービスメッシュは data-plane として Envoy を採用し、control-plane は自作、という構成を選択をしました。すでにサービスメッシュとして実装されている Istio を導入することも当初は検討したのですが、クックパッドではほぼ全てのアプリケーションが AWS の ECS というコンテナ管理サービスの上で動作している関係上 Kubernetes との連携メリットが得られないことと、当初実現したいことと Istio のソフトウェア自体の複雑さを考慮して、小さく始められる自作 control-plane という道を選びました。

今回実装したサービスメッシュの control-plane 部分はいくつかのコンポーネントによって構成されています。各コンポーネントの役割と動作の流れを説明します:

  • サービスメッシュの設定を中央管理するリポジトリ
  • kumonos*3 という gem を用いて上記の設定ファイルから Envoy xDS API*4 用のレスポンス JSON を生成
  • 生成したレスポンス JSON を Amazon S3 上に配置して Envoy から xDS API として利用する

中央のリポジトリで設定を管理している理由は、

  • 変更履歴を理由付きで管理して後から追えるようにしておきたい
  • 設定の変更を SRE 等の組織横断的なチームもレビューできるようにしておきたい

という2点です。

ロードバランシングについては、基本的には Internal ELB に任せるという方式で設計したのですが、gRPC アプリケーション用のインフラ整備も途中から要件に入った*5ので、SDS (Service Discovery Service) API を用意して client-side load balancing できるようにしています*6。app コンテナに対するヘルスチェックを行い SDS API に接続先情報を登録する side-car コンテナを ECS タスク内にデプロイしています。

f:id:aladhi:20180501141121p:plain

メトリクス周りは次のように構成しています:

  • メトリクスは全て Prometheus に保存する
  • dog_statsd sink*7 を使用してタグ付きメトリクスを ECS コンテナホスト(EC2 インスタンス)上の statsd_exporter*8 に送信
    • 固定タグ設定*9を利用してノード区別のためにアプリケーション名をタグに含めています
  • Prometheus からは EC2 SD*10 を利用して statsd_exporter のメトリクスを pull
    • ポート管理のために exporter_proxy*11 を間に置いています
  • Grafana、Vizceral*12 で可視化

ECS, Docker を利用せずに EC2 インスタンス上で直接アプリケーションプロセス動かしている場合は、Envoy プロセスも直接インスタンス内のデーモンとして動かしていますが、構成としてはほぼ同じです。直接 Prometheus から Envoy に対して pull を設定していないのは理由があり、まだ Envoy の Prometheus 互換エンドポイントからは histogram メトリクスを引き出せないからです*13。これは今後改善される予定なのでその時は stasd_exporter を廃止する予定です。

f:id:aladhi:20180502132413p:plain

Grafana 上ではサービス毎に各 upstream の RPS やタイムアウト発生等の状況が見れるダッシュボードと Envoy 全体のダッシュボードを用意しています。サービス×サービスの粒度のダッシュボードも用意する予定です。

サービス毎のダッシュボード

f:id:aladhi:20180501175232p:plain

Upstream 障害時のサーキットブレーカー関連のメトリクス

f:id:aladhi:20180502144146p:plain

Envoy 全体のダッシュボード

f:id:aladhi:20180501175222p:plain

サービス構成は Netflix が開発している Vizceral を利用して可視化しています。実装には promviz*14 と promviz-front*15 を fork して開発したのもの*16を利用しています。まだ一部のサービスにのみ導入しているので現在表示されているノード数は少なめですが、次のようなダッシュボードが見れるようにしています:

リージョン毎のサービス構成図と RPS、エラーレート

f:id:aladhi:20180501175213p:plain

特定のサービスの downstream/upstream

f:id:aladhi:20180501175217p:plain

またサービスメッシュのサブシステムとして、開発者の手元から staging 環境の gRPC サーバーアプリケーションにアクセスするためのゲートウェイをデプロイしています*17。これは hako-console という社内のアプリケーションを管理しているソフトウェア*18と SDS API と Envoy を組み合わせて構築しています。

  • Gateway app (Envoy) が gateway controller に xDS API リクエストを送る
  • Gateway controller は hako-console から staging 環境かつ gRPC アプリケーションの一覧を取得して、それを基に Route Discovery Service/Cluster Discovery Service API レスポンスを返す
  • Gateway app はレスポンスを基に SDS API から実際の接続先を取得する
  • 開発者の手元からは AWS ELB の Network Load Balancer を参照し Gateway app がルーティングを行う

f:id:aladhi:20180502132905p:plain

効果

サービスメッシュの導入で最も顕著だったのが一時的な障害の影響を抑えることができた点です。トラフィックの多いサービス同士の連携部分が複数あり、今までそれらでは1時間に5,6件ほどのネットワーク起因の trivial なエラーが恒常的に発生していた*19のですが、それらがサービスメッシュによる適切なリトライ設定によって1週間に1件出るか出ないか程度に下がりました。

モニタリングの面では様々なメトリクスが見れるようになってきましたが、一部のサービスのみに導入していることと導入から日が浅く本格的な活用には至ってないので今後の活用を期待しています。管理の面では、サービス同士の繋がりが可視化されると大変わかりやすくなったので、全サービスへ導入することで見落としや考慮漏れを防いでいきたいと考えています。

今後の展開

v2 API への移行、Istio への移行

設計当初の状況と、S3 を配信バックエンドに使いたいという要求から xDS API は v1 を使用してきましたが、v1 API は非推奨になっているのでこれを v2 へ移行する予定です。同時に control-plane を Istio へ移行することも検討しています。また、仮に control-plane を自作するとしたら、今のところの考えでは go-control-plane*20 を使用して LSD/RDS/CDS/EDS API*21 を作成することになると思います。

Reverse proxy の置き換え

今までクックパッドでは reverse proxy として NGINX を活用してきましたが、内部実装の知識の差や gRPC 対応、取得メトリクスの豊富さを考慮して reverse proxy や edge proxy を NGINX から Envoy に置き換えることを検討しています。

トラフィックコントロール

Client-side load balancing への置き換えと reverse proxy の置き換えを達成すると、Envoy を操作してトラフィックを自在に変更できるようになるので、canary deployment や traffic shifting、request shadowing を実現できるようになる予定です。

Fault injection

適切に管理された環境でディレイや失敗を意図的に注入して、実際のサービス群が適切に連携するかテストする仕組みです。Envoy に各種機能があります*22

分散トレーシングを data-plane 層で行う

クックパッドでは分散トレーシングシステムとして AWS X-Ray を利用しています*23。現在はライブラリとして分散トレーシングの機能を実装していますが、これを data-plane に移してネットワーク層で実現することを予定しています。

認証認可ゲートウェイ

これはユーザーリクエストを受ける最もフロントのサーバーでのみ認証認可処理を行い、以降のサーバーではその結果を引き回し利用するものです。以前はライブラリとして不完全に実装していましたが、これも data-plane に移すことで out of process モデルの利点を享受することができます。

終わりに

以上、クックパッドでのサービスメッシュの現状と今後について紹介しました。すでに多くの機能を手軽に実現できるようになっており、今後さらにサービスメッシュの層でできることが増えていくので、マイクロサービス各位に大変おすすめです。

*1:https://blog.twitter.com/engineering/en_us/a/2013/observability-at-twitter.html

*2:https://medium.com/@copyconstruct/monitoring-and-observability-8417d1952e1c

*3:https://github.com/taiki45/kumonos

*4:https://github.com/envoyproxy/data-plane-api/blob/5ea10b04a950260e1af0572aa244846b6599a38f/API_OVERVIEW.md

*5:gRPC アプリケーションはすでに本仕組みを利用して本番環境で稼働しています

*6:単純に Internal ELB (NLB or TCP mode CLB) を使った server-side load balancing ではバランシングの偏りからパフォーマンス面で不利であり、さらに取得できるメトリクスの面でも十分ではないと判断しました

*7:https://www.envoyproxy.io/docs/envoy/v1.6.0/api-v2/config/metrics/v2/stats.proto#config-metrics-v2-dogstatsdsink 最初は自前拡張として実装したのですが後ほどパッチを送りました: https://github.com/envoyproxy/envoy/pull/2158

*8:https://github.com/prometheus/statsd_exporter

*9:https://www.envoyproxy.io/docs/envoy/v1.6.0/api-v2/config/metrics/v2/stats.proto#config-metrics-v2-statsconfig こちらも実装しました: https://github.com/envoyproxy/envoy/pull/2357

*10:https://prometheus.io/docs/prometheus/latest/configuration/configuration/

*11:https://github.com/rrreeeyyy/exporter_proxy

*12:https://medium.com/netflix-techblog/vizceral-open-source-acc0c32113fe

*13:https://github.com/envoyproxy/envoy/issues/1947

*14:https://github.com/nghialv/promviz

*15:https://github.com/mjhd-devlion/promviz-front

*16:NGINX で配信したりクックパッド内のサービス構成に合わせる都合上

*17:client-side load balancing を用いたアクセスを想定している関係で接続先の解決を行うコンポーネントが必要

*18:http://techlife.cookpad.com/entry/2018/04/02/140846

*19:リトライを設定している箇所もあったのだが

*20:https://github.com/envoyproxy/go-control-plane

*21:https://github.com/envoyproxy/data-plane-api/blob/5ea10b04a950260e1af0572aa244846b6599a38f/API_OVERVIEW.md#apis

*22:https://www.envoyproxy.io/docs/envoy/v1.6.0/configuration/http_filters/fault_filter.html

*23:http://techlife.cookpad.com/entry/2017/09/06/115710

Cookpad Spring 1day Internship 2018 を開催しました

技術広報を担当している外村です。

今年クックパッドでは、2月から3月にかけて、一日で最新の技術を学ぶインターンシップを以下の4コース開催しました。

  • サービス開発コース
  • インフラストラクチャーコース
  • Rustプログラミングコース
  • 超絶技巧プログラミングコース

Cookpad Spring 1day Internship 2018

たくさんの学生に参加していただき、真剣に課題に取り組んでくれました。参加していただいが学生のみなさん、ありがとうございました。

それぞれのコースの内容について簡単に紹介します。

サービス開発コース

サービス開発コースはクックパッドのサービス開発プロセスを一日で学ぶという内容のコースです。「大学生のおでかけの課題を解決するスマホアプリ」をテーマに、エンジニアとデザイナーがペアになって課題解決のためのサービスを考えてもらるという内容でした。

ユーザーインタビューやペルソナの作成から始まり、価値仮説やユーザーストーリを考えて最終的にペーパープロトタイピングを作ってもらいました。全チームがプロトタイプを作成するところまでやり終え、クックパッドのサービス開発プロセスを体感してもらうことができました。

インフラストラクチャーコース

インフラストラクチャーコースでは、AWS を使った Web アプリケーションインフラ構築を行いました。小さな Rails アプリケーションを題材に、サービスの成長に沿って必要になってくる要素について解説し、それを構築して使う、というものです。

実際の流れは以下のようなものでした。

  • まずは小さい EC2 インスタンスにとりあえずアプリケーションをデプロイ
  • Itamae を使ってインスタンスをセットアップしてみる
  • systemd を触ってみる
  • Rack サーバを production 向きのものに変更する
  • コマンドやプロファイラを使ってパフォーマンスモニタリングをする
  • RDBMS のチューニングをする (インデックスの作成や N+1 クエリへの対処)
  • Web サーバ (nginx) のチューニングをする
  • DB を Amazon RDS に分離し、アプリケーションサーバをスケールアウトする
  • ロードバランサを導入する
  • memcached などのキャッシュを導入する

構築しているアプリケーションは常にベンチマーカーによってチェックされており、作業によるパフォーマンスアップを実際に感じることができるようになっていました。

「なかなかインフラに関わることってなくて...」という方に多くご参加いただけたので、なかなか新鮮な体験をしていただけたようです。

Rustプログラミングコース

Rust プログラミングコースでは、プログラミング言語 Rust を使ってリバーシゲームを作りました。

午前パートでは Rust の鬼門と言われる Ownership、 Borrowing、 Lifetime という概念の解説をしました。いかにして Rust が GC なしで安全なメモリ管理を実現しているのかについて、スクリプト言語の経験しかない受講者にも理解できるよう、ヒープとスタックの違いなどといったプロセスのメモリモデルの説明から紐解いていきました。

午後のパートでは実際にコードを書き、講師が用意したコードの穴を埋める形でリバーシゲームを実装していきました。実際に遊べるゲームが完成することで、受講者は達成感を感じられていたようです。早めに完動させることに成功した受講者は各々自由な改造をして楽しんでいました。

講義で利用したコードは以下になります。

KOBA789/rust-reversi

超絶技巧プログラミングコース

超絶技巧プログラミングコースは、あなたの知らない超絶技巧プログラミングの世界の著者で、Rubyコミッタでもある遠藤を講師として、技巧を駆使した実用性のないプログラムを作成する手法を学ぶという内容の講義でした。

前半の講義ではプログラムをアスキーアートにする方法や、Quine(自分自身を出力するプログラム)を書く方法について学び、後半は実習として各自に超絶技巧プログラム書いてもらいました。テトリスや将棋のQuineなど、皆さん超絶技巧を使って面白い成果物を作成されていました。

Summer Internshipのご案内

Cookpad Summer Internship 2018

クックパッドでは、毎年恒例になっているサマーインターンシップを今年も開催いたします!今年のインターンシップは、サービス開発エンジニア向けに 10 Day Tech インターンシップ、リサーチエンジニア向けに 5 Day R&D インターンシップ、デザイナー向けに 5 Day Design インターンシップを行なうというかたちになっています。

10 Day Tech インターンシップでは、モバイルアプリケーションからサーバーサイド、インフラストラクチャーまで、フルスタックエンジニアとして必要な技術をぎゅっと前半の講義・課題パートに詰め込んでいます。後半では、クックパッドの現場に配属されて研修を行なうOJTコースと、前半で学んだ内容をチームで実践するPBLコースに分かれてインターンシップを行ないます。

5 Day R&D インターンシップは、機械学習や自然言語処理を専攻とする修士課程・博士課程の方を対象に実施します。クックパッドの膨大なデータという大学の研究では経験することが難しい生のデータに触れることのできる、貴重な機会です。

また、5 Day Designは、参加者同士でチームを組み、料理に関する課題を解決するサービスを発想し形にします。メンターからサポートを受けながら、ユーザーが抱える課題をインタビューを通して理解し、その解決策のデザイン・プロトタイピングを行ないます。実践と平行して、クックパッドが持つサービスデザイン手法やノウハウを講義形式で学びます。

皆さまのご応募をお待ちしております!

S3に保存したログファイルをストリーム処理するサーバーレスアプリケーションの紹介

インフラストラクチャー部セキュリティグループの水谷(@m_mizutani)です。

クックパッドでは現在セキュリティ監視の高度化に取り組んでおり、その一環としてセキュリティ関連のログ収集およびその分析に力を入れています。ログ収集の部分では可用性などの観点からAWSのオブジェクトストレージサービスであるS3に一部のサービスやサーバのログをまず保存し、後から保存されたファイルを読み込んで分析などに利用しています。分析のためにS3に保存したファイルを前処理する方法としてAWS Glueなどを用いたバッチ処理がありますが、到着したログをなるべくストリームデータのように扱いたい場合もあります。特にセキュリティ関連のログでは以下のようなユースケースで利用しています。

  • アラートの検出: ログを検査してその中から危険度の高いと考えられるログを探し出し、アラートとして発報します。アラートの具体的な例としてはオフィス環境からの不審な通信やスタッフ用クラウドサービスでの不審な活動、スタッフPC上の不審イベントなどが挙げられ、ログの各種パラメータをチェックして危険度を判定します。
  • ログの変換と転送: S3に蓄積されたログの形式を変換して別のデータソースへ転送します。具体的にはGraylogに転送してログ分析をしやすいようにしたり、Athenaによって検索できるように変換して別のS3バケットに配置するといったことをしています。

こうした処理はバッチで対応できないこともありませんが、例えばセキュリティ分析の文脈においては、2, 3分程度の遅延であれば許容できても、1時間単位の遅れが発生するのは少し困ります。できる限り到着したものから順次処理して分析までのレイテンシを短くしたいところです。本記事ではこのような処理に対し、S3に保存したログファイルをなるべくレイテンシが短くなるように処理するためのアーキテクチャ、そしてそのアーキテクチャのためのフレームワークを紹介したいと思います。

最もシンプルなS3オブジェクトの処理構成とその課題

AWSの環境において、S3に到着したログを処理するのに最もシンプルな構成はどのようなものでしょうか? おそらく下の図のように、S3にオブジェクト(ファイル)が生成されたというイベントによってサーバーレスコンピューティングサービスであるAWS Lambdaが起動し、その後起動したLambdaがオブジェクトそのものをダウンロードして処理する、という構成かと思います。

f:id:mztnex:20180502110255p:plain

この構成はシンプルで構築しやすいのですが、実際に運用してみるといくつかの課題が見えてきました。

  • S3のObjectCreatedイベントの送信は、1つのS3バケットにつき1つの宛先にしか設定できない: 単独のサービスしか動かしていない場合は直接Lambdaを起動するだけで事足ります。しかし、S3にオブジェクトが生成されたタイミングで何か処理をしたいという要求が2つ以上でてくると、直接Lambdaを起動するのではなく別の方法を考えないとなりません。
  • 流量制限が難しい: 例えばS3に保存されたオブジェクトを変換して別にデータストアに投入するというタスクでは、投入する先のデータストアに対する流量を気遣う必要があります。データストアの種類にもよりますが流量を超えすぎると全体のパフォーマンスが低下したり、データをロスするといったことが起きる可能性があります。当然、予想される流量に対して受け側のキャパシティを確保してくのは重要なのですが、ログの種類によっては一過性のバーストがしばしば起こります。S3から直接Lambdaを起動しているとこういった任意のタイミングで外部から流量をコントロールするのは難しいです。AWSの機能でLambdaの同時実行数を制限してスロットリングさせる方法もありますが、処理順序が時系列と大幅にずれたり複数回失敗して自前で再試行しないとならないといったところで煩雑になってしまいます。
  • エラーのリトライが大変: システムやサービスを常に更新・拡張していると実運用におけるエラーの発生は避けられません。よってエラーが起きたリクエストに対して再試行するという場面はたびたびあるのですが、これをなるべく簡単にやりたいという思いがあります。例えばエラーの発生はCloudWatch Logsに実行時ログとして残すことができますが、実行時ログの量が多くなるとそもそも検索が難しくなるなどの問題があります。また、どのエラーに対して再試行したのか・していないのかを手動で管理することになり、煩雑になってしまいます。

実際に使われているサーバーレスアプリケーションの構成図

f:id:mztnex:20180502110311p:plain

いくつかの課題を解決してスムーズな運用を試行錯誤した結果、上記の図のような構成に落ち着きました。シンプルな構成に比べるとそれなりに複雑なサーバーレスアプリケーションに仕上がりましたが、課題となっていた部分を解決しています。S3にオブジェクトが作成されてからどのように処理されるかというワークフローを、順を追って以下で説明します。

  1. S3のイベントはLambdaで直で受けるのではなくSNSで配信するようにしています。これによって複数の処理を実行したい場合でもSNSからイベントを受け取ることができるようになります。
  2. S3のObjectCreatedイベントを受けた EventPusher というLambdaが一度これをKinesis Streamにイベントを流します。
    • Kinesis Streamは一度データを投入すると指定時間(デフォルトで24時間)保持されるので、これ以降の処理を一時的に止めなければいけなくなってもイベントを蓄積し、あとから自由に読み出すことが可能です。
    • Kinesis Streamには Fast-laneSlow-lane という2つを用意しています。前者が高優先、後者が低優先にS3オブジェクトを処理するもので、EventPusher が送られてきたイベントの中身に応じてどちらかに振り分けます。それぞれのStreamに設定の違いはありませんが、この先にあるDispatcherの役割が異なります。
  3. それぞれKinesis Streamからイベントを受け取ったFastDispatcher と SlowDispatcherMain を非同期で呼び出します。Main はシンプルな構成のLambdaに該当し、最終的にユーザがやらせたい処理を請け負います
    • 便宜上、 FastDispatcherSlowDispatcher と名前をつけていますが、構成に違いはありません。そのかわり、 EventPusher でなるべく遅延なく処理したいと思うものを Fast-lane からの FastDispatcher 、 多少遅延してもいいものを Slow-lane からの SlowDispatcher にそれぞれ振り分けます
    • 各 Dispatcher には DELAY という環境変数が設定されており、これに整数値 N をセットすることで Main を呼び出した後に指定した N 秒 sleep した後に Dispatcher が終了します。Kinesis StreamからはLambdaは直列&同期的に呼び出されるため、時間単位で呼び出される回数が抑制され、全体の流量が抑制されます。基本的には SlowDispatcher の遅延を増やすことを想定しています。
    • 特に流量制限はこのサーバーレスアプリケーションの外部のメトリック値(例えばDBに実際に投入されるデータの流量など)を参照するためこの図には流量制御の仕組みはでてきませんが、例として定期的にメトリック値をチェックしたりCloudWatch Alarmのような仕組みで遅延を調整するという方法が考えられます。
  4. 起動した Main はイベント情報として作成されたS3オブジェクトのバケット名とキーが引き渡されるので、実際にS3からオブジェクトをダウンロードして必要な処理をします。
  5. Main の処理が適切に終了しなかった場合、2回まで再試行されますが、それでも正常終了しなかった場合はDLQにエラーの情報が引き渡されます。ここでは実装上の都合などからSNSを使っています。
  6. DLQ(SNS)経由で Reporter が呼び出され Lambda の requset_id と呼び出し時の引数(EventPusher から射出されたイベント)が引き渡されます
  7. Reporter は渡されたエラーの内容を粛々と ErrorTaskTable に投入します。ここまでで自動的に処理されるデータフローは一度終了します。

これ以降は保存されたエラーをどのように対応するか、というワークフローになります。

  1. 任意のタイミングでユーザが Drain を起動します。瞬間的なバーストや不安定なリソースによる失敗は (5) の2回までの再試行である程度解消されると期待されますが、それ以外のコード由来のエラーなどは何度試行しても同じ結果になるためループさせるとシステム全体の負荷になってしまします。なので、ここまでエラーで残ったものについてはユーザが確認してから再度処理のワークフローに戻すのが望ましいため手動で発火させる仕様にしています。
  2. 起動した DrainErrorTaskTable からエラーを全て、あるいは選択的に吸い出してます。
  3. Drain は吸い出したエラーになったイベントを再度Kinesis Streamに放流します。この時送信するイベントは EventPusher が作成したものですが、 EventPusher はKinesis Stream投入時にどちらのstreamに投入したかの情報を付与しているため、もともとのstreamに戻されます。

このようなワークフローで処理することで、エラーが起きた場合でも少ない手間で修正・再実行し開発と運用のサイクルを回していくことができます。

実装

上記のサーバーレスアプリケーションを展開するために slips というフレームワークを実装して利用しています。このフレームワークはPythonのライブラリとして動作し、導入したプロジェクトを先程のサーバーレスアプリケーションの構成で動かすための大部分をサポートしてくれます。サーバーレスアプリケーションを作成する時に苦労するポイントの1つにコンポーネントの設定や各コンポーネント同士のつなぎ込みといった作業があるかと思います。slipsは「S3からログファイルを読み込んで順次処理をする」というタスクを実行するサーバーレスアプリケーション作成におけるコンポーネントの設定、および統合をサポートしてくれます。これによって開発者がサーバーレスアプリケーションのコンポーネントを構成する際の消耗を最小限におさえ、ログデータを処理する部分の開発に集中できます。

slipsの実態としてはCloudFormationのラッパーになっています。サーバーレスアプリケーションのコンポーネントを制御するフレームワークとしては他にもAWS Server Application ModelServerless Frameworkが挙げられますが、ここの部分を自作したのは以下のような理由によります。

  • 構成の共通化: 冒頭で複数の処理に利用したいと書きましたが、最終的に読み取ったログの処理する部分以外はほとんどが共通した構成のサーバーレスアプリケーションになります。また、構成そのものをアップデートしてよりよい処理に変えていく必要があるため、複数のほぼ同じ構成を管理するのは冗長でメンテナンスの負荷が大きくなってしまいます。そのため、大部分の構成の定義を自動的に生成してCloudFormationで展開するというアプローチになっています。
  • 環境による差分の調整: ほとんどが同じ構成と上述しましたが、一方で環境ごとに微妙な構成の差もあるためこれを吸収する必要があります。例えばある環境ではコンポーネントを自動生成できるが、別の環境では専用の承認フローに基づいて作成されたコンポーネントを利用する必要がある、といった状況を吸収する必要があります。既存フレームワーク内にこういった環境ごとの差分を簡単なロジックで書き込むことは可能ですが、数が多くなったりロジックが複雑になったりすると管理が難しくなるので、割り切ってPythonのコードとしてロジックを作成しています。

このフレームワークを利用することで、先程のサーバーレスアプリケーションにでてきたAWS上の構成をほぼ自動で展開してくれます。先程のアーキテクチャの図を元に説明すると以下のような構成になります。

f:id:mztnex:20180502110328p:plain

S3へのログ保存とSNSへの通知設定、そしてS3に蓄えられたログをパースした後の処理(例えば、形式を変換したり、どこかへ転送したり、ログの内容を検査したりなど)を実施するためのコードは自前で用意する必要がありますが、それ以外の部分のコンポーネントをCloudFormationを利用してシュッと構築してくれます。これを利用することで、S3のログで何か処理をしたいユーザ(開発者)はS3に保存されたログが途中どのように扱われるかを意識することなく、最終的な処理の部分に集中することができるようになります。

導入と設定ファイルの作成

まずPythonのプロジェクト用ディレクトリを作成します。適当に作成したディレクトリに入って virtualenv などで環境を作成し、pipenv で slips を導入します。

$ virtualenv venv
$ source venv/bin/activate
$ pipenv install -e 'git+https://github.com/m-mizutani/slips.git#egg=slips'

次に設定ファイルを作成します。ファイル名を config.yml とします。サンプルはここ にありますが、本記事では要点のみ解説します。

backend:
  role_arn:
    event_pusher: arn:aws:iam::1234xxxxxxx:role/LambdaSlamProcessorEventPusher
  sns_topics:
    - name: SecLogUplaod
      arn: arn:aws:sns:ap-northeast-1:1234xxxxxx:seclog-event

上記は構築するサーバーレスアプリケーションの各要素の設定を記述します。ObjectCrated のイベントが流れてくるSNSトピックの指定は必要ですが、 role_arn の項では既存のIAMロールを割り当てることもできますし、省略すると必要なIAMロールをCloudFormationで自動的に追加します。同様にKinesis StreamやDLQに使われるSNSも、ARNを指定すれば既存のものが利用され、指定しなければ必要なものが自動的に生成されます。

これは組織や環境によって各リソースの管理方法が異なるため、別途作成されたリソースを使う場合とCloudFormationで自動的に生成する場合を切り替えることができるようにしています。例えばクックパッドでは本番環境のIAM権限などいくつかのリソースはGithub Enterprise上のレポジトリで厳密に管理されていますが、開発環境では比較的自由にリソースを作成できるようになっています。このような環境にあわせて容易に展開できるような実装になっています。

handler:
  path: src/handler.py
  args:
    sample

上記の設定は自分が実行させたい処理についての定義をしています。path で実行させたいファイルのパスを指定し、args で実行時に渡したい引数の内容を指定します。

bucket_mapping:
  mizutani-test:
    - prefix: logs/azure_ad/signinEvents/
      format: [s3-lines, json, azure-ad-event]
    - prefix: logs/g_suite/
      format: [s3-lines, json, g-suite-login]

上記のパートでどのS3バケットにどのようなフォーマットのオブジェクトがアップロードされるかを指定します。これを記述しておくことで、自分で書くスクリプトはパース済みのデータを受け取れるようになります。backet_mapping 以下のキーがS3バケット名、その下のリストでS3キーのプレフィックスとパースするためのフォーマットを指定します。上記例にでてくる指定は以下のような意味になります。

  • s3-lines: S3のオブジェクトをテキストファイルとして1行ずつファイルを読み込む
  • json: 1行をJSON形式としてパースする
  • azure-ad-event: AzureADのログ形式だと仮定して、タグ付けしたりタイムスタンプを取得する

コードの準備

設定の準備ができたら、自分が処理したいコードを用意します。デフォルトだと ./src/ 以下においたコードがzipファイルにアーカイブされてAWS上にアップロードされるので、今回は ./src/handler.py というファイルを作成します。サンプルとしてログの件数をCloudWatchのカスタムメトリクスに送信するコードを書いてみます。

import boto3
import slips.interface

class LogCounter(slips.interface.Handler):
    def setup(self, args):
        self._namespace = args['namespace'],
        self._count = 0

    def recv(self, meta, event):
        self._count += 1

    def result(self):
        metric = {
            'MetricName': 'LogCount',
            'Value': float(self._count),
            'Unit': 'Count',
        }

        cloudwatch = boto3.client('cloudwatch')
        cloudwatch.put_metric_data(Namespace=self._namespace, MetricData=[metric])

        return 'ok'  # Return some value if you need.

slips.interface モジュールに含まれている slips.interface.Handler クラスを継承してクラスを定義すると、これが読み出されて実行されます。新しく作成するクラスには以下のメソッドをそれぞれ定義します。

  • def setup(self, args): 最初に1度だけ呼び出されます。argsには handler設定項目内の args で指定した構造データが渡されます
  • def recv(self, meta, event): パースしたログ1件に対して1度呼び出されます。meta はパースする過程で得られた付加情報が格納されています。event は辞書型のパース済みログデータが渡されます
  • def result(self): 終了時に呼び出されます。returnで値を返してテストなど利用します

デプロイ

以上の準備ができたらAWSの環境にデプロイできます。ディレクトリには以下のようになっているかと思います。参考までに、サンプルの構成をgithubにあげておきます。

$ tree
.
├── Pipfile
├── Pipfile.lock
├── README.md
├── config.yml
├── src
│   └── handler.py
└── venv
    ├── bin
(省略)

この状態で slipsdeploy コマンドを実行します。実行にはCLI用のAWSのCredential情報が必要です(詳しくはこちら)以下が実行結果になります。

$ slips -c config.yml deploy
2018-04-13 14:40:07.379 INFO [cli.py:466] Bulding stack: log-counter
2018-04-13 14:40:07.379 INFO [cli.py:154] no package file is given, building
2018-04-13 14:40:07.736 INFO [cli.py:470] package file: /var/folders/3_/nv_wpjw173vgvd3ct4vzjp2r0000gp/T/tmpem2eyhgf.zip
2018-04-13 14:40:07.736 INFO [cli.py:474] no SAM template file is given, building
2018-04-13 14:40:07.763 INFO [cli.py:481] SAM template file: /var/folders/3_/nv_wpjw173vgvd3ct4vzjp2r0000gp/T/tmp1bahl7dn.yml
2018-04-13 14:40:07.763 INFO [cli.py:418] package command: aws cloudformation package --template-file /var/folders/3_/nv_wpjw173vgvd3ct4vzjp2r0000gp/T/tmp1bahl7dn.yml --s3-bucket home-network.mgmt --output-template-file /var/folders/3_/nv_wpjw173vgvd3ct4vzjp2r0000gp/T/tmpa1r9z4e7.yml
2018-04-13 14:40:08.652 INFO [cli.py:431] generated SAM file: /var/folders/3_/nv_wpjw173vgvd3ct4vzjp2r0000gp/T/tmpa1r9z4e7.yml
2018-04-13 14:42:11.138 INFO [cli.py:461] Completed (Applied)
2018-04-13 14:42:11.138 INFO [cli.py:559] exit

これがうまくいくとバックグラウンドでCloudFormationが実行され、対象のアカウントに必要なリソースがバーンと展開されます。同時に、CloudWatchのダッシュボードも自動生成されるので、それを見るとこのサーバーレスアプリケーションの実行状況がある程度把握できます。

f:id:mztnex:20180502110345p:plain

まとめ

AWS Lambdaや各種マネージドサービスを使ったサーバーレスアプリケーションは便利に利用できる反面、実行の制御やエラー処理などの勝手がインスタンス上とは異なり、実行の制御やエラー処理をスムーズに実施できるような構成にする必要があります。これをサポートする機能やサービスもAWSには充実しており、クックパッドでは日々それらを活用しながら改善に取り組んでいます。

今回はS3からログを順次読み取って処理するサーバーレスアプリケーションの構成と、それを補助するためのフレームワーク slips を紹介しました。今後、新たにサーバーレスアプリケーションを構築する方の参考になれば幸いです。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/