とりあえず MediaLive を触りたい時に使える CloudFormation テンプレートを作ってみた

MediaLive+MediaStoreを一発で作成するCloudFormationテンプレートを作成してみましたので、公開します。
2020.02.21

こんにちは、大前です。今日も今日とて MediaServices を触っていきます。

背景

私は MediaServices のブログを書くことが多いため、事あるごとに MediaServices のリソースを作成しているのですが、多くのケースでは検証するために複数のサービス(MediaLive+MediaStore+CloudFrontとか)を立てる必要があり、何かと面倒でした。

また、MediaLive の Input と Channel に関してはアイドル状態でも微課金が発生してしまうため、毎回検証が終わる度に削除する必要があります。

AWS Elemental MediaLive の料金

アイドルリソース AWS Elemental MediaLive charges for each push input and channel when these resources are not in use. Only push inputs, RTP PUSH and RTMP PUSH, incur a cost when idle. Pulled inputs, HLS PULL and RTMP PULL, do not incur an idle resource cost. The pricing for idle resources is:

0.01USD per hour on a pro rata basis for each push input not associated with a running channel 0.01USD per hour on a pro rata basis for each channel or statmux pool not in a running state

正直、何回も作成しているので特にドキュメント等を見なくても大枠のリソースは作成出来る様になってしまったのですが、CloudFormation で Media 系のサービスを作成してみたいなという気持ちもあり、この度 CloudFormation テンプレートを作成してみました。

タイトルにある通り、「とりあえず触りたい」場合向けなので細かい設定値は適当な部分が多いです。

そのため、本番ワークフロー等に活用する場合には設定値の確認・検討はしっかりと実施ください。

やってみた

CloudFormationで作成する範囲

CloudFormationでは以下図の水色背景部分を作成します。

作成される主なリソースとしては、MediLive、IAM Role、MediaStore の3つです。

MediaLive の入力は RTMP を受け付ける形で作成されるため、OBS 等からのライブ配信がすぐに行える環境が作成できます。

作成後は、MediaLive の出力設定を弄ってみたり、CloudFront を追加してみたりと自由に遊べます。

CloudFormationテンプレート

以下がテンプレートになります。

個人的には、MediaLive に設定する IAM Role の作成や、MediaStore への CORS 設定等をテンプレートに落とし込めたのが嬉しいポイントです。

また、削除ポリシーはあえて設定していないので、不要になったら CloudFormation のスタックを削除すれば全リソースが削除されます。(スタック作成後に手動で設定を変更していたりすると、うまく削除できない場合があります)

使用する際には適宜修正してお使いください。

AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  #ProjectName
  ProjectName:
    Description: "Name of MediaLiveInput"
    Default: "Sample"
    Type: String
  #StreamKey
  StreamKey:
    Description: "StreamKeyA of Input"
    Type: String
    Default: "streamkey"
  #ChannelClass
  ChannelClass:
    Description: "ChannelClass"
    Type: String
    Default: "SINGLE_PIPELINE"
    AllowedValues:
      - STANDARD
      - SINGLE_PIPELINE
  #SecurityGroupWhitelistCIDR
  SecurityGroupWhitelistCIDR:
    Description: "Whitelist CIDR of MediaLiveInputSecurityGroup."
    Type: String
    Default: "0.0.0.0/0"
  #MediaLiveDestinationName
  MediaLiveDestinationName:
    Description: "MediaLive DestinationName."
    Type: String
    Default: "MediaLiveDest1"

Resources:
  # ----------------
  # MediaLiveInput
  # ----------------
  MediaLiveInput:
    Type: AWS::MediaLive::Input
    Properties:
      Destinations:
        - StreamName: !Sub "${ProjectName}/${StreamKey}-A"
        - StreamName: !Sub "${ProjectName}/${StreamKey}-B"
      InputSecurityGroups:
        - !Ref MediaLiveInputSecurityGroup
      Name: !Sub "${ProjectName}-input"
      Tags: { 'Name' : !Sub "${ProjectName}-input"}
      Type: "RTMP_PUSH"

  # -----------------------------
  # MediaLiveInputSecurityGroup
  # -----------------------------
  MediaLiveInputSecurityGroup:
    Type: AWS::MediaLive::InputSecurityGroup
    Properties:
      Tags: { 'Name' : !Sub "${ProjectName}-inputsg"}
      WhitelistRules:
        - Cidr: !Ref SecurityGroupWhitelistCIDR

  # ------------------
  # MediaLiveChannel
  # ------------------
  MediaLiveChannel:
    Type: AWS::MediaLive::Channel
    Properties:
      ChannelClass: !Ref ChannelClass
      Destinations:
        - Id: !Ref MediaLiveDestinationName
          Settings:
            - PasswordParam: ""
              StreamName: ""
              Url: !Sub
                - "mediastoressl://${MediaStoreEndpoint}/${ProjectName}/destA"
                - MediaStoreEndpoint: !Select [ 1, !Split [ "//", !GetAtt MediaStore.Endpoint ]]
              Username: ""
      EncoderSettings:
        AudioDescriptions:
          - AudioSelectorName: "default"
            CodecSettings:
              AacSettings:
                Bitrate: 96000
                RawFormat: "NONE"
                Spec: "MPEG4"
            AudioTypeControl: "FOLLOW_INPUT"
            LanguageCodeControl: "FOLLOW_INPUT"
            Name: "audio_3_aac96"
        OutputGroups:
          - OutputGroupSettings:
              HlsGroupSettings:
                CaptionLanguageSetting: "OMIT"
                HlsCdnSettings":
                  HlsBasicPutSettings:
                    NumRetries: 5
                    ConnectionRetryInterval: 30
                    RestartDelay: 5
                    FilecacheDuration: 300
                InputLossAction: "EMIT_OUTPUT"
                ManifestCompression: "NONE"
                Destination:
                  DestinationRefId: !Ref MediaLiveDestinationName
                IvInManifest: "INCLUDE"
                IvSource: "FOLLOWS_SEGMENT_NUMBER"
                ClientCache: "ENABLED"
                TsFileMode: "SEGMENTED_FILES"
                ManifestDurationFormat: "FLOATING_POINT"
                SegmentationMode: "USE_SEGMENT_DURATION"
                RedundantManifest: "DISABLED"
                OutputSelection: "MANIFESTS_AND_SEGMENTS"
                StreamInfResolution: "INCLUDE"
                IFrameOnlyPlaylists: "DISABLED"
                IndexNSegments: 10
                ProgramDateTime: "INCLUDE"
                ProgramDateTimePeriod: 600
                KeepSegments: 21
                SegmentLength: 6
                TimedMetadataId3Frame: "PRIV"
                TimedMetadataId3Period: 10
                HlsId3SegmentTagging: "DISABLED"
                CodecSpecification: "RFC_4281"
                DirectoryStructure: "SINGLE_DIRECTORY"
                SegmentsPerSubdirectory: 10000
                Mode: "LIVE"
            Name: "TN2224"
            Outputs:
              - OutputSettings:
                  HlsOutputSettings:
                    NameModifier: "_1280x720_3300k"
                    HlsSettings:
                      StandardHlsSettings:
                        M3u8Settings:
                          AudioFramesPerPes: 4
                          AudioPids: "492-498"
                          EcmPid: "8182"
                          NielsenId3Behavior: "NO_PASSTHROUGH"
                          PcrControl: "PCR_EVERY_PES_PACKET"
                          PmtPid: "480"
                          ProgramNum: 1
                          Scte35Pid: "500"
                          Scte35Behavior: "NO_PASSTHROUGH"
                          TimedMetadataPid: "502"
                          TimedMetadataBehavior: "NO_PASSTHROUGH"
                          VideoPid: "481"
                        AudioRenditionSets: "program_audio"
                    H265PackagingType: "HVC1"
                VideoDescriptionName: "video_1280_720_1"
                AudioDescriptionNames:
                  - "audio_3_aac96"
        TimecodeConfig:
          Source: "SYSTEMCLOCK"
        VideoDescriptions:
          - CodecSettings:
              H264Settings:
                AfdSignaling: "NONE"
                ColorMetadata: "INSERT"
                AdaptiveQuantization: "HIGH"
                Bitrate: 3300000
                EntropyEncoding: "CABAC"
                FlickerAq: "ENABLED"
                FramerateControl: "SPECIFIED"
                FramerateNumerator: 30000
                FramerateDenominator: 1001
                GopBReference: "ENABLED"
                GopClosedCadence: 1
                GopNumBFrames: 3
                GopSize: 60
                GopSizeUnits: "FRAMES"
                SubgopLength: "FIXED"
                ScanType: "PROGRESSIVE"
                Level: "H264_LEVEL_4_1"
                LookAheadRateControl: "HIGH"
                NumRefFrames: 1
                ParControl: "INITIALIZE_FROM_SOURCE"
                Profile: "HIGH"
                RateControlMode: "CBR"
                Syntax: "DEFAULT"
                SceneChangeDetect: "ENABLED"
                SpatialAq: "ENABLED"
                TemporalAq: "ENABLED"
                TimecodeInsertion: "DISABLED"
            Height: 720
            Name: "video_1280_720_1"
            RespondToAfd: "NONE"
            Sharpness: 50
            ScalingBehavior: "DEFAULT"
            Width: 1280
      InputAttachments:
        - InputAttachmentName: !Sub "${ProjectName}-channelinput"
          InputId: !Ref MediaLiveInput
          InputSettings:
            DeblockFilter: "DISABLED"
            DenoiseFilter: "DISABLED"
            FilterStrength: 1
            InputFilter: "AUTO"
            SourceEndBehavior: "CONTINUE"
      InputSpecification:
          Codec: "AVC"
          MaximumBitrate: "MAX_20_MBPS"
          Resolution: "HD"
      Name: !Sub "${ProjectName}-channel"
      RoleArn: !GetAtt MediaLiveAccessRole.Arn
      Tags: { 'Name' : !Sub "${ProjectName}-channel"}

  # ------------
  # MediaStore
  # ------------
  MediaStore:
    Type: AWS::MediaStore::Container
    Properties:
      ContainerName: !Sub "${ProjectName}-container"
      CorsPolicy:
        - AllowedHeaders:
            - "*"
          AllowedMethods:
            - "GET"
            - "HEAD"
          AllowedOrigins:
            - "*"
          ExposeHeaders:
            - String
          MaxAgeSeconds: 3000
      LifecyclePolicy:
        '{
          "rules": [
            {
              "definition": {
                "path": [ { "prefix": "" } ],
                "days_since_create": [
                    {"numeric": [">" , 5]}
                ]
              },
              "action": "EXPIRE"
            }
          ]
        }'
      Policy:
        !Sub
          - '{
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Sid": "PublicReadOverHttps",
                  "Effect": "Allow",
                  "Action": ["mediastore:GetObject", "mediastore:DescribeObject"],
                  "Principal": "*",
                  "Resource": "arn:aws:mediastore:${AWS::Region}:${AWS::AccountId}:container/${CONTAINER}/*",
                  "Condition": {
                    "Bool": {
                        "aws:SecureTransport": "true"
                    }
                  }
                }
              ]
            }'
          - CONTAINER: !Sub "${ProjectName}-container"

  # ---------------------
  # MediaLiveAccessRole
  # ---------------------
  MediaLiveAccessRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          -
            Effect: "Allow"
            Principal:
              Service:
                - "medialive.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess
      Path: /
      Policies:
        - PolicyName: !Sub "${ProjectName}-MediaLiveCustomPolicy"
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - s3:ListBucket
                  - s3:PutObject
                  - s3:GetObject
                  - s3:DeleteObject
                Resource: '*'
              - Effect: Allow
                Action:
                  - mediastore:ListContainers
                  - mediastore:PutObject
                  - mediastore:GetObject
                  - mediastore:DeleteObject
                  - mediastore:DescribeObject
                Resource: '*'
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - logs:DescribeLogStreams
                  - logs:DescribeLogGroups
                Resource: 'arn:aws:logs:*:*:*'
              - Effect: Allow
                Action:
                  - mediaconnect:ManagedDescribeFlow
                  - mediaconnect:ManagedAddOutput
                  - mediaconnect:ManagedRemoveOutput
                Resource: '*'
              - Effect: Allow
                Action:
                  - ec2:describeSubnets
                  - ec2:describeNetworkInterfaces
                  - ec2:createNetworkInterface
                  - ec2:createNetworkInterfacePermission
                  - ec2:deleteNetworkInterface
                  - ec2:deleteNetworkInterfacePermission
                  - ec2:describeSecurityGroups
                Resource: '*'
              - Effect: Allow
                Action:
                  - mediapackage:DescribeChannel
                Resource: '*'
      RoleName: !Sub "${ProjectName}-MediaLiveAccessRole"

終わりに

とりあえず MediaLive 触りたい時に使えそうな CloudFormation テンプレートを作成してみました。

aws-samples ではカスタムリソースを使って MediaLive を作成していたりして、私が調べた限りだと純粋な CloudFormation のサンプルが見つかりませんでした。

ですので、「CloudFormation で MediaLive 立ててみたいけどよくわからん。。。」な方にこのテンプレートが役に立つ事を願っております。

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

余談

Terraform は MediaLive に対応していないのですが、調べると feature request の issue があるそうです。

feature request: medialive resources #4936

需要が高まれば、そのうち実装されるのかもしれませんね。楽しみに待ちたいと思います。

参考

AWS Elemental MediaLive リソースタイプのリファレンス

MediaStore リソースタイプのリファレンス