AWSから行うセキュア動画配信 ~MediaConvertで動画変換~

## はじめに

個人情報を含む動画をAWSからセキュアな環境で配信してみる、の中編の投稿になります。

* [前編](https://qiita.com/r-mizuno/items/79ec52ca77ac0bb1b503)：　配信するための動画を、AWS Key Management System (KMS) から生成した共通鍵で暗号化し、S3へ格納する
* 中編：　**AWS Elemental MediaConvert を利用して、HLS+AES暗号化の形へ動画を変換する（今回）**
* 後編：　Cloudfrontの署名付きCookieを利用して、アクセス可能なユーザを制限する

前回は、KMSを使用してS3へ格納する動画を暗号化(CSE)する方法をご紹介しました。
今回は、S3へ保存された動画をMediaConvertを使用して、HLS+AESの形へ動画を変換して配信用のバケットへ保存していきます。

## 構築予定の環境

構築予定のシステム構成図は以下のようになります。
本記事では、赤字となっている部分を解説します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/22a6cb1c-f55d-49b4-5bc7-9fe94f362e4f.png)


## HLSってなに？

HTTP Live Streamingの略で、Appleによって開発された動画配信のためのHTTPベースストリーミングプロトコルです。

プレイリストとセグメントファイルに分かれて構成されています。
プレイリストは.m3u8ファイルによって定義され、セグメントファイルの場所や再生順序、暗号化されている場合は復号鍵の場所などが記載されています。
セグメントファイルはtsファイルと呼ばれるMPEG-2 Transport Stream形式で、動画を何秒かで細かく分割した複数の動画データになります。

chromeやfirefoxなどブラウザではデフォルトでは再生することができません。(safariやスマートフォンのブラウザなら再生可能)
各ブラウザの対応状況は[こちら](https://caniuse.com/?search=hls)を参照してみてください。未対応ブラウザ上でHLSを再生するには、[hls.js](https://github.com/video-dev/hls.js/)を採用するのが手っ取り早そうです。

## 作業手順

以下の手順でAWS上に動画変換の為の環境を設定していきます。

1. [動画配信用のS3バケットの作成](#1-動画配信用のS3バケットの作成)
2. [MediaConvertのジョブテンプレートの作成](#2-MediaConvertのジョブテンプレートの作成)
3. [MediaConvertジョブ実行のためのIAMロール作成](#3-MediaConvertジョブ実行のためのIAMロール作成)
4. [lambda関数の作成及び設定](#4-lambda関数の作成及び設定)
5. [KMSカスタマーキー使用権限の付与](#5-KMSカスタマーキー使用権限の付与)
6. [動作確認](#6-動作確認)

### 1. 動画配信用のS3バケットの作成

まず、HLSに変換した動画と復号鍵を格納するS3バケットを作成します。
以下の図のように、2つバケットを作成してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/ec4ed2a4-ea97-1905-4a96-1a34d9291f42.png)


### 2. MediaConvertのジョブテンプレートの作成

次に、MediaConvertのジョブテンプレートを作成します。
MediaConvert → ジョブテンプレート → テンプレートの作成を選択し、以下のように設定していきます。

#### 一般設定

名前、カテゴリ、説明を入力します。カテゴリ、説明は任意です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/17138e31-8fd2-9f6b-6ba3-90da6b2d5fdc.png)

#### 入力

入力に関しての設定を行います。入力の横にある追加ボタンを押下します。
今回は特に変更しないので、そのまま次に進みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/0440e0d0-639b-83f7-8a06-50d8e47a11b3.png)



#### 出力グループ

出力に関しての設定を行います。
まず出力グループの横にある追加ボタンを押下し、Apple HLSを選択します。

![](/uploads/upload_9e0465c68b3dc7d0558db78b7e845fc9.png)![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/a2836d3d-5a20-585a-6cf9-c2130d433bf1.png)


その後、出力グループの下のH.264, AACを選択し、名前修飾子、セグメント修飾子、ビットレートを入力していきます。
名前修飾子とセグメント修飾子は変換後のファイル名に追加される文字列です。
ビットレートは変換元の動画に合わせて入力してください。ここでは2000000を入力しました。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/dfd73db9-97bf-cf79-4ddd-94084ea05aae.png)

上述の項目がすべて入力できたら、ページ下部の作成ボタンを押下してジョブテンプレートの作成作業は完了です！

### 3. MediaConvertジョブ実行のためのIAMロール作成

次に、MediaConvertを実行する際に必要なIAMロールの設定を行います。
IAMロールの作成画面から、MediaConvertを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/6a0c43c2-fa8d-ddaa-f974-783235dafa15.png)

アクセス権限→タグとデフォルトのまま確認を押下し、確認画面で任意のロール名を入力し、ロールの生成を実行します。

### 4. lambda関数の作成及び設定

動画保存用のS3への動画ファイル配置イベントを検知し、MediaConvertジョブを実行するlambda関数を生成していきます。

#### lambda関数の作成

以下の条件でlambda関数を生成します。
* 関数名；任意
* ランタイム：Python 3.8
* 実行ロール：AWSポリシーテンプレートから新しいロールを作成
    * ロール名：任意
    * Amazon S3オブジェクトの読み取り専用アクセス権限

#### イベントの設定

作成したlambda関数 → トリガーを追加 より、S3へファイルが配置された際にlambda関数が実行されるように設定を行います。
以下のように入力を行ってください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/b5fb5bdc-23db-2c9d-cedb-1cd8c9e6ce7f.png)

バケットは、前編で動画を配置したS3バケットを指定します。
「[1．動画配信用のS3バケットの作成](#1-動画配信用のS3バケットの作成)」で作成したS3バケットではない点に注意です。

サフィックスは.mp4としていますが、変換したい動画ファイルに合わせて変更してください。

#### IAMロール設定

lambda関数作成時にIAMロールも作成しましたが、このままだと必要な権限が足りていません。
具体的には、MediaConvertの実行権限、復号鍵をS3バケットへ格納するための権限を追加します。

IAMコンソールから、作成したlambda関数の実行ロールの概要画面に遷移し、ポリシーをアタッチします。
以下のような画面が表示されますが、ポリシーの作成を押下してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/b7c984e6-fe9a-c7e2-1cca-9dc7d6aea894.png)

ポリシーの作成画面から、JSONタブを選択して以下を入力します。

``` json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "0",
            "Effect": "Allow",
            "Action": [
                "iam:PassRole",
                "mediaconvert:CreateJob"
            ],
            "Resource": "*"
        },
        {
            "Sid": "1",
            "Effect": "Allow",
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::<鍵用s3バケット名>/*"
        }
    ]
}
```

鍵用s3バケット名>には、「[1．動画配信用のS3バケットの作成](#1-動画配信用のS3バケットの作成)」で作成した復号鍵格納用のS3バケット名を指定してください。
ポリシーの確認に進み、任意のポリシー名を指定してポリシーを作成します。

IAMロールのポリシーアタッチ画面に戻り、作成したポリシーをアタッチしておきます。

#### コードの書き換え

ロールの設定ができたら、関数を次のように書き換えます。

```python:lambda_function.py
import boto3
import base64
import codecs
import json
import os
import secrets

s3Client = boto3.client('s3')

# 環境変数より値を取得
DESTINATION_BUCKET = os.environ['DESTINATION_BUCKET']
KEY_BUCKET_NAME = os.environ['KEY_BUCKET_NAME']
KEY_URL = os.environ['KEY_URL']
KMS_KEY_ID = os.environ['KMS_KEY_ID']
MEDIACONVERT_ENDPOINT = os.environ['MEDIACONVERT_ENDPOINT']
MEDIACONVERT_JOB_TEMPLATE_NAME = os.environ['MEDIACONVERT_JOB_TEMPLATE_NAME']
MEDIACONVERT_ROLE_ARN = os.environ['MEDIACONVERT_ROLE_ARN']


def lambda_handler(event, context):
    bucket = event['Records'][0]['s3']['bucket']['name']
    movie_file = event['Records'][0]['s3']['object']['key']
    
    # s3上のオブジェクトのメタデータから復号に使用するキー、初期化ベクトルを取得
    headObj = s3Client.head_object(Bucket=bucket, Key=movie_file)
    metaKey = headObj['ResponseMetadata']['HTTPHeaders']['x-amz-meta-x-amz-key']
    metaIv = headObj['ResponseMetadata']['HTTPHeaders']['x-amz-meta-x-amz-iv']
    # 初期化ベクトルはkmsで暗号化していない状態で指定する必要があるので復号する
    kmsClient = boto3.client('kms')
    result = kmsClient.decrypt(
        CiphertextBlob = base64.b64decode(metaIv),
        EncryptionContext={
            'service': 'mediaconvert.amazonaws.com'
        },
        KeyId=KMS_KEY_ID
    )
    decIv = base64.b64encode(result['Plaintext']).decode()
    
    try:
        with open("settings.json", "r") as jsonfile:
            settings = json.load(jsonfile)
    
        # 変換元動画ファイルの情報と復号情報
        settings['Inputs'][0]['FileInput'] = "s3://{}/{}".format(bucket, movie_file)
        settings['Inputs'][0]['DecryptionSettings']['EncryptedDecryptionKey'] = metaKey
        settings['Inputs'][0]['DecryptionSettings']['InitializationVector'] = decIv
        
        # 暗号化に使用する16バイトの16進数を生成
        encKey = secrets.token_hex(16)
        encIv = secrets.token_hex(16)
        settings['OutputGroups'][0]['OutputGroupSettings']['HlsGroupSettings']['Encryption']['StaticKeyProvider']['StaticKeyValue'] = encKey
        settings['OutputGroups'][0]['OutputGroupSettings']['HlsGroupSettings']['Encryption']['ConstantInitializationVector'] = encIv
        
        # 保存先のS3バケットの指定
        settings['OutputGroups'][0]['OutputGroupSettings']['HlsGroupSettings']['Destination'] = DESTINATION_BUCKET
        # キー情報の指定
        settings['OutputGroups'][0]['OutputGroupSettings']['HlsGroupSettings']['Encryption']['StaticKeyProvider']['Url'] = KEY_URL + movie_file.split('.')[0] + '.key'
        
        # mediaconvert job 実行
        mediaconvertClient = boto3.client('mediaconvert', endpoint_url=MEDIACONVERT_ENDPOINT)
        result = mediaconvertClient.create_job(
            Role = MEDIACONVERT_ROLE_ARN,
            JobTemplate = MEDIACONVERT_JOB_TEMPLATE_NAME,
            Settings = settings
        )
        
        # keyをs3バケットへ保存
        s3Client.put_object(
            Bucket=KEY_BUCKET_NAME,
            Key='{}.key'.format(movie_file.split('.')[0]),
            Body=codecs.decode(encKey, 'hex_codec')
        )
    except Exception as e:
        print(e)
        raise e
```

また、settings.jsonというファイルを新規に作って、MediaConvertの設定を記述した以下のjsonを追記します。

```json:settings.json
{
    "Inputs": [
        {
            "FileInput": "",
            "DecryptionSettings": {
                "DecryptionMode": "AES_CBC",
                "EncryptedDecryptionKey": "",
                "InitializationVector": "",
                "KmsKeyRegion": "ap-northeast-1"
            }
        }
    ],
    "OutputGroups": [
        {
            "Name": "Apple HLS",
            "OutputGroupSettings": {
                "Type": "HLS_GROUP_SETTINGS",
                "HlsGroupSettings": {
                    "Destination": "",
                    "Encryption": {
                        "EncryptionMethod": "AES128",
                        "ConstantInitializationVector": "",
                        "InitializationVectorInManifest": "INCLUDE",
                        "OfflineEncrypted": "DISABLED",
                        "StaticKeyProvider": {
                            "StaticKeyValue": "",
                            "Url": ""
                        },
                        "Type": "STATIC_KEY"
                    }
                }
            }
        }
    ]
}
```

#### 環境変数の設定

上述のコードを正常に実行するために、これまでに作成してきたAWS上のリソースを環境変数へ設定する必要があります。
以下の環境変数を作成してください。

* DESTINATION_BUCKET
    * 変換後のファイルの出力先のs3バケットを指定します。
    * 例：s3://<HLS格納先S3バケット名>/
* KEY_BUCKET_NAME
    * 復号鍵を格納するS3バケット名を指定します。
* KEY_URL
    * 鍵を参照可能なURLを指定します。中編ではCloudFrontを使用しての鍵の公開は行わないので、S3バケットのアクセス先を指定します。鍵のファイル名は指定不要です。
    * 例：https://<復号鍵を格納するS3バケット名>.s3.amazonaws.com/
* KMS_KEY_ID
    * KMSカスタマーキーのIDを指定します。
* MEDIACONVERT_ENDPOINT
    * MediaConvertのAPIエンドポイントを指定します。MediaConvertコンソール → アカウント から取得可能です。
* MEDIACONVERT_JOB_TEMPLATE_NAME
    * 作成したMediaConvertのジョブテンプレート名を指定します。
* MEDIACONVERT_ROLE_ARN
    * 作成したMediaConvert用のIAMロールのARNを指定します。

### 5. KMSカスタマーキー使用権限の付与

MediaConvertとLambda上でKMSのカスタマーキーを使用する必要があるので、それぞれに割り当たっているIAMロールに対してカスタマーキーの使用権限を付与していきます。

KMSコンソール → カスタマー管理型のキー → 使用しているカスタマーキー の順に進みます。
キーユーザーの追加を押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/54f8da54-394a-b0a8-1a7d-451950671b95.png)


MediaConvertのジョブ実行に使用するIAMロールとLambdaの実行ロールを選択し、追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/5b7d92be-236c-bd0a-256f-ed77269c85c5.png)


以上ですべての設定が完了しました！

### 6. 動作確認

それでは動作確認してみましょう。

動画格納用のS3へ、前編に記載の手順で暗号化した動画を格納します。
しばらくすると、以下のようにHLS格納先のバケットと復号鍵格納用のバケットに動画と鍵が出力されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/2af90240-593b-d3cc-7698-295c572429ed.png)


正常に再生できるかどうか、一時的にS3バケットをパブリックアクセス可能にして確認してみます。

safariやスマートフォンのブラウザでm3u8ファイルへアクセスしてみてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1168722/7a71ea57-03b3-5b2d-4526-f0ae2adc1ea7.png)


正常に再生することができました！
また、復号鍵のS3バケットのみパブリックアクセスを無効にしてみると、再生不可となるので試してみてください。

**確認が終わったらパブリックアクセスを無効にするのをお忘れなく！**

 
## 終わりに

AWS Elemental MediaConvert を利用して、HLS+AES暗号化の形へ動画を変換する手順をご紹介しました。

次回は、CloudFrontの署名付きCookieを利用して、認証されたユーザのみ動画を参照可能とする手順をご紹介する予定です。