고객이 업로드하는 동영상은 정말 다양한 파일크기, 다양한 확장자와 다양한 해상도와 다양한 여러 옵션으로 만들어진 결과물이었다.
우리의 목표는 품질을 최대한 떨어뜨리지 않으며, 영상을 서비스하는데 비용을 아낄 수 있도록 데이터트래픽 비용을 감소하는 것 이었다.
학교에서 배웠던 것을 기억해보면 동영상은 빠르게 화면을 변경해주고, 그 잔상효과로 움직이는 것처럼 느끼게 해주며, 그 위에? 뒤에? 소리가 함께 재생되도록 한 것이었다.
주니어 때 OPEN CV 를 이용해 사람얼굴에 대한 패턴을 영상에 적용해 내 뒤통수에도 눈이 달려 있는 것처럼, 그래서 사무실에서 내 모니터에 대한 보안을 위한 프로그램을 만들었을 때도, 빠르게 찍어내는 사진에 패턴이 적용되고 패턴이 적용된 곳에 사각프레임을 씌우고, BEEP 음을 내도록 했었다.
내 머릿속의 기억은 동영상 = 시간 정렬된 겁나 많은 사진들이었다.
기억은 여기까지였고, 목표를 이루기 위해서는 동영상에 대해 먼저 이해가 필요했다.
인/디코딩방식, 코덱의 종류, 해상도, 초당프레임수(FPS), BIT RATE 등 대충 아는 것과 모르는 것을 모두 뽑아 대충 조사하였다.
그래서 비디오 포멧은?
컨테이너 포멧형식에 따라 무엇이 화질이 좋은가? 라고 생각했던 건 초반의 멍청한 생각이었다. 그러면 우리는 웹에서 표현하기에 가장 적절하고 용이한 포멧을 골라 변환한다. 라는 생각을 했고, 그 중에 wepM 이라는 포멧형식을 선택했었다. (무려 구글의 후원을 받았… 다고 하니 )

WebM(웹엠)은 로열티 비용이 없는 개방형 고화질 영상 압축 형식의 영상 포맷이며 HTML5 비디오와 함께 이용한다. 2010년 5월 19일에 처음 나왔으며 이 프로젝트는 구글의 후원을 받아 개발된다.
WebM 파일은 VP8 비디오와 Vorbis 오디오 스트림으로 이루어져 있으며, 2013년에 VP9 비디오와 Opus 오디오를 수용하는 것으로 갱신되었다. WebM 컨테이너는 마트료시카 프로파일에 기반을 둔다. 이 프로젝트는 BSD 라이선스 하에서 WebM 관련 소프트웨어를 출시하며 모든 사용자는 아무런 비용 없이 이를 이용할 수 있다.

우리 앱은 웹앱으로 제작된 것으로 안드로이드와 iOS 에서 모두 지원되어야 하며, 모바일환경에서 앱과 웹 모두 지원되어야 했기에 주저없이 선택했다.
그래서 용량은?
우리는 원본을 그대로 보여줄 수는 없고(80MB 짜리도 있음 -_- 4K = 가로세로가 3,840개, 세로에 2,160개, 총 830만 개의 픽셀이 있는 해상도 ) 우리사이트의 최적해상도 480px에 맞추어 영상을 퍼블리싱하도록 변환해야만 한다. 를 알 수 있었다.
다시 생각해보니 영상은 정지화면의 시간정렬 집합이고,
이 정지화면을 1초에 얼마나 많이 보여주는가?
그리고 1초에 데이터를 얼마나 처리하는가?
정지화면은 가로세로 크기가 얼마나 되는가?
를 고려해야 한다.
여기엔 정답이 없었다. 영상은 천차만별이었고, 화질(영상의품질)도 고정이 아니었다. 용량이 크다고 화질이 좋은 것도 아니었다. 그래서 얻은 결론은
우리가 확보한 영상들을 기준으로 하는 우리만의 최적화를 위한 TEST !!!!!
실험을 통해
위의 3가지를 알 수 있었고, 가장 중요한 원판불변의 법칙(ㅎㅎ) 을 다시 깨닫게 되었다.
원본이 그지 같으면 그 이상의 품질을 용량을 줄이면서 만들 수는 없……
1번과 2번은 결정되었는데 3번의 결정이 어려워 검색을 해 본 결과, 우리가 사용한 고정 bit rate 말고 가변 bit rate(variable bitrate, VBR) 가 있다는 것을 알게되었다.
정지화면에서 영상의 색과 표현되는 객체가 복잡한 경우 bit rate 를 높게, 그렇지 않을 경우 bit rate 를 낮게 하는 방식이다.
또한 품질 기반 가변 비트레이트(QVBR) 가 있다는 것을 알게 되었고 우리가 사용할 MediaConvert에서 제공하고 있다는 것도 알게 되었다.
인코딩한 비디오의 가장 중요한 특성이 품질이라면 이를 달성하는 간단하고 확실한 방법은 일정한 비디오 품질을 제공하고 프로세스에서 제로 비트를 낭비하지 않도록 설계된 품질 기반 가변 비트레이트(QVBR) 제어를 사용하는 것입니다.
즉, 비디오 품질 요구 사항이 높지만 대역폭 예산이 낮은 경우 QVBR은 두 가지 요구를 모두 충족시킵니다. AWS Elemental 소프트웨어에는 추가 라이선스 비용 없이 QVBR이 포함되어 있습니다.
위에서 언급한 wepM 은 QVBR을 지원하지 않는다. 따라서 QVBR 을 지원하면서 범용성이 좋은 mp4 인코딩 컨테이너로 결정하였다.
이제는
품질의 손상을 최소화하면서 트래픽 비용을 감소하도록 하는 전략이 만들어 졌다.

S3 에 원본을 올린다.
S3 putitem event trigger 가 감지되면 Lambda Function 이 MediaConvert를 호출하고
영상에 대한 변환을 지정한 옵션에 맞추어 진행한 후,
MediaConvert 가 S3 에 변환된 영상을 저장한다.
CloudFront 는 변환된 영상이 지정된 S3를 원본으로 하여 CDN 서비스를 사용하도록 한다.
작업이 완료되면 SNS 를 통해 메일 혹은 Slack 으로 영상변환에 대한 알림을 한다.

인코딩 속도가 중요해서 단일패스HQ 를 선택

{
"Description": "(SO0146) Video on Demand on AWS Foundation Solution Implementation. Version v1.3.4",
"Mappings": {
"Send": {
"AnonymizedUsage": {
"Data": "Yes"
}
}
},
"Parameters": {
"emailAddress": {
"Type": "String",
"AllowedPattern": "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)(\\.[A-Za-z]{2,})$",
"Description": "The admin email address to receive SNS notifications for job status."
}
},
"Resources": {
"Logs6819BB44": {
"Type": "AWS::S3::Bucket",
"Properties": {
"AccessControl": "LogDeliveryWrite",
"BucketEncryption": {
"ServerSideEncryptionConfiguration": [
{
"ServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}
]
},
"OwnershipControls": {
"Rules": [
{
"ObjectOwnership": "ObjectWriter"
}
]
},
"PublicAccessBlockConfiguration": {
"BlockPublicAcls": true,
"BlockPublicPolicy": true,
"IgnorePublicAcls": true,
"RestrictPublicBuckets": true
},
"VersioningConfiguration": {
"Status": "Enabled"
}
},
"UpdateReplacePolicy": "Retain",
"DeletionPolicy": "Retain",
"Metadata": {
"cfn_nag": {
"rules_to_suppress": [
{
"id": "W35",
"reason": "Logs bucket does not require logging configuration"
},
{
"id": "W51",
"reason": "Logs bucket is private and does not require a bucket policy"
}
]
},
"cdk_nag": {
"rules_to_suppress": [
{
"reason": "Used to store access logs for other buckets",
"id": "AwsSolutions-S1"
},
{
"reason": "Bucket is private and is not using HTTP",
"id": "AwsSolutions-S10"
}
]
}
}
},
"LogsPolicy90DB40C9": {
"Type": "AWS::S3::BucketPolicy",
"Properties": {
"Bucket": {
"Ref": "Logs6819BB44"
},
"PolicyDocument": {
"Statement": [
{
"Action": "s3:",
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
},
"Effect": "Deny",
"Principal": {
"AWS": ""
},
"Resource": [
{
"Fn::GetAtt": [
"Logs6819BB44",
"Arn"
]
},
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"Logs6819BB44",
"Arn"
]
},
"/"
]
]
}
]
}
],
"Version": "2012-10-17"
}
},
"Metadata": {
"aws:cdk:path": "VodFoundation/Logs/Policy/Resource"
}
},
"Source71E471F1": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": "vastyle-veed-origin",
"BucketEncryption": {
"ServerSideEncryptionConfiguration": [
{
"ServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}
]
},
"LoggingConfiguration": {
"DestinationBucketName": {
"Ref": "Logs6819BB44"
},
"LogFilePrefix": "source-bucket-logs/"
},
"PublicAccessBlockConfiguration": {
"BlockPublicAcls": true,
"BlockPublicPolicy": true,
"IgnorePublicAcls": true,
"RestrictPublicBuckets": true
},
"VersioningConfiguration": {
"Status": "Enabled"
}
},
"UpdateReplacePolicy": "Retain",
"DeletionPolicy": "Retain",
"Metadata": {
"cfn_nag": {
"rules_to_suppress": [
{
"id": "W51",
"reason": "source bucket is private and does not require a bucket policy"
}
]
},
"cdk_nag": {
"rules_to_suppress": [
{
"reason": "Bucket is private and is not using HTTP",
"id": "AwsSolutions-S10"
}
]
}
}
},
"SourcePolicyE5AB5F73": {
"Type": "AWS::S3::BucketPolicy",
"Properties": {
"Bucket": {
"Ref": "Source71E471F1"
},
"PolicyDocument": {
"Statement": [
{
"Action": "s3:",
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
},
"Effect": "Deny",
"Principal": {
"AWS": ""
},
"Resource": [
{
"Fn::GetAtt": [
"Source71E471F1",
"Arn"
]
},
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"Source71E471F1",
"Arn"
]
},
"/"
]
]
}
]
}
],
"Version": "2012-10-17"
}
},
"Metadata": {
"aws:cdk:path": "VodFoundation/Source/Policy/Resource"
}
},
"Destination920A3C57": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BucketName": "vastyle-veed-converted",
"BucketEncryption": {
"ServerSideEncryptionConfiguration": [
{
"ServerSideEncryptionByDefault": {
"SSEAlgorithm": "AES256"
}
}
]
},
"CorsConfiguration": {
"CorsRules": [
{
"AllowedHeaders": [
""
],
"AllowedMethods": [
"GET"
],
"AllowedOrigins": [
""
],
"MaxAge": 3000
}
]
},
"LoggingConfiguration": {
"DestinationBucketName": {
"Ref": "Logs6819BB44"
},
"LogFilePrefix": "destination-bucket-logs/"
},
"PublicAccessBlockConfiguration": {
"BlockPublicAcls": true,
"BlockPublicPolicy": true,
"IgnorePublicAcls": true,
"RestrictPublicBuckets": true
},
"VersioningConfiguration": {
"Status": "Enabled"
}
},
"UpdateReplacePolicy": "Retain",
"DeletionPolicy": "Retain",
"Metadata": {
"aws:cdk:path": "VodFoundation/Destination/Resource",
"cdk_nag": {
"rules_to_suppress": [
{
"reason": "Bucket is private and is not using HTTP",
"id": "AwsSolutions-S10"
}
]
}
}
},
"DestinationPolicy7982387E": {
"Type": "AWS::S3::BucketPolicy",
"Properties": {
"Bucket": {
"Ref": "Destination920A3C57"
},
"PolicyDocument": {
"Statement": [
{
"Action": "s3:",
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
},
"Effect": "Deny",
"Principal": {
"AWS": ""
},
"Resource": [
{
"Fn::GetAtt": [
"Destination920A3C57",
"Arn"
]
},
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"Destination920A3C57",
"Arn"
]
},
"/"
]
]
}
]
},
{
"Action": "s3:GetObject",
"Effect": "Allow",
"Principal": {
"CanonicalUser": {
"Fn::GetAtt": [
"CloudFrontCloudFrontDistributionOrigin1S3Origin17B88F1A",
"S3CanonicalUserId"
]
}
},
"Resource": {
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"Destination920A3C57",
"Arn"
]
},
"/"
]
]
}
}
],
"Version": "2012-10-17"
}
},
"Metadata": {
"aws:cdk:path": "VodFoundation/Destination/Policy/Resource",
"cfn_nag": {
"rules_to_suppress": [
{
"id": "F16",
"reason": "Public website bucket policy requires a wildcard principal"
}
]
},
"cdk_nag": {
"rules_to_suppress": [
{
"reason": "Bucket is private and is not using HTTP",
"id": "AwsSolutions-S10"
}
]
}
}
},
"CloudFrontCloudFrontDistributionOrigin1S3Origin17B88F1A": {
"Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity",
"Properties": {
"CloudFrontOriginAccessIdentityConfig": {
"Comment": "Identity for VodFoundationCloudFrontCloudFrontDistributionOrigin1F191A578"
}
},
"Metadata": {
"aws:cdk:path": "VodFoundation/CloudFront/CloudFrontDistribution/Origin1/S3Origin/Resource"
}
},
"CloudFrontCloudFrontDistribution824F3346": {
"Type": "AWS::CloudFront::Distribution",
"Properties": {
"DistributionConfig": {
"Comment": {
"Fn::Join": [
"",
[
{
"Ref": "AWS::StackName"
},
" Video on Demand Foundation"
]
]
},
"DefaultCacheBehavior": {
"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
"Compress": true,
"TargetOriginId": "VodFoundationCloudFrontCloudFrontDistributionOrigin1F191A578",
"ViewerProtocolPolicy": "redirect-to-https"
},
"DefaultRootObject": "index.html",
"Enabled": true,
"HttpVersion": "http2",
"IPV6Enabled": true,
"Logging": {
"Bucket": {
"Fn::GetAtt": [
"Logs6819BB44",
"RegionalDomainName"
]
},
"Prefix": "cloudfront-logs/"
},
"Origins": [
{
"DomainName": {
"Fn::GetAtt": [
"Destination920A3C57",
"RegionalDomainName"
]
},
"Id": "VodFoundationCloudFrontCloudFrontDistributionOrigin1F191A578",
"S3OriginConfig": {
"OriginAccessIdentity": {
"Fn::Join": [
"",
[
"origin-access-identity/cloudfront/",
{
"Ref": "CloudFrontCloudFrontDistributionOrigin1S3Origin17B88F1A"
}
]
]
}
}
}
]
}
},
"Metadata": {
"aws:cdk:path": "VodFoundation/CloudFront/CloudFrontDistribution/Resource",
"cfn_nag": {
"rules_to_suppress": [
{
"id": "W70",
"reason": "Since the distribution uses the CloudFront domain name, CloudFront automatically sets the security policy to TLSv1 regardless of the value of MinimumProtocolVersion"
}
]
},
"cdk_nag": {
"rules_to_suppress": [
{
"reason": "Use case does not warrant CloudFront Geo restriction",
"id": "AwsSolutions-CFR1"
},
{
"reason": "Use case does not warrant CloudFront integration with AWS WAF",
"id": "AwsSolutions-CFR2"
},
{
"reason": "CloudFront automatically sets the security policy to TLSv1 when the distribution uses the CloudFront domain name",
"id": "AwsSolutions-CFR4"
}
]
}
}
},
"MediaConvertRole031A64A9": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "mediaconvert.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
},
"Metadata": {
"aws:cdk:path": "VodFoundation/MediaConvertRole/Resource"
}
},
"MediaconvertPolicy9E3026EC": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Effect": "Allow",
"Resource": [
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"Source71E471F1",
"Arn"
]
},
"/"
]
]
},
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"Destination920A3C57",
"Arn"
]
},
"/"
]
]
}
]
},
{
"Action": "execute-api:Invoke",
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":execute-api:",
{
"Ref": "AWS::Region"
},
":",
{
"Ref": "AWS::AccountId"
},
":"
]
]
}
}
],
"Version": "2012-10-17"
},
"PolicyName": "MediaconvertPolicy9E3026EC",
"Roles": [
{
"Ref": "MediaConvertRole031A64A9"
}
]
},
"Metadata": {
"aws:cdk:path": "VodFoundation/MediaconvertPolicy/Resource",
"cdk_nag": {
"rules_to_suppress": [
{
"reason": "/ required to get/put objects to S3",
"id": "AwsSolutions-IAM5"
}
]
}
}
},
"CustomResourceRoleAB1EF463": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
},
"Metadata": {
"aws:cdk:path": "VodFoundation/CustomResourceRole/Resource"
}
},
"CustomResourcePolicy79526710": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"s3:PutObject",
"s3:PutBucketNotification"
],
"Effect": "Allow",
"Resource": [
{
"Fn::GetAtt": [
"Source71E471F1",
"Arn"
]
},
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"Source71E471F1",
"Arn"
]
},
"/"
]
]
}
]
},
{
"Action": "mediaconvert:DescribeEndpoints",
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:aws:mediaconvert:",
{
"Ref": "AWS::Region"
},
":",
{
"Ref": "AWS::AccountId"
},
":"
]
]
}
},
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Effect": "Allow",
"Resource": ""
}
],
"Version": "2012-10-17"
},
"PolicyName": "CustomResourcePolicy79526710",
"Roles": [
{
"Ref": "CustomResourceRoleAB1EF463"
}
]
},
"Metadata": {
"aws:cdk:path": "VodFoundation/CustomResourcePolicy/Resource",
"cdk_nag": {
"rules_to_suppress": [
{
"reason": "Resource ARNs are not generated at the time of policy creation",
"id": "AwsSolutions-IAM5"
}
]
},
"cfn_nag": {
"rules_to_suppress": [
{
"id": "W12",
"reason": "Resource ARNs are not generated at the time of policy creation"
}
]
}
}
},
"CustomResource8CDCD7A7": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": {
"Fn::Sub": "solutions-${AWS::Region}"
},
"S3Key": "video-on-demand-on-aws-foundation/v1.3.4/asset593ffc2c71667806ba8de7036f9925188e1e3c155d93996a4f18a4b478c8b8c8.zip"
},
"Description": "CFN Custom resource to copy assets to S3 and get the MediaConvert endpoint",
"Environment": {
"Variables": {
"SOLUTION_IDENTIFIER": "AwsSolution/SO0146/v1.3.4"
}
},
"Handler": "asset593ffc2c71667806ba8de7036f9925188e1e3c155d93996a4f18a4b478c8b8c8/index.handler",
"Role": {
"Fn::GetAtt": [
"CustomResourceRoleAB1EF463",
"Arn"
]
},
"Runtime": "nodejs18.x",
"Timeout": 30
},
"DependsOn": [
"CustomResourcePolicy79526710",
"CustomResourceRoleAB1EF463"
],
"Metadata": {
"cfn_nag": {
"rules_to_suppress": [
{
"id": "W58",
"reason": "Invalid warning: function has access to cloudwatch"
},
{
"id": "W89",
"reason": "Invalid warning: lambda not needed in VPC"
},
{
"id": "W92",
"reason": "Invalid warning: lambda does not need ReservedConcurrentExecutions"
}
]
}
}
},
"Endpoint": {
"Type": "AWS::CloudFormation::CustomResource",
"Properties": {
"ServiceToken": {
"Fn::GetAtt": [
"CustomResource8CDCD7A7",
"Arn"
]
}
},
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete",
"Metadata": {
"aws:cdk:path": "VodFoundation/Endpoint/Default"
}
},
"JobSubmitRole4FA8E972": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
},
"Metadata": {
"aws:cdk:path": "VodFoundation/JobSubmitRole/Resource"
}
},
"JobSubmitRoleDefaultPolicy20E077D9": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "sns:Publish",
"Effect": "Allow",
"Resource": {
"Ref": "NotificationSnsTopicB941FD22"
}
}
],
"Version": "2012-10-17"
},
"PolicyName": "JobSubmitRoleDefaultPolicy20E077D9",
"Roles": [
{
"Ref": "JobSubmitRole4FA8E972"
}
]
},
"Metadata": {
"aws:cdk:path": "VodFoundation/JobSubmitRole/DefaultPolicy/Resource"
}
},
"JobSubmitPolicy098DF0F8": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "iam:PassRole",
"Effect": "Allow",
"Resource": {
"Fn::GetAtt": [
"MediaConvertRole031A64A9",
"Arn"
]
}
},
{
"Action": "mediaconvert:CreateJob",
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":mediaconvert:",
{
"Ref": "AWS::Region"
},
":",
{
"Ref": "AWS::AccountId"
},
":"
]
]
}
},
{
"Action": "s3:GetObject",
"Effect": "Allow",
"Resource": [
{
"Fn::GetAtt": [
"Source71E471F1",
"Arn"
]
},
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"Source71E471F1",
"Arn"
]
},
"/"
]
]
}
]
},
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Effect": "Allow",
"Resource": ""
}
],
"Version": "2012-10-17"
},
"PolicyName": "JobSubmitPolicy098DF0F8",
"Roles": [
{
"Ref": "JobSubmitRole4FA8E972"
}
]
},
"Metadata": {
"aws:cdk:path": "VodFoundation/JobSubmitPolicy/Resource",
"cdk_nag": {
"rules_to_suppress": [
{
"reason": "Resource ARNs are not generated at the time of policy creation",
"id": "AwsSolutions-IAM5"
}
]
},
"cfn_nag": {
"rules_to_suppress": [
{
"id": "W12",
"reason": "Resource ARNs are not generated at the time of policy creation"
}
]
}
}
},
"jobSubmitB391E42F": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": {
"Fn::Sub": "solutions-${AWS::Region}"
},
"S3Key": "video-on-demand-on-aws-foundation/v1.3.4/asset44691a20d2ce91fc1c3225baffe7e83a47587d2272d208d2f5df001811fc3865.zip"
},
"Description": "Submits an Encoding job to MediaConvert",
"Environment": {
"Variables": {
"MEDIACONVERT_ENDPOINT": {
"Fn::GetAtt": [
"Endpoint",
"Endpoint"
]
},
"MEDIACONVERT_ROLE": {
"Fn::GetAtt": [
"MediaConvertRole031A64A9",
"Arn"
]
},
"JOB_SETTINGS": "job-settings.json",
"DESTINATION_BUCKET": {
"Ref": "Destination920A3C57"
},
"SOLUTION_ID": "SO0146",
"STACKNAME": {
"Ref": "AWS::StackName"
},
"SOLUTION_IDENTIFIER": "AwsSolution/SO0146/v1.3.4",
"SNS_TOPIC_ARN": {
"Ref": "NotificationSnsTopicB941FD22"
},
"SNS_TOPIC_NAME": {
"Fn::GetAtt": [
"NotificationSnsTopicB941FD22",
"TopicName"
]
}
}
},
"Handler": "asset44691a20d2ce91fc1c3225baffe7e83a47587d2272d208d2f5df001811fc3865/index.handler",
"Role": {
"Fn::GetAtt": [
"JobSubmitRole4FA8E972",
"Arn"
]
},
"Runtime": "nodejs18.x",
"Timeout": 30
},
"DependsOn": [
"JobSubmitPolicy098DF0F8",
"JobSubmitRoleDefaultPolicy20E077D9",
"JobSubmitRole4FA8E972"
],
"Metadata": {
"cfn_nag": {
"rules_to_suppress": [
{
"id": "W58",
"reason": "Invalid warning: function has access to cloudwatch"
},
{
"id": "W89",
"reason": "Invalid warning: lambda not needed in VPC"
},
{
"id": "W92",
"reason": "Invalid warning: lambda does not need ReservedConcurrentExecutions"
}
]
}
}
},
"jobSubmitEventInvokeConfig0385F502": {
"Type": "AWS::Lambda::EventInvokeConfig",
"Properties": {
"FunctionName": {
"Ref": "jobSubmitB391E42F"
},
"MaximumRetryAttempts": 0,
"Qualifier": "$LATEST"
},
"DependsOn": [
"JobSubmitPolicy098DF0F8",
"JobSubmitRoleDefaultPolicy20E077D9",
"JobSubmitRole4FA8E972"
],
"Metadata": {
"aws:cdk:path": "VodFoundation/jobSubmit/EventInvokeConfig/Resource"
}
},
"jobSubmitS3Trigger3DEB8D7C": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Fn::GetAtt": [
"jobSubmitB391E42F",
"Arn"
]
},
"Principal": "s3.amazonaws.com",
"SourceAccount": {
"Ref": "AWS::AccountId"
}
},
"DependsOn": [
"JobSubmitPolicy098DF0F8",
"JobSubmitRoleDefaultPolicy20E077D9",
"JobSubmitRole4FA8E972"
],
"Metadata": {
"aws:cdk:path": "VodFoundation/jobSubmit/S3Trigger"
}
},
"JobCompleteRole81FD9028": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
}
},
"Metadata": {
"aws:cdk:path": "VodFoundation/JobCompleteRole/Resource"
}
},
"JobCompleteRoleDefaultPolicyD4DC2F12": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "sns:Publish",
"Effect": "Allow",
"Resource": {
"Ref": "NotificationSnsTopicB941FD22"
}
}
],
"Version": "2012-10-17"
},
"PolicyName": "JobCompleteRoleDefaultPolicyD4DC2F12",
"Roles": [
{
"Ref": "JobCompleteRole81FD9028"
}
]
},
"Metadata": {
"aws:cdk:path": "VodFoundation/JobCompleteRole/DefaultPolicy/Resource"
}
},
"JobCompletePolicyBBFD3892": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "mediaconvert:GetJob",
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":mediaconvert:",
{
"Ref": "AWS::Region"
},
":",
{
"Ref": "AWS::AccountId"
},
":"
]
]
}
},
{
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Effect": "Allow",
"Resource": {
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"Source71E471F1",
"Arn"
]
},
"/"
]
]
}
},
{
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Effect": "Allow",
"Resource": ""
}
],
"Version": "2012-10-17"
},
"PolicyName": "JobCompletePolicyBBFD3892",
"Roles": [
{
"Ref": "JobCompleteRole81FD9028"
}
]
},
"Metadata": {
"aws:cdk:path": "VodFoundation/JobCompletePolicy/Resource",
"cdk_nag": {
"rules_to_suppress": [
{
"reason": "Resource ARNs are not generated at the time of policy creation",
"id": "AwsSolutions-IAM5"
}
]
},
"cfn_nag": {
"rules_to_suppress": [
{
"id": "W12",
"reason": "Resource ARNs are not generated at the time of policy creation"
}
]
}
}
},
"JobComplete703682D0": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"S3Bucket": {
"Fn::Sub": "solutions-${AWS::Region}"
},
"S3Key": "video-on-demand-on-aws-foundation/v1.3.4/asset4d8cbdf2ef743ca7aaac8177d00f15e4f064ceaf4e6c0d1b44ec4441dc06ce95.zip"
},
"Description": "Triggered by EventBridge,processes completed MediaConvert jobs.",
"Environment": {
"Variables": {
"MEDIACONVERT_ENDPOINT": {
"Fn::GetAtt": [
"Endpoint",
"Endpoint"
]
},
"CLOUDFRONT_DOMAIN": {
"Fn::GetAtt": [
"CloudFrontCloudFrontDistribution824F3346",
"DomainName"
]
},
"SOURCE_BUCKET": {
"Ref": "Source71E471F1"
},
"JOB_MANIFEST": "jobs-manifest.json",
"STACKNAME": {
"Ref": "AWS::StackName"
},
"METRICS": {
"Fn::FindInMap": [
"Send",
"AnonymizedUsage",
"Data"
]
},
"SOLUTION_ID": "SO0146",
"VERSION": "v1.3.4",
"UUID": {
"Fn::GetAtt": [
"Endpoint",
"UUID"
]
},
"SOLUTION_IDENTIFIER": "AwsSolution/SO0146/v1.3.4",
"SNS_TOPIC_ARN": {
"Ref": "NotificationSnsTopicB941FD22"
},
"SNS_TOPIC_NAME": {
"Fn::GetAtt": [
"NotificationSnsTopicB941FD22",
"TopicName"
]
}
}
},
"Handler": "asset4d8cbdf2ef743ca7aaac8177d00f15e4f064ceaf4e6c0d1b44ec4441dc06ce95/index.handler",
"Role": {
"Fn::GetAtt": [
"JobCompleteRole81FD9028",
"Arn"
]
},
"Runtime": "nodejs18.x",
"Timeout": 30
},
"DependsOn": [
"JobCompletePolicyBBFD3892",
"JobCompleteRoleDefaultPolicyD4DC2F12",
"JobCompleteRole81FD9028"
],
"Metadata": {
"cfn_nag": {
"rules_to_suppress": [
{
"id": "W58",
"reason": "Invalid warning: function has access to cloudwatch"
},
{
"id": "W89",
"reason": "Invalid warning: lambda not needed in VPC"
},
{
"id": "W92",
"reason": "Invalid warning: lambda does not need ReservedConcurrentExecutions"
}
]
}
}
},
"JobCompleteEventInvokeConfig692D89BE": {
"Type": "AWS::Lambda::EventInvokeConfig",
"Properties": {
"FunctionName": {
"Ref": "JobComplete703682D0"
},
"MaximumRetryAttempts": 0,
"Qualifier": "$LATEST"
},
"DependsOn": [
"JobCompletePolicyBBFD3892",
"JobCompleteRoleDefaultPolicyD4DC2F12",
"JobCompleteRole81FD9028"
],
"Metadata": {
"aws:cdk:path": "VodFoundation/JobComplete/EventInvokeConfig/Resource"
}
},
"JobCompleteAwsEventsLambdaInvokePermission1ED79B615": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Fn::GetAtt": [
"JobComplete703682D0",
"Arn"
]
},
"Principal": "events.amazonaws.com",
"SourceArn": {
"Fn::GetAtt": [
"EventTriggerEventsRule76A88FDF",
"Arn"
]
}
},
"DependsOn": [
"JobCompletePolicyBBFD3892",
"JobCompleteRoleDefaultPolicyD4DC2F12",
"JobCompleteRole81FD9028"
],
"Metadata": {
"aws:cdk:path": "VodFoundation/JobComplete/AwsEventsLambdaInvokePermission-1"
}
},
"S3Config": {
"Type": "AWS::CloudFormation::CustomResource",
"Properties": {
"ServiceToken": {
"Fn::GetAtt": [
"CustomResource8CDCD7A7",
"Arn"
]
},
"SourceBucket": {
"Ref": "Source71E471F1"
},
"LambdaArn": {
"Fn::GetAtt": [
"jobSubmitB391E42F",
"Arn"
]
}
},
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete",
"Metadata": {
"aws:cdk:path": "VodFoundation/S3Config/Default"
}
},
"EventTriggerEventsRule76A88FDF": {
"Type": "AWS::Events::Rule",
"Properties": {
"EventPattern": {
"source": [
"aws.mediaconvert"
],
"detail": {
"userMetadata": {
"StackName": [
{
"Ref": "AWS::StackName"
}
]
},
"status": [
"COMPLETE",
"ERROR",
"CANCELED",
"INPUT_INFORMATION"
]
}
},
"State": "ENABLED",
"Targets": [
{
"Arn": {
"Fn::GetAtt": [
"JobComplete703682D0",
"Arn"
]
},
"Id": "Target0"
}
]
},
"Metadata": {
"aws:cdk:path": "VodFoundation/EventTrigger/EventsRule/Resource"
}
},
"NotificationSnsTopicB941FD22": {
"Type": "AWS::SNS::Topic",
"Properties": {
"KmsMasterKeyId": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":kms:",
{
"Ref": "AWS::Region"
},
":",
{
"Ref": "AWS::AccountId"
},
":alias/aws/sns"
]
]
}
},
"Metadata": {
"aws:cdk:path": "VodFoundation/Notification/SnsTopic/Resource"
}
},
"NotificationSnsTopicPolicy4027082A": {
"Type": "AWS::SNS::TopicPolicy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": [
"SNS:Publish",
"SNS:RemovePermission",
"SNS:SetTopicAttributes",
"SNS:DeleteTopic",
"SNS:ListSubscriptionsByTopic",
"SNS:GetTopicAttributes",
"SNS:Receive",
"SNS:AddPermission",
"SNS:Subscribe"
],
"Condition": {
"StringEquals": {
"AWS:SourceOwner": {
"Ref": "AWS::AccountId"
}
}
},
"Effect": "Allow",
"Principal": {
"AWS": {
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::",
{
"Ref": "AWS::AccountId"
},
":root"
]
]
}
},
"Resource": {
"Ref": "NotificationSnsTopicB941FD22"
},
"Sid": "TopicOwnerOnlyAccess"
},
{
"Action": [
"SNS:Publish",
"SNS:RemovePermission",
"SNS:SetTopicAttributes",
"SNS:DeleteTopic",
"SNS:ListSubscriptionsByTopic",
"SNS:GetTopicAttributes",
"SNS:Receive",
"SNS:AddPermission",
"SNS:Subscribe"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
},
"Effect": "Deny",
"Principal": {
"AWS": ""
},
"Resource": {
"Ref": "NotificationSnsTopicB941FD22"
},
"Sid": "HttpsOnly"
}
],
"Version": "2012-10-17"
},
"Topics": [
{
"Ref": "NotificationSnsTopicB941FD22"
}
]
},
"Metadata": {
"aws:cdk:path": "VodFoundation/Notification/SnsTopic/Policy/Resource"
}
},
"NotificationSnsTopicTokenSubscription1209F3ABA": {
"Type": "AWS::SNS::Subscription",
"Properties": {
"Endpoint": {
"Ref": "emailAddress"
},
"Protocol": "email",
"TopicArn": {
"Ref": "NotificationSnsTopicB941FD22"
}
},
"Metadata": {
"aws:cdk:path": "VodFoundation/Notification/SnsTopic/TokenSubscription:1/Resource"
}
},
"AppRegistryAttributeGroup7AF07446": {
"Type": "AWS::ServiceCatalogAppRegistry::AttributeGroup",
"Properties": {
"Attributes": {
"ApplicationType": "AWS-Solutions",
"SolutionVersion": "v1.3.4",
"SolutionID": "SO0146",
"SolutionName": "Video on Demand on AWS Foundation"
},
"Description": "Attribute group for solution information.",
"Name": {
"Fn::Join": [
"",
[
{
"Ref": "AWS::Region"
},
"-",
{
"Ref": "AWS::StackName"
}
]
]
}
},
"Metadata": {
"aws:cdk:path": "VodFoundation/AppRegistryAttributeGroup/Resource"
}
},
"AppRegistryAttributeGroupApplicationAttributeGroupAssociatione92bb36ecd06C2AD27E6": {
"Type": "AWS::ServiceCatalogAppRegistry::AttributeGroupAssociation",
"Properties": {
"Application": {
"Fn::GetAtt": [
"AppRegistryApp5349BE86",
"Id"
]
},
"AttributeGroup": {
"Fn::GetAtt": [
"AppRegistryAttributeGroup7AF07446",
"Id"
]
}
},
"Metadata": {
"aws:cdk:path": "VodFoundation/AppRegistryAttributeGroup/ApplicationAttributeGroupAssociatione92bb36ecd06"
}
},
"AppRegistryApp5349BE86": {
"Type": "AWS::ServiceCatalogAppRegistry::Application",
"Properties": {
"Description": "Service Catalog application to track and manage all your resources. The SolutionId is SO0146 and SolutionVersion is v1.3.4.",
"Name": {
"Fn::Join": [
"",
[
"vod-foundation-",
{
"Ref": "AWS::Region"
},
"-",
{
"Ref": "AWS::AccountId"
},
"-",
{
"Ref": "AWS::StackName"
}
]
]
},
"Tags": {
"Solutions:ApplicationType": "AWS-Solutions",
"Solutions:SolutionID": "SO0146",
"Solutions:SolutionName": "Video on Demand on AWS Foundation",
"Solutions:SolutionVersion": "v1.3.4"
}
},
"Metadata": {
"aws:cdk:path": "VodFoundation/AppRegistryApp/Resource"
}
},
"AppRegistryAssociation": {
"Type": "AWS::ServiceCatalogAppRegistry::ResourceAssociation",
"Properties": {
"Application": {
"Fn::GetAtt": [
"AppRegistryApp5349BE86",
"Id"
]
},
"Resource": {
"Ref": "AWS::StackId"
},
"ResourceType": "CFN_STACK"
},
"Metadata": {
"aws:cdk:path": "VodFoundation/AppRegistryAssociation"
}
},
"CDKMetadata": {
"Type": "AWS::CDK::Metadata",
"Properties": {
"Analytics": "v2:deflate64:H4sIAAAAAAAA/3VSTW/bMAz9LbnLatL0sN6Weu1QoEODOPdClmmPjS0JopSiEPTfB8lObGzrRSQfv6l3y8P9lof1SnxQIZtT0WPNQ+WEPLGyVb+EMai6pO6FFQM4sEx80FugLQ8PXp7AJeekjWKve5SfMzzZo/EgCCKTvfZNa7VyPPxAchZr71Ar9mqxQ7WTEoieG1AOXa5UpoSnlPBVxLJMZCgGHg66h+TKcp5q1CLrxVA3gocnr2RuXrbqqj+eQblnddYnKLVqMd/gv+Ae7IBEuS1t3wQROOK7JBikBOLh4KdJfA+RkSIejtqgTNio5HeecWlWviZp0VxmXNqREdgzSpDCiV53whgLXTrFJw9lq3ZuPAr8tNob9g+yI9ISxaX0zpge5dU8AGlvJSyiYmQXlOUdKye6TBFPTg9X3yI76a/eGZ+pUmrV4Dj6uhC9+S34evV9ot9Nkl9uNIWHv5ZaDB0ju7u91CPdZzZQIbUiZ710lBuEmX4FbfnMraOutuOf1RabDoqJIo8zdNQvGZvYU6S/HJGjrhTFyJRugL/TzXnzjW/u+Wb1ToiF9crhAPwwyj8bMrXOeAMAAA=="
},
"Metadata": {
"aws:cdk:path": "VodFoundation/CDKMetadata/Default"
},
"Condition": "CDKMetadataAvailable"
}
},
"Outputs": {
"SourceBucket": {
"Description": "Source S3 Bucket used to host source video and MediaConvert job settings files",
"Value": {
"Ref": "Source71E471F1"
},
"Export": {
"Name": {
"Fn::Join": [
"",
[
{
"Ref": "AWS::StackName"
},
"-SourceBucket"
]
]
}
}
},
"DestinationBucket": {
"Description": "Source S3 Bucket used to host all MediaConvert ouputs",
"Value": {
"Ref": "Destination920A3C57"
},
"Export": {
"Name": {
"Fn::Join": [
"",
[
{
"Ref": "AWS::StackName"
},
"-DestinationBucket"
]
]
}
}
},
"CloudFrontDomain": {
"Description": "CloudFront Domain Name",
"Value": {
"Fn::GetAtt": [
"CloudFrontCloudFrontDistribution824F3346",
"DomainName"
]
},
"Export": {
"Name": {
"Fn::Join": [
"",
[
{
"Ref": "AWS::StackName"
},
"-CloudFrontDomain"
]
]
}
}
},
"SnsTopic": {
"Description": "SNS Topic used to capture the VOD workflow outputs including errors",
"Value": {
"Fn::GetAtt": [
"NotificationSnsTopicB941FD22",
"TopicName"
]
},
"Export": {
"Name": {
"Fn::Join": [
"",
[
{
"Ref": "AWS::StackName"
},
"-SnsTopic"
]
]
}
}
}
},
"Conditions": {
"CDKMetadataAvailable": {
"Fn::Or": [
{
"Fn::Or": [
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"af-south-1"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"ap-east-1"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"ap-northeast-1"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"ap-northeast-2"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"ap-south-1"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"ap-southeast-1"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"ap-southeast-2"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"ca-central-1"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"cn-north-1"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"cn-northwest-1"
]
}
]
},
{
"Fn::Or": [
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"eu-central-1"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"eu-north-1"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"eu-south-1"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"eu-west-1"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"eu-west-2"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"eu-west-3"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"me-south-1"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"sa-east-1"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"us-east-1"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"us-east-2"
]
}
]
},
{
"Fn::Or": [
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"us-west-1"
]
},
{
"Fn::Equals": [
{
"Ref": "AWS::Region"
},
"us-west-2"
]
}
]
}
]
}
}
}
역할을 생성하고 권한에 대해 정책을 할당해야 함. 아래는 필수 정책 연결 항목.
AmazonEventBridge
AmazonS3
AmazonSNS
AWSBatchServiceEventTargetRole
AWSElementalMediaConvert
AWSLambda
AWSServiceCatalogAppRegistry
CloudFront
IAM

AWS Partner 사와 CloudFront 협약으로 70% 비용할인