Amazon Interactive Video Service が生成するアーカイブから不要な解像度セグメントを自動削除するためのステートマシンを作ってみた

Amazon IVS のアーカイブ出力、いらない解像度を自動で削除してみませんか
2022.02.22

こんにちは、大前です。

Amazon Interactive Video Service(以下 IVS)には S3 へのアーカイブ機能があり、有効化しておくことでライブストリーミングを行いながらアーカイブを S3 に保存することができます。

参考 : Amazon S3 への自動録画 - Amazon Interactive Video Service

上記機能によって生成されるファイルは HLS コンテンツとなっており、下記画像の様にマニフェストファイル(master.m3u8 等)や複数レンディションのコンテンツ(720p60, 360p30 等)が含まれます。

IVS のアーカイブ出力先を利用して録画ファイルの配信などを行うのであれば、複数レンディションが生成されているのはありがたいのですが、単純にアーカイブファイルとして保持しておきたいだけなのであれば、一番大きい解像度のみ(例えば 1080p)を残して他のレンディションは削除したいケースもあるかもしれません。

今回は、Step Functions を利用し、IVS が生成するアーカイブファイルから不要なレンディションを自動で削除する様な仕組みを作ってみました。

構成

IVS はアーカイブ記録の開始時と終了時にイベントを発行するため、それを利用して Step Functions のステートマシンを発火させ、S3 に生成されているアーカイブファイルから特定のレンディションを削除する構成としました。

参考 : 例: 録画状態の変化 - Amazon Interactive Video Service

やってみた

IVS は現状東京リージョンに対応していないため、以下作業は全てバージニア北部(us-east-1)にて行いました。

Step Functions ステートマシンの作成

後ほど細かい部分の説明をしますが、全体像としては以下のステートマシンを作成しました。今回は 160p30 のレンディションを削除する様な設定としています。流用する際は、IVS のアーカイブ保存先の S3 バケット名を記載してください。

{
  "Comment": "This is your state machine",
  "StartAt": "ListObjectsV2",
  "States": {
    "ListObjectsV2": {
      "Type": "Task",
      "Next": "Map",
      "Parameters": {
        "Bucket": "<IVS アーカイブ保存先の S3 バケット名>",
        "Prefix.$": "States.Format('{}/media/hls/160p30', $.recording_s3_key_prefix)",
        "MaxKeys": 100
      },
      "Resource": "arn:aws:states:::aws-sdk:s3:listObjectsV2",
      "InputPath": "$.detail",
      "ResultPath": "$.listresult"
    },
    "Map": {
      "Type": "Map",
      "Iterator": {
        "StartAt": "Pass",
        "States": {
          "Pass": {
            "Type": "Pass",
            "End": true,
            "Parameters": {
              "Key.$": "$.Key"
            }
          }
        }
      },
      "ItemsPath": "$.listresult.Contents",
      "Next": "DeleteObjects",
      "ResultPath": "$.mapresult"
    },
    "DeleteObjects": {
      "Type": "Task",
      "Parameters": {
        "Bucket": "<IVS アーカイブ保存先の S3 バケット名>",
        "Delete": {
          "Objects.$": "$.mapresult"
        }
      },
      "Resource": "arn:aws:states:::aws-sdk:s3:deleteObjects",
      "ResultPath": "$.deleteresult",
      "Next": "Choice"
    },
    "Choice": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.listresult.IsTruncated",
          "BooleanEquals": true,
          "Next": "ListObjectsV2"
        }
      ],
      "Default": "Finish"
    },
    "Finish": {
      "Type": "Pass",
      "End": true,
      "InputPath": "$.deleteresult"
    }
  }
}

Workflow Studio 上の図

削除対象のオブジェクトの S3 Prefix 一覧を取得

ListObjectsV2 を利用し、削除対象とするオブジェクトの S3 Prefix 一覧を取得しています。

{
  "Bucket": "<IVS アーカイブ保存先の S3 バケット名>",
  "Prefix.$": "States.Format('{}/media/hls/160p30', $.recording_s3_key_prefix)",
  "MaxKeys": 100
}

API に渡しているパラメータは Bucket/Prefix/MaxKeys の 3つです。

EventBridge から渡されるイベントからアーカイブが生成されるパス(recording_s3_key_prefix)を取得し、Step Functions の組み込み関数を利用して文字列を結合することで、削除したいレンディションのパスを Prefix として指定しています。

参考 : 組み込み関数 - AWS Step Functions

また、ListObjectsV2 はデフォルトで 1000個のオブジェクトを取得できますが、後述するループ部分の検証のために MaxKeys を指定して一度に取得するオブジェクト数を指定しています。MaxKeys を 1000 とした方がステートマシン上の状態遷移が少なくなるためコスト的なメリットはありそうですが、副作用などが起こらないかは検証の上でご利用ください。

削除 API に渡すパラメータの整形

ListObjectsV2 にて取得したオブジェクトに対して DeleteObjects を実行し不要なレンディションを削除するのですが、List の結果はそのままでは利用できないため、少しパラメータを整形する必要があります。

ListObjectsV2 の結果は Contents 配下に配列として格納されるため、MapPass を利用して配列から必要な値だけを抽出して DeleteObjects に渡すパラメータを整形します。

参考 : マップ - AWS Step Functions

参考 : パス - AWS Step Functions

オブジェクトの削除を実行

DeleteObjects を実行し、指定したレンディションのオブジェクトを削除します。

{
  "Bucket": "ivs-archive-042108753690",
  "Delete": {
    "Objects.$": "$.mapresult"
  }
}

特に特筆する箇所はありませんが、前段の Map で処理した結果が $.mapresult に格納されているので、それを Objects パラメータに渡してあげています。

ループ判定

ライブストリームが数時間に及ぶ場合、一度の 取得→削除 処理では全てのオブジェクトを削除できない場合も考えられるため、Choice を利用してループ判定を行っています。

参考 : 選択 - AWS Step Functions

"Choices": [
    {
        "Variable": "$.listresult.IsTruncated",
        "BooleanEquals": true,
        "Next": "ListObjectsV2"
    }
]

ListObjectsV2 は取得できるオブジェクトが残っている場合に IsTruncated に true をセットしてレスポンスを返却するため、今回はこれを利用してループ判定を行なっています。true ならまだオブジェクトが残っているとみなして再度 List→Delete を実施し、false ならオブジェクトが無くなったとして処理を完了させます。

参考 : ListObjectsV2 Response Syntax - Amazon Simple Storage Service

EventBridge ルールの作成

IVS のアーカイブ記録が完了したタイミングで Step Functions を自動起動させたいので、以下のイベントパターンを定義した EventBridge ルールを作成し、ターゲットには上記で作成したステートマシンを指定しました。

{
  "source": ["aws.ivs"],
  "detail-type": ["IVS Recording State Change"],
  "detail": {
    "recording_status": ["Recording End"]
  }
}

アーカイブが削除されるか確認してみる

Step Functions ステートマシン、EventBridge ルールの用意ができたら準備完了です。アーカイブ記録機能を有効にした IVS で配信を行い、指定したレンディション(今回は 160p30)が削除されるか確認してみます。

IVS の利用やアーカイブ記録の設定方法は今回省略しますが、必要に応じて以下ブログを参照ください。

今回は大体 40~50分 配信を行なってみました。IVS がアーカイブとして出力するセグメント長は 10秒であるため、約 10秒に 1回のペースでアーカイブのセグメントが追加されており、今回は 250超えのファイルが生成されていることが確認できます。

IVS の配信を終了し、Step Functions のステートマシンを確認すると、ステートマシンが無事実行されたことが確認できます。40秒ほどで実行が完了している様です。

アーカイブ出力先の S3 バケットを確認しに行くと、160p30 配下のオブジェクトが全て削除されているためパス毎無くなっていることが確認できます。

おわりに

IVS が出力するアーカイブのうち、不要なレンディションに関するオブジェクトを自動で削除するための Step Functions ステートマシンを作成してみました。個人的には Lambda なしで実現できたため満足しています。IVS のアーカイブ機能を利用する目的によっては有用なケースもあるかと思いますので、参考にしていただけますと幸いです。

注意点として、この仕組みでは生成されるマスターマニフェストファイル(master.m3u8)には何も更新を行っていないため、レンディション削除後に master.m3u8 を利用したコンテンツ再生を試みると予期せぬ挙動が起こる可能性も考えられます。もし特定のレンディションは削除しつつ、master.m3u8 を利用した配信は行いたい場合、マニフェストの修正はお忘れなく。

以上、AWS 事業本部の大前でした。

利用した CloudFormation テンプレート

検証のために作成した CloudFormation テンプレートを置いておきます。ご参考までに。

delete-ivs-archive-sample.yml
AWSTemplateFormatVersion: "2010-09-09"
Description: "A template for automation to delete unnecessary ivs archive rendition."
Parameters:
  # 作成リソースに付与する接頭語
  Prefix:
    Description: "Prefix of each resource"
    Type: "String"
    Default: "sample"
  # IVS のアーカイブ保存先バケット名
  BucketName:
    Description: "Target Bucket name to delete ivs archive"
    Type: "String"
  # 削除するレンディション
  DeleteRendition:
    Description: "Target Rendition to delete ivs archive"
    Type: "String"
    Default: "160p30"
    AllowedValues:
      - "1080p"
      - "720p30"
      - "480p30"
      - "360p30"
      - "160p30"

Resources:
  # Step Functions StateMachine
  StateMachine:
    Type: "AWS::StepFunctions::StateMachine"
    Properties:
      StateMachineName: !Sub "${Prefix}-delete-ivs-archive-statemachine"
      DefinitionS3Location:
        Bucket: "<Step Functions ステートマシンの定義ファイル保存先 S3 バケット名>"
        Key: "delete-ivs-archive-sample.json"
      DefinitionSubstitutions:
        DeleteRendition: !Ref DeleteRendition
        BucketName: !Ref BucketName
      RoleArn: !GetAtt StateMachineRole.Arn
      Tags:
        - Key: "Name"
          Value: !Sub "${Prefix}-delete-ivs-archive-statemachine"
  ## IAM Role for StateMachie
  StateMachineRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: !Sub "${Prefix}-delete-ivs-archive-statemachine-role"
      Path: "/service-role/"
      AssumeRolePolicyDocument:
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - "states.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      ManagedPolicyArns:
        - !Ref DeleteIVSArchivePolicy
  ## IAM Policy for DeleteIVSArchive
  DeleteIVSArchivePolicy:
    Type: "AWS::IAM::ManagedPolicy"
    Properties:
      ManagedPolicyName: !Sub "${Prefix}-delete-ivs-archive-policy"
      Path: "/service-role/"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action:
              - "s3:ListBucket"
            Resource:
              - !Join
                - ""
                - - "arn:aws:s3:::"
                  - !Ref BucketName
          - Effect: "Allow"
            Action:
              - "s3:DeleteObject"
            Resource:
              - !Join
                - ""
                - - "arn:aws:s3:::"
                  - !Ref BucketName
                  - "/*"
  # EventRule for CreateManagedAccount
  EventRule:
    Type: "AWS::Events::Rule"
    Properties:
      Description: "Catch 'Finish IVS Recording' event"
      EventPattern: |-
        {
          "source": ["aws.ivs"],
          "detail-type": ["IVS Recording State Change"],
          "detail": {
            "recording_status": ["Recording End"]
          }
        }
      Name: !Sub "${Prefix}-catch-FinishIVSRecording"
      State: "ENABLED"
      Targets:
        - Arn: !Ref StateMachine
          Id: !Sub "${Prefix}-delete-ivs-archive-statemachine"
          RoleArn: !GetAtt EventBridgeRole.Arn
  ## IAM Role for EventBridge
  EventBridgeRole:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: !Sub "${Prefix}-catch-FinishIVSRecording-role"
      Path: "/service-role/"
      AssumeRolePolicyDocument:
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - "events.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      ManagedPolicyArns:
        - !Ref InvokeStepFunctionsPolicy
  ## IAM Policy to invoke Step Functions
  InvokeStepFunctionsPolicy:
    Type: "AWS::IAM::ManagedPolicy"
    Properties:
      ManagedPolicyName: !Sub "${Prefix}-invoke-step-functions-from-eventbridge-policy"
      Path: "/service-role/"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Resource:
              - !Ref StateMachine
            Effect: "Allow"
            Action:
              - "states:StartExecution"
delete-ivs-archive-sample.json
{
    "Comment": "This is your state machine",
    "StartAt": "ListObjectsV2",
    "States": {
        "ListObjectsV2": {
            "Type": "Task",
            "Next": "Map",
            "Parameters": {
                "Bucket": "${BucketName}",
                "Prefix.$": "States.Format('{}/media/hls/${DeleteRendition}', $.recording_s3_key_prefix)",
                "MaxKeys": 100
            },
            "Resource": "arn:aws:states:::aws-sdk:s3:listObjectsV2",
            "InputPath": "$.detail",
            "ResultPath": "$.listresult"
        },
        "Map": {
            "Type": "Map",
            "Iterator": {
                "StartAt": "Pass",
                "States": {
                    "Pass": {
                        "Type": "Pass",
                        "End": true,
                        "Parameters": {
                            "Key.$": "$.Key"
                        }
                    }
                }
            },
            "ItemsPath": "$.listresult.Contents",
            "Next": "DeleteObjects",
            "ResultPath": "$.mapresult"
        },
        "DeleteObjects": {
            "Type": "Task",
            "Parameters": {
                "Bucket": "${BucketName}",
                "Delete": {
                    "Objects.$": "$.mapresult"
                }
            },
            "Resource": "arn:aws:states:::aws-sdk:s3:deleteObjects",
            "ResultPath": "$.deleteresult",
            "Next": "Choice"
        },
        "Choice": {
            "Type": "Choice",
            "Choices": [
                {
                    "Variable": "$.listresult.IsTruncated",
                    "BooleanEquals": true,
                    "Next": "ListObjectsV2"
                }
            ],
            "Default": "Finish"
        },
        "Finish": {
            "Type": "Pass",
            "End": true,
            "InputPath": "$.deleteresult"
        }
    }
}

参考