in source/cdk/lib/vod-foundation-stack.ts [16:378]
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
/**
* CloudFormation Template Descrption
*/
this.templateOptions.description = '(SO0146) v1.1.0: Video on Demand on AWS Foundation Solution Implementation';
/**
* Mapping for sending anonymous metrics to AWS Solution Builders API
*/
new cdk.CfnMapping(this, 'Send', {
mapping: {
AnonymousUsage: {
Data: 'Yes'
}
}
});
/**
* Cfn Parameters
*/
const adminEmail = new cdk.CfnParameter(this, "emailAddress", {
type: "String",
description: "The admin email address to receive SNS notifications for job status.",
allowedPattern: "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"
});
/**
* Logs bucket for S3 and CloudFront
*/
const logsBucket = new s3.Bucket(this, 'Logs', {
encryption: s3.BucketEncryption.S3_MANAGED,
publicReadAccess: false,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
/**
* Get Cfn Resource for the logs bucket and add CFN_NAG rule
*/
const cfnLogsBucket = logsBucket.node.findChild('Resource') as s3.CfnBucket;
cfnLogsBucket.cfnOptions.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'
}]
}
};
/**
* Source S3 bucket to host source videos and jobSettings JSON files
*/
const source = new s3.Bucket(this, 'Source', {
serverAccessLogsBucket: logsBucket,
serverAccessLogsPrefix: 'source-bucket-logs/',
encryption: s3.BucketEncryption.S3_MANAGED,
publicReadAccess: false,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
});
const cfnSource = source.node.findChild('Resource') as s3.CfnBucket;
cfnSource.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [{
id: 'W51',
reason: 'source bucket is private and does not require a bucket policy'
}]
}
};
/**
* Destination S3 bucket to host the mediaconvert outputs
*/
const destination = new s3.Bucket(this, 'Destination', {
serverAccessLogsBucket: logsBucket,
serverAccessLogsPrefix: 'destination-bucket-logs/',
encryption: s3.BucketEncryption.S3_MANAGED,
publicReadAccess: false,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
cors: [
{
maxAge: 3000,
allowedOrigins: ['*'],
allowedHeaders: ['*'],
allowedMethods: [HttpMethods.GET]
},
],
});
/**
* Solutions construct to create Cloudfrotnt with an s3 bucket as the origin
* https://docs.aws.amazon.com/solutions/latest/constructs/aws-cloudfront-s3.html
* insertHttpSecurityHeaders is set to false as this requires the deployment to be in us-east-1
*/
const cloudFront = new CloudFrontToS3(this, 'CloudFront', {
existingBucketObj: destination,
insertHttpSecurityHeaders: false,
cloudFrontDistributionProps: {
comment:`${cdk.Aws.STACK_NAME} Video on Demand Foundation`,
defaultCacheBehavior: {
allowedMethods: [ 'GET', 'HEAD','OPTIONS' ],
Compress: false,
forwardedValues: {
queryString: false,
headers: [ 'Origin', 'Access-Control-Request-Method','Access-Control-Request-Headers' ],
cookies: { forward: 'none' }
},
viewerProtocolPolicy: 'allow-all'
},
loggingConfig: {
bucket: logsBucket,
prefix: 'cloudfront-logs'
}
}
});
/**
* MediaConvert Service Role to grant Mediaconvert Access to the source and Destination Bucket,
* API invoke * is also required for the services.
*/
const mediaconvertRole = new iam.Role(this, 'MediaConvertRole', {
assumedBy: new iam.ServicePrincipal('mediaconvert.amazonaws.com'),
});
const mediaconvertPolicy = new iam.Policy(this, 'MediaconvertPolicy', {
statements: [
new iam.PolicyStatement({
resources: [`${source.bucketArn}/*`, `${destination.bucketArn}/*`],
actions: ['s3:GetObject', 's3:PutObject']
}),
new iam.PolicyStatement({
resources: [`arn:${cdk.Aws.PARTITION}:execute-api:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:*`],
actions: ['execute-api:Invoke']
})
]
});
mediaconvertPolicy.attachToRole(mediaconvertRole);
/**
* Custom Resource, Role and Policy.
*/
const customResourceLambda = new lambda.Function(this, 'CustomResource', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
description: 'CFN Custom resource to copy assets to S3 and get the MediaConvert endpoint',
environment: {
SOLUTION_IDENTIFIER: 'AwsSolution/SO0146/v1.1.0'
},
code: lambda.Code.fromAsset('../custom-resource'),
timeout: cdk.Duration.seconds(30),
initialPolicy: [
new iam.PolicyStatement({
actions: ["s3:PutObject","s3:PutBucketNotification"],
resources: [source.bucketArn, `${source.bucketArn}/*`]
}),
new iam.PolicyStatement({
actions: ["mediaconvert:DescribeEndpoints"],
resources: [`arn:aws:mediaconvert:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:*`],
})
]
});
/** get the cfn resource for the role and attach cfn_nag rule */
const cfnCustomResource = customResourceLambda.node.findChild('Resource') as lambda.CfnFunction;
cfnCustomResource.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [{
id: 'W58',
reason: 'Invalid warning: function has access to cloudwatch'
},{
id: 'W89',
reason: 'AWS Lambda does not require VPC for this solution.'
},{
id: 'W92',
reason: 'ReservedConcurrentExecutions not required'
}]
}
};
/**
* Call the custom resource, this will return the MediaConvert endpoint and a UUID
*/
const customResourceEndpoint = new cdk.CustomResource(this, 'Endpoint', {
serviceToken: customResourceLambda.functionArn
});
/**
* Job submit Lambda function, triggered by S3 Put events in the source S3 bucket
*/
const jobSubmit = new lambda.Function(this, 'jobSubmit', {
code: lambda.Code.fromAsset(`../job-submit`),
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
timeout: cdk.Duration.seconds(30),
retryAttempts:0,
description: 'Submits an Encoding job to MediaConvert',
environment: {
MEDIACONVERT_ENDPOINT: customResourceEndpoint.getAttString('Endpoint'),
MEDIACONVERT_ROLE: mediaconvertRole.roleArn,
JOB_SETTINGS: 'job-settings.json',
DESTINATION_BUCKET: destination.bucketName,
SOLUTION_ID: 'SO0146',
STACKNAME: cdk.Aws.STACK_NAME,
SOLUTION_IDENTIFIER: 'AwsSolution/SO0146/v1.1.0'
/** SNS_TOPIC_ARN: added by the solution construct below */
},
initialPolicy: [
new iam.PolicyStatement({
actions: ["iam:PassRole"],
resources: [mediaconvertRole.roleArn]
}),
new iam.PolicyStatement({
actions: ["mediaconvert:CreateJob"],
resources: [`arn:${cdk.Aws.PARTITION}:mediaconvert:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:*`]
}),
new iam.PolicyStatement({
actions: ["s3:GetObject"],
resources: [source.bucketArn, `${source.bucketArn}/*`]
})
]
});
/** Give S3 permission to trigger the job submit lambda function */
jobSubmit.addPermission('S3Trigger', {
principal: new iam.ServicePrincipal('s3.amazonaws.com'),
action: 'lambda:InvokeFunction',
sourceAccount: cdk.Aws.ACCOUNT_ID
});
/** get the cfn resource for the role and attach cfn_nag rule */
const cfnJobSubmit = jobSubmit.node.findChild('Resource') as lambda.CfnFunction;
cfnJobSubmit.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [{
id: 'W58',
reason: 'Invalid warning: function has access to cloudwatch'
},{
id: 'W89',
reason: 'AWS Lambda does not require VPC for this solution.'
},{
id: 'W92',
reason: 'ReservedConcurrentExecutions not required'
}]
}
};
/**
* Process outputs lambda function, invoked by CloudWatch events for MediaConvert.
* Parses the CW event outputs, creates the CloudFront URLs for the outputs, updates
* a manifest file in the destination bucket and send an SNS notfication.
* Enviroment variables for the destination bucket and SNS topic are added by the
* solutions constructs
*/
const jobComplete = new lambda.Function(this, 'JobComplete', {
code: lambda.Code.fromAsset(`../job-complete`),
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
timeout: cdk.Duration.seconds(30),
retryAttempts:0,
description: 'Triggered by Cloudwatch Events,processes completed MediaConvert jobs.',
environment: {
MEDIACONVERT_ENDPOINT: customResourceEndpoint.getAttString('Endpoint'),
CLOUDFRONT_DOMAIN: cloudFront.cloudFrontWebDistribution.distributionDomainName,
/** SNS_TOPIC_ARN: added by the solution construct below */
SOURCE_BUCKET: source.bucketName,
JOB_MANIFEST: 'jobs-manifest.json',
STACKNAME: cdk.Aws.STACK_NAME,
METRICS: cdk.Fn.findInMap('Send', 'AnonymousUsage', 'Data'),
SOLUTION_ID:'SO0146',
VERSION:'1.1.0',
UUID:customResourceEndpoint.getAttString('UUID'),
SOLUTION_IDENTIFIER: 'AwsSolution/SO0146/v1.1.0'
},
initialPolicy: [
new iam.PolicyStatement({
actions: ["mediaconvert:GetJob"],
resources: [`arn:${cdk.Aws.PARTITION}:mediaconvert:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:*`]
}),
new iam.PolicyStatement({
actions: ["s3:GetObject", "s3:PutObject"],
resources: [`${source.bucketArn}/*`]
})
]
});
const cfnJobComplete = jobComplete.node.findChild('Resource') as lambda.CfnFunction;
cfnJobComplete.cfnOptions.metadata = {
cfn_nag: {
rules_to_suppress: [{
id: 'W58',
reason: 'Invalid warning: function has access to cloudwatch'
},{
id: 'W89',
reason: 'AWS Lambda does not require VPC for this solution.'
},{
id: 'W92',
reason: 'ReservedConcurrentExecutions not required'
}]
}
};
/**
* Custom resource to configure the source S3 bucket; upload default job-settings file and
* enabble event notifications to trigger the job-submit lambda function
*/
new cdk.CustomResource(this, 'S3Config', {
serviceToken: customResourceLambda.functionArn,
properties: {
SourceBucket: source.bucketName,
LambdaArn: jobSubmit.functionArn
}
});
/**
* Solution constructs, creates a CloudWatch event rule to trigger the process
* outputs lambda functions.
*/
new EventsRuleToLambda(this, 'EventTrigger', {
existingLambdaObj: jobComplete,
eventRuleProps: {
enabled: true,
eventPattern: {
"source": ["aws.mediaconvert"],
"detail": {
"userMetadata": {
"StackName": [
cdk.Aws.STACK_NAME
]
},
"status": [
"COMPLETE",
"ERROR",
"CANCELED",
"INPUT_INFORMATION"
]
}
}
}
});
/**
* Solutions construct, creates an SNS topic and a Lambda function with permission
* to publish messages to the topic. Also adds the SNS topic to the lambda Enviroment
* varribles
*/
const snsTopic = new LambdaToSns(this, 'Notification', {
existingLambdaObj: jobSubmit
});
new LambdaToSns(this, 'CompleteSNS', {
existingLambdaObj: jobComplete,
existingTopicObj: snsTopic.snsTopic
});
/**
* Subscribe the admin email address to the SNS topic created but the construct.
*/
snsTopic.snsTopic.addSubscription(new subs.EmailSubscription(adminEmail.valueAsString))
/**
* Stack Outputs
*/
new cdk.CfnOutput(this, 'SourceBucket', {
value: source.bucketName,
description: 'Source S3 Bucket used to host source video and MediaConvert job settings files',
exportName: `${ cdk.Aws.STACK_NAME}-SourceBucket`
});
new cdk.CfnOutput(this, 'DestinationBucket', {
value: destination.bucketName,
description: 'Source S3 Bucket used to host all MediaConvert ouputs',
exportName: `${ cdk.Aws.STACK_NAME}-DestinationBucket`
});
new cdk.CfnOutput(this, 'CloudFrontDomain', {
value: cloudFront.cloudFrontWebDistribution.distributionDomainName,
description: 'CloudFront Domain Name',
exportName: `${ cdk.Aws.STACK_NAME}-CloudFrontDomain`
});
new cdk.CfnOutput(this, 'SnsTopic', {
value: snsTopic.snsTopic.topicName,
description: 'SNS Topic used to capture the VOD workflow outputs including errors',
exportName: `${ cdk.Aws.STACK_NAME}-SnsTopic`
});
}