akiyoko blog

akiyoko の IT技術系ブログです

Amazon CloudFront で HLS動画のプライベートオンデマンド配信を行う方法

1. はじめに

Aamazon Web Services(AWS)でプライベート動画のオンデマンド配信をするには、Amazon CloudFront の「署名付きURL(Signed URL)」という機能を使い、一定期間のみ有効となるワンタイムの URL を発行することで実現可能です。

 

プライベートオンデマンド配信の仕組み

今回は、動画の「オンデマンド配信」に加えて、企業内のみに限定された配信や課金のために必須の要素である「プライベート配信」を考慮した「プライベートオンデマンド配信(プライベート動画のオンデマンド配信)」の仕組みを検討します。
具体的には、「Amazon Web Services クラウドデザインパターン 実装ガイド 改訂版」の「3-5. キャッシュで負荷軽減する "Cache Distributionパターン"」と「3-7. その他の適用可能なパターン」の "Private Distributionパターン" を併用します。

Amazon Web Services クラウドデザインパターン 実装ガイド 改訂版 日経BP Next ICT選書

Amazon Web Services クラウドデザインパターン 実装ガイド 改訂版 日経BP Next ICT選書

  • 作者: アマゾンデータサービスジャパン玉川憲,片山暁雄,アイレット鈴木宏康
  • 出版社/メーカー: 日経BP社
  • 発売日: 2015/04/01
  • メディア: Kindle版
  • この商品を含むブログを見る


最終的なプライベートオンデマンド配信の方式は、このような形になります。(スライドでは「HLSセキュアオンデマンド配信」として解説されています。)


 

Amazon CloudFront とは

Amazon CloudFront とは、AWS のコンテンツ配信サービスで、世界中の至るところに配置されたエッジロケーション(キャッシュサーバ)にコンテンツのコピーをキャッシュすることができるため(2013年時点でのエッジロケーション数は 53)、エンドユーザからのコンテンツのダウンロードの高速化が期待でき、動画のグローバル配信をする場合には最適なソリューションの一つとなります。また、コンテンツの参照元(オリジンサーバ)には S3バケットを指定できるので、より高度にサービスを統合することができます。オリジンサーバは S3 以外のサービスも指定することができるようですが、ほとんどの場合は S3 を使うことになるでしょう。S3 から CloudFront に転送されるデータの通信料も無料のようですし。

なお、動画配信をするだけなら、CloudFront を使わず S3 の Static Website Hosting を利用すれば、S3 単独でも動画配信は可能と言えば可能なのですが、CloudFront が提供してくれる以下のようなメリットが享受できなくなるので、できる限り CloudFront を利用したいところです。

  • レイテンシの低減(エッジロケーション)
  • DDoS 緩和


参考
[レポート]Amazon CloudFront から Edge Services へ ~CDN を再定義する AWS の新たな取り組み~ #AWSSummit | Developers.IO



 

手順

まず、CloudFront を使ったシンプルなオンデマンド配信の設定方法について説明し、次に、CloudFront の「署名付きURL(Signed URL)」機能を使ったプライベートオンデマンド配信の設定方法について解説します。


「オンデマンド配信」の設定手順、および「プライベートオンデマンド配信」の設定手順の概要については、それぞれ以下のようになります。

オンデマンド配信の設定
  1. CloudFront ディストリビューションの作成
  2. S3 の Bucket Policy を確認
  3. S3 の CORS Configuration の設定
  4. オンデマンド配信のテスト


オンデマンド配信の全体像は以下のようになります。
f:id:akiyoko:20150815230853p:plain

プライベートオンデマンド配信の設定
  1. CloudFront ディストリビューションの設定変更
  2. S3 の CORS Configuration の設定
  3. IAMユーザを作成
  4. CloudFront用キーペアを発行
  5. 署名付きURL を作成(Boto を使ったテスト)
  6. S3 からマニフェストファイルを取得(Boto を使ったテスト)
  7. 署名付きURL でマニュフェストファイルを書き換え(Boto を使ったテスト)
  8. 書き換えたマニフェストファイルをレスポンスとして返す(Django を利用)
  9. プライベートオンデマンド配信のテスト


プライベートオンデマンド配信の全体像は以下のようになります。
f:id:akiyoko:20150815222019p:plain



前半の手順は、ほとんどこちらの記事と同じです。


こちらの記事も大変参考になりました。大体同じようなことをやろうとされています。
AWS CloudFront でプライベート動画の配信手順の確立 — 株式会社CMSコミュニケーションズ


公式ドキュメントも必読です。



なお、動画は HLSファイル形式で配信することを前提としています。

参考(過去記事)




 

2. オンデマンド配信の設定

まずは、Amazon CloudFront を使って、署名付きURL を使用しないシンプルな(プライベート配信を考慮しない)オンデマンド配信の設定をします。

f:id:akiyoko:20150815230853p:plain


なお、HLS動画ファイル(m3u8ファイル, tsファイル)は、Amazon S3 の「app1-transcoder-out」バケットに以下のように格納しているものとします。


《 HLS動画ファイル格納用バケット 》

app1-transcoder-out/
 └─HLS/
   └─1M/
     └─D0002021500_00000/
        ├─sample.m3u8
        ├─sample00000.ts
        ├─sample00001.ts
        ├─sample00002.ts
        ├─sample00003.ts
        ├─sample00004.ts
        └─sample00005.ts

参考(過去記事)



実際に、AWS Management Console 上で作業を進めていきます。


 

2.1. CloudFront ディストリビューションの作成

AWS Management Console から、「CloudFront」を選択します。

f:id:akiyoko:20150815205652p:plain

「Create Distribution」をクリック。
f:id:akiyoko:20150815205705p:plain

ディストリビューションの種類を選択します。
HLS動画の配信をする場合には、(「Distribute media files using HTTP or HTTPS.」とあるように)「Web」側の「Get Started」ボタンを選択します。
f:id:akiyoko:20150815205543p:plain


次に、ディストリビューションの各項目を設定するのですが、ここでは大きなポイントが二つあります。


一つめのポイントは「オリジンアクセスアイデンティティ(Origin Access Identity)」で、オリジンサーバ(S3 バケット等)へのアクセスを限定し、特定のオリジンアクセスアイデンティティを指定したディストリビューションからのアクセスのみを許可するように設定することができます。

[Origin Access Identity] で "Create a New Identity" とすることで、新規にオリジンアクセスアイデンティティが作成されます。また [Grant Read Permissions on Bucket] を "Yes, Update Bucket Policy" とすると、S3バケットのバケットポリシーを書き換えてくれます。


CloudFront+S3で署名付きURLでプライベートコンテンツを配信する | Developers.IO」より


二つめのポイントは「Restrict Viewer Access」で、署名付きURL を使うかどうかを設定します。まずは、これを「No」に設定して、署名付きURL を使用しないシンプルな(プライベート配信を考慮しない)オンデマンド配信の設定を行うことにします。なお、この設定はディストリビューションを作成した後でも変更することができます。


以下を入力後、「Create Distribution」ボタンをクリックすると、ディストリビューションが作成されます。

Origin Settings
設定項目 意味 入力例
Origin Domain Name オリジンサーバ名。S3の場合はドロップダウンリストで選択 app1-transcoder-out.s3.amazonaws.com
Origin ID オリジンサーバに割り当てる識別子。「Origin Domain Name」を選択すると自動設定される S3-app1-transcoder-out
Restrict Bucket Access 「Yes」にすることで、アクセス制限ができるようになる Yes
Origin Access Identity CloudFrontを経由しない場合にS3へのアクセスを制限するためのオリジンアクセスアイデンティティの設定 Create a New Identity
Comment オリジンアクセスアイデンティティを新規に作成すると自動生成される access-identity-app1-transcoder-in.s3.amazonaws.com
Grant Read Permissions on Bucket Bucket Policyを更新するかどうか Yes, Update Bucket Policy

 

Default Cache Behavior Settings
設定項目 意味 入力例
Viewer Protocol Policy 「HTTPとHTTPS」の両方を対象とするか「HTTPS」のみを対象とするかを設定 HTTPS Only
Allowed HTTP Methods 許可するHTTPメソッドを設定。参照するだけの場合は「GET, HEAD」だけで十分 GET, HEAD
Object Caching キャッシュの保持時間を設定。「Use Origin Cache Headers」を選択すると、オリジンサーバから送信されてくる Cache-Control, Expireなどのヘッダーの値を見てキャッシュの保持時間が決定される。Cache-Control, Expireなどのヘッダーが送信されていない場合は24時間がデフォルトの保持期限となる Use Origin Cache Headers
Forward Cookies Cookieを転送するかどうかの設定。「None (Improves Caching)」の場合はCookieを転送しない。「Whitelist」を選択した場合は、「Whitelist Cookies」に記載されているCookieだけを転送する。「All」の場合は全てのCookieを転送する None (Improves Caching)
Forward Query Strings クエリー文字列を転送するかどうかの設定 No (Improves Caching)
Restrict Viewer Access (Use Signed URLs or Signed Cookies) 署名付きURLを要求するかどうかを設定 No

 

Distribution Settings
設定項目 意味 入力例
Price Class どのエッジサーバを使うかを設定。利用するエッジサーバを制限することでコストを抑えることができる Use All Edge Locations (Best Performance)
Alternate Domain Names (CNAMEs) このエッジサーバに別名でアクセスするときは、そのドメイン名を列挙(最大10個まで) (なし)
SSL Certificate HTTPS通信かつ独自ドメインで運用する場合、その独自ドメインに設定された SSL証明書を設定 Default CloudFront Certificate (*.cloudfront.net)
Default Root Object パスだけを指定されたときに、返すデフォルトのファイル名を指定 (なし)
Logging アクセスログを記録するかどうかの設定 Off
Comment 任意のコメント (なし)
Distribution State ディストリビューションの作成直後すぐに有効にするかどうか Enabled

(「Amazon Web Services クラウドデザインパターン 実装ガイド 改訂版」の「3-5. キャッシュで負荷軽減する "Cache Distributionパターン"」より抜粋・編集)


f:id:akiyoko:20150819081757p:plain





ディストリビューション作成直後は、ステータスが「In Progress」となっていますが、
f:id:akiyoko:20150816124405p:plain
15分ほど待つと、「Deployed」になりました。
f:id:akiyoko:20150816124414p:plain

変更がすべてのエッジロケーションに瞬時に伝達されるとは限りません。すべてのエッジロケーションへの伝達に 15 分ほどかかる場合があります。伝達が完了すると、ディストリビューションのステータスが [InProgress] から [Deployed] に変わります。


「Domain Name」が CloudFront のホスト名になります。
f:id:akiyoko:20150815212743p:plain



2.2. S3 の Bucket Policy を確認

S3 の Bucket Policy をチェックすると、以下のように、CloudFront の「Origin Access Identity」のポリシーが追加されているのが確認できます。
f:id:akiyoko:20150815222902p:plain

ディストリビューション作成時に、「Grant Read Permissions on Bucket(Bucket Policyを更新するかどうか)」に「Yes, Update Bucket Policy」を選択しましたが、すでに設定されていた Bucket Policy は削除されないので、例えば上記のように、

		{
			"Sid": "Stmt1434989800735",
			"Effect": "Allow",
			"Principal": "*",
			"Action": "s3:GetObject",
			"Resource": "arn:aws:s3:::app1-transcoder-out/*"
		},

という既存のポリシーがあった場合は削除してしまいます(CloudFront からのアクセスのみを許可したいので)。

f:id:akiyoko:20150815222938p:plain


2.3. S3 の CORS Configuration の設定

S3 の CORS Configuration の設定をしていないと、Video.js から HLS動画を再生したときに同一制限元ポリシー(Same Origin Policy)のエラーが発生してしまうので、以下のように設定を行います(最低限の設定です)。

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
    <CORSRule>
        <AllowedOrigin>*</AllowedOrigin>
        <AllowedMethod>GET</AllowedMethod>
        <MaxAgeSeconds>3000</MaxAgeSeconds>
        <AllowedHeader>Authorization</AllowedHeader>
    </CORSRule>
</CORSConfiguration>

f:id:akiyoko:20150821015751p:plain


ハマったところ

S3 のファイルを CloudFront でキャッシュしている状況で 上記のように CORS Configuration の設定を変更すると、その設定が反映される(浸透する)まで丸一日ほど経たないと Same Origin Policy のエラーが解消されない、ということがありました(Object Caching の設定時間が影響??)。
原因や条件等は深追いしていませんが、CORS の設定を変えた場合は気長に待つことにしましょう。



 

2.4. オンデマンド配信のテスト

以下の設定で Amazon EC2インスタンス(Ubuntu 14.04 LTS)を起動し、CloudFront にキャッシュした HLS動画ファイルを再生するための Webページを作成して、Nginx で公開してみます。なお、HLS動画をストリーミング配信するために、Video.js を利用します。


参考(過去記事)


項目 内容
Region Tokyo
VPC default
Subnet No preference
SecurityGroup 22, 80番を許可(MyIPのみ)
AMI Ubuntu Server 14.04 LTS (HVM)
Elastic IP 52.68.xxx.xxx

 

2.4.1. SSH で乗り込む(on Mac)

EC2 インスタンスに SSH で乗り込みます。

$ ssh -i ~/Downloads/T1-key.pem ubuntu@52.68.xxx.xxx

 

2.4.2. Nginx のインストールおよび設定(on EC2)
$ sudo apt-get update
$ sudo apt-get install -y nginx
$ nginx -v
nginx version: nginx/1.4.6 (Ubuntu)

$ sudo vi /etc/nginx/sites-available/default
---
server {
    listen 80;
    location / {
        root /var/www/html;
        index index.html;
    }
}
---
$ sudo service nginx reload

$ sudo mkdir -p /var/www/html/

 

2.4.3. Video.js のファイル転送(on Mac)

ローカルから、Video.js 関連の css, js ファイルを転送します。

$ scp -i ~/Downloads/T1-key.pem -r /Users/akiyoko/github/videojs-sample/dist ubuntu@52.68.xxx.xxx:/home/ubuntu/


 

2.4.4. index.html の作成(on EC2)
$ sudo mv /home/ubuntu/dist/* /var/www/html/
$ sudo vi /var/www/html/index.html
---
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Video.js Test | akiyoko blog</title>
  <link href="css/video-js.min.css" rel="stylesheet">
  <script src="js/video.js"></script>
  <script src="js/videojs-media-sources.js"></script>
  <script src="js/videojs.hls.min.js"></script>
  <script type="text/javascript">
    videojs.options.flash.swf = "js/video-js.swf";
  </script>
</head>
<body>
<video id="test" class="video-js vjs-default-skin vjs-big-play-centered" controls preload="auto" width="640" height="360" data-setup="{}">
  <source src="https://d6xxxxxxxxxxx.cloudfront.net/HLS/1M/D0002021500_00000/sample.m3u8" type="application/x-mpegURL">
</video>
</body>
</html>
---


最後に、Webページにアクセスして、HLS動画がストリーミング再生できるかどうかを確かめます。

f:id:akiyoko:20150819075900p:plain





3. プライベートオンデマンド配信の設定

次に、Amazon CloudFront の「署名付きURL(Signed URL)」という機能を使って一定期間のみ有効となるワンタイムの URL を発行することで、プライベートな動画を容易にダウンロードされないような仕組みで、HLS動画のストリーミング配信を行います。


以下のような最終形を目指します(図は再掲)。
f:id:akiyoko:20150815222019p:plain



参考


Amazon Web Services CloudFront Signed URLs



 

3.1. CloudFront ディストリビューションの設定変更

作成したディストリビューションに対して、著名付きURL を使用するように設定変更します。この設定をすることで、署名付きURL を使わないとアクセスができないように制限をかけることができます。


まず、CloudFront のディストリビューション一覧から対象となるディストリビューションを選択し、「Behaviors」タブからビヘイビアをチェックして「Edit」ボタンをクリックします。
f:id:akiyoko:20150821234453p:plain

「Restrict Viewer Access (Use Signed URLs or Signed Cookies)」を「Yes」に変更し、「Yes, Edit」ボタンをクリックして設定を完了させます。
f:id:akiyoko:20150821234859p:plain

ステータスが「Deployed」になるまで、再び 15分ほど掛かります。


 

3.2. S3 の CORS Configuration の設定

シンプルなオンデマンド配信をする場合と同様に、CORS Configuration の設定が必要です。


なお、Same Origin Policy のエラー対策として、ディストリビューションの「Forward Headers」の項目に「Origin」を追加しないとダメ、という記事をちらほら見かけましたが、今回のケースでは、「Forward Headers」の設定は「None」のままで問題ありませんでした。むしろ「Origin」を設定すると Same Origin Policy エラーが発生してしまいました。

f:id:akiyoko:20150821235903p:plain

参考(にならなかったが非常によくまとまっている)
css - Amazon S3 CORS (Cross-Origin Resource Sharing) and Firefox cross-domain font loading - Stack Overflow





 

3.3. IAMユーザを作成

CloudFront や S3 のサービスを利用するのに AWS のアクセスキー(アクセスキー ID およびシークレットアクセスキー)を使うのですが、AWSルートアカウントでアクセスキーを作成してしまうと漏洩してしまったときのインパクトが大きすぎるので、管理者用の「Admin」ユーザを IAM で作成して、そのユーザに対してアクセスキーを発行することにします。


以下は、ルートアカウントでの操作です。


IAM のダッシュボードから、[Users] -> [Create New Users] を選択し、ユーザ名「Admin」を入力して「Create」ボタンをクリックし、新規の IAMユーザーを作成します。

f:id:akiyoko:20150812021905p:plain


Users 一覧からユーザ名「Admin」を選択して、ユーザの詳細情報ページに移動します。
まずは、「User ARN」を確認しておきます。この User ARN (Amazon Resource Name) は、S3 の Bucket Policy でアクセスを許可する IAMユーザを特定するために使用します。

f:id:akiyoko:20150813005145p:plain


次に、
[Security Credentials] -> [Access Keys] -> [Create Access Key] をクリックして、アクセスキーを作成します。このアクセスキーは、Boto のプログラム内で S3 への認証・認可を行うのに使用します。

f:id:akiyoko:20150812022148p:plain


ここで、Adminユーザに AWS 管理ポリシーをアタッチして、完全な管理者アクセス権限(AdministratorAccess)を与えます。*1
f:id:akiyoko:20150922195029p:plain

f:id:akiyoko:20150923024916p:plain

実際のポリシーはこのようになります。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "*",
      "Resource": "*"
    }
  ]
}


最後に、
[Security Credentials] -> [Sign-In Credentials] -> [Manage Password] をクリックして、IAMユーザにパスワードを付けます。

f:id:akiyoko:20150812022317p:plain
f:id:akiyoko:20150812022327p:plain


一旦ログアウトし、
https://{AWS Account ID}.signin.aws.amazon.com/console
から、Adminユーザの新パスワードでログインできるかを確認しておきます。


なお、「AWS Account ID」は、AWSアカウントに紐づく 12桁の数字で、My Account ページの [Accout Settings] からも確認できます。
f:id:akiyoko:20150813011624p:plain




3.4. CloudFront用キーペアを発行

署名付きURLを発行するための CloudFront用キーペアを作成します。

AWSルートアカウントのメニューから、「Security Credentials」を選択。
f:id:akiyoko:20150816001220p:plain

「CloudFront Key Pairs」の「Create Key Pair」をクリックして、 CloudFront用キーペアを発行します。
f:id:akiyoko:20150816001227p:plain

「Download Private Key File」をクリックして、秘密鍵をダウンロードしておきます(公開鍵は後からでもダウンロードできますが、秘密鍵はこのタイミングでしかダウンロードできません)。秘密鍵のファイル名は、「pk-{CloudFront Key Pairs の Access Key ID}.pem」となっています。
f:id:akiyoko:20150816001240p:plain


3.5. 署名付きURL を作成(Boto を使ったテスト)

Boto で署名付きURL(Signed URL)を作成してみます。

その際、3.3. で取得した「Admin」ユーザのアクセスキー(アクセスキー ID およびシークレットアクセスキー)、および 3.4. で取得した CloudFront用キーペアの ID と秘密鍵を利用します。


なお、

のサンプルを参考にさせていただきました。



まず先に、ローカルから、以下のようにして CloudFront用キーペアの RSA秘密鍵ファイルを EC2インスタンスに転送しておきます。

$ scp -i ~/Downloads/T1-key.pem ~/Downloads/pk-APKXXXXXXXXXXXXXXXXX.pem ubuntu@52.68.xxx.xxx:/home/ubuntu/


 

テストコード

test.py

#!/usr/bin/python
import time
from boto.cloudfront import CloudFrontConnection
from boto.cloudfront.distribution import Distribution

ACCESS_KEY_ID = 'AKIXXXXXXXXXXXXXXXXX'
SECRET_ACCESS_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
CLOUDFRONT_KEYPAIR_ID = "APKXXXXXXXXXXXXXXXXX"
CLOUDFRONT_PRIVATE_KEY_FILE_LOCATION = "/home/ubuntu/pk-APKXXXXXXXXXXXXXXXXX.pem"


def test():
    url = "https://d6xxxxxxxxxxx.cloudfront.net/HLS/1M/D0002021500_00000/playlist.m3u8"
    expire_time = int(time.time() + 60 * 60)  # 60 mins

    conn = CloudFrontConnection(ACCESS_KEY_ID, SECRET_ACCESS_KEY)
    dist = Distribution(conn)
    signed_url = dist.create_signed_url(url, CLOUDFRONT_KEYPAIR_ID, expire_time, private_key_file=CLOUDFRONT_PRIVATE_KEY_FILE_LOCATION)
    print signed_url


if __name__ == '__main__':
    test()
実行結果
# python test.py
https://d6xxxxxxxxxxx.cloudfront.net/HLS/1M/D0002021500_00000/playlist.m3u8?Expires=1440095109&Signature=NqJzsvO04PEAD7Ge3bRYml2PlXhuQEFXwGYicX6UP5zt7in2UnUMSm6bBz8Qvi6WOXRIJpNWseA7RoYjn7oGF0bNnK2~DvYlUzBcNuWa~Sif8PjghPt3Rw8KSMhVd7I5C4k5cgeb0XOQVR9u7fAh7qfu1zyKkg0utq7d06pHIGz4SX3mvLeCmw~mWp2ur7KByYZ1uAj~gl~80sXL2qzfK4xcj0UnifGCsrteLAF2dPIMb7rJHiBJOBMM5h7v5bEWBnAkQgUJNgoFuUHJ9BsdyRvOGUv4TzML8yTstViF~shrSMNKoF4fmO11hgWrOts3~bNQP8HslCQ1ucOOzSpXFw__&Key-Pair-Id=APKXXXXXXXXXXXXXXXXX


なお、まっさらな Ubuntu 14.04 で実行する場合は、先に、

$ pip install rsa

が必要かもしれません。


 

3.6. S3 からマニフェストファイルを取得(Boto を使ったテスト)

次に、Boto を使って S3上のマニフェストファイルを取得してみます。


参考(過去記事)


マニフェストファイル(m3u8ファイル)は、S3 の「app1-transcoder-out」バケットに以下のように配置されているものと仮定します。

app1-transcoder-out/
 └─HLS/
   └─1M/
     └─D0002021500_00000/
        ├─sample.m3u8
        ├─sample00000.ts
        ├─sample00001.ts
        ├─sample00002.ts
        ├─sample00003.ts
        ├─sample00004.ts
        └─sample00005.ts


 

Bucket Policy を設定

S3 からマニフェストファイルを取得するために、まずは、S3 の Bucket Policy の設定を行い、Adminユーザに対して ListBucket/GetObject を許可するようにします。


オリジンアクセスアイデンティティ作成時に更新した Bucket Policy を残しつつ、Adminユーザへの ListBucket/GetObject を許可するポリシーを追加します。最終形は、以下のようなものになります。

{
  "Version": "2012-10-17",
  "Id": "PolicyForCloudFrontPrivateContent",
  "Statement": [
    {
      "Sid": "2",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity XXXXXXXXXXXXXX"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::app1-transcoder-out/*"
    },
    {
      "Sid": "Stmt1439041726404",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::xxxxxxxxxxxx:user/Admin"
      },
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::app1-transcoder-out"
    },
    {
      "Sid": "Stmt1439040494019",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::xxxxxxxxxxxx:user/Admin"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::app1-transcoder-out/*"
    }
  ]
}



S3 からファイルを取得する方法はいろいろあるのですが、今回は、boto.s3.key.Key の get_contents_to_file() を使用しました。

CloudFront — boto v2.38.0


 

テストコード

test.py

#!/usr/bin/python
from cStringIO import StringIO
from boto import connect_s3

ACCESS_KEY_ID = 'AKIXXXXXXXXXXXXXXXXX'
SECRET_ACCESS_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
BUCKET_NAME = 'app1-transcoder-out'
KEY_NAME = 'HLS/1M/D0002021500_00000/sample.m3u8'


def test():
    conn = connect_s3(ACCESS_KEY_ID, SECRET_ACCESS_KEY)
    bucket = conn.get_bucket(BUCKET_NAME)
    key = bucket.get_key(KEY_NAME)

    if key is None:
        raise Exception("No such key was found. key={}".format(key))

    fp = StringIO()
    key.get_contents_to_file(fp)
    fp.seek(0)

    print fp.getvalue()
    fp.close()


if __name__ == '__main__':
    test()
実行結果
$ python test.py
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:13
#EXTINF:12.078744,
sample00000.ts
#EXTINF:9.009011,
sample00001.ts
#EXTINF:9.009011,
sample00002.ts
#EXTINF:12.012011,
sample00003.ts
#EXTINF:9.009011,
sample00004.ts
#EXTINF:2.068733,
sample00005.ts
#EXT-X-ENDLIST



 

3.7. 署名付きURL でマニュフェストファイルを書き換え(Boto を使ったテスト)

S3 から取得したマニフェストファイルを、CloudFront の署名付きURL で書き換えます。署名付きURL の作成は、boto.cloudfront.distribution.Distribution の create_signed_url() を使用します。

CloudFront — boto v2.38.0


テストコード

test.py

from cStringIO import StringIO
import os.path
import re
import time

from boto import connect_s3
from boto.cloudfront import CloudFrontConnection
from boto.cloudfront.distribution import Distribution

ACCESS_KEY_ID = 'AKIXXXXXXXXXXXXXXXXX'
SECRET_ACCESS_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
BUCKET_NAME = 'app1-transcoder-out'
KEY_NAME = 'HLS/1M/D0002021500_00000/sample.m3u8'
TS_PATTERN = r'^(?!#)\w+\.ts$'
CLOUDFRONT_URL_PREFIX = 'https://d6xxxxxxxxxxx.cloudfront.net/'
CLOUDFRONT_KEYPAIR_ID = "APKXXXXXXXXXXXXXXXXX"
CLOUDFRONT_PRIVATE_KEY_FILE_LOCATION = "/home/ubuntu/pk-APKXXXXXXXXXXXXXXXXX.pem"


def get_playlist_with_signed_url():
    # Get file from S3
    s3_conn = connect_s3(ACCESS_KEY_ID, SECRET_ACCESS_KEY)
    bucket = s3_conn.get_bucket(BUCKET_NAME)
    key = bucket.get_key(KEY_NAME)

    if key is None:
        raise Exception("No such key was found. key=%s" % key)
    fp = StringIO()
    key.get_contents_to_file(fp)
    fp.seek(0)

    # Convert to signed url
    cf_conn = CloudFrontConnection(ACCESS_KEY_ID, SECRET_ACCESS_KEY)
    dist = Distribution(cf_conn)
    expire_time = int(time.time() + 60 * 60)  # 60 mins

    outlines = []
    for line in fp.readlines():
        line = line.rstrip()
        matchObj = re.search(TS_PATTERN, line)
        if matchObj is not None:
            file_name = matchObj.group()
            url = os.path.join(os.path.dirname(os.path.join(CLOUDFRONT_URL_PREFIX, KEY_NAME)), file_name)
            signed_url = dist.create_signed_url(url, CLOUDFRONT_KEYPAIR_ID, expire_time, private_key_file=CLOUDFRONT_PRIVATE_KEY_FILE_LOCATION)
            outlines.append(signed_url)
        else:
            outlines.append(line)
    fp.close()
    return '\n'.join(outlines)


if __name__ == '__main__':
    playlist = get_playlist_with_signed_url()
    print playlist
実行結果
$ python test.py 
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:13
#EXTINF:12.078744,
https://d6xxxxxxxxxxx.cloudfront.net/HLS/1M/D0002021500_00000/sample00000.ts?Expires=1439836247&Signature=ClwpcGw9zQkYkP8sy-fctVoxevM6ylgJmKC4RRTxWCZZQLh-H6oI0SyYLCDQ2mLt4xWuDd6UrvMApzwoE79ZuNcH-IdDqZWOJej2DHx~6~qgXT2icWow75sO-FjR6lF15tQPGlf7kKLh1~VLk4v5msy5Wbiv1EFSpI3DjVOqM~LdS0e97qcy2CDRNYpfDNx49z4rHZfdZOdGSB85OL8MxpKENHzqylx-t2WPlkA8wPoz2O9lr5HRVxwa-~JUzZJ5YmS0d~FQU-22dXF3VPdj0lt-uZGnd0B0Xhq-3shIF6ohXPTCSGCgK42eFFjHRPqhJ9IbIZHGhDWRRM0rpcaPlQ__&Key-Pair-Id=APKXXXXXXXXXXXXXXXXX
#EXTINF:9.009011,
https://d6xxxxxxxxxxx.cloudfront.net/HLS/1M/D0002021500_00000/sample00001.ts?Expires=1439836247&Signature=E0Ia-SU1TlrU7FSYrEk-zrEYH1e7BgUXA7yjXKIuBmZW-fs4XvJXjd1GqNvqnTddBZ7iuc0NA3y5CwU6B8NoINCbUon-ZsLkB4l0bEqfYYBE9~kVTo8MkiJqtp6494BixEH9Mqxurn2NYHG5iO-O5D7pghztCf7~fCQQY6q35NtKmpQoyzZd9hS8lFfwCqApk3zhN80ti0kLqFeMAYEZ8CX7wViTfUEinoPRssZaRspYnn3Q74E9JfNarxX~jQ38v~dNnvJJDCVStWUSNETPEA8X8I7JeiLvHYURBOBN271cR4ceBoS5Pjy7eDn57TSqqFP~8poxDhZxsxl4JOihYw__&Key-Pair-Id=APKXXXXXXXXXXXXXXXXX
#EXTINF:9.009011,
https://d6xxxxxxxxxxx.cloudfront.net/HLS/1M/D0002021500_00000/sample00002.ts?Expires=1439836247&Signature=ef8rw3f0KJvlUkJ7JSpetYsfNQ8~eK1nHRR~XQ~m-4Fv-aSqoxsh~jDJh94S3lc1EwoFpBgIds2FNwCIUPf-wjPE9ETObPqW2bXrdG9c133kgR31Xq5OKiwMSSUvo9LexCVFRfn9mamodvoHyyXR4BfxF4ZfCVSdhmCAXNg1CNVbDKlDlhaVL4DQ5~BPpmzdAzZ90f9tBrneuhJ1c9wPz7wQEYRp34ULLw9q3yj-TMg3remBGG~gz7e24mCaVU-xwTlkkkTRt~VXTVNYb7w2uJU6DoFGeckSfI3J3ahLyf3IS0rooVFtexzoFcrLclf9jyT-QmYo3rJLW2ddzV5sSw__&Key-Pair-Id=APKXXXXXXXXXXXXXXXXX
#EXTINF:12.012011,
https://d6xxxxxxxxxxx.cloudfront.net/HLS/1M/D0002021500_00000/sample00003.ts?Expires=1439836247&Signature=JfYqwOWBaIdOATGVKj~6fqwMK2Lho30sVSl-Hkx09E3VHyf2JUBM1MNwJJ8292uWCwGRNMi0mQuCDiCYjTuyEfAZqP0e8Pk-7WiAIOW3L0B5bl8Ho61kbWO1W0LIwHdfon8c2raUj4d5B1kGBmhk3NbwHqTzysGl1wUBSXVlJul0GGSOD23YaBWbm-VuCETSlWR92zR6VRChYYWcpttRyfq9WIJVPx7Xw4OJANhnRtC61aH2e3uFlh7IbRZAsAlk~8e8XOPY4zTAJJ2cWfcRn2lfsVpovF7hgQxo0WSmkV0Z78vdVas54yzJKx68tkMymhG7rss0FtMaqVtk33VDjg__&Key-Pair-Id=APKXXXXXXXXXXXXXXXXX
#EXTINF:9.009011,
https://d6xxxxxxxxxxx.cloudfront.net/HLS/1M/D0002021500_00000/sample00004.ts?Expires=1439836247&Signature=LZxLa5fxeZa-~25lHaKCXsfDwNGtrnaqZFRq9CRfsdFWfDMzaWWXZVW3Di4TFF2NjXInAUYiw4tvfc4hxhcT2U6CCO0MTGq-Nmzl3f3NZJkm1H~G1~DxLX-p1wAX53sHt-jf~RqcBI5IiXlmmZ0BIZZTzsHAuAWJRWMnfM0wnAVJYJS~tVj1tX0nzZmEzZ2GJhnXgMuiNH0qAmuPSEtnMLQ0hSp54NONSu1j4PuJaqxPFto95NvkxFyghbWrQbS9ruWPDcvjjUTc-eEnGDupYr0kTHkRQXwXO7nTZN8ZxOMzmbZ9QbIWO094cIV0P5qm6wopa5NMnfy8KGBxQSyBTw__&Key-Pair-Id=APKXXXXXXXXXXXXXXXXX
#EXTINF:2.068733,
https://d6xxxxxxxxxxx.cloudfront.net/HLS/1M/D0002021500_00000/sample00005.ts?Expires=1439836247&Signature=OP~cifDJ0GdbAq6rUf4QbsfXPwq7loyHkhzm641ngQOUErd6qMGOuXhEuiGWnnwTh1Urbf4VtPhRkCTMoS5Z1YGqcMyrffNaZ3CN~LJFgDRHdPn3ObHcESqNPRHZ3469VKHUCKjt5k1xwccxhoJj58zWnGosSEGcydSK7TY-0DdSqEh-A3p5zQe65vmj89A0ROaTouMAq8EaDJE7Ais-mrNHpRJ82jN-vlhTtiqrdTSI1~dvABoHsj3mhfXil4mRtKKAzH7SypdNovkDtE~q-Jaq978Pdh7slyKW6kT4ON-zuKxXVcEqkHRGrCT3xMwCSU0uU77VGce6gE9KbfPZWg__&Key-Pair-Id=APKXXXXXXXXXXXXXXXXX
#EXT-X-ENDLIST

参考
Pythonでの正規表現の使い方 - Qiita


なお、「NotImplementedError: Boto depends on the python rsa library to generate signed URLs for CloudFront」というエラーが出た場合は、RSA のライブラリが不足しているので、

$ pip install rsa

とします。




3.8. 書き換えたマニフェストファイルをレスポンスとして返す(Django を利用)

3.5 から 3.7 までのテストコードを踏まえて、実際に、HLS動画のプライベートオンデマンド配信を行うアプリケーションを構築してみます。

アプリケーションの構築には、(Python製の)Django + Gunicorn + Nginx を使用しましたが、もちろんこの組み合わせでないとダメという訳ではありません。署名付きURL で書き換えたマニフェストファイル(m3u8ファイル)をレスポンスとして返す View と、HLS動画ファイルを再生するための Webページをレスポンスとして返す View を提供できれば、HLS動画のプライベートオンデマンド配信は可能です。


3.8.1. Django アプリケーションの作成

まずは、Django をインストールし、アプリケーションを作成します。

### pip をインストール
$ sudo apt-get update
$ sudo apt-get install -y python-pip python-dev

### virtualenv をインストール
$ sudo pip install virtualenv

### Django をインストールし、/opt/webapps配下に Djangoプロジェクトを作成
$ sudo mkdir -p /opt/webapps
$ sudo chown `whoami`. /opt/webapps
$ cd /opt/webapps/
$ virtualenv venv
$ source venv/bin/activate
(venv)$ pip install django
(venv)$ pip list | grep Django
Django (1.8.3)
(venv)$ pip install boto rsa

### Djangoプロジェクトを作成
(venv)$ django-admin.py startproject myproject
(venv)$ cd myproject/
(venv)$ python manage.py migrate
(venv)$ python manage.py createsuperuser
(admin/admin@example.com/adminpass)

$ python manage.py startapp videos
$ vi myproject/settings.py
---
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'videos',
)
---



Django アプリケーションの実装は、以下の通りとします。

参考
akiyoko/django-videojs-sample · GitHub


videos/models.py

from django.db import models

class Video(models.Model):
    playlist_path = models.CharField(max_length=200)

vi videos/admin.py

from django.contrib import admin
from videos.models import Video

admin.site.register(Video)

vi myproject/settings.py

    ・
    ・
# Application settings
ACCESS_KEY_ID = 'AKIXXXXXXXXXXXXXXXXX'
SECRET_ACCESS_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
PLAYLIST_BUCKET_NAME = 'app1-transcoder-out'
CLOUDFRONT_URL_PREFIX = 'https://d6xxxxxxxxxxx.cloudfront.net/'
CLOUDFRONT_KEYPAIR_ID = "APKXXXXXXXXXXXXXXXXX"
CLOUDFRONT_PRIVATE_KEY_FILE_LOCATION = "/home/ubuntu/pk-APKXXXXXXXXXXXXXXXXX.pem"

vi videos/views.py

from cStringIO import StringIO
import os.path
import re
import time

from boto import connect_s3
from boto.cloudfront import CloudFrontConnection
from boto.cloudfront.distribution import Distribution
from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import render_to_response
from videos.models import Video

TS_PATTERN = r'^(?!#)\w+\.ts$'


def detail(request, video_id):
    try:
        video = Video.objects.get(pk=video_id)
    except Video.DoesNotExist:
        raise Http404("Video does not exist")
    context = {'video': video}
    return render_to_response('videos/detail.html', context)


def get_playlist(request, video_id):
    try:
        video = Video.objects.get(pk=video_id)
    except Video.DoesNotExist:
        raise Http404("Video does not exist")

    playlist = get_playlist_with_signed_url(video.playlist_path)
    response = HttpResponse(playlist, content_type="text/plain")
    return response


def get_playlist_with_signed_url(playlist_path):
    # Get playlist file from S3
    s3_conn = connect_s3(settings.ACCESS_KEY_ID, settings.SECRET_ACCESS_KEY)
    bucket = s3_conn.get_bucket(settings.PLAYLIST_BUCKET_NAME)
    key = bucket.get_key(playlist_path)

    if key is None:
        raise Exception("No such key was found. key={}".format(key))

    fp = StringIO()
    key.get_contents_to_file(fp)
    fp.seek(0)

    # Convert with signed url
    cf_conn = CloudFrontConnection(settings.ACCESS_KEY_ID, settings.SECRET_ACCESS_KEY)
    dist = Distribution(cf_conn)
    expire_time = int(time.time() + 60 * 60)  # 60 mins

    outlines = []
    for line in fp.readlines():
        line = line.rstrip()
        matchObj = re.search(TS_PATTERN, line)
        if matchObj is not None:
            file_name = matchObj.group()
            url = os.path.join(os.path.dirname(os.path.join(settings.CLOUDFRONT_URL_PREFIX, playlist_path)), file_name)
            signed_url = dist.create_signed_url(url, settings.CLOUDFRONT_KEYPAIR_ID, expire_time, private_key_file=settings.CLOUDFRONT_PRIVATE_KEY_FILE_LOCATION)
            outlines.append(signed_url)
        else:
            outlines.append(line)
    fp.close()
    return '\n'.join(outlines)

vi videos/urls.py

from django.conf.urls import patterns, url
from videos import views

urlpatterns = patterns('',
    url(r'^(?P<video_id>\d+)/$', views.detail, name='detail'),
    url(r'^get_playlist/(?P<video_id>\d+)/$', views.get_playlist, name='get_playlist'),
)

vi myproject/urls.py

from django.conf.urls import include, url
from django.contrib import admin

urlpatterns = [
    url(r'^videos/', include('videos.urls')),
    url(r'^admin/', include(admin.site.urls)),
]

vi videos/templates/videos/detail.html

{% load staticfiles %}
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>Video.js Test | akiyoko blog</title>
  <link href="{% static "videos/css/video-js.min.css" %}" rel="stylesheet">
  <script src="{% static "videos/js/video.js" %}"></script>
  <script src="{% static "videos/js/videojs-media-sources.js" %}"></script>
  <script src="{% static "videos/js/videojs.hls.min.js" %}"></script>
  <script type="text/javascript">
    videojs.options.flash.swf = "{% static "videos/js/video-js.swf" %}";
  </script>
</head>
<body>
<video id="test" class="video-js vjs-default-skin vjs-big-play-centered" controls preload="auto" width="640" height="360" data-setup="{}">
  <source src="/videos/get_playlist/{{ video.id }}" type="application/x-mpegURL">
</video>
</body>
</html>


DBを作成します。

$ python manage.py makemigrations
$ python manage.py migrate


ローカルから、Video.js 関連のファイルを転送します。

$ scp -i ~/Downloads/T1-key.pem -r /Users/akiyoko/github/videojs-sample/dist ubuntu@52.68.xxx.xxx:/home/ubuntu/


公開用の static ファイルを配置します。

$ mkdir -p videos/static/videos/
$ cp -a /home/ubuntu/dist/* videos/static/videos/

### settings.py に最後の一行を加える
(venv)$ vi myproject/settings.py
---
  ・
  ・
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static/')
---

(venv)$ python manage.py collectstatic


 

3.8.2. Gunicorn の起動

Django アプリケーションを Gunicorn で起動します。

(venv)$ pip install gunicorn

### デーモンとして実行
(venv)$ gunicorn --workers 3 --bind unix:/tmp/myproject.sock --daemon myproject.wsgi:application


 

3.8.3. Nginx の設定

Gunicorn で起動した Django アプリケーションを、Nginx で外部に公開します。

$ sudo vi /etc/nginx/sites-available/default
---
upstream myproject_backend {
    server unix:/tmp/myproject.sock fail_timeout=0;
}

server {
    listen 80;
    server_name _;

    location = /favicon.ico { access_log off; log_not_found off; }
    location /static/ {
        root /opt/webapps/myproject;
    }

    location / {
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        proxy_pass http://myproject_backend;
    }
}
---

$ sudo service nginx reload


動画配信のテストをする前に、Django の Adminサイトで、

playlist_path: HLS/1M/D0002021500_00001/sample.m3u8

のダミーデータを投入しておきます。



 

3.9. プライベートオンデマンド配信のテスト

http://52.68.xxx.xxx/videos/1/
にブラウザでアクセスし、HLS動画がストリーミング配信されているかチェックします。

f:id:akiyoko:20150819080800p:plain



なお、expire_time(今回は 60分)が経過した後は、各 tsファイルへのリクエストが 403 になり、再生ボタンを押しても何も反応しなくなります。

f:id:akiyoko:20150824190026p:plain



 

4. まとめ

プライベート動画のオンデマンド配信を実現するには、Amazon CloudFront の「オリジンアクセスアイデンティティ(Origin Access Identity)」という機能を使用してアクセスを CloudFront からのものだけに制限しつつ、「署名付きURL(Signed URL)」という機能を使って一定期間のみ有効となるワンタイムの URL を発行することで、プライベート動画のセキュリティを確保する必要があります。

また今回のように、Video.js で HLS動画のストリーミング配信を実現しようとした場合には、同一制限元ポリシー(Same Origin Policy)のエラーを考慮して、CORS Configuration の設定をしなければいけないのもポイントとなります。


今回の記事を書くまでに、いろいろなトライアンドエラーを繰り返したため、約 2ヶ月ほどの調査・検証期間が必要になってしまいました。そのためか、30,000字を超える長文記事になっていますが、最後まで読んでいただければありがたいです。

*1: 今回の手順としては必須の設定ではありませんが、Adminユーザのアクセスキーを使用して AWS CLI を利用する場合などで必要となります。