RubyとAWSでつくるメディアストレージ基盤

  • 250
    いいね
  • 1
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

今年の4月にドワンゴに入社し、アプリケーションエンジニアからインフラエンジニアに転向した経緯をもつ筆者が、アプリケーションエンジニアとしての知識を活かし開発した最初の成果ともいえるメディアストレージ基盤について、その概要と実装方法について述べる。

この基盤は主に、新規プロダクトを開発するにあたって必要となってくるメディアファイルに関連する一連の処理を括り出して共通化・基盤化しておくことで、新規プロダクトの開発効率を向上させるという狙いがある。
本稿ではその目的をAWSを用いて達成し、かつAWS独自の仕組みに則ることで実現した部分について着目しながら解説したい。

用語

用語 説明
本基盤 本稿の主題であるメディアストレージ基盤を指す。
利用サービス 本基盤を利用するアプリケーションを指す。
利用者 利用サービスのユーザーを指す。
一時認証情報 利用者がアップロード用S3にファイルをアップロードするための一時認証情報
アップロードチケット 上述の一時認証情報を含む、ファイルをアップロードする際に必要となる情報群の名称。
ジョブ アップロードチケット発行時に内部的に作成するファイル変換・配信処理の取り扱いのための情報群の名称。DynamoDBを使って管理が行われる。

概要

本基盤の果たす役割としては、「利用者本基盤に向けてファイルをアップロードし、なんらかの(変換を含む)処理を行って利用サービス側に通知する」というものになる。
そこで、想定する利用イメージを大まかにでも理解してもらうため、抽象的なイメージを図示する。

abstract.png

ファイルをアップロードしたいユーザーは、まず本基盤利用サービスに対してアップロード権限の発行を依頼する。
図では省略したものの、利用サービス側はその依頼を受けて、本基盤に対してアップロードチケットの発行を依頼し、取得した情報をアップロードしたいユーザーに対して返す。
アップロードユーザーはそれを受けて、本基盤に対してファイルのアップロードを行い、アップロード・バリデーション・変換が済んだものについては本基盤利用サービスに結果を通知するというのが大まかな流れとなる。

次に、本基盤の持つ責務について簡単に解説したい。
本基盤は、メディアファイルのアップロード、およびメディアファイルの配信・変換・通知の4つの責務を持つ。

ファイルアップロード

  • アップロード用S3に対する一時的なファイルのアップロードの権限を利用者に発行するために、本基盤一時認証情報を払い出す。
  • 利用者は払い出された一時認証情報を用いて、アップロード用S3に対してメディアファイルのアップロードを行う。

配信

  • 配信用S3の前段にあるCloud Front経由で、オリジナルファイルを配信するほか、画像ファイルについてはパスによる制御から画像のリサイズや切り取りを行う。

変換

初期段階でサポートしている変換機能は次の三つとなる。

  • 画像ファイルのサムネイル作成(resize・crop)。
  • PDFファイルのページ毎の画像ファイルへの切り分け。
  • 音声ファイルのエンコード。

通知

  • ファイルに対する処理状況や配信用URLなどは、ジョブAPIを通して確認することができる。
  • ファイルの処理状況に応じて、本基盤利用サービスに対する通知を行うことができる。

利用するAWSのリソース・サービス

ここからは、AWSのリソース・サービスをどのように活用しているかという点に着目しながら解説する。

EC2

まず、EC2で運用するサーバ群の種類は次の通りになる。

名称 説明
APIサーバ アップロードチケットの発行や、DynamoDBで管理しているジョブの現在の情報返すなど、利用サービス向けのAPIを提供するAPIサーバ。
バッチサーバ SQSを監視するデーモンを運用し、取り出したメッセージの情報とDynamoDB上のジョブとの情報を照合した上で適切に処理を振り分けるいわばコントローラ的なサーバ。AWSのサービスを使った変換ができないものについては、このバッチサーバ自身が処理を請け負う。
通知サーバ SNSによって変換状態が通知されたものについて、AWSからのプッシュかどうかを署名を使って検証し、必要となる情報をまとめて利用サービスに通知する通知サーバ。
サムネイル作成サーバ 画像のリサイズ・切り取り処理を行うサムネイル作成サーバ。

なお、サムネイル作成サーバを除くすべてのサーバ上では、Rubyを用いたアプリケーションを実装・運用している。

APIサーバ

アップロードチケットの発行やジョブの状態API、リトライなどの、利用サービス向けのAPIを運用するサーバである。
Grape単体でAPIアプリケーションは実装している。
API専任であれば、PadrinoやRailsを使わずとも、Grape単体で充分であるように思う。

また、APIを開発する際にDynamoDBの簡易的なORMを実装したので、これについては近日中にOSSとしてgithubで公開する予定でいる。 dinamoというgemを公開した。

Note: 一時認証情報について

AWSのsts:GetFederationTokenという仕組みに則った払い出しを行っている。
この仕組みを採用することで、一時認証情報の払い出しに使うIAMアカウントが持つポリシーと、sts:GetFederationTokenアクションの実行時にアタッチしたポリシーの両方を照らし合わせて、いずれのポリシーにも共通して存在しているアクションのみが許可されたフェデレーティッドユーザー用の一時認証情報を作成することができる。

詳細はPermissions for GetFederationTokenを参照のこと。

バッチサーバ

PDFの分解やElastic Transcoderにおけるジョブの作成など、Lambdaで引き受けられない処理を引き受けるサーバである。
SQSを監視するデーモンを主に運用することとなる。
監視用のデーモンの実装にはShoryukenを採用した。
ShoryukenはRuby製のSQS監視ワーカーを作成するためのライブラリで、導入も非常に容易である。
SQSから取り出したメッセージに関した処理が正常に終了した場合、メッセージをキューから削除するといったことも、次のようなコードを一行設定するだけで実現できる。

shoryuken_options queue: 'default', auto_delete: true

Note: ShoryukenからSQSを監視する際の注意

SQSは複数のデータセンターでreplicationされている都合上、同一のメッセージを複数回取得してしまう可能性がある。
Shoryuken単体ではこの問題には対応できないため、別途自身で排他制御を実装しなければならない。

通知サーバ

詳しい解説は後述の利用サービスへのwebフック通知の流れに譲るが、各サーバからsns:Publishされたメッセージは一旦この通知サーバが受ける。
その後、本基盤独自の署名を施した上で、利用サービス側に対してジョブの現在の値を送信する。

SNSからの通知内容が「変換処理の完了」を意味するものであった場合は、配信用URLの配列を通知内容に含めて、利用サービスに通知する。

このサーバ上でのAPIについても、前述のAPIサーバ同様にGrapeを用いて実装されている。

サムネイル作成サーバ

後述の配信用S3に対してリバースプロキシを行い、コンテンツを取得後、nginxのモジュールを用いて画像のリサイズなどを行う。このサーバに関してのみ、すべての処理はnginxで完結している。
サムネイル作成時に利用サービスは事前に受け取ってある秘密の鍵を使い、指定されたアルゴリズムに基づいてハッシュ値を算出し、URLの一部としてURLを生成する。
サムネイル作成サーバは、そのハッシュ値をもとにURLに対するバリデーションを行い、妥当であると判定できなければ、画像変換は行わずに400 Bad Requestを返すといった実装になっている。

S3

利用者から直接ファイルのアップロードを受けるアップロード用S3と、CloudFrontのoriginとして指定する配信用S3の2つを用意する。

アップロード用S3

APIサーバによって払い出されたアップロードチケットを用いて、利用者がファイルをアップロードすることのできるS3。

利用者からのファイルアップロードを受け付けるため、次の制限を課すものとする。

  • (アップロードチケットに含まれる)一時認証情報をもとにファイルをアップロードできるのは、アップロード用S3にの一意に定められたキーのみであり、複数個のファイルアップロードは認めない。
  • アップロードされたファイルは外部から確認できないようにする。
  • アップロードされたファイルは後述のLambdaやその他バッチサーバを用いて適切にバリデーション(ヘッダ検証・サイズ検証)を行い、不正なファイルであると判断した際には即座に削除される。
  • 同一箇所へのファイルの上書きについては、S3が提供するVersioningの仕組みを用いて、初回にアップロードされたファイル以降のファイルデータを即座に削除する。

Lambdaとの連携のために、本S3についてはS3 Notificationの仕組みを使って後述のLambdaを起動するように設定されている。

配信用S3

利用者の目に触れることになるファイルを配置するための配信用S3。

全てのバリデーション・変換処理を通過したファイルが、最終的にLambda・バッチサーバ・Elastic Transcoderからアップロードされる場所でもある。

なお、配信用S3に対して利用者が直接アクセスすることはなく、後述のCloud Front、もしくはサムネイル作成サーバ経由でファイルを取得し、エンドユーザーに返すこととなる。

DynamoDB

ジョブを管理するためのデータベース。
主に次のような構成を持っており、変換・配信状態の情報は全てDynamoDBを使って管理する。

{
  "data": {
      "job": {
          "convert_method": "変換方法", // 変換方法
          "job_id": "ジョブID", // 一意に定められるプライマリキー
          "service_name": "サービス名" // *利用サービス*名
          "status": "ジョブの状態", // uploaded, invalid, completionなど
          "urls": ["配信用URL"] // 配信用URL
          "mode": "配信状態の管理" // distributed, suspendedなど
      }
  }
}

Elastic Transcoder

音声ファイルのエンコードを行う。
Elastic Transcoderに対して前述のアップロード用S3と配信用S3を指定したElastic Transcoder Pipelineを作成し、そこに適切にジョブを作成するだけでエンコード処理が実行される。

処理が終了した際には、Elastic TranscoderからSNSを経由して通知サーバへと通知が行われるようになっている。

音声ファイル変換時のジョブ更新については通知サーバで行われる。

CloudFront

ファイルの配信を行うためのCDNを提供する。

本基盤においては基本的に配信用S3にあるファイルを配信するが、特定の文字列から始まるパスに対してリクエストが発生した際には、キャッシュを除いてサムネイル作成サーバにリクエストを移譲する。

SSL対応サイトにおいてもファイルの配信に対応するため、SSL証明書を適用してhttpsなURLを提供する。

Lambda

前述のアップロード用S3に対してファイルがアップロードされた際に、同期的に起動してファイルに対する処理を行うインスタンス。

本基盤におけるLambdaが行う処理の流れは次の通りである。

  1. アップロード用S3にファイルがアップロードされると、Lambdaが起動する。
  2. 一つのファイルに対するLambdaの起動が重複している危険性があるため、既に処理の始まっているもかどうかを、ジョブの現在の情報を取得して確認する。重複していれば処理はストップする。
  3. アップロードされたファイルの、サイズ・ヘッダ情報に対するバリデーションを行う。不正なファイルと判断すれば、ジョブの情報を更新して、invalidなファイルと認定する。
  4. Lambdaによるバリデーションは通過したため、ジョブの情報を更新して、正常にファイルがアップロードされたものとする。この時、ファイルがアップロードされたことを利用サービスに対して通知するため、SNSに対するpushを送信する。

ここから、変換方法に応じて処理が大きく分岐する。

  • 画像ファイルのアップロードであれば、この時点で配信用S3に対するファイルのアップロードを行う。完了すれば利用サービスに対してそのことを通知するため、SNSに対するpushを送信する。
  • その他PDF・音声ファイルについては、必要情報をSQSに送信し、前述のバッチサーバに処理を移譲する。この時点で利用サービスに対する通知は行わない。

SNS

本基盤では、ジョブの更新時に必要であれば利用サービスに対して通知を行う。

ただし変換方法によって、通知を行いたいタイミングはまちまちであるため、通知を行いたい全てのタイミングでSNSにpushを行い、前述の通知用インスタンスがそれを受けて、必要となる情報を利用サービスに対して通知していくという流れになる。

通知は全て利用サービスのAPIをエンドポイントとしてリクエストを行うため、この方式が妥当であると判断した。

SNSによるリクエストの署名検証

AWS SNSから送信されるメッセージには署名が含まれており、aws-sdkを使って簡単に署名の検証を行える。

次のコードは、署名検証の一例である。本来はRack Middlewareとして実装すべきだろう。

def verify!(json)
  verifier = Aws::SNS::MessageVerifier.new
  error!('VerificationError', 400) unless verifier.authentic?(json)
rescue
  error!('Bad Request', 400)
end

SNSからのRequestのContent-Typeに注意

SNSからのRequestのContent-Typeは、たとえリクエストボディがJSONであったとしてもtext/plainとなっている。
そのため、それを考慮したRack Middlewareなどを実装すべきだと考えられる。

SQS

本基盤におけるLambdaではカバーしきれない処理をバッチサーバに移譲するためのメッセージキュー。

SQSに送信する情報はアップロード用S3のバケット名と、キー名のみである。

EC2の項にて説明したバッチサーバ上でSQSを監視するデーモンを起動しており、新たにメッセージを取得した際には、バケット名・キー名を元にアップロード用S3からファイルを取得して処理を開始することになる。

各処理の流れ

前項までが、本基盤で用いられるAWSの各サービスの利用についての解説となる。
本項では、それらのサービスを組み合わせ、どのような流れで処理が進んでいくのか、というところに着目して解説したい。

なお、全ての処理において、共通してジョブの情報取得のためにDynamoDBを利用するが、図が複雑化しそうであったため省略した。
すなわちどのサーバからも、 原則的に DynamoDB上で管理されているジョブの値に基づいて処理を行なっているという点に注意されたい。

一時認証情報の払い出しの流れ

  1. 利用者利用サービスに対して、ファイルをアップロードしたいという旨のリクエストをする。
  2. 利用サービスはそれを受け、本基盤に対して、利用者本基盤の用意するアップロード用S3へのアップロードを行うための一時認証情報を含むアップロードチケットをリクエストする。
  3. リクエストを受けた本基盤のAPIサーバは、sts:GetFederationTokenを用いた一時認証情報の生成を試みる。
  4. STSを用い、アップロード用S3の特定のキーへのs3:PutObject権限を移譲する一時認証情報を生成する。
  5. アップロードチケット(4で生成した一時認証情報とジョブの現在の情報を統合したもの)を利用サービスに対して返す。
  6. 利用サービス利用者に対して、ファイルのアップロードに必要な情報をアップロードチケットから切り出して返す。
  7. 利用者はクライアントサイドの実装(JavaScript, objective-c, swift)から、一時認証情報をもとにアップロード用S3に対してアップロードを試みる。

flow-temporary-credentials.png

画像ファイルの処理の流れ

  1. 利用者は払い出された一時認証情報を用い、アップロード用S3に対してファイルのアップロードを行う。
  2. アップロード用S3にファイルがアップロードされたタイミングでS3 Notificationの仕組みから、Lambdaが起動する。
  3. Lambdaにて、アップロードされた画像ファイルのバリデーションを行い、問題がなければ配信用S3にファイルをs3:CopyObjectを用い、コピーする。invalidであると判断された場合には、即座に処理を中止し、利用サービスに対して通知を行う。

flow-upload_image.png

PDFファイルの処理の流れ

  1. 画像ファイルの処理同様、利用者は払い出された一時認証情報を用い、アップロード用S3に対してファイルのアップロードを行う。
  2. アップロード用S3にファイルがアップロードされたタイミングでS3 Notificationの仕組みから、Lambdaが起動する。
  3. Lambdaにて、アップロードされたPDFファイルに対して簡易的なバリデーション(サイズ・ヘッダ検証など)を行い、問題がなければ、ジョブを一意に特定するプライマリキーをメッセージとして、SQSに対してsqs:SendMessageを実行する。invalidであると判断された場合には、即座に処理を中止し、利用サービスに対して通知を行う。
  4. SQSを監視するバッチサーバがqueueされたメッセージを取り出し、改めてPDFファイルに対するバリデーションを行う。問題がなければ、バッチサーバ上でPDFファイルを画像に切り分ける。
  5. 切り分けられた画像群と、元のPDFファイルを配信用S3に対してs3:PutObjectを用い、アップロードする。

flow-extract_pdf.png

音声ファイルの処理の流れ

  1. 画像ファイルの処理同様、利用者は払い出された一時認証情報を用い、アップロード用S3に対してファイルのアップロードを行う。
  2. アップロード用S3にファイルがアップロードされたタイミングでS3 Notificationの仕組みから、Lambdaが起動する。
  3. Lambdaにて、アップロードされた音声ファイルに対して簡易的なバリデーション(サイズ・ヘッダ検証など)を行い、問題がなければ、ジョブを一意に特定するプライマリキーをメッセージとして、SQSに対してsqs:SendMessageを実行する。invalidであると判断された場合には、即座に処理を中止し、利用サービスに対して通知を行う。
  4. SQSを監視するバッチサーバがqueueされたメッセージを取り出し、音声ファイルに対するバリデーションを行う。
  5. 4にて、問題がなければ予め用意してあるElastic Transcoder Pipelineの仕組みに則ってジョブを作成する。
  6. Elastic Transcoderによるエンコードが完了すると、自動的に配信用S3にエンコード後のファイルがアップロードされる。

flow-encode_audio.png

利用サービスへのwebフック通知の流れ

次のタイミングで各サーバからSNSに対し、sns:Publishに則ってメッセージをPublishし、利用サービスに対して変更状態を通知する。

  • ファイルがアップロードされたタイミングで起動するLambdaにて、対象ファイルに対するvalidationを行い、その結果を通知する。
  • 画像ファイルが配信用S3へコピーされたタイミングで、画像ファイルにおける一連の処理が完了したことを通知する。
  • PDFファイルの切り分け処理が正常に終了し、配信用S3へファイル群がアップロードされたタイミングで、PDFファイルにおける一連の処理が完了したことを通知する。
  • 音声ファイルのエンコードが正常に終了したことを、Elastic Transcoderからの通知で確認したタイミングで、音声ファイルにおける一連の処理が完了したことを通知する。

flow-push.png

ファイル配信の流れ

すべてのファイルはCloudFrontを経由して配信される。
原則的にはoriginに設定されたS3のファイルを返すが、特定のプレフィックスが付与されているケースでは、サムネイル作成サーバのLoadbaracerに対して接続され、適宜画像変換処理をした上で返す。

flow-thumbnail.png

所感

AWSが提供するサービスを利用することで、CDNを用いたファイル配信を手軽に実現できるようになったほか、従来のオンプレ環境では困難だったオートスケーリングの仕組みを容易に導入することができた。

複数の利用サービスからの持続的な利用が想定されるこのような基盤は、十分な可用性と耐障害性を担保する存在でなければならず、AWSはそれらの問題の解消を容易にする。
いずれ、この開発・運用を通して得た知見についてはQiitaなど、技術系コミュニティの場で共有していこうと考えている。

おわりに

後半にかけてだいぶ駆け足となってしまったが、ドワンゴではオンプレだけでなく、AWSも使っているということが伝わっているとありがたい。
本基盤はすでに超パーティ2015で大々的に発表されたニコナレで導入されており、今後リリースされるサービスにも導入される予定である。
筆者はそれらのサービスのインフラエンジニアとしても参加しているので、私的な感情ではあるが、ぜひ、それらのサービスについても興味を持っていただけると幸いである。

この投稿は ドワンゴ Advent Calendar 20151日目の記事です。