AWS CloudFormationで動画配信環境を構築した！

# 概要
AWS CloudFormationを用いて簡単なHLS形式の動画配信環境を構築します。構成は次の通りです。
![live-streaming-demo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429912/afde3724-d9af-f6f1-3ea9-fceff06e8c8b.png)

# 注意事項
- AWS CloudFormationの使い方に慣れていて、AWS Media Servicesについてもある程度知見をお持ちの方を対象にしていますが、初めての方もチャレンジしてみて下さい！
- PoCのため、各サービスのパラメーターはライブ配信に最適なチューニングはされず、ほぼデフォルトの値になっております。
- AWS CloudFormation テンプレートはYAML形式で記述しています。
- Tokyoリージョン (ap-northeast-1)で構築しています。
- 料金が発生しますので検証が終わりましたらCloudFormationスタックの削除を忘れないで下さい。

# 利用するAWSサービス
- [AWS CloudFormation](https://aws.amazon.com/jp/cloudformation/) (以下 CloudFormation)
- [AWS Elemental MediaLive](https://aws.amazon.com/jp/medialive/) (以下 MediaLive)
- [AWS Elemental MediaStore](https://aws.amazon.com/jp/mediastore/) (以下 MediaStore)
- [Amazon CloudFront](https://aws.amazon.com/jp/cloudfront/) (以下 CloudFront)

# 手順
## CloudFormation
今回は下記セッションに分けて記述していきます。リソースセッションに関しては必須なのでテンプレートに含める必要があります。
* 形式バージョン
* Description
* リソース
* 出力

他に記述可能なセッションについては下記をご参照下さい。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-anatomy.html

一気に全てのサービスをテンプレートファイルに書くよりも、まずはCloudFormationのテンプレートに追加する各サービスを分けて設定を見ていきます。

## 1. MediaStore

:::note warn
MediaLiveのDestinationにMediaStoreのEndpointを指定する必要があるため、先にMediaStoreのリソースを作成します。
:::
- MediaStoreで設定するリソース
  - Container
#### Container
■YAML構文

```yaml
Type: AWS::MediaStore::Container
Properties: 
  AccessLoggingEnabled: Boolean
  ContainerName: String
  CorsPolicy: 
    - CorsRule
  LifecyclePolicy: String
  MetricPolicy: 
    MetricPolicy
  Policy: String
  Tags: 
    - Tag
```

■設定例
```yaml
  MediaStoreContainer:
    Type: AWS::MediaStore::Container
    Properties:
      AccessLoggingEnabled: false
      ContainerName: !Ref AWS::StackName
      CorsPolicy:
        - AllowedOrigins:
            - '*'
          AllowedMethods:
            - GET
            - HEAD
          AllowedHeaders:
            - '*'
          MaxAgeSeconds: 3000
      LifecyclePolicy:
        "{\n  \"rules\": [\n    {\n      \"definition\": {\n        \"path\": [\n          {\n            \"prefix\": \"\"\n          }\n        ],\n        \"days_since_create\": [\n          {\n            \"numeric\": [\n              \">\",\n              1\n            ]\n          }\n        ]\n      },\n      \"action\": \"EXPIRE\"\n    }\n  ]\n}"
      MetricPolicy:
        ContainerLevelMetrics: DISABLED
      Policy:
        !Sub "{\n  \"Version\" : \"2012-10-17\",\n  \"Statement\" : [{\n    \"Sid\" : \"PublicReadOverHttps\",\n    \"Effect\" : \"Allow\",\n    \"Principal\" : \"*\",\n    \"Action\" : [\n        \"mediastore:GetObject\",\n        \"mediastore:DescribeObject\"\n    ],\n    \"Resource\" : \"arn:aws:mediastore:${AWS::Region}:${AWS::AccountId}:container/${AWS::StackName}/*\",\n    \"Condition\" : {\"Bool\" : {\n        \"aws:SecureTransport\" : \"true\"\n      }\n    }\n  }]\n}\n"
      Tags:
        - Key: "environment"
          Value: "demo"
```

## 2.MediaLive
- MediaLiveで設定するリソース
  - IAM role
  - Input security group
  - Input
  - Channel

#### IAM role
■YAML構文
```yaml
Type: AWS::IAM::Role
Properties: 
  AssumeRolePolicyDocument: Json
  Description: String
  ManagedPolicyArns: 
    - String
  MaxSessionDuration: Integer
  Path: String
  PermissionsBoundary: String
  Policies: 
    - Policy
  RoleName: String
  Tags: 
    - Tag
```
■設定例

:::note warn
検証のため、PolicyはMediaStoreとCloudWatch Logsにのみ限定しています。
:::

```yaml
  MediaLiveRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join ['-', [ Ref: "AWS::StackName", "MediaLive-role", !Select [4, !Split ['-', !Select [2, !Split ['/', Ref: "AWS::StackId"]]]]]]
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          -
            Effect: Allow
            Principal:
              Service:
                - "medialive.amazonaws.com"
            Action:
              - sts:AssumeRole
      Policies:
        -
          PolicyName: !Sub "${AWS::StackName}-medialive-policy"
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - mediastore:ListContainers
                  - mediastore:PutObject
                  - mediastore:GetObject
                  - mediastore:DeleteObject
                  - mediastore:DescribeObject
                Resource: !Join ["", ["arn:aws:mediastore:", Ref: "AWS::Region", ":", Ref: "AWS::AccountId", ":*"]]
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - logs:DescribeLogStreams
                  - logs:DescribeLogGroups
                Resource: arn:aws:logs:*:*:*
      Tags:
        - Key: "environment"
          Value: "demo"
```
#### Input security group
■YAML構文
```yaml
Type: AWS::MediaLive::InputSecurityGroup
Properties: 
  Tags: Json
  WhitelistRules: 
    - InputWhitelistRuleCidr
```
■設定例
```yaml
  MediaLiveInputSecurityGroup:
    Type: AWS::MediaLive::InputSecurityGroup
    Properties:
      WhitelistRules:
        - Cidr: 0.0.0.0/0
```
#### Input
■YAML構文
```yaml
Type: AWS::MediaLive::Input
Properties: 
  Destinations: 
    - InputDestinationRequest
  InputDevices: 
    - InputDeviceSettings
  InputSecurityGroups: 
    - String
  MediaConnectFlows: 
    - MediaConnectFlowRequest
  Name: String
  RoleArn: String
  Sources: 
    - InputSourceRequest
  Tags: Json
  Type: String
  Vpc: 
    InputVpcRequest
```
■設定例
```yaml
  MediaLiveInput:
    Type: AWS::MediaLive::Input
    Properties:
      Destinations:
        - StreamName: live/stream
      InputSecurityGroups:
        - !Ref MediaLiveInputSecurityGroup
      Name: !Join ["-", [ Ref: "AWS::StackName", "ml-input"]]
      Tags:
        environment: demo
      Type: RTMP_PUSH
```
#### Channel
■YAML構文
```yaml
Type: AWS::MediaLive::Channel
Properties: 
  CdiInputSpecification: 
    CdiInputSpecification
  ChannelClass: String
  Destinations: 
    - OutputDestination
  EncoderSettings: 
    EncoderSettings
  InputAttachments: 
    - InputAttachment
  InputSpecification: 
    InputSpecification
  LogLevel: String
  Name: String
  RoleArn: String
  Tags: Json
  Vpc: 
    VpcOutputSettings
```
■設定例

:::note info
MediaLive Channelの設定プロパティーはとても豊富なので慣れてきたら色々チューニングするのもいいかもしれません！
:::
調整するパラメータ値

- MediaLiveの一般設定
  - Channel class : SINGLE_PIPELINE
  - Maximum input bitrate : MAX_10_MBPS
  - Log level : Error
- Video
  - 解像度 : 1280x720
  - Codec Settings : H264
  - Rate Control Mode : QVBR
  - Bitrate : 2000000 bps
  - Max Bitrate : 2000000 bps
  - Framerate : 30 fps
  - ABR : なし
- Audio
  - Codec Settings : AAC
  - Bitrate : 128000 bps

```yaml
  MediaLiveChannel:
    Type: AWS::MediaLive::Channel
    Properties:
      ChannelClass: "SINGLE_PIPELINE"
      Destinations:
        - Id: msdest
          MediaPackageSettings: []
          Settings:
            - Url: !Join [ "",  [ "mediastoressl://", !Select [1, !Split [ "://", !GetAtt MediaStoreContainer.Endpoint ] ], "/live" ] ]
      EncoderSettings:
        AudioDescriptions:
          - AudioSelectorName: Audio1
            AudioTypeControl: FOLLOW_INPUT
            CodecSettings:
              AacSettings:
                Bitrate: 128000
                CodingMode: CODING_MODE_2_0
                InputType: NORMAL
                Profile: LC
                RateControlMode: CBR
                RawFormat: NONE
                SampleRate: 48000
                Spec: MPEG4
            LanguageCodeControl: FOLLOW_INPUT
            Name: audio_720p128k
        CaptionDescriptions: []
        OutputGroups:
          - Name: MediaStore Group
            OutputGroupSettings:
              HlsGroupSettings:
                AdMarkers: []
                CaptionLanguageMappings: []
                CaptionLanguageSetting: OMIT
                ClientCache: ENABLED
                CodecSpecification: RFC_4281
                Destination:
                  DestinationRefId: msdest
                DirectoryStructure: SINGLE_DIRECTORY
                DiscontinuityTags: INSERT
                HlsCdnSettings:
                  HlsMediaStoreSettings:
                    ConnectionRetryInterval: 1
                    FilecacheDuration: 300
                    MediaStoreStorageClass: TEMPORAL
                    NumRetries: 10
                    RestartDelay: 15
                HlsId3SegmentTagging: DISABLED
                IFrameOnlyPlaylists: DISABLED
                IncompleteSegmentBehavior: AUTO
                IndexNSegments: 10
                InputLossAction: EMIT_OUTPUT
                IvInManifest: INCLUDE
                IvSource: FOLLOWS_SEGMENT_NUMBER
                KeepSegments: 21
                ManifestCompression: NONE
                ManifestDurationFormat: FLOATING_POINT
                Mode: LIVE
                OutputSelection: MANIFESTS_AND_SEGMENTS
                ProgramDateTime: EXCLUDE
                ProgramDateTimePeriod: 600
                RedundantManifest: DISABLED
                SegmentLength: 10
                SegmentationMode: USE_SEGMENT_DURATION
                SegmentsPerSubdirectory: 10000
                StreamInfResolution: INCLUDE
                TimedMetadataId3Frame: PRIV
                TimedMetadataId3Period: 10
                TsFileMode: SEGMENTED_FILES
            Outputs:
              - AudioDescriptionNames:
                  - audio_720p128k
                CaptionDescriptionNames: []
                OutputName: 720p2m
                OutputSettings:
                  HlsOutputSettings:
                    H265PackagingType: HVC1
                    HlsSettings:
                      StandardHlsSettings:
                        AudioRenditionSets: program_audio
                        M3u8Settings:
                          AudioFramesPerPes: 4
                          AudioPids: 492-498
                          NielsenId3Behavior: NO_PASSTHROUGH
                          PcrControl: PCR_EVERY_PES_PACKET
                          PmtPid: '480'
                          ProgramNum: 1
                          Scte35Behavior: NO_PASSTHROUGH
                          Scte35Pid: '500'
                          TimedMetadataBehavior: NO_PASSTHROUGH
                          TimedMetadataPid: '502'
                          VideoPid: '481'
                    NameModifier: "_2m"
                VideoDescriptionName: video_720p2m
        TimecodeConfig:
          Source: EMBEDDED
        VideoDescriptions:
          - CodecSettings:
              H264Settings:
                AdaptiveQuantization: AUTO
                AfdSignaling: NONE
                Bitrate: 2000000
                ColorMetadata: INSERT
                EntropyEncoding: CABAC
                FlickerAq: ENABLED
                ForceFieldPictures: DISABLED
                FramerateControl: SPECIFIED
                FramerateDenominator: 996
                FramerateNumerator: 30000
                GopBReference: DISABLED
                GopClosedCadence: 1
                GopSize: 90
                GopSizeUnits: FRAMES
                Level: H264_LEVEL_AUTO
                LookAheadRateControl: MEDIUM
                MaxBitrate: 2000000
                NumRefFrames: 1
                ParControl: INITIALIZE_FROM_SOURCE
                Profile: MAIN
                RateControlMode: QVBR
                ScanType: PROGRESSIVE
                SceneChangeDetect: ENABLED
                SpatialAq: ENABLED
                SubgopLength: FIXED
                Syntax: DEFAULT
                TemporalAq: ENABLED
                TimecodeInsertion: DISABLED
            Height: 720
            Name: video_720p2m
            RespondToAfd: NONE
            ScalingBehavior: DEFAULT
            Sharpness: 100
            Width: 1280
      InputAttachments:
        - InputAttachmentName: !Join ["-", [ Ref: "AWS::StackName", "ml-input"]]
          InputId: !Ref MediaLiveInput
          InputSettings:
            AudioSelectors:
              - Name: Audio1
            CaptionSelectors: []
            DeblockFilter: DISABLED
            DenoiseFilter: DISABLED
            FilterStrength: 1
            InputFilter: AUTO
            Smpte2038DataPreference: IGNORE
            SourceEndBehavior: CONTINUE
      InputSpecification:
        Codec: AVC
        MaximumBitrate: MAX_10_MBPS
        Resolution: HD
      LogLevel: ERROR
      Name: !Join ["-", [ Ref: "AWS::StackName", "ml-ch"]]
      RoleArn: !GetAtt MediaLiveRole.Arn
      Tags:
        environment: demo
```
#### 4.CloudFront
- CloudFrontで設定するリソース
  - Distribution

調整するパラメータ値
- Protocol : HTTPS only
- Viewer protocol policy : Redirect HTTP to HTTPS
- Allowed HTTP methods : GET, HEAD, OPTIONS
- Headers : Access-Control-Request-Method, Access-Control-Request-Headers, Origin
- Object caching : Minimum TTL, Maximum TTL, Default TTL = 1s


■YAML構文
```yaml
{
  "Type" : "AWS::CloudFront::Distribution",
  "Properties" : {
      "DistributionConfig" : DistributionConfig,
      "Tags" : [ Tag, ... ]
    }
}
```
■設定例

:::note info
ここでは設定を省きますが、m3u8ファイルとtsファイルをそれぞれ適切なTTLにすることもできます。
:::
```yaml
  CloudFront:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
          - Id: !Join ["-", [ Ref: "AWS::StackName", "ms-ctn"]]
            DomainName: !Sub
             - ${ENDPOINT}
             - { ENDPOINT: !Select [1, !Split [ "://", !GetAtt MediaStoreContainer.Endpoint ]]}
            CustomOriginConfig:
              OriginProtocolPolicy: https-only
        Enabled: true
        Comment: "CloudFront for demo livestreaming"
        Logging:
          IncludeCookies: false
          Bucket: ''
          Prefix: ''
        DefaultCacheBehavior:
          AllowedMethods:
          - GET
          - HEAD
          - OPTIONS
          Compress: true
          MaxTTL: 1
          MinTTL: 1
          ViewerProtocolPolicy: "redirect-to-https"
          DefaultTTL: 1
          TargetOriginId: !Join ["-", [ Ref: "AWS::StackName", "ms-ctn"]]
          ForwardedValues:
            QueryString: false
            Cookies:
              Forward: none
            Headers:
              - Origin
              - Access-Control-Request-Method
              - Access-Control-Request-Headers
        PriceClass: PriceClass_All
        ViewerCertificate:
          CloudFrontDefaultCertificate: true
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W70
            reason: "CloudFront automatically sets the security policy to TLSv1 when the distribution uses the CloudFront domain name (CloudFrontDefaultCertificate=true)"
```
## 最終形態
先程設定した各サービスのリソースセッションをCloudFormationテンプレートに落とし込みます。記述が長いので設定例をクリックしてご確認下さい。

<details><summary>設定例</summary>

```yaml
Description: "Cloudformation for MediaServices - MediaLive, MediaStore, CloudFront"
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  MediaStoreContainer:
    Type: AWS::MediaStore::Container
    Properties:
      AccessLoggingEnabled: false
      ContainerName: !Ref AWS::StackName
      CorsPolicy:
        - AllowedOrigins:
            - '*'
          AllowedMethods:
            - GET
            - HEAD
          AllowedHeaders:
            - '*'
          MaxAgeSeconds: 3000
      LifecyclePolicy:
        "{\n  \"rules\": [\n    {\n      \"definition\": {\n        \"path\": [\n          {\n            \"prefix\": \"\"\n          }\n        ],\n        \"days_since_create\": [\n          {\n            \"numeric\": [\n              \">\",\n              1\n            ]\n          }\n        ]\n      },\n      \"action\": \"EXPIRE\"\n    }\n  ]\n}"
      MetricPolicy:
        ContainerLevelMetrics: DISABLED
      Policy:
        !Sub "{\n  \"Version\" : \"2012-10-17\",\n  \"Statement\" : [{\n    \"Sid\" : \"PublicReadOverHttps\",\n    \"Effect\" : \"Allow\",\n    \"Principal\" : \"*\",\n    \"Action\" : [\n        \"mediastore:GetObject\",\n        \"mediastore:DescribeObject\"\n    ],\n    \"Resource\" : \"arn:aws:mediastore:${AWS::Region}:${AWS::AccountId}:container/${AWS::StackName}/*\",\n    \"Condition\" : {\"Bool\" : {\n        \"aws:SecureTransport\" : \"true\"\n      }\n    }\n  }]\n}\n"
      Tags:
        - Key: "environment"
          Value: "demo"

  MediaLiveRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join ['-', [ Ref: "AWS::StackName", "MediaLive-role", !Select [4, !Split ['-', !Select [2, !Split ['/', Ref: "AWS::StackId"]]]]]]
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          -
            Effect: Allow
            Principal:
              Service:
                - "medialive.amazonaws.com"
            Action:
              - sts:AssumeRole
      Policies:
        -
          PolicyName: !Sub "${AWS::StackName}-medialive-policy"
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - mediastore:ListContainers
                  - mediastore:PutObject
                  - mediastore:GetObject
                  - mediastore:DeleteObject
                  - mediastore:DescribeObject
                Resource: !Join ["", ["arn:aws:mediastore:", Ref: "AWS::Region", ":", Ref: "AWS::AccountId", ":*"]]
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - logs:DescribeLogStreams
                  - logs:DescribeLogGroups
                Resource: arn:aws:logs:*:*:*
      Tags:
        - Key: "environment"
          Value: "demo"

  MediaLiveInputSecurityGroup:
    Type: AWS::MediaLive::InputSecurityGroup
    Properties:
      WhitelistRules:
        - Cidr: 0.0.0.0/0

  MediaLiveInput:
    Type: AWS::MediaLive::Input
    Properties:
      Destinations:
        - StreamName: live/stream
      InputSecurityGroups:
        - !Ref MediaLiveInputSecurityGroup
      Name: !Join ["-", [ Ref: "AWS::StackName", "ml-input"]]
      Tags:
        environment: demo
      Type: RTMP_PUSH

  MediaLiveChannel:
    Type: AWS::MediaLive::Channel
    Properties:
      ChannelClass: "SINGLE_PIPELINE"
      Destinations:
        - Id: msdest
          MediaPackageSettings: []
          Settings:
            - Url: !Join [ "",  [ "mediastoressl://", !Select [1, !Split [ "://", !GetAtt MediaStoreContainer.Endpoint ] ], "/live" ] ]
      EncoderSettings:
        AudioDescriptions:
          - AudioSelectorName: Audio1
            AudioTypeControl: FOLLOW_INPUT
            CodecSettings:
              AacSettings:
                Bitrate: 128000
                CodingMode: CODING_MODE_2_0
                InputType: NORMAL
                Profile: LC
                RateControlMode: CBR
                RawFormat: NONE
                SampleRate: 48000
                Spec: MPEG4
            LanguageCodeControl: FOLLOW_INPUT
            Name: audio_720p128k
        CaptionDescriptions: []
        OutputGroups:
          - Name: MediaStore Group
            OutputGroupSettings:
              HlsGroupSettings:
                AdMarkers: []
                CaptionLanguageMappings: []
                CaptionLanguageSetting: OMIT
                ClientCache: ENABLED
                CodecSpecification: RFC_4281
                Destination:
                  DestinationRefId: msdest
                DirectoryStructure: SINGLE_DIRECTORY
                DiscontinuityTags: INSERT
                HlsCdnSettings:
                  HlsMediaStoreSettings:
                    ConnectionRetryInterval: 1
                    FilecacheDuration: 300
                    MediaStoreStorageClass: TEMPORAL
                    NumRetries: 10
                    RestartDelay: 15
                HlsId3SegmentTagging: DISABLED
                IFrameOnlyPlaylists: DISABLED
                IncompleteSegmentBehavior: AUTO
                IndexNSegments: 10
                InputLossAction: EMIT_OUTPUT
                IvInManifest: INCLUDE
                IvSource: FOLLOWS_SEGMENT_NUMBER
                KeepSegments: 21
                ManifestCompression: NONE
                ManifestDurationFormat: FLOATING_POINT
                Mode: LIVE
                OutputSelection: MANIFESTS_AND_SEGMENTS
                ProgramDateTime: EXCLUDE
                ProgramDateTimePeriod: 600
                RedundantManifest: DISABLED
                SegmentLength: 10
                SegmentationMode: USE_SEGMENT_DURATION
                SegmentsPerSubdirectory: 10000
                StreamInfResolution: INCLUDE
                TimedMetadataId3Frame: PRIV
                TimedMetadataId3Period: 10
                TsFileMode: SEGMENTED_FILES
            Outputs:
              - AudioDescriptionNames:
                  - audio_720p128k
                CaptionDescriptionNames: []
                OutputName: 720p2m
                OutputSettings:
                  HlsOutputSettings:
                    H265PackagingType: HVC1
                    HlsSettings:
                      StandardHlsSettings:
                        AudioRenditionSets: program_audio
                        M3u8Settings:
                          AudioFramesPerPes: 4
                          AudioPids: 492-498
                          NielsenId3Behavior: NO_PASSTHROUGH
                          PcrControl: PCR_EVERY_PES_PACKET
                          PmtPid: '480'
                          ProgramNum: 1
                          Scte35Behavior: NO_PASSTHROUGH
                          Scte35Pid: '500'
                          TimedMetadataBehavior: NO_PASSTHROUGH
                          TimedMetadataPid: '502'
                          VideoPid: '481'
                    NameModifier: "_2m"
                VideoDescriptionName: video_720p2m
        TimecodeConfig:
          Source: EMBEDDED
        VideoDescriptions:
          - CodecSettings:
              H264Settings:
                AdaptiveQuantization: AUTO
                AfdSignaling: NONE
                Bitrate: 2000000
                ColorMetadata: INSERT
                EntropyEncoding: CABAC
                FlickerAq: ENABLED
                ForceFieldPictures: DISABLED
                FramerateControl: SPECIFIED
                FramerateDenominator: 996
                FramerateNumerator: 30000
                GopBReference: DISABLED
                GopClosedCadence: 1
                GopSize: 90
                GopSizeUnits: FRAMES
                Level: H264_LEVEL_AUTO
                LookAheadRateControl: MEDIUM
                MaxBitrate: 2000000
                NumRefFrames: 1
                ParControl: INITIALIZE_FROM_SOURCE
                Profile: MAIN
                RateControlMode: QVBR
                ScanType: PROGRESSIVE
                SceneChangeDetect: ENABLED
                SpatialAq: ENABLED
                SubgopLength: FIXED
                Syntax: DEFAULT
                TemporalAq: ENABLED
                TimecodeInsertion: DISABLED
            Height: 720
            Name: video_720p2m
            RespondToAfd: NONE
            ScalingBehavior: DEFAULT
            Sharpness: 100
            Width: 1280
      InputAttachments:
        - InputAttachmentName: !Join ["-", [ Ref: "AWS::StackName", "ml-input"]]
          InputId: !Ref MediaLiveInput
          InputSettings:
            AudioSelectors:
              - Name: Audio1
            CaptionSelectors: []
            DeblockFilter: DISABLED
            DenoiseFilter: DISABLED
            FilterStrength: 1
            InputFilter: AUTO
            Smpte2038DataPreference: IGNORE
            SourceEndBehavior: CONTINUE
      InputSpecification:
        Codec: AVC
        MaximumBitrate: MAX_10_MBPS
        Resolution: HD
      LogLevel: ERROR
      Name: !Join ["-", [ Ref: "AWS::StackName", "ml-ch"]]
      RoleArn: !GetAtt MediaLiveRole.Arn
      Tags:
        environment: demo

  CloudFront:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
          - Id: !Join ["-", [ Ref: "AWS::StackName", "ms-ctn"]]
            DomainName: !Sub
             - ${ENDPOINT}
             - { ENDPOINT: !Select [1, !Split [ "://", !GetAtt MediaStoreContainer.Endpoint ]]}
            CustomOriginConfig:
              OriginProtocolPolicy: https-only
        Enabled: true
        Comment: "CloudFront for demo livestreaming"
        Logging:
          IncludeCookies: false
          Bucket: ''
          Prefix: ''
        DefaultCacheBehavior:
          AllowedMethods:
          - GET
          - HEAD
          - OPTIONS
          Compress: true
          MaxTTL: 1
          MinTTL: 1
          ViewerProtocolPolicy: "redirect-to-https"
          DefaultTTL: 1
          TargetOriginId: !Join ["-", [ Ref: "AWS::StackName", "ms-ctn"]]
          ForwardedValues:
            QueryString: false
            Cookies:
              Forward: none
            Headers:
              - Origin
              - Access-Control-Request-Method
              - Access-Control-Request-Headers
        PriceClass: PriceClass_All
        ViewerCertificate:
          CloudFrontDefaultCertificate: true
    Metadata:
      cfn_nag:
        rules_to_suppress:
          - id: W70
            reason: "CloudFront automatically sets the security policy to TLSv1 when the distribution uses the CloudFront domain name (CloudFrontDefaultCertificate=true)"

Outputs:
  LiveCloudFront:
    Description: CloudFront Live Playback URL
    Value: !Sub "https://${CloudFront.DomainName}/live.m3u8"

  MediaLiveChannelConsole:
    Description: This is the MediaLive Console. Click the link, then start the new channel.
    Value: !Sub "https://console.aws.amazon.com/medialive/home?region=${AWS::Region}#!/channels:"

  MediaLiveRTMPDest:
    Description: RTMP Destination for MediaLive Input
    Value: !Select [ 0, !GetAtt MediaLiveInput.Destinations]

  MediaStoreLiveStreamUrl:
    Description: MediaStore Playback URL
    Value: !Sub "${MediaStoreContainer.Endpoint}/live.m3u8"
```
</details>


## スタック作成
1\. CloudFormationスタックを作成します。

:::note info
最初はChange setsで作成予定のリソースを確認した方がいいかもしれません。
:::
![スクリーンショット 2022-11-18 13.14.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429912/0f30d571-5c1b-b44b-0a33-82abef8b2f8e.png)

![スクリーンショット 2022-11-18 13.15.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429912/30cfcf02-c77f-4055-5956-687d2c7093c5.png)

![スクリーンショット 2022-11-18 13.15.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429912/dd1226fa-0c56-5a7c-e154-d64d4952d6f3.png)

2\. 作成成功。Resourceタブから作成済みのリソースを確認できました。
- Resourcesタブ
![スクリーンショット 2022-11-18 13.23.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429912/2e50488a-65de-5e70-8fde-53507968270b.png)

- Outputsタブ
CLoudFormationテンプレートにOutputセッションも追加してあったのでこちらも確認できました。配信と視聴情報が一目で分かるので楽です！
![スクリーンショット 2022-11-18 13.23.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429912/b6b3f53e-af9b-6b49-cda4-f7e869835cae.png)


## 配信と視聴テスト
1\. MediaLive Channelを起動します。

:::note info
OutputsのMediaLiveChannelConsoleから移動できます。
:::
![スクリーンショット 2022-11-18 14.10.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429912/3cba6575-060c-67bc-07fa-d19557e5a353.png)


2\. OBSやffmpegを使ってRTMP PushでMediaLive Inputの送信先にストリームを送ります。

:::note info
RTMPの送信先はOutputsのMediaLiveRTMPDestで確認できます。
:::

MP4ファイルでテスト配信をする場合のffmpegの実行例 :
```
ffmpeg -re -stream_loop -1 -i {INPUTFILE} -acodec copy -vcodec copy -f flv {MediaLiveRTMPDest}
```
3\. 配信が出てきていることを確認します。

MediaLiveコンソール -> Alertsでアラートが消えていればMediaLiveにストリームが無事入ってきていることを意味します。

■ストリームが入ってくる前
![スクリーンショット 2022-11-18 14.25.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429912/90197048-c578-7d26-e20a-03f35f6b6e19.png)

■ストリームが入ってきた後
![スクリーンショット 2022-11-18 14.25.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429912/540baa8a-616e-0c63-e80c-7dec9cca0d3d.png)


3\. CloudFrontの視聴URLでSafariブラウザーか[VLC Media Player](https://www.videolan.org/vlc/index.ja.html)から視聴します。

:::note info
視聴URLはOutputsのLiveCloudFrontから取得できます。
:::

:::note warn
HLS(m3u8)形式での視聴はSafariブラウザーやVLC Media Playerといったメディアプレイヤーが必要です。
:::

■Mac Safari
![スクリーンショット 2022-11-18 16.27.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429912/cb5f22fb-dd2c-dbbd-d49b-6176f139fd22.png)

■VLC Media Player
![スクリーンショット 2022-11-18 16.29.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/429912/c00860dc-e171-2492-8f16-71e0e37c321d.png)

## スタックの削除
不要な料金が発生するのを防ぐために、動作確認が終わったらスタックを削除することをお勧めします。

- 削除の流れとしては

1\. MediaLive Channelを停止
2\. MediaStore Containerのオブジェクトを削除
3\. CloudFormationのスタックを削除

:::note warn
Channelが起動中、もしくはContainerの中身が空ではない状態でスタックの削除を試みるとエラーが出ます。
:::
 # まとめ
上記CloudFormationを使ってライブ配信一連のリソースを作成してみました。DVR機能が必要な場合はMediaStoreの代わりにAWS Elemental MediaPackageを利用することも可能です。尚、今回は作成する各リソースは一つのみですが、強力なTransform セクションを使えば複数の同じリソースを同時に構築が出来ます。少しでも参考になれば幸いです。
# 参考
#### CloudFormation
- MediaStore
  - Container
    https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-mediastore-container.html 
- MediaLive
  - Input security group
    https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-medialive-inputsecuritygroup.html
  - Input
    https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-medialive-input.html
  - Channel
    https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-medialive-channel.html
- CloudFront
  - Distribution
    https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-cloudfront-distribution.html