MediaPackage が CloudFormation 対応したので MediaLive + MediaPackage + CloudFront によるライブ配信構成を一発で作成するテンプレートを作成してみた
こんにちは、大前です。
先日のアップデートでついに AWS Elemental MediaPackage(以下 MediaPackage)を CloudFormation で作成できる様になりました。
これはもう MediaPackage を使ったライブ配信構成を一発で作るテンプレートを書くしかないよね! という事で作成してみました。
投稿時点(2020/11)での動作は確認しておりますが、今後のアップデート等による影響で動かなくなる可能性がある点についてはご容赦ください。
作成するテンプレート
下記構成図のリソースを作成する CloudFormation テンプレートを作成します。
RTMP で MediaLive に映像を入力し、CloudFront 経由で HLS のストリームを配信する構成になります。
構成図にはありませんが、MediaLive にアタッチする為の IAM ロールも作成します。
MediaLive の設定概要は以下です。
- H.264, AAC
- 720p
- 1Mbps
- 29.97fps
作成したテンプレート
早速ですが、作成したテンプレートは下記になります。そこそこ長いです。
template.json
{ "AWSTemplateFormatVersion": "2010-09-09", "Parameters": { "StreamKeyA": { "Description": "StreamKey of Stream A", "Type": "String", "Default": "streamA" }, "StreamKeyB": { "Description": "StreamKey of Stream B", "Type": "String", "Default": "streamB" }, "AllowedMediaLiveInputCidr": { "Description": "Allowed CIDR of MediaLive Input", "Type": "String", "Default": "0.0.0.0/0" }, "SegmentLength": { "Description": "Segment Length", "Type": "Number", "Default": "6" } }, "Resources": { "MediaLiveInput": { "Type": "AWS::MediaLive::Input", "Properties": { "Destinations": [ { "StreamName": { "Fn::Sub" : "${AWS::StackName}/${StreamKeyA}" } }, { "StreamName": { "Fn::Sub" : "${AWS::StackName}/${StreamKeyB}" } } ], "InputSecurityGroups": [ { "Ref" : "MediaLiveInputSecurityGroup" } ], "Name": { "Fn::Sub" : "${AWS::StackName}-input" }, "Tags": { "Name": { "Fn::Sub" : "${AWS::StackName}-input" } }, "Type": "RTMP_PUSH" } }, "MediaLiveInputSecurityGroup": { "Type": "AWS::MediaLive::InputSecurityGroup", "Properties": { "Tags": { "Name": { "Fn::Sub" : "${AWS::StackName}-inputsg" } }, "WhitelistRules": [ { "Cidr": { "Ref" : "AllowedMediaLiveInputCidr" } } ] } }, "MediaLiveChannel": { "Type": "AWS::MediaLive::Channel", "Properties": { "ChannelClass": "STANDARD", "Destinations": [ { "Id": {"Ref" : "MediaPackageChannel"}, "MediaPackageSettings": [ { "ChannelId": {"Ref" : "MediaPackageChannel"} } ] } ], "EncoderSettings": { "AudioDescriptions": [ { "AudioSelectorName": "default", "CodecSettings": { "AacSettings": { "Bitrate": 96000, "RawFormat": "NONE", "Spec": "MPEG4" } }, "AudioTypeControl": "FOLLOW_INPUT", "LanguageCodeControl": "FOLLOW_INPUT", "Name": "audio_3_aac96" } ], "OutputGroups": [ { "OutputGroupSettings": { "MediaPackageGroupSettings": { "Destination": { "DestinationRefId": {"Ref" : "MediaPackageChannel"}} } }, "Outputs": [ { "OutputSettings": { "MediaPackageOutputSettings": {} }, "OutputName": "1280_720_1", "VideoDescriptionName": "video_1280_720_1", "AudioDescriptionNames": [ "audio_3_aac96" ] } ] } ], "TimecodeConfig": { "Source": "SYSTEMCLOCK" }, "VideoDescriptions": [ { "CodecSettings": { "H264Settings": { "AfdSignaling": "NONE", "ColorMetadata": "INSERT", "AdaptiveQuantization": "HIGH", "Bitrate": 1000000, "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": "SPECIFIED", "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": { "Fn::Sub" : "${AWS::StackName}-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": { "Fn::Sub" : "${AWS::StackName}-Channel" }, "RoleArn": { "Fn::GetAtt" : [ "MediaLiveAccessRole", "Arn" ] }, "Tags": { "Name": { "Fn::Sub" : "${AWS::StackName}-Channel" } } } }, "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": "Sample-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": { "Fn::Sub" : "${AWS::StackName}-MediaLiveAccessRole" } } }, "MediaPackageChannel": { "Type": "AWS::MediaPackage::Channel", "Properties": { "Description": "Deploy from CloudFormation", "Id": { "Fn::Sub" : "${AWS::StackName}" }, "Tags": [ { "Key": "Name", "Value": { "Fn::Sub" : "${AWS::StackName}" } } ] } }, "MediaPackageEndpoint1": { "Type": "AWS::MediaPackage::OriginEndpoint", "Properties": { "ChannelId": {"Ref" : "MediaPackageChannel"}, "Description": "Endpoint 1 for HLS packaging", "HlsPackage": { "PlaylistWindowSeconds": 60, "SegmentDurationSeconds": { "Ref" : "SegmentLength" } }, "Id": { "Fn::Sub" : "${AWS::StackName}-Endpoint1" }, "ManifestName": "index", "Origination": "ALLOW", "Tags": [ { "Key": "Name", "Value": { "Fn::Sub" : "${AWS::StackName}-Endpoint1" } } ] } }, "CloudFrontDistribution": { "Type": "AWS::CloudFront::Distribution", "Properties" : { "DistributionConfig" : { "CacheBehaviors" : [ { "AllowedMethods" : [ "GET", "HEAD", "OPTIONS" ], "CachePolicyId" : { "Ref" : "CloudFrontCachePolicy"}, "OriginRequestPolicyId" : "59781a5b-3903-41f3-afcb-af62929ccde1", "PathPattern" : "*.m3u8", "TargetOriginId" : { "Fn::Sub" : "${AWS::StackName}-originId" }, "ViewerProtocolPolicy" : "redirect-to-https" }, { "AllowedMethods" : [ "GET", "HEAD", "OPTIONS" ], "CachePolicyId" : { "Ref" : "CloudFrontCachePolicy"}, "OriginRequestPolicyId" : "59781a5b-3903-41f3-afcb-af62929ccde1", "PathPattern" : "*.ts", "TargetOriginId" : { "Fn::Sub" : "${AWS::StackName}-originId" }, "ViewerProtocolPolicy" : "redirect-to-https" } ], "Comment" : { "Fn::Sub" : "${AWS::StackName}-cf" }, "DefaultCacheBehavior" : { "AllowedMethods" : [ "GET", "HEAD", "OPTIONS" ], "CachePolicyId" : { "Ref" : "CloudFrontCachePolicy"}, "OriginRequestPolicyId" : "59781a5b-3903-41f3-afcb-af62929ccde1", "TargetOriginId" : { "Fn::Sub" : "${AWS::StackName}-originId" }, "ViewerProtocolPolicy" : "redirect-to-https" }, "Enabled" : true, "Origins" : [ { "CustomOriginConfig" : { "HTTPPort" : 80, "HTTPSPort" : 443, "OriginProtocolPolicy" : "match-viewer" }, "DomainName" : { "Fn::Select" : [ 1, { "Fn::Split" : [ "//", { "Fn::Select" : [ 0, { "Fn::Split" : [ "/out", { "Fn::GetAtt" : [ "MediaPackageEndpoint1", "Url" ] } ] } ] } ] } ] }, "Id" : { "Fn::Sub" : "${AWS::StackName}-originId" } } ] }, "Tags" : [ { "Key": "Name", "Value": { "Fn::Sub" : "${AWS::StackName}-cf" } } ] } }, "CloudFrontCachePolicy": { "Type" : "AWS::CloudFront::CachePolicy", "Properties" : { "CachePolicyConfig" : { "DefaultTTL" : { "Ref" : "SegmentLength" }, "MaxTTL" : { "Ref" : "SegmentLength" }, "MinTTL" : { "Ref" : "SegmentLength" }, "Name" : { "Fn::Sub" : "${AWS::StackName}-cachepolicy" }, "ParametersInCacheKeyAndForwardedToOrigin" : { "EnableAcceptEncodingGzip" : true, "HeadersConfig" : { "HeaderBehavior" : "whitelist", "Headers" : [ "origin" ] }, "CookiesConfig" : { "CookieBehavior" : "none" }, "QueryStringsConfig" : { "QueryStringBehavior" : "none" } } } } } }, "Outputs": { "RtmpEndpointA": { "Description": "RTMP EndPoint of Stream A", "Value": { "Fn::Select" : [ 0, { "Fn::Split" : [ ",", { "Fn::Join" : [ ",", { "Fn::GetAtt" : [ "MediaLiveInput", "Destinations" ] } ] } ] } ] } }, "RtmpEndpointB": { "Description": "RTMP EndPoint of Stream B", "Value": { "Fn::Select" : [ 1, { "Fn::Split" : [ ",", { "Fn::Join" : [ ",", { "Fn::GetAtt" : [ "MediaLiveInput", "Destinations" ] } ] } ] } ] } }, "HlsEndpoint": { "Description": "HLS Endpoint", "Value": { "Fn::Join" : [ "/", [ "https:/", { "Fn::GetAtt" : [ "CloudFrontDistribution", "DomainName" ] }, "out/v1", { "Fn::Select" : [ 1, { "Fn::Split" : [ "/out/v1/", { "Fn::GetAtt" : [ "MediaPackageEndpoint1", "Url" ] } ] } ] }] ] } } } }
作成にあたって苦戦した部分など
テンプレートの中身全てを細かく説明すると大変なので、テンプレート作成中にハマった部分などのみピックアップしたいと思います。
MediaLive Channel - 出力設定
MediaLive の出力タイプとして MediaPackage を指定する形になるのですが、CloudFormation での記法が少し独特で苦戦しました。
"Outputs": [ { "OutputSettings": { "MediaPackageOutputSettings": {} }, "OutputName": "1280_720_1", "VideoDescriptionName": "video_1280_720_1", "AudioDescriptionNames": [ "audio_3_aac96" ] } ]
上記記載の通り、空の MediaPackageOutputSettings 要素を指定する必要があります。
AWS::MediaLive::Channel MediaPackageOutputSettings
これを yaml で記載しようするとすると CloudFormation のテンプレートアップロード時にエラーが発生してしまう為、現状は json 形式でしかテンプレートを作成出来ない形になっていると思われます。(yaml で作成しはじめたので json への置換が大変だった。。。)
CloudFront Distribution - オリジンドメイン設定
作成する CloudFront Distribution のオリジンとして MediaPackage のエンドポイントドメインを指定する必要があるのですが、AWS::MediaPackage::OriginEndpoint からはエンドポイントの URL しか取得出来ない為、組み込み関数を駆使してドメインを抽出しています。
AWS::MediaPackage::OriginEndpoint
"Origins" : [ { "CustomOriginConfig" : { "HTTPPort" : 80, "HTTPSPort" : 443, "OriginProtocolPolicy" : "match-viewer" }, "DomainName" : { "Fn::Select" : [ 1, { "Fn::Split" : [ "//", { "Fn::Select" : [ 0, { "Fn::Split" : [ "/out", { "Fn::GetAtt" : [ "MediaPackageEndpoint1", "Url" ] } ] } ] } ] } ] }, "Id" : { "Fn::Sub" : "${AWS::StackName}-originId" } } ]
CloudFront - キャッシュポリシー
少し前のアップデートで、CloudFront のキャッシュ動作などはキャッシュポリシーを別途作成し、アタッチする事が推奨されています。
AWS が用意しているキャッシュポリシーとして Managed-Elemental-MediaPackage というものがあるのですが、キャッシュ TTL をクライアント側からリクエストする必要がある設定となっている為、キャッシュ TTL をセグメント長に合わせたポリシーを別途作成し、アタッチする様にしています。
"CloudFrontCachePolicy": { "Type" : "AWS::CloudFront::CachePolicy", "Properties" : { "CachePolicyConfig" : { "DefaultTTL" : { "Ref" : "SegmentLength" }, "MaxTTL" : { "Ref" : "SegmentLength" }, "MinTTL" : { "Ref" : "SegmentLength" }, "Name" : { "Fn::Sub" : "${AWS::StackName}-cachepolicy" }, "ParametersInCacheKeyAndForwardedToOrigin" : { "EnableAcceptEncodingGzip" : true, "HeadersConfig" : { "HeaderBehavior" : "whitelist", "Headers" : [ "origin" ] }, "CookiesConfig" : { "CookieBehavior" : "none" }, "QueryStringsConfig" : { "QueryStringBehavior" : "none" } } } } }
スタックの出力設定
CloudFormation のスタック出力に RTMP のエンドポイントと HLS のエンドポイントを表示したかった為、Outputs を記載しています。
こちらも、各リソースから単純に取得できる値ではなかった為、組み込み関数を駆使して出力を整形しています。
"Outputs": { "RtmpEndpointA": { "Description": "RTMP EndPoint of Stream A", "Value": { "Fn::Select" : [ 0, { "Fn::Split" : [ ",", { "Fn::Join" : [ ",", { "Fn::GetAtt" : [ "MediaLiveInput", "Destinations" ] } ] } ] } ] } }, "RtmpEndpointB": { "Description": "RTMP EndPoint of Stream B", "Value": { "Fn::Select" : [ 1, { "Fn::Split" : [ ",", { "Fn::Join" : [ ",", { "Fn::GetAtt" : [ "MediaLiveInput", "Destinations" ] } ] } ] } ] } }, "HlsEndpoint": { "Description": "HLS Endpoint", "Value": { "Fn::Join" : [ "/", [ "https:/", { "Fn::GetAtt" : [ "CloudFrontDistribution", "DomainName" ] }, "out/v1", { "Fn::Select" : [ 1, { "Fn::Split" : [ "/out/v1/", { "Fn::GetAtt" : [ "MediaPackageEndpoint1", "Url" ] } ] } ] }] ] } } }
あまり綺麗な書き方ではありませんが、ちゃんと下記の様に出力されます。
出力された RTMP エンドポイントに配信を行い、HLS エンドポイントを指定して再生を行えば良い形になっているので、使いやすいはずです。
おわりに
CloudFormation を使用して MediaLive + MediaPackage + CloudFront を一括で作成するテンプレートを作成してみました。
思っていた以上に作成に手間取りましたが、無事動くものが作成できて良かったです。
以上、AWS 事業本部の大前でした。