tk_ch’s blog

インフラエンジニアのブログ

AWS MediaConvertで作成したHLS形式動画へのアクセスを、CloudFront署名付きCookieでユーザグループ単位に制限する

AWSで以下を実現したい。

  • AWSにアップロードした動画ファイルをHLS形式(HTTP Live Streaming)で配信する。
  • 運用コストを抑える&拡張を容易にするため、HLS形式への変換や、HLS動画の配信基盤はサーバーレスでやりたい(EC2の管理したくない)。
  • 特定のユーザ(何らかの認証に通ったユーザ)のみが動画を視聴できるようにする。
  • ユーザの権限(所属グループ)に応じて視聴できる動画を制御する(グループ001所属ユーザは、グループ001所属のユーザがアップロードした動画しか視聴できない、といったイメージ)。

→HLS形式に変換するのはMediaConvert、配信はCloudFront+S3で手軽にできそう。
視聴できる動画のグループ単位の制限は、CloudFrontの署名付きCookieを使うことで実現出来そうなので、試してみる。

環境

  • Terraform:1.2.5

構成

構成図

今回構築する環境は以下の通り。

構成図
構成図

説明

  • HLS形式への変換は、MediaConvertで行う。
  • S3イベントとLambdaを使い、オリジナル映像用S3バケットに動画がアップロードされると、自動的にMediaConvertジョブが登録・実行される仕組み
    S3イベントは直接Lambdaに送信してもよいが、耐障害性や機能拡張性(アップロード通知の追加や、動画アップロードを契機とした別処理を追加するなど)を考慮して、SNS、SQSを介している。
  • ユーザはグループ001、グループ002のいずれかに属していることとする。
  • ユーザはオリジナル映像用S3バケットの所属グループに対応するパス(「video/001/」or「video/002/」)配下にのみファイルアップロードできる。
    →この制限はIAMロールやS3バケットポリシー等で実現できるが、今回は本題ではないので割愛する。
  • ユーザはHLS動画用S3バケットの所属グループに対応するパス(「video/001/」or「video/002/」)に存在するHLS動画のみ視聴できるようにする。
    →CloudFrontの署名付きCookieとカスタムポリシーを使用して、Cookieに応じて特定のパス配下の動画のみアクセスできるように制限することにした。
    署名付きURLではなく署名付きCookieを使用する理由は後述。
  • ユーザの認証や、ユーザの所属グループに応じた認証情報やCookieをユーザに返す仕組みは、別途構築する想定。
    Cognito、API Gateway、Lambdaなどでこちらもサーバーレスに実現できる見込み。
    →本記事では、動作確認のためCookieの作成をコマンドで行う。

補足:CloudFrontのアクセス制限方法について

CloudFrontでコンテンツにアクセスできるユーザーを制限する機能としては、署名付き URL署名付き Cookie がある。
どちらを使うべきかについて、「署名付き URL と署名付き Cookie の選択」に以下の記載がある。

次のような場合は、署名付き URL を使用します。

・個別のファイル (アプリケーションのインストールダウンロード) へのアクセスを制限する場合。

・ユーザーが Cookie をサポートしていないクライアント (カスタム HTTP クライアントなど) を使用している場合。

 

次のような場合は、署名付き Cookie を使用します。

・複数の制限されたファイル (HLS 形式の動画のすべてのファイルやウェブサイトの購読者の領域にあるすべてのファイルなど) へのアクセスを提供する場合。

・現在の URL を変更したくない場合。

HLS形式の動画は、複数のファイル(インデックスファイルとセグメントファイル)で構成されるので、署名付きCookieが適している。
また、今回はグループごとのパスの配下にHLS形式の動画を格納し、その全てにアクセス出来るようにしたいため、「ウェブサイトの購読者の領域にあるすべてのファイル」に該当し、そういった意味でも署名付きCookieが適している。
そのため、今回は署名付きCookieを選択した。

実施内容

CloudFrontの署名付きCookie用キーペアの作成

署名付きCookie用のキーペアを作成する。
作成方法はこちらを参考にした。

キーペア作成

RSA キーペアを生成する
$ openssl genrsa -out private_key.pem 2048

公開鍵と秘密鍵の両方が含まれているため、公開鍵を抽出する
$ openssl rsa -pubout -in private_key.pem -out public_key.pem

確認
$ ls -l
total 8
-rw-rw-r-- 1 ec2-user ec2-user 1679 Nov 17 05:56 private_key.pem
-rw-rw-r-- 1 ec2-user ec2-user  451 Nov 17 05:57 public_key.pem

Terraformでのリソース作成

MediaConvertジョブ登録用Lambda関数と、MediaConvertジョブテンプレート以外のリソースをTerraformで作成する。

作成するリソース

Terraformで作成するリソースの概要は以下の通り。

  • オリジナル動画用S3バケット。mp4ファイルがアップロードされたら、SNSトピックにS3イベント通知を行う。
  • S3イベント通知を受け取るSNSトピック、SQSキュー。
  • HLS動画用S3バケット。CloudFront経由でのみアクセスできるバケットポリシーを設定。CORSも設定する。
  • CloudFrontの署名付きCookie用の公開鍵を登録し、キーグループを作成。
  • HLS動画用S3バケットをオリジンとするCloudFrontディストリビューション
    上記キーグループを設定し、「video/*」には署名付きCookieが無しではアクセス出来ないようにする。
    CloudFrontにキャッシュが残っていると動作確認時に挙動が分かりづらくなるかもしれないので、キャッシュポリシーはManaged-CachingDisabledを指定しキャッシュを無効にしている。
  • MediaConvertの実行用のIAMポリシー、ロール。
    権限については「IAM での IAM ロールの作成」を参考に、オリジナル動画用S3バケットへのGETと、HLS動画用S3バケットへのPUTを許可する。
    ※上記ドキュメントのポリシー例ではAPI Gatewayへのアクセスも許可しているが、これはこちらによると「SPEKE によるデジタル著作権管理、ニールセン非線形透かし」といった機能を使用する場合にのみ必要らしい。 詳細は調べていないが、通常は使用する必要は無さそうなので、許可していない。
  • MediaConvertジョブ登録用Lambda関数の実行ロール。
    権限については「AWS Elemental MediaConvert コンソールを使用するために必要なアクセス許可」を参考に、Lambdaからのジョブ登録に必要な権限に絞った。
     

Terraformコード

Terraformコードは以下の通り。
(先ほど作成した公開鍵は、tfファイルの配置場所にある「cloudfront_key」というディレクトリに格納している想定。)
以下のTerraform Moduleを使用している。

local.tf

# ローカル変数定義
locals {

  name_prefix = "hlstest"
  common_tags = {
    Terraform = "True" #Terraformで作成したリソースだとわかるようにタグを付ける。付けなくても処理には影響ない。
  }

}

main.tf

# オリジナル動画用S3バケット
module "s3_bucket_source" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "3.3.0"

  bucket        = "${local.name_prefix}-s3-source"
  force_destroy = false

  # デフォルトの暗号化。原則S3マスターキーでの暗号化を有効にする。
  server_side_encryption_configuration = {
    rule = {
      apply_server_side_encryption_by_default = {
        sse_algorithm = "AES256"
      }
    }
  }

  # ブロックパブリックアクセス。原則全てtrueにする。
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-s3-source"
  })
}

# オリジナル動画用S3バケットにmp4がアップロードされた際のS3イベント通知
module "s3_notifications_hls" {
  source  = "terraform-aws-modules/s3-bucket/aws//modules/notification"
  version = "3.3.0"

  bucket = module.s3_bucket_source.s3_bucket_id

  # Common error - Error putting S3 notification configuration: InvalidArgument: Configuration is ambiguously defined. Cannot have overlapping suffixes in two rules if the prefixes are overlapping for the same event type.

  sns_notifications = {
    video_uploaded = {
      topic_arn     = aws_sns_topic.video_upload.arn
      events        = ["s3:ObjectCreated:*"]
      filter_prefix = "video/"
      filter_suffix = ".mp4"
    }
  }

  create_sns_policy = true
}

# S3イベント通知の送信先SNSトピック
resource "aws_sns_topic" "video_upload" {
  name = "${local.name_prefix}-sns-upload"

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-sns-upload"
  })
}

# S3イベント通知の送信先SQSキュー
resource "aws_sqs_queue" "video_upload" {
  name = "${local.name_prefix}-sqs-upload"

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-sqs-upload"
  })
}

# S3イベント通知の送信先SQSキューのポリシー設定
resource "aws_sqs_queue_policy" "sqs_video_upload_policy" {
  queue_url = aws_sqs_queue.video_upload.id
  policy    = data.aws_iam_policy_document.sqs_video_upload_policy.json
}

# AWSアカウントIDを取得
data "aws_caller_identity" "now" {}

# S3イベント通知の送信先SQSキューのポリシー内容
data "aws_iam_policy_document" "sqs_video_upload_policy" {
  statement {
    sid = "__owner_statement"
    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::${data.aws_caller_identity.now.account_id}:root"]
    }
    effect    = "Allow"
    actions   = ["SQS:*"]
    resources = ["${aws_sqs_queue.video_upload.arn}"]
  }
  statement {
    sid = "Allow-SNS-SendMessage"
    principals {
      type        = "Service"
      identifiers = ["sns.amazonaws.com"]
    }
    effect    = "Allow"
    actions   = ["sqs:SendMessage"]
    resources = ["${aws_sqs_queue.video_upload.arn}"]
    condition {
      test     = "ArnEquals"
      variable = "aws:SourceArn"
      values   = ["${aws_sns_topic.video_upload.arn}"]
    }
  }
}

# SNSトピックからSQSキューへのサブスクライブ設定
resource "aws_sns_topic_subscription" "sqs_request_mosaic" {
  topic_arn = aws_sns_topic.video_upload.arn
  protocol  = "sqs"
  endpoint  = aws_sqs_queue.video_upload.arn
}

# HLS動画用S3バケット
module "s3_bucket_hls" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = "3.3.0"

  bucket        = "${local.name_prefix}-s3-hls"
  force_destroy = false

  attach_policy = true
  policy        = data.aws_iam_policy_document.s3_bucket_hls_policy.json

  # デフォルトの暗号化。原則S3マスターキーでの暗号化を有効にする。
  server_side_encryption_configuration = {
    rule = {
      apply_server_side_encryption_by_default = {
        sse_algorithm = "AES256"
      }
    }
  }

  # ブロックパブリックアクセス。原則全てtrueにする。
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true

  # CORS設定
  cors_rule = [
    {
      allowed_methods = ["GET"]
      allowed_origins = ["*"]
      allowed_headers = ["*"]
      expose_headers  = []
      max_age_seconds = 3000
    }
  ]

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-s3-hls"
  })
}

# HLS動画用S3バケットのバケットポリシー内容
data "aws_iam_policy_document" "s3_bucket_hls_policy" {
  statement {
    principals {
      type        = "AWS"
      identifiers = module.cloudfront.cloudfront_origin_access_identity_iam_arns
    }
    actions = [
      "s3:GetObject",
    ]
    resources = [
      "${module.s3_bucket_hls.s3_bucket_arn}/*",
    ]
  }
}

# HLS配信用CloudFront
module "cloudfront" {
  source  = "terraform-aws-modules/cloudfront/aws"
  version = "2.8.0"

  enabled             = true
  is_ipv6_enabled     = true
  price_class         = "PriceClass_All"
  retain_on_delete    = false # terraform destroy時に削除でなく無効にするか
  wait_for_deployment = false
  default_root_object = "index.html"

  create_origin_access_identity = true
  origin_access_identities = {
    s3_bucket_one = "access-identity-${module.s3_bucket_hls.s3_bucket_bucket_regional_domain_name}"
  }

  origin = {
    "${module.s3_bucket_hls.s3_bucket_bucket_regional_domain_name}" = {
      domain_name = module.s3_bucket_hls.s3_bucket_bucket_regional_domain_name
      s3_origin_config = {
        origin_access_identity = "s3_bucket_one" # key in `origin_access_identities`
      }
    }
  }

  default_cache_behavior = {
    target_origin_id       = module.s3_bucket_hls.s3_bucket_bucket_regional_domain_name
    viewer_protocol_policy = "redirect-to-https"
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    use_forwarded_values   = false # https://github.com/hashicorp/terraform-provider-aws/issues/19041
    cache_policy_id        = data.aws_cloudfront_cache_policy.managed_caching_disabled.id
  }

  ordered_cache_behavior = [
    {
      path_pattern           = "video/*"
      target_origin_id       = module.s3_bucket_hls.s3_bucket_bucket_regional_domain_name
      viewer_protocol_policy = "redirect-to-https"
      allowed_methods        = ["GET", "HEAD"]
      cached_methods         = ["GET", "HEAD"]
      compress               = true
      trusted_key_groups     = [aws_cloudfront_key_group.key_group_hls.id]
      use_forwarded_values   = false # https://github.com/hashicorp/terraform-provider-aws/issues/19041
      cache_policy_id        = data.aws_cloudfront_cache_policy.managed_caching_disabled.id
    }
  ]

  tags = local.common_tags
}

# CloudFrontのキャッシュポリシー
data "aws_cloudfront_cache_policy" "managed_caching_disabled" {
  name = "Managed-CachingDisabled"
}

# 公開鍵とキーグループ
resource "aws_cloudfront_public_key" "key_hls" {
  name        = "${local.name_prefix}-cloudfront-public-key"
  comment     = "HLS配信用パブリックキー"
  encoded_key = file("cloudfront_key/public_key.pem")
}
resource "aws_cloudfront_key_group" "key_group_hls" {
  name    = "${local.name_prefix}-cloudfront-key-group"
  comment = "HLS配信用キーグループ"
  items   = [aws_cloudfront_public_key.key_hls.id]
}

# MediaConvert用IAMポリシー
resource "aws_iam_policy" "mediaconvert" {
  name   = "${local.name_prefix}-iampol-mediaconvert"
  policy = data.aws_iam_policy_document.mediaconvert_policy.json

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-iampol-mediaconvert"
  })
}

# MediaConvert用IAMポリシーの内容
data "aws_iam_policy_document" "mediaconvert_policy" {
  statement {
    sid     = "AllowGetSourceVideo"
    effect  = "Allow"
    actions = ["s3:GetObject"]
    resources = [
      "${module.s3_bucket_source.s3_bucket_arn}",
      "${module.s3_bucket_source.s3_bucket_arn}/*"
    ]
  }
  statement {
    sid     = "AllowPutHLSVideo"
    effect  = "Allow"
    actions = ["s3:PutObject"]
    resources = [
      "${module.s3_bucket_hls.s3_bucket_arn}",
      "${module.s3_bucket_hls.s3_bucket_arn}/*"
    ]
  }
}

# MediaConvert用IAMロール
resource "aws_iam_role" "mediaconvert" {
  name                = "${local.name_prefix}-role-mediaconvert"
  managed_policy_arns = [aws_iam_policy.mediaconvert.arn]

  assume_role_policy = data.aws_iam_policy_document.mediaconvert_assume_role_policy.json

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-role-mediaconvert"
  })
}

# MediaConvert用IAMロールの信頼ポリシー内容
data "aws_iam_policy_document" "mediaconvert_assume_role_policy" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["mediaconvert.amazonaws.com"]
    }
  }
}

# MediaConvertジョブ登録Lambda用IAMポリシー
resource "aws_iam_policy" "lambda_submit_mcjob" {
  name   = "${local.name_prefix}-iampol-lambda_submit_mcjob"
  policy = data.aws_iam_policy_document.lambda_submit_mcjob_policy.json

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-iampol-lambda_submit_mcjob"
  })
}

# MediaConvertジョブ登録Lambda用IAMポリシー内容
data "aws_iam_policy_document" "lambda_submit_mcjob_policy" {
  statement {
    sid     = "AllowPassRole"
    effect  = "Allow"
    actions = ["iam:PassRole"]
    resources = [
      "${aws_iam_role.mediaconvert.arn}"
    ]
  }
  statement {
    sid     = "AllowCreateMCJob"
    effect  = "Allow"
    actions = ["mediaconvert:CreateJob"]
    resources = [
      "arn:aws:mediaconvert:ap-northeast-1:${data.aws_caller_identity.now.account_id}:*"
    ]
  }
}

# MediaConvertジョブ登録Lambda用IAMロール
resource "aws_iam_role" "lambda_submit_mcjob" {
  name = "${local.name_prefix}-role-lambda_submit_mcjob"
  managed_policy_arns = [
    aws_iam_policy.lambda_submit_mcjob.arn,
    data.aws_iam_policy.AWSLambdaSQSQueueExecutionRole.arn
  ]

  assume_role_policy = data.aws_iam_policy_document.lambda_assume_role_policy.json

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-role-lambda_submit_mcjob"
  })
}

# SQSトリガーのLambda用AWS管理ポリシー取得
data "aws_iam_policy" "AWSLambdaSQSQueueExecutionRole" {
  arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole"
}

# MediaConvertジョブ登録Lambda用IAMロールの信頼ポリシー
data "aws_iam_policy_document" "lambda_assume_role_policy" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

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

MediaConvertにはジョブテンプレートを保存する機能がある。
テンプレートを使えば毎回細かい設定値を指定しなくてよくなるので、活用する。
マネジメントコンソールのMediaConvertのページで、「ジョブテンプレート」→「テンプレートをインポート」と進み、以下のJSONをインポートして「hlstest-mcjob-template」というジョブテンプレートを作成した。
JSONの内容は、こちらのチュートリアルで作成されるジョブ内容を参考に、以下の点を変更して作成した。

  • Output動画が様々なサイズが作成されるところを、640×360の1種類だけに変更
  • Output動画の出力先S3バケットを空欄に変更(ジョブ登録時にLambdaで指定するため)

hlstest-mcjob-template.json

{
    "Queue": "Default",
    "Role": "",
    "Settings": {
      "OutputGroups": [
        {
          "Name": "Apple HLS",
          "Outputs": [
            {
              "ContainerSettings": {
                "Container": "M3U8",
                "M3u8Settings": {
                  "AudioFramesPerPes": 4,
                  "PcrControl": "PCR_EVERY_PES_PACKET",
                  "PmtPid": 480,
                  "PrivateMetadataPid": 503,
                  "ProgramNumber": 1,
                  "PatInterval": 0,
                  "PmtInterval": 0,
                  "VideoPid": 481,
                  "AudioPids": [
                    482,
                    483,
                    484,
                    485,
                    486,
                    487,
                    488,
                    489,
                    490,
                    491,
                    492,
                    493,
                    494,
                    495,
                    496,
                    497,
                    498
                  ]
                }
              },
              "VideoDescription": {
                "Width": 640,
                "ScalingBehavior": "DEFAULT",
                "Height": 360,
                "TimecodeInsertion": "DISABLED",
                "AntiAlias": "ENABLED",
                "Sharpness": 100,
                "CodecSettings": {
                  "Codec": "H_264",
                  "H264Settings": {
                    "InterlaceMode": "PROGRESSIVE",
                    "ParNumerator": 1,
                    "NumberReferenceFrames": 3,
                    "Syntax": "DEFAULT",
                    "GopClosedCadence": 1,
                    "HrdBufferInitialFillPercentage": 90,
                    "GopSize": 3,
                    "Slices": 1,
                    "GopBReference": "ENABLED",
                    "HrdBufferSize": 3750000,
                    "MaxBitrate": 1500000,
                    "SlowPal": "DISABLED",
                    "ParDenominator": 1,
                    "SpatialAdaptiveQuantization": "ENABLED",
                    "TemporalAdaptiveQuantization": "ENABLED",
                    "FlickerAdaptiveQuantization": "ENABLED",
                    "EntropyEncoding": "CABAC",
                    "RateControlMode": "QVBR",
                    "QvbrSettings": {
                      "QvbrQualityLevel": 7
                    },
                    "CodecProfile": "HIGH",
                    "Telecine": "NONE",
                    "MinIInterval": 0,
                    "AdaptiveQuantization": "MEDIUM",
                    "CodecLevel": "AUTO",
                    "FieldEncoding": "PAFF",
                    "SceneChangeDetect": "ENABLED",
                    "QualityTuningLevel": "SINGLE_PASS_HQ",
                    "UnregisteredSeiTimecode": "DISABLED",
                    "GopSizeUnits": "SECONDS",
                    "ParControl": "SPECIFIED",
                    "NumberBFramesBetweenReferenceFrames": 5,
                    "RepeatPps": "DISABLED",
                    "DynamicSubGop": "ADAPTIVE"
                  }
                },
                "AfdSignaling": "NONE",
                "DropFrameTimecode": "ENABLED",
                "RespondToAfd": "NONE",
                "ColorMetadata": "INSERT"
              },
              "AudioDescriptions": [
                {
                  "AudioTypeControl": "FOLLOW_INPUT",
                  "AudioSourceName": "Audio Selector 1",
                  "CodecSettings": {
                    "Codec": "AAC",
                    "AacSettings": {
                      "AudioDescriptionBroadcasterMix": "NORMAL",
                      "Bitrate": 64000,
                      "RateControlMode": "CBR",
                      "CodecProfile": "HEV1",
                      "CodingMode": "CODING_MODE_2_0",
                      "RawFormat": "NONE",
                      "SampleRate": 48000,
                      "Specification": "MPEG4"
                    }
                  },
                  "LanguageCodeControl": "FOLLOW_INPUT",
                  "AudioType": 0
                }
              ],
              "NameModifier": "_Ott_Hls_Ts_Avc_Aac_16x9_640x360p_1.5Mbps_qvbr"
            }
          ],
          "OutputGroupSettings": {
            "Type": "HLS_GROUP_SETTINGS",
            "HlsGroupSettings": {
              "ManifestDurationFormat": "INTEGER",
              "SegmentLength": 3,
              "TimedMetadataId3Period": 10,
              "CaptionLanguageSetting": "OMIT",
              "TimedMetadataId3Frame": "PRIV",
              "CodecSpecification": "RFC_4281",
              "OutputSelection": "MANIFESTS_AND_SEGMENTS",
              "ProgramDateTimePeriod": 600,
              "MinSegmentLength": 0,
              "DirectoryStructure": "SINGLE_DIRECTORY",
              "ProgramDateTime": "EXCLUDE",
              "SegmentControl": "SEGMENTED_FILES",
              "ManifestCompression": "NONE",
              "ClientCache": "ENABLED",
              "StreamInfResolution": "INCLUDE"
            }
          }
        }
      ],
      "AdAvailOffset": 0,
      "Inputs": [
        {
          "AudioSelectors": {
            "Audio Selector 1": {
              "Offset": 0,
              "DefaultSelection": "DEFAULT",
              "ProgramSelection": 1
            }
          },
          "VideoSelector": {
            "ColorSpace": "FOLLOW",
            "Rotate": "DEGREE_0",
            "AlphaBehavior": "DISCARD"
          },
          "FilterEnable": "AUTO",
          "PsiControl": "USE_PSI",
          "FilterStrength": 0,
          "DeblockFilter": "DISABLED",
          "DenoiseFilter": "DISABLED",
          "TimecodeSource": "ZEROBASED",
          "FileInput": "s3://sourcebucket/assets01/test.mp4"
        }
      ]
    },
    "AccelerationSettings": {
      "Mode": "PREFERRED"
    },
    "StatusUpdateInterval": "SECONDS_60"
  }

※詳細な設定値については確認していないが、チュートリアルによるとこの設定値で以下の動画フォーマットをインプットとしてHLS形式に変換できる模様。

  • .MOV
  • .mpeg
  • .mp4
  • .mkv
  • .m4v
  • .M4V
  • .mov
  • .M2TS
  • .MKV
  • .MP4
  • .MPEG
  • .WEBM
  • .WMV
  • .mpg
  • .M3U8
  • .h264
  • .MPG
  • .webm
  • .mxf
  • .m3u8
  • .H264
  • .wmv
  • .m2ts
  • .MXF

MediaConvertジョブ登録用Lambda関数の作成

マネジメントコンソールで、Lambda関数を作成する。
関数のコードはこちらを参考にさせていただいた。

処理概要

Lambda関数内で実行する処理の概要は以下。

  • SQSから受信したメッセージからS3イベントを取り出す。
  • アップロードされたmp4ファイルのパスから、HLS動画用S3バケットのどのパスに出力するか決定する。
    (オリジナル動画用S3バケットの「video/<グループ名>/<mp4ファイルID>.mp4」としてアップロードされたmp4動画の出力先は、HLS動画用S3バケットの「video/<グループ名>/hls_<mp4ファイルID>/」配下になる。)
  • 先ほど作成したMediaConvertのテンプレートを元に、mp4とHLS出力先のパスを指定して、MediaConvertジョブを登録する。

関数作成時の注意点

  • 関数の実行ロールはTerraformで作成した「hlstest-role-lambda_submit_mcjob」を指定する。
  • トリガーにはTerraformで作成したSQSキュー「hlstest-sqs-upload」を指定する。
  • 環境変数に以下を設定する。
    • JOB_TEMPLATE_NAME:MediaConvertのジョブテンプレート名。今回はTerraformで作成した「hlstest-mcjob-template」。
    • MEDIACONVERT_ENDPOINT:MediaConvertのAPI エンドポイント。マネジメントコンソールのMediaConvertのページで、「アカウント」を選択すると表示される。
    • MEDIACONVERT_ROLE:MediaConvert実行用のIAMロールのARN。今回はTerraformで作成した「hlstest-role-mediaconvert」のARN。
    • OUTPUT_S3_NAME:HLS形式動画の出力先S3バケットの名前。今回はTerraformで作成した「hlstest-s3-hls」。

関数コード

import os
import urllib.parse
import boto3
import json

# MediaConvertのエンドポイントURL
MEDIACONVERT_ENDPOINT = os.environ['MEDIACONVERT_ENDPOINT']

# MediaConvert用のロールのARN
MEDIACONVERT_ROLE = os.environ['MEDIACONVERT_ROLE']

# MediaConvertジョブテンプレートの名前
JOB_TEMPLATE_NAME = os.environ['JOB_TEMPLATE_NAME']

# 変換後ファイルの出力先S3のバケット名
OUTPUT_S3_NAME = os.environ['OUTPUT_S3_NAME']

def lambda_handler(event, context):
    print(f'boto3 version: {boto3.__version__}')
    
    for record in event['Records']:
        message_str = json.loads(record["body"])['Message']
        message = json.loads(message_str)
    
        bucket = message['Records'][0]['s3']['bucket']['name']
        key = urllib.parse.unquote_plus(message['Records'][0]['s3']['object']['key'], encoding='utf-8')
    
        settings = make_settings(bucket, key)
        user_metadata = {
            'JobCreatedBy': 'videoConvertSample',
        }
    
        client = boto3.client('mediaconvert', endpoint_url = MEDIACONVERT_ENDPOINT)
        result = client.create_job(
            Role = MEDIACONVERT_ROLE,
            JobTemplate = JOB_TEMPLATE_NAME,
            Settings=settings,
            UserMetadata=user_metadata,
        )

def make_settings(bucket, key):
    basename = os.path.basename(key).split('.')[0]
    dirname = os.path.dirname(key)

    return \
    {
        "Inputs": [
            {
                "FileInput": f"s3://{bucket}/{key}",
            }
        ],
        "OutputGroups": [
            {
                "Name": "Apple HLS",
                "OutputGroupSettings": {
                    "Type": "HLS_GROUP_SETTINGS",
                    "HlsGroupSettings": {
                        "Destination": f"s3://{OUTPUT_S3_NAME}/{dirname}/hls_{basename}/",
                    },
                },
            },
        ],
    }

補足:SNS、SQSを介さない場合の関数コード

今回はオリジナル動画用S3バケットのS3イベント通知を、SNS、SQSを介してLambda関数に渡している。
S3イベント通知を直接Lamba関数に渡す構成にする場合のコードは以下。
イベント内容を読み取る部分のみ異なる。

import os
import urllib.parse
import boto3

# MediaConvertのエンドポイントURL
MEDIACONVERT_ENDPOINT = os.environ['MEDIACONVERT_ENDPOINT']

# MediaConvert用のロールのARN
MEDIACONVERT_ROLE = os.environ['MEDIACONVERT_ROLE']

# MediaConvertジョブテンプレートの名前
JOB_TEMPLATE_NAME = os.environ['JOB_TEMPLATE_NAME']

# 変換後ファイルの出力先S3のバケット名
OUTPUT_S3_NAME = os.environ['OUTPUT_S3_NAME']

def lambda_handler(event, context):
    print(f'boto3 version: {boto3.__version__}')
    
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')

    settings = make_settings(bucket, key)
    user_metadata = {
        'JobCreatedBy': 'videoConvertSample',
    }

    client = boto3.client('mediaconvert', endpoint_url = MEDIACONVERT_ENDPOINT)
    result = client.create_job(
        Role = MEDIACONVERT_ROLE,
        JobTemplate = JOB_TEMPLATE_NAME,
        Settings=settings,
        UserMetadata=user_metadata,
    )

def make_settings(bucket, key):
    basename = os.path.basename(key).split('.')[0]
    dirname = os.path.dirname(key)

    return \
    {
        "Inputs": [
            {
                "FileInput": f"s3://{bucket}/{key}",
            }
        ],
        "OutputGroups": [
            {
                "Name": "Apple HLS",
                "OutputGroupSettings": {
                    "Type": "HLS_GROUP_SETTINGS",
                    "HlsGroupSettings": {
                        "Destination": f"s3://{OUTPUT_S3_NAME}/{dirname}/hls_{basename}/",
                    },
                },
            },
        ],
    }

mp4動画をHLS形式に変換する

動画の準備

適当なmp4動画ファイルを「test.mp4」というファイル名で用意する。
自分はPexelsというフリー素材サイトのこちらの動画をダウンロードして使った。
動画の詳細は以下の通り。

使用した動画のプロパティ
使用した動画のプロパティ

001グループ用のHLS形式動画を作成

「test.mp4」を、オリジナル動画用S3バケットの「video/001/」にアップロードする。
MediaConvertのページの「ジョブ」を確認すると、以下のようにHLS形式への変換ジョブが登録され、PROGRESSING(進行中)にステータスになっていることが確認できる。

MediaConvertジョブ(PROGRESSING)
MediaConvertジョブ(PROGRESSING)

少し待って画面を更新すると、以下のようにジョブのステータスがCOMPLETEになる。

MediaConvertジョブ(COMPLETE)
MediaConvertジョブ(COMPLETE)

ジョブIDをクリックすると、以下のようにジョブの詳細が確認できる。
6秒の動画を5秒でトランスコードしている。

MediaConvertジョブ詳細
MediaConvertジョブ詳細

HLS動画用S3バケットを確認すると、以下のように「video/001/hls_test」配下にHLS形式のインデックスファイルとセグメントファイルが出力されている。

グループ001用のHLS形式ファイル
グループ001用のHLS形式ファイル

作成されたインデックスファイルの中身は以下。

test.m3u8

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-STREAM-INF:BANDWIDTH=1011690,AVERAGE-BANDWIDTH=980106,CODECS="avc1.64001e,mp4a.40.5",RESOLUTION=640x360,FRAME-RATE=30.000
test_Ott_Hls_Ts_Avc_Aac_16x9_640x360p_1.5Mbps_qvbr.m3u8

test_Ott_Hls_Ts_Avc_Aac_16x9_640x360p_1.5Mbps_qvbr.m3u8

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:3
#EXT-X-MEDIA-SEQUENCE:1
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:3,
test_Ott_Hls_Ts_Avc_Aac_16x9_640x360p_1.5Mbps_qvbr_00001.ts
#EXTINF:3,
test_Ott_Hls_Ts_Avc_Aac_16x9_640x360p_1.5Mbps_qvbr_00002.ts
#EXTINF:1,
test_Ott_Hls_Ts_Avc_Aac_16x9_640x360p_1.5Mbps_qvbr_00003.ts
#EXT-X-ENDLIST

002グループ用のHLS形式動画を作成

「test.mp4」を、オリジナル動画用S3バケットの「video/002/」にアップロードする。 先ほどと同様にMediaConvertジョブが実行され、HLS動画用S3バケットの「video/002/hls_test」配下にHLS形式のインデックスファイルとセグメントファイルが出力される。

グループ002用のHLS形式ファイル
グループ002用のHLS形式ファイル

curlコマンドで接続確認する

curlコマンドで、HLS形式のファイルにアクセス署名付きCookieを持つユーザのみがアクセス出来ることを確認する。
記載している手順はインターネットに接続できる環境で実行すること。
また、<CloudFrontのドメイン名>にはTerraformで作成したCloudFrontディストリビューションドメイン名を入れる。
※本例では、アクセス対象はインデックスファイル(test.m3u8)にしているが、セグメントファイル(*.ts)についても同様の挙動となる。

署名付きCookie無しではアクセスできないことを確認

CloudFrontドメイン名を設定
$ CF_DOMAIN=<CloudFrontのドメイン名>

グループ001用のHLS形式ファイル(インデックスファイル)へアクセスする
$ curl https://${CF_DOMAIN}/video/001/hls_test/test.m3u8
<?xml version="1.0" encoding="UTF-8"?><Error><Code>MissingKey</Code><Message>Missing Key-Pair-Id query parameter or cookie value</Message></Error>

グループ002用のHLS形式ファイル(インデックスファイル)へアクセスする
$ curl https://${CF_DOMAIN}/video/002/hls_test/test.m3u8
<?xml version="1.0" encoding="UTF-8"?><Error><Code>MissingKey</Code><Message>Missing Key-Pair-Id query parameter or cookie value</Message></Error>

→CloudFrontにて「video/*」に署名付きCookieの設定をしているため、Cookieの無い状態でこれらのパスにアクセスすると「Missing Key-Pair-Id query parameter or cookie value」というエラーになる。

署名付きCookieを使うとアクセスできることを確認

次に、署名付きCookieを使えばアクセスできることを確認する。

署名付きCookieのポリシーについて

署名付きCookieを作成する場合、Cookieの有効期間やパスなど署名付きCookieでの制限内容を指定するJSON形式のポリシーステートメントを用意する必要がある。
ポリシーには既定ポリシーとカスタムポリシーがあり、違いは「署名付き Cookie の既定ポリシーとカスタムポリシーの選択」に記載されている通り。
→ポリシーはグループごとに固定したいので、対象のパス指定にはワイルドカードで「video/001/*」のように指定したい。
既定ポリシーではワイルドカードを使った指定ができないので、カスタムポリシーを使う。

署名付きCookie生成用のカスタムポリシーを用意する

カスタムポリシーのJSONファイルを作成する。
グループ001とグループ002でアクセスできるパスを変えたいので、それぞれのJSONファイルを作成する。
 
カスタムポリシーを使用する署名付き Cookie の設定」を参照し、以下の内容で作成した。

  • Resourceにはアクセスを許可するURLを記載する。今回は、ここにグループごとのパスを記載した。
    <CloudFrontのドメイン名>はCloudFrontのドメイン名に書き換える。
  • DateLessThanはURL の有効期限切れ日時。Unix 時間形式 (秒単位) および協定世界時 (UTC) で指定する。 今回は適当に未来日時を「$ date -d "2023-11-11 08:38:25" +%s」を実行してエポックタイムに変換したものを記載している。

policy_001.json

{
    "Statement": [
        {
            "Resource": "https://<CloudFrontのドメイン名>/video/001/*",
            "Condition": {
                "DateLessThan": {
                    "AWS:EpochTime": 1699691905
                }
            }
        }
    ]
}

policy_002.json

{
    "Statement": [
        {
            "Resource": "https://<CloudFrontのドメイン名>/video/002/*",
            "Condition": {
                "DateLessThan": {
                    "AWS:EpochTime": 1699691905
                }
            }
        }
    ]
}

署名付きCookieを作成して、アクセスできるか確認する

カスタムポリシーを使用する署名付き Cookie の設定」によると、カスタムポリシーを使用して署名付きCookieを設定する場合、以下の3つの名前と値のペアが必要となる。

以下の作業は秘密鍵とカスタムポリシーJSONファイルがあるディレクトリで行う。

$ ls -l
total 16
-rw-rw-r-- 1 ec2-user ec2-user  266 Nov 15 03:29 policy_001.json
-rw-rw-r-- 1 ec2-user ec2-user  266 Nov 15 03:29 policy_002.json
-rw-rw-r-- 1 ec2-user ec2-user 1679 Nov 17 05:56 private_key.pem
-rw-rw-r-- 1 ec2-user ec2-user  451 Nov 17 05:57 public_key.pem

まずグループ001のユーザを想定して署名付きCookieを作成する。
以下の手順を実行する。
手順中の<CloudFrontのドメイン名>と<公開鍵のID>は環境に応じて記載する。

CloudFrontドメイン名を環境変数に格納
$ CF_DOMAIN=<CloudFrontのドメイン名>

公開鍵のIDを環境変数に格納
$ CF_KEYPAIR_ID=<公開鍵のID>

ポリシーファイルのパスを環境変数に格納
$ CF_POLICY_FILE=policy_001.json

秘密鍵のパスを環境変数に格納
$ CF_PRIVATE_KEY=private_key.pem

カスタムポリシーのJSONファイルをbase64エンコードして環境変数に格納
$ CF_POLICY=$(cat ${CF_POLICY_FILE} | tr -d "\n" | tr -d " \t\n\r" | openssl base64 -A | tr -- '+=/' '-_~')

カスタムポリシーのJSONファイルに秘密鍵で署名し、base64エンコードして環境変数に格納
$ CF_SIGNATURE=$(cat ${CF_POLICY_FILE} | tr -d "\n" | tr -d " \t\n\r" | openssl sha1 -sign ${CF_PRIVATE_KEY} | openssl base64 -A | tr -- '+=/' '-_~')

Cookieをセットして接続すると、グループ001用のパスにあるHLS形式ファイル(インデックスファイル)にアクセスできる
$ curl https://${CF_DOMAIN}/video/001/hls_test/test.m3u8 -b "CloudFront-Policy=${CF_POLICY}; CloudFront-Signature=${CF_SIGNATURE}; CloudFront-Key-Pair-Id=${CF_KEYPAIR_ID}"
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-STREAM-INF:BANDWIDTH=1011690,AVERAGE-BANDWIDTH=980106,CODECS="avc1.64001e,mp4a.40.5",RESOLUTION=640x360,FRAME-RATE=30.000
test_Ott_Hls_Ts_Avc_Aac_16x9_640x360p_1.5Mbps_qvbr.m3u8

Cookieをセットして接続しても、グループ002用のパスにあるHLS形式ファイル(インデックスファイル)にはアクセスできない。
$ curl https://${CF_DOMAIN}/video/002/hls_test/test.m3u8 -b "CloudFront-Policy=${CF_POLICY}; CloudFront-Signature=${CF_SIGNATURE}; CloudFront-Key-Pair-Id=${CF_KEYPAIR_ID}"                           
<?xml version="1.0" encoding="UTF-8"?><Error><Code>AccessDenied</Code><Message>Access denied</Message></Error>

後ほどブラウザで確認する際に使うので、以下の値は書き留めておく
$ echo ${CF_POLICY}
$ echo ${CF_SIGNATURE}

次に、グループ002のユーザを想定して署名付きCookieを作成する。
ポリシーJSONファイルだけグループ002用のものに変えて、先ほどと同じ手順を実行する。

CloudFrontドメイン名を環境変数に格納
$ CF_DOMAIN=<CloudFrontのドメイン名>

公開鍵のIDを環境変数に格納
$ CF_KEYPAIR_ID=<公開鍵のID>

ポリシーファイルのパスを環境変数に格納
$ CF_POLICY_FILE=policy_001.json

秘密鍵のパスを環境変数に格納
$ CF_PRIVATE_KEY=private_key.pem

カスタムポリシーのJSONファイルをbase64エンコードして環境変数に格納
$ CF_POLICY=$(cat ${CF_POLICY_FILE} | tr -d "\n" | tr -d " \t\n\r" | openssl base64 -A | tr -- '+=/' '-_~')

カスタムポリシーのJSONファイルに秘密鍵で署名し、base64エンコードして環境変数に格納
$ CF_SIGNATURE=$(cat ${CF_POLICY_FILE} | tr -d "\n" | tr -d " \t\n\r" | openssl sha1 -sign ${CF_PRIVATE_KEY} | openssl base64 -A | tr -- '+=/' '-_~')

Cookieをセットして接続しても、グループ001用のパスにあるHLS形式ファイル(インデックスファイル)にはアクセスできない。
$ curl https://${CF_DOMAIN}/video/001/hls_test/test.m3u8 -b "CloudFront-Policy=${CF_POLICY}; CloudFront-Signature=${CF_SIGNATURE}; CloudFront-Key-Pair-Id=${CF_KEYPAIR_ID}"
<?xml version="1.0" encoding="UTF-8"?><Error><Code>AccessDenied</Code><Message>Access denied</Message></Error>

Cookieをセットして接続すると、グループ002用のパスにあるHLS形式ファイル(インデックスファイル)にアクセスできる
$ curl https://${CF_DOMAIN}/video/002/hls_test/test.m3u8 -b "CloudFront-Policy=${CF_POLICY}; CloudFront-Signature=${CF_SIGNATURE}; CloudFront-Key-Pair-Id=${CF_KEYPAIR_ID}"   
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-STREAM-INF:BANDWIDTH=1011690,AVERAGE-BANDWIDTH=980106,CODECS="avc1.64001e,mp4a.40.5",RESOLUTION=640x360,FRAME-RATE=30.000
test_Ott_Hls_Ts_Avc_Aac_16x9_640x360p_1.5Mbps_qvbr.m3u8                        

後ほどブラウザで確認する際に使うので、以下の値は書き留めておく
$ echo ${CF_POLICY}
$ echo ${CF_SIGNATURE}

→カスタムポリシーを使い分けることで、所属グループ(権限)ごとにアクセスできるパスを制御することができた。

ブラウザで接続確認する

ブラウザからアクセスして、実際に視聴できるか確認する。

視聴確認ページの作成

HLS形式の動画を視聴するためのページが必要なので、Video.jsvideojs-http-streamingを使った視聴確認用ページを作成する。

以下をHLS動画用S3バケットの「index.html」にアップロードする。 <CloudFrontのドメイン名>にはTerraformで作成したCloudFrontディストリビューションドメイン名を入れる。
※HTMLコードはこちらを参考にさせていただいた。
index.html

<html>
  <head>
    <title>Video.js HLS TEST</title>
    <link href="https://vjs.zencdn.net/7.15.4/video-js.css" rel="stylesheet">
  </head>
  <body>
    <video-js id=video001 width=640 height=360
              class="vjs-default-skin" controls>
      <source
         src="https://<CloudFrontのドメイン名>/video/001/hls_test/test.m3u8"
         type="application/x-mpegURL">
    </video-js>
    <script src="https://vjs.zencdn.net/7.15.4/video.js"></script>
    <script>
      var player = videojs('video001');
    </script>
    <br>
    <video-js id=video002 width=640 height=360
              class="vjs-default-skin" controls>
      <source
         src="https://<CloudFrontのドメイン名>/video/002/hls_test/test.m3u8"
         type="application/x-mpegURL">
    </video-js>
    <script src="https://vjs.zencdn.net/7.15.4/video.js"></script>
    <script>
      var player = videojs('video002');
    </script>
  </body>
</html>

ブラウザで「http://<CloudFrontのドメイン名>」に接続してみる。 Cookieを設定していないので、以下のようにグループ001用動画、グループ002用動画どちらも「The media could not be loaded, either because the server or network failed or because the format is not supported.」となり視聴できない。

Cookieなしで接続した場合
Cookieなしで接続した場合

Cookieをセットして接続する

何らかの方法でブラウザにCookieをセットして接続確認してみる。
自分は、ChromeEditThisCookieという拡張機能を使った。
※参考: EditThisCookieでクッキーが追加できない

curlコマンドでの実行時に作成した、グループ001用署名付きCookieをブラウザにセットして「http://<CloudFrontのドメイン名>」に接続する。
以下のように、グループ001の動画のみ再生でき、グループ002用動画は「The media could not be loaded, either because the server or network failed or because the format is not supported.」となり視聴できない。

グループ001用Cookieを設定して接続した場合
グループ001用Cookieを設定して接続した場合

→想定通りの制限が出来ていることを、ブラウザからの接続でも確認できた。

補足:EditThisCookieを使わずにHTMLにCookieを埋め込みたい場合

EditThisCookieを使うのでは無く、HTMLにCookieを埋め込んでもいい。
その場合は、HLS動画用S3バケットの「index.html」を以下のものに差し替える。
<CloudFrontのドメイン名>、<公開鍵のID>、<先ほど環境変数「CF_POLICY」に入れた値>、<先ほど環境変数「CF_SIGNATURE」に入れた値>は環境と試したいグループに応じて記載する。

index.html

<html>
  <head>
    <title>Video.js HLS TEST</title>
    <link href="https://vjs.zencdn.net/7.15.4/video-js.css" rel="stylesheet">
  </head>
  <body>
    <p id="msg">Cookie List</p>
    <script>
      document.cookie = "CloudFront-Policy=<先ほど環境変数「CF_POLICY」に入れた値>; domain=<CloudFrontのドメイン名>; path=/; secure; max-age=3600";
      document.cookie = "CloudFront-Signature=<先ほど環境変数「CF_SIGNATURE」に入れた値>; domain=<CloudFrontのドメイン名>; path=/; secure; max-age=3600";
      document.cookie = "CloudFront-Key-Pair-Id=<公開鍵のID>; domain=<CloudFrontのドメイン名>; path=/; secure; max-age=3600";
      let e = document.getElementById('msg');
      e.insertAdjacentHTML('afterend', '<p>' + document.cookie + '</p>');
    </script>
    <video-js id=video001 width=640 height=360
              class="vjs-default-skin" controls>
      <source
         src="https://<CloudFrontのドメイン名>/video/001/hls_test/test.m3u8"
         type="application/x-mpegURL">
    </video-js>
    <script src="https://vjs.zencdn.net/7.15.4/video.js"></script>
    <script>
      var player = videojs('video001');
    </script>
    <br>
    <video-js id=video002 width=640 height=360
              class="vjs-default-skin" controls>
      <source
         src="https://<CloudFrontのドメイン名>/video/002/hls_test/test.m3u8"
         type="application/x-mpegURL">
    </video-js>
    <script src="https://vjs.zencdn.net/7.15.4/video.js"></script>
    <script>
      var player = videojs('video002');
    </script>
  </body>
</html>

まとめ

  • MediaConvertによって、HLS形式への変換が簡単に行えることを確認できた。
  • CloudFrontの署名付きCookie作成時のカスタムポリシーを工夫することで、「所属グループ用パスのみアクセス可」といった細かいアクセス制御が可能なことが確認できた。

おまけ:商用環境導入に向けた検討ポイント

今回の構成を商用環境に導入する場合に検討が必要そうなポイント、気になる点などを記載しておく。

  • MediaConvertのコスト見積もり
    →MediaConvertの料金体系はこちら参照。
    基本的に出力する動画の長さによって変動し、時間あたりの料金はコーデック、解像度等によって異なる。
    本記事の処理例だと、以下の条件となるため1分当たり料金は「0.0085USD」となる。

    • 階層:プロフェッショナル階層の機能を使っていないため、「ベーシック」
    • 解像度:640x360pに変換しており、 720未満なので「SD」
    • FPS:入力映像から変えない設定(Follow source)なの入力映像の「30」
  • MediaConvertの同時処理数
    MediaConvertのサービスクォータを確認すると、東京リージョンの「すべてのオンデマンドキューにおける同時ジョブ (ベースライン)」は「20」となっている。
    21個以上の動画を同時に変換させたい場合は、上限緩和申請をする必要がある。

  • MediaConvertの監視・リトライ処理
    →Lambda関数からMediaConvertにジョブ登録した後で、ジョブが何らかの理由で失敗した場合、ジョブ登録自体は上手くいっているのでLambda側で失敗を補足して通知したり、リトライすることはできない。
    その場合はEventBridgeでMediaConvertジョブ失敗のイベントパターンを登録することで、通知やリトライを出来そう(未検証)。
    こちらがイベントパターンの参考になりそう。

  • HLS形式動画の暗号化 →MediaConvertでの変換時に、コンテンツ保護対策としてHLS形式動画をAES鍵で暗号化することができるらしい。
    別途試してみる予定。
    続きの記事でやってみた。

  • アクセス元環境でCookieが使えない場合や、もっと細かい単位で制御したい場合(グループではなくユーザごとにアクセスできる映像が異なる場合等)はどうするか
    →「ユーザー毎に視聴の可否が異なる動画ファイルをCloudFrontで配信する方法の考察」にあるように、Lambda Edgeを使う方法で実現出来そう。

参考文献