AWS + Rails で動画配信サイトを作る （4）S3 上の動画ファイルを CloudFront で配信する方法

#1. この記事で扱う内容
AWSの各種サービスとRailsを使用して動画配信（VOD）サイトを構築する手順を、複数回に分けて紹介しています。　全体の概要については 「[（１）概要](https://qiita.com/mas_yam/items/5d6e4973fbdfeb9b7489)」 を参照してください。

このページでは、S3 に 保存された動画ファイルを CloudFront を経由して配信する方法について紹介します。

記載した内容は、私が開発中に試行錯誤した結果であって、使用する環境や目的によっては必ずしも最善ではないかもしれませんので、その点をご注意ください。

#1-1. システム全体の構成
詳しくは 「[（１）概要](https://qiita.com/mas_yam/items/5d6e4973fbdfeb9b7489)」 を参照してください。

#1-2. この記事で使用する環境と前提条件
**環境**

- CentOS 6.5
- Rails 5.2
- ruby 2.4.1
- Gem
 - aws-sdk-rails 2.0.1
 - aws-sdk-s3 1.23.0

**前提**
以下の作業では、Rails アプリから CloudFront へ Cookie を送信するように設定します。　Cookie は異なるドメイン間では送信できないので、Rails アプリが使用するドメインを CloudFront にも割り当てる必要があります。

そこで、Rails アプリおよび CloudFront に割り当てることのできる独自ドメインを所有しているものとして、手順を紹介します（ここではドメインの取得方法については扱いませんので、他のサイト等を参考にして予め取得しておいてください）。

#2. CloudFront の基本設定

CloudFront とは、AWS が提供するコンテンツ配信ネットワーク（CDN）サービスです。　

CloudFront はエッジロケーションと呼ばれるサーバーにコンテンツをキャッシュしており、当該コンテンツにアクセスがあるとそのキャッシュを返します。　したがって、オリジン（実際にコンテンツが保存してあるサーバー）への通信はキャッシュを更新する場合に限られるため、オリジンの負荷を抑えることができます。　特に、AWSのようにデータ転送量に応じて課金される場合は、このキャッシュ機能により使用料を抑えることができます。

また、署名付き URL や署名付き Cookie を使用することで、コンテンツへのアクセスを自サイトのユーザーにのみ許可するといった制限をかけることもできます。

参照：

- https://aws.amazon.com/jp/cloudfront/
- https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/Introduction.html


CloudFront では、コンテンツ配信の各種設定をディストリビューションと呼ばれる単位で管理します（https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/distribution-working-with.html）。

**(1)**
AWS マネジメントコンソールで検索ボックスに「cloudfr」と入力して表示される CloudFront を選択して、CloudFront のダッシュボードを開きます。

**(2)**
CloudFront ダッシュボードの左側メニューから [Distributions] を選び、[Create Distribution]ボタンを押します。

**(3)**
配信方法の選択画面で [Web] の [Get Started]ボタンを押します。
![fig25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/5f8a3d69-b1d3-e62b-8924-9b27d8fc15f2.png)

**(4)**
Create Distribution 画面で表示された項目を入力します。
**Origin Settings**
![fig26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/da9b09d7-e62e-23dd-3830-ea5e734cb6fb.png)

- Origin Domain Name : 配信するコンテンツを保存した場所（Origin）を指定します（プルダウンでアカウントに紐づいたS3バケットが表示されるので、配信するコンテンツが入ったバケットを選択します）
- Origin Path : 特定のディレクトリ配下のコンテンツのみ配信する場合は、ここにディレクトリ名を指定します（「/testdir」と指定すると、/testdir 内のファイルのみ配信される）
- Origin ID : ディストリビューション内でオリジンを識別する一意の文字列を指定します（Origin Domain Name を指定すると自動で入力される）
- Restrict Bucket Access : S3バケットへのアクセスを CloudFront 経由に限定する（S3 URL でアクセスできないようにする）場合は [Yes] を選びます
- Origin Access Identity : CloudFront から S3 バケットにアクセスするための権限を付与した認証情報を指定します。初めて使用する場合は「Create a New Identity」にチェックを入れます。
- Comment : 作成される Origin Access Identity の名称が自動で入力されます
- Grant Read Permissions on Bucket : 上で指定した Origin Access Identity がバケットにアクセスするためのバケットポリシーを自動で設定する場合は「Yes, Update Bucket Policy」を選択します
- Origin Custom Headers : CloudFrontがリクエストをオリジンに転送する際に、追加するヘッダーがある場合は、ここに指定します

**Default Cache Behavior Settings**
![fig27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/4f1dce2b-a253-730f-98e1-380bb6380c61.png)

- Viewer Protocol Policy : 使用するプロトコルを選択します（ここではデフォルトの設定のままにします）
- Allowed HTTP Methods :  許可する HTTP メソッドを選択します。Javascript の動画プレイヤーで HLS ファイルにアクセスする場合、GET メソッドの前に OPTIONS メソッドを送信する場合があるので、「GET, HEAD, OPTIONS」を選択します。
- Field-level Encryption Config : HTML コンテンツ内のフォームのフィールドを暗号化する場合の設定を入力します。今回は、フォームを含む HTML コンテンツは扱わないので空欄のままにしておきます。
- Cached HTTP Methods : キャッシュする HTTP メソッドを指定します。今回はデフォルトの設定のままにしておきます。
- Cache Based on Selected Request Headers : キャッシュの対象とするリクエストヘッダを指定します。今回は特定のヘッダーを指定しするので、「whitelist」を指定します。
- Whitelist Headers : キャッシュの対象とするリクエストヘッダを指定します。今回は「Origin」を指定します。　後程 S3 バケットの CORS 設定で同様に Origin を指定します。

「Default Cache Behavior Settings」のその他の設定はデフォルトのままにしておきます。

**Distribution Settings**
![fig28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/c4ef2fb8-2381-e630-4d7e-795e0bbbe5c3.png)

- Price Class : 使用するエッジロケーションを選択します。選択するクラスによって使用料が異なるので、適切なものを選びます。今回は Asia を含むクラスを選択します。

「Distribution Settings」のその他の設定はデフォルトのままにしておきます。

**(5)**
必要な項目の入力が終わったら、画面右下の「Create Distribution」ボタンを押して、ディストリビューションを作成します。

**(6)**
Distributions 画面に戻り、作成したディストリビューションが表示されていることを確認します。　設定が反映されると、[Status] 欄が「In Progress」から「Deployed」に変わります（表示が変わるまで数十分かかります）。
![fig29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/0ce35661-da17-01ef-1156-c787616f7802.png)

**(7)**
当該ディストリビューションの ID 欄を押すと詳細画面が表示されます。[Domain Name] にある URL で S3 バケットにアクセスできます。
※  実際にアクセスする場合は、「[Domain Name]/ファイル名」でアクセスします
![fig30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/e71c2d09-2e5b-a1d4-5204-e546bafd152e.png)

この場合は「http(s)://d108xxxx.croudfront.net/dir/file-name」でアクセスできます。

#3. S3 バケットの CORS 設定
CORS（オリジン間リソース共有）とは、あるオリジン（ドメイン）から別のオリジン（ドメイン）のサーバーにあるファイルにアクセスできるようにする仕組みです（https://developer.mozilla.org/ja/docs/Web/HTTP/CORS）。

通常、オリジン間リクエストは Javascript の処理等によって生じますが、セキュリティ上の観点から、予め設定してある CORS ヘッダを持つリクエストのみが許可されるようになっています。

ここでいう同一オリジンとは、プロトコル（http, https）、ホスト名（www, www2 など）とドメイン（example.comなど）の組み合わせが完全に一致するものを指し、一つでも異なる部分があるものは異なるオリジンとみなされます（`http://www.example.com`と`https://www.example.com`は異なるオリジンです）。

この記事で構築するシステムは、S3 とは別のサーバーに Web アプリを配置し、その Web アプリに組み込んだ Javascript の動画プレイヤーが S3 上の動画ファイルを読み込むことを想定しているので、リクエストはオリジン間リクエストとなります。　したがって、リクエストを受け付ける側の S3 バケットにおいて、どのようなリクエストを許可するかを設定しておく必要があります。

S3 のバケットには CORS を設定する項目がありますので、以下に紹介します。

**(1)**
S3 ダッシュボードを開き、CloudFront 経由で配信する動画ファイルが保存されたバケットを開きます。

**(2)**
「アクセス権限」タブの「CORSの設定」を選びます。
![fig31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/5e9fd3ef-e4c5-be5a-03d8-0489859293fd.png)

**(3)**
CORS 構成エディタに以下のように入力し、「保存」ボタンを押します。

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

- AllowedOrigin : アクセス元のオリジンを指定します
- AllowedMethod : 許可する HTTP メソッドを指定します
- MaxAgeSeconds : レスポンスをブラウザでキャッシュできる時間を秒単位で指定します
- AllowedHeader : 許可するヘッダを指定します

参照：https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/cors.html

#4. CloudFrontの署名付きクッキーを使用する
特定のユーザーにのみコンテンツアクセスを許可するには、次の方法を併用します。

- 署名付き URL または署名付き Cookie を使用してプライベートコンテンツにアクセスするようユーザーに要求する（ユーザー認証のため）
- S3 コンテンツへのアクセスにユーザーが Amazon S3 URL ではなく CloudFront URL を使用するよう要求する（バイパス禁止のため）

参照：https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html）

CloudFront の署名付き Cookie とは、認証情報を用いて作成した署名データを Cookie として送信する方法です。

参照 ： https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-cookies.html

署名データには以下の4つのデータを含める必要があります。

- CloudFront-Expires
- CloudFront-Policy
- CloudFront-Signature
- CloudFront-Key-Pair-Id


##4-1. CloudFront 認証情報の取得
署名データの作成に必要となる認証情報を取得します。

**(1)**
AWS にルートアカウントでログインし、「セキュリティ認証情報」を開きます。
![fig32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/5fb98f20-585c-10d4-cb60-acf4a6cc20eb.png)

**(2)**
「CloudFront のキーペア」にある「新しいキーペアの作成」を押します。
![fig33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/7c1803cd-dd41-4ca9-976d-968ab5400803.png)

**(3)**
キーペアが作成されるので、ダウンロードボタンをおして2つのキーファイルをダウンロードします。

##4-2. CloudFront に認証情報を登録する
上で作成したキーペアをCloudFrontに登録します。

**(1)**
CloudFrontダッシュボードから目的のDistributionを開き、「Behaviors」タブでOriginを選択して「Edit」ボタンを押します。
![fig34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/dc86e5ed-7bc5-5f41-d58a-75f5b9f05f68.png)

**(2)**
Edit Behavior 画面で以下の項目を設定し、画面右下の「Yes, Edit」ボタンを押します。

- Restrict Viewer Access　⇒　Yes ： 署名が付いたアクセスのみ有効にします
- Trusted Signers　⇒　Self ： 署名に上で作成したキーペアを使用します

![fig35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/7be17773-cd86-b21f-8ad3-562b9255f174.png)

##4-3. CloudFront に独自ドメインを設定し、HTTPS を使えるようにする
**<h3>AWS Certificate Manager でSSL証明書を発行する</h3>**
AWS Certificate Manager（以下、ACM）とは、SSL証明書の発行・管理・デプロイを行うサービスで、無料で使用できます（https://aws.amazon.com/jp/certificate-manager/）。

ACMで発行されたSSL証明書は、CloudFrontから選べるようになるので、AWS以外の機関で発行した場合と比べて設定が簡単です。

**(1)**
AWS マネジメントコンソールで、検索ボックスに「acm」と入力すると表示される「Certificate Manager」を選択します。

**(2)**
ACM ダッシュボードを開いたら、「米国東部（バージニア北部）」リージョンを選びます。
※ CloudFrontでSSL証明書を使用するには、「米国東部（バージニア北部）」リージョンを使用する必要があります（https://docs.aws.amazon.com/acm/latest/userguide/acm-regions.html）。

**(3)**
初めて作成する場合は、「証明書のプロビジョニング」と「プライベート認証機関」を選択する画面が表示されるので、「証明書のプロビジョニング」の下にある「今すぐ始める」ボタンを押します。
![fig36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/964c7abd-0d13-ba42-ac18-2953a3aa7250.png)

**(4)**
証明書のタイプを選ぶ画面が表示されるので、「パブリック証明書のリクエスト」を選んで、右下にある「証明書のリクエスト」ボタンを押します。
![fig37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/2d8ca95d-8a67-19e2-0822-1ee3c2578d15.png)

**(5)**
「ドメイン名の追加」画面が表示されるので、CloudFrontの代替ドメイン名として設定しようとしているドメイン名（この場合は、取得しているドメイン「example.com」にホスト名「cdn」を付けた「cdn.example.com」）を入力して、右下にある「次へ」ボタンを押します。
![fig38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/683868e0-da7a-be7e-f8b6-705f8d02e3bf.png)

**(6)**
「検証方法の選択」画面が表示されるので、「DNSの検証」を選んで、右下にある「次へ」ボタンを押します。

**(7)**
「確認とリクエスト」画面が表示されるので、設定した内容に間違いがなければ、右下にある「確定とリクエスト」ボタンを押します。

**(8)**
「検証」画面が表示され、指定したドメイン名（今の場合cdn.example.com）の下にDNSレコードが表示されるので、NameとValueをメモし、これをドメイン管理のDNSサーバに登録します。
![fig39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/5c23e26e-38b8-610c-2cb6-13566f1bab28.png)

**(9)**
DNSの設定が反映されてAWS側で名前解決ができるようになると、ACMダッシュボードの「証明書」画面で、設定したドメイン名の状況が「発行済み」に変わり、SSL証明書を使用できる状態になります。
![fig40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/769cd063-abf7-4a01-ab80-d8984aeff217.png)

**<h3>CloudFrontで代替ドメイン名とSSL証明書を設定する</h3>**
**(1)**
CloudFrontダッシュボードで対象のディストリビューションを選び、Generalタブにある「Edit」ボタンを押します

**(2)**
「Edit Distribution」画面が表示されるので、以下の項目を設定し、画面右下の「Yes, Edit」ボタンを押します。

- Alternate Domain Names (CNAMEs) : 代替ドメイン名　⇒　ACMでSSL証明書を発行したドメイン名を指定（今の場合、cdn.example.com）
- SSL Certificate　⇒　「Custom SSL Certificate」 を選び、その下のリストからACMで作成した証明書を選択する
- Custom SSL Client Support　⇒　「Only Clients that Support Server Name Indication (SNI)」を選択
- Security Policy　⇒　recommended が付いたものを選択

![fig41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/23002dfe-6782-dcf3-f18c-e8db28221e46.png)

![fig42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/413038/da568026-71bc-e32e-1309-7d039005796e.png)

**(3)**
CloudFrontダッシュボードのディストリビューション一覧で当該ディストリビューションのStatusが「Deployed」になれば完了です。　

**(4)**
当該ディストリビューションに表示されているDomain NameとCNAMEsをDNSサーバーに登録します。

- ホスト名：cdn（代替ドメイン名のホスト名部分）
- TYPE：CNAME
- VALUE : xxxx.cloudfront.net （CloudFrontディストリビューションのオリジナルのドメイン名）


##4-4. 署名を生成するモジュールの作成
Rails アプリにおいて、4-1 で取得したキーペアを用いて署名を生成するモジュールを作成します。

ここでは、キーペアの情報を以下の環境変数で管理しているものとします。

- ENV['KEYPAREID'] ： キーペアのID
- ENV['PRIVATEKEY'] ： プライベートキー

今回は、app/controllers/concerns/common.rb に以下のコードを記述します。
参照：https://tech.medpeer.co.jp/entry/2018/01/18/174542

```ruby:app/controllers/concerns/common.rb
module Common
  extend ActiveSupport::Concern

  # CloudFront 署名付き Cookie 生成
  def cookie_create(resource, expiry)
    
    key_pair_id = ENV["KEYPAREID"] # 4-1で取得したキーペアのID

    raw_policy = policy(resource, expiry)
    {
      'CloudFront-Expires' => expiry.utc.to_i,
      'CloudFront-Policy' => safe_base64(raw_policy),
      'CloudFront-Signature' => sign(raw_policy),
      'CloudFront-Key-Pair-Id' => key_pair_id,
    }
  end

  # CloudFront 署名付き URL 生成
  def get_cloudfront_signed_url(resource, expiry)

    key_pair_id = ENV["KEYPAREID"] # 4-1で取得したキーペアのID

    expire      = expiry.utc.to_i
    raw_policy  = policy(resource, expiry)
    signature   = sign(raw_policy)

    "#{resource}?Expires=#{expire}
         &Signature=#{signature}&Key-Pair-Id=#{key_pair_id}"
  end

  private

  def policy(resource, expiry)
    {
      "Statement" => [
        {
          "Resource" => resource,
          "Condition" => {
            "DateLessThan" => {
                "AWS:EpochTime" => expiry.utc.to_i },
          },
        },
      ],
    }.to_json.gsub(/\s+/, '')
  end

  def safe_base64(data)
    Base64.strict_encode64(data).tr('+=/', '-_~')
  end

  def sign(data)
    private_key = ENV['PRIVATEKEY'] # 4-1で取得したプライベートキー

    digest = OpenSSL::Digest::SHA1.new
    key    = OpenSSL::PKey::RSA.new private_key
    result = key.sign digest, data
    safe_base64(result)
  end
end
```

##4-5. 署名付きクッキーを使用して動画を再生する
Rails アプリに動画再生用のページが用意されているものとします。

**(1)**
コントローラの当該アクションに、署名付き Cookie を生成するための以下のコードを追加します。

```ruby
# 署名付き Cookie の作成
cookie_create(
  'https://cdn.example.com/*', # CloudFront（代替ドメイン）のURL
  90.minutes.from_now # クッキーの有効期限
).each do |k, v|
    cookies[k] = {
      value: v,
      domain: 'example.com', # クッキーが有効なドメイン
      path: '/' # クッキーが有効なパス
    }
end
```

**(2)**
使用する動画プレイヤーに応じて Cookie 送信を可能にする設定を追加します。　例えば、videojs プレイヤーの場合は、以下の設定を追加します。

```javascript
// video.jsの設定
var player = videojs('some-video-id');
player.src({
    src: <動画ファイルのURL>,
    type: 'application/x-mpegURL',
    withCredentials: true // cookieを送信するための設定
  });
```

参照 : https://github.com/videojs/videojs-contrib-hls#withcredentials

<br/>
以上ですべての設定が完了しましたので、動画再生ページで正常に動画を再生できることを確認します。
<br/>

# 目次
本記事では、システム構築の手順を以下のように数回に分けて紹介します。

- （１）[概要](https://qiita.com/mas_yam/items/5d6e4973fbdfeb9b7489)
- （２）[開発環境から S3 へのアクセス](https://qiita.com/mas_yam/items/568c1f2f5098ec1aae4b)  
aws-sdk を使用して Rails から S3 にアクセスする方法について紹介します。
- （３）[S3 上の動画ファイルを Lambdaで自動的に動画変換する方法](https://qiita.com/mas_yam/items/788d08b7143108d4a3b1)  
S3 に動画ファイルを保存すると、自動的に Elastic Transcoder による動画変換を実行する方法について紹介します。
- （４）S3 上の動画ファイルを CloudFront で配信する方法  
このページ