diff --git a/aws-custom-cluster/template-ssl.yaml b/aws-custom-cluster/template-ssl.yaml new file mode 100644 index 00000000..846f1094 --- /dev/null +++ b/aws-custom-cluster/template-ssl.yaml @@ -0,0 +1,1082 @@ +AWSTemplateFormatVersion: '2010-09-09' + +Description: Ant Media Server - Self-Hosted Solution +Parameters: + KeyName: + Description: Name of an existing EC2 KeyPair + Type: AWS::EC2::KeyPair::KeyName + ConstraintDescription: must be the name of an existing EC2 KeyPair. + InstanceType: + Description: Ant Media Server Edge EC2 instance type + Type: String + Default: c5.xlarge + AllowedValues: + - t2.large + - t2.xlarge + - t2.2xlarge + - m3.large + - m3.xlarge + - m3.2xlarge + - m4.large + - m4.xlarge + - m4.2xlarge + - m4.4xlarge + - m4.10xlarge + - m4.16xlarge + - m5.large + - m5.xlarge + - m5.2xlarge + - m5.4xlarge + - m5.12xlarge + - m5.24xlarge + - c3.large + - c3.xlarge + - c3.2xlarge + - c3.4xlarge + - c3.8xlarge + - c4.large + - c4.xlarge + - c4.2xlarge + - c4.4xlarge + - c4.8xlarge + - c5.large + - c5.xlarge + - c5.2xlarge + - c5.4xlarge + - c5.9xlarge + - c5.12xlarge + - c5.18xlarge + - c5.24xlarge + - c5d.large + - c5d.xlarge + - c5d.2xlarge + - c5d.4xlarge + - c5d.9xlarge + - c5d.18xlarge + - c5n.large + - c5n.xlarge + - c5n.2xlarge + - c5n.4xlarge + - c5n.9xlarge + - c5n.18xlarge + - r3.large + - r3.xlarge + - r3.2xlarge + - r3.4xlarge + - r3.8xlarge + ConstraintDescription: must be a valid EC2 instance type. + +Resources: + + DescribeImagesRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: lambda.amazonaws.com + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: DescribeImages + PolicyDocument: + Version: '2012-10-17' + Statement: + - Action: ec2:DescribeImages + Effect: Allow + Resource: "*" + AMSGetLatestAMI: + Type: AWS::Lambda::Function + Properties: + Runtime: python3.11 + Handler: index.handler + Role: !Sub ${DescribeImagesRole.Arn} + Timeout: 60 + Code: + ZipFile: | + import boto3 + import cfnresponse + import json + import traceback + + def handler(event, context): + try: + response = boto3.client('ec2').describe_images( + Filters=[ + {'Name': 'product-code', 'Values': [event['ResourceProperties']['ProductId']]}, + {'Name': 'name', 'Values': [event['ResourceProperties']['Name']]}, + {'Name': 'architecture', 'Values': [event['ResourceProperties']['Architecture']]}, + {'Name': 'root-device-type', 'Values': ['ebs']}, + ], + ) + + amis = sorted(response['Images'], + key=lambda x: x['CreationDate'], + reverse=True) + id = amis[0]['ImageId'] + + cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, id) + except: + traceback.print_last() + cfnresponse.send(event, context, cfnresponse.FAIL, {}, "ok") + ACMCertificateImportFunction: + Type: AWS::Lambda::Function + Properties: + Code: + ZipFile: | + import boto3 + import cfnresponse + + def lambda_handler(event, context): + try: + acm_client = boto3.client('acm') + response = acm_client.import_certificate( + Certificate=event['ResourceProperties']['CertificateBody'], + PrivateKey=event['ResourceProperties']['PrivateKey'], + CertificateChain=event['ResourceProperties']['CertificateChain'] + ) + certificate_arn = response['CertificateArn'] + cfnresponse.send(event, context, cfnresponse.SUCCESS, {'CertificateArn': certificate_arn}) + except Exception as e: + print("Error:", e) + cfnresponse.send(event, context, cfnresponse.FAILED, {}) + + Handler: index.lambda_handler + Role: !Sub '${AcmCertificateImportLambdaExecutionRole.Arn}' + Runtime: python3.11 + AcmCertificateImportLambdaExecutionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: ['sts:AssumeRole'] + ManagedPolicyArns: [!Sub 'arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'] + Policies: + - PolicyName: main + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: Acm + Effect: Allow + Action: + - 'acm:AddTagsToCertificate' + - 'acm:ImportCertificate' + Resource: '*' + + CertificateImportCustomResource: + Type: Custom::ACMCertificateImport + Properties: + ServiceToken: !GetAtt ACMCertificateImportFunction.Arn + CertificateBody: | +---CertificateBody--- + PrivateKey: | +---PrivateKey--- + CertificateChain: | +---CertificaChain--- + AntMediaAmi: + Type: Custom::FindAMI + Properties: + ServiceToken: !Sub ${AMSGetLatestAMI.Arn} + ProductId: "4wh7rhpic3wfwamyp5905tsbt" + Name: "AntMedia-AWS-Marketplace-EE-*" + Architecture: "x86_64" + + AntMediaVPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: 10.0.0.0/16 + EnableDnsHostnames: true + EnableDnsSupport: true + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-AntMedia-VPC + + OriginZone: + Type: AWS::EC2::Subnet + DependsOn: AntMediaVPC + Properties: + VpcId: !Ref AntMediaVPC + CidrBlock: 10.0.1.0/24 + MapPublicIpOnLaunch: true + AvailabilityZone: + Fn::Select: + - 0 + - Fn::GetAZs: "" + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-AntMedia-Origin-Subnet + + EdgeZone: + Type: AWS::EC2::Subnet + DependsOn: AntMediaVPC + Properties: + VpcId: !Ref AntMediaVPC + CidrBlock: 10.0.2.0/24 + MapPublicIpOnLaunch: true + AvailabilityZone: + Fn::Select: + - 1 + - Fn::GetAZs: "" + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-AntMedia-Edge-Subnet + + DefaultGateway: + Type: AWS::EC2::InternetGateway + + InternetGatewayAttachment: + Type: AWS::EC2::VPCGatewayAttachment + Properties: + InternetGatewayId: !Ref DefaultGateway + VpcId: !Ref AntMediaVPC + + RouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref AntMediaVPC + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-AntMedia-Route-Table + + DefaultRoute: + Type: AWS::EC2::Route + DependsOn: InternetGatewayAttachment + Properties: + RouteTableId: !Ref RouteTable + GatewayId: !Ref DefaultGateway + DestinationCidrBlock: 0.0.0.0/0 + + SubnetRouteTableAssociationOrigin: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref RouteTable + SubnetId: !Ref OriginZone + + SubnetRouteTableAssociationEdge: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref RouteTable + SubnetId: !Ref EdgeZone + + ELBSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Allows access + VpcId: !Ref AntMediaVPC + SecurityGroupIngress: + - CidrIp: 0.0.0.0/0 + IpProtocol: tcp + FromPort: 80 + ToPort: 80 + Description: Allow 80. Port for Origin Instances + - CidrIp: 0.0.0.0/0 + IpProtocol: tcp + FromPort: 443 + ToPort: 443 + Description: Allow 443. Port for Origin Instances + - CidrIp: 0.0.0.0/0 + IpProtocol: tcp + FromPort: 5080 + ToPort: 5080 + Description: Allow 5080. Port for Edge Instances + - CidrIp: 0.0.0.0/0 + IpProtocol: tcp + FromPort: 5443 + ToPort: 5443 + Description: Allow 5443. Port for Edge Instances + - CidrIp: 0.0.0.0/0 + IpProtocol: tcp + FromPort: 4444 + ToPort: 4444 + Description: Allow 4444. Port for accessing Dashboard + + ApplicationLoadBalancer: + Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer' + Properties: + Subnets: + - !Ref OriginZone + - !Ref EdgeZone + SecurityGroups: + - !GetAtt [ ELBSecurityGroup, GroupId ] + + ALBTargetGroupOrigin: + Type: 'AWS::ElasticLoadBalancingV2::TargetGroup' + Properties: + HealthCheckIntervalSeconds: 30 + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 3 + Port: 5080 + Protocol: HTTP + UnhealthyThresholdCount: 5 + VpcId: !Ref AntMediaVPC + TargetGroupAttributes: + - Key: stickiness.enabled + Value: 'true' + - Key: stickiness.type + Value: lb_cookie + - Key: stickiness.lb_cookie.duration_seconds + Value: '30' + - Key: deregistration_delay.timeout_seconds + Value: 0 + + ALBTargetGroupLambda: + Type: 'AWS::ElasticLoadBalancingV2::TargetGroup' + Properties: + # Name: ALBTargetGroupLambda + TargetType: lambda + Targets: + - Id: !GetAtt LambdaFunctionTrigger.Arn + + ALBListener443: + Type: 'AWS::ElasticLoadBalancingV2::Listener' + Properties: + Certificates: + - CertificateArn: !GetAtt CertificateImportCustomResource.CertificateArn + DefaultActions: + - Type: forward + TargetGroupArn: !Ref ALBTargetGroupOrigin + LoadBalancerArn: !Ref ApplicationLoadBalancer + Port: '443' + Protocol: HTTPS + + ALBListenerRuleWithPath: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Properties: + Actions: + - Type: forward + TargetGroupArn: !Ref ALBTargetGroupLambda + Conditions: + - Field: path-pattern + Values: [ "*" ] + ListenerArn: !Ref ALBListener443 + Priority: 1 + + RedisServerlessCache: + Type: AWS::ElastiCache::ServerlessCache + Properties: + DailySnapshotTime: "03:00" + Description: "Ant Media Server - Redis Serverless Cache" + Engine: "Redis" + SecurityGroupIds: + - !GetAtt "InstanceSecurityGroup.GroupId" + ServerlessCacheName: !Sub ${AWS::StackName}-ServerlessRedis + SubnetIds: + - !Ref OriginZone + - !Ref EdgeZone + Tags: + - Key: Name + Value: RedisCache + + OriginGroup: + Type: 'AWS::AutoScaling::AutoScalingGroup' + DependsOn: + - LaunchTemplateOrigin + Properties: + VPCZoneIdentifier: + - !Ref OriginZone + LaunchTemplate: + LaunchTemplateName: !Sub ${AWS::StackName}-AntMedia-LaunchTemplateOrigin + Version: !GetAtt 'LaunchTemplateOrigin.LatestVersionNumber' + MinSize: 0 + MaxSize: 100 + DesiredCapacity: 0 + TargetGroupARNs: + - !Ref ALBTargetGroupOrigin + Tags: + - Key: Name + Value: Ant-Media-Server + PropagateAtLaunch: 'true' + + LaunchTemplateOrigin: + Type: 'AWS::EC2::LaunchTemplate' + Properties: + LaunchTemplateName: !Sub ${AWS::StackName}-AntMedia-LaunchTemplateOrigin + LaunchTemplateData: + InstanceType: !Ref InstanceType + KeyName: !Ref KeyName + ImageId: !Ref AntMediaAmi + SecurityGroupIds: + - !GetAtt "InstanceSecurityGroup.GroupId" + BlockDeviceMappings: + - DeviceName: /dev/sda1 + Ebs: + VolumeSize: 10 + VolumeType: gp2 + DeleteOnTermination: true + UserData: + Fn::Base64: !Sub | + #!/bin/bash + sudo apt-get update -y + sudo apt-get install stunnel python3-pip -y + sudo tee /etc/stunnel/stunnel.conf > /dev/null < { + console.log('Request successfully sent.'); + console.log(api); + console.log(api_key) + }) + .catch(error => { + console.error('Error:', error); + }); + + }; + + var onExpire = function(response) { + var logEl = document.querySelector(".hcaptcha-success"); + if (!logEl) { + logEl = document.createElement("div"); + logEl.className = "hcaptcha-success"; + document.body.appendChild(logEl); + } + logEl.innerHTML = "Token expired."; + }; + """ + + # if response.status_code == 200: + html_content = f""" + + + + + Ant Media Server will be starting + + + +

Get Ready!

+

Ant Media Server will be starting

+
+
+ +
+
+
+ +
+ + + + """ + + # else: + # html_content = "Waiting for the server to be ready" + + response = { + "statusCode": 200, + "headers": { + "Content-Type": "text/html", + }, + "body": html_content + } + + return response + + Handler: "index.lambda_handler" + Role: !GetAtt LambdaIamRole.Arn + Runtime: python3.12 + Timeout: 60 + MemorySize: 128 + Environment: + Variables: + API_URL: !Sub "https://${ApiGatewayRestApi}.execute-api.${AWS::Region}.amazonaws.com/" + #TARGETGROUP_ARN: !Ref ALBTargetGroupLambda + + + + LambdaCloudwatchFunction: + Type: AWS::Lambda::Function + Properties: + Code: + ZipFile: | + import boto3, os + + def lambda_handler(event, context): + listener_arn = os.environ['LISTENER_ARN'] + origin_asg = os.environ['ORIGIN_ASG'] + target_group_arn = os.environ['TARGETGROUP_ARN'] + autoscaling_client = boto3.client('autoscaling') + elb_client = boto3.client('elbv2') + asg_names = autoscaling_client.describe_auto_scaling_groups() + asg_origin_name = [group for group in asg_names['AutoScalingGroups'] if + origin_asg in group['AutoScalingGroupName']] + asg_origin_group_names = [group['AutoScalingGroupName'] for group in asg_origin_name][0] + + origin_calculate_total_instance = autoscaling_client.describe_auto_scaling_groups(AutoScalingGroupNames=[asg_origin_group_names]) + origin_current_capacity = len(origin_calculate_total_instance['AutoScalingGroups'][0]['Instances']) + + if origin_current_capacity >= 1: + origin_response = autoscaling_client.update_auto_scaling_group( + AutoScalingGroupName=asg_origin_group_names, + MinSize=0, + DesiredCapacity=0 + ) + + create_rule = elb_client.create_rule( + Actions=[ + { + 'Type': 'forward', + 'TargetGroupArn': target_group_arn + } + ], + Conditions=[ + { + 'Field': 'path-pattern', + 'Values': ['*'] + } + ], + ListenerArn=listener_arn, + Priority=1 + ) + + print(origin_response) + + return { + 'statusCode': 200, + 'body': 'Auto Scaling Group updated successfully!' + } + else: + return { + 'statusCode': 200, + 'body': 'Auto Scaling Group does not require update.' + } + #FunctionName: InstanceDeleteFunction + Handler: "index.lambda_handler" + Role: !GetAtt LambdaIamRole.Arn + Runtime: python3.12 + Timeout: 60 + MemorySize: 128 + Environment: + Variables: + ORIGIN_ASG: !Sub "${AWS::StackName}-OriginGroup" + LISTENER_ARN: !Ref ALBListener443 + TARGETGROUP_ARN: !Ref ALBTargetGroupLambda + LambdaGetApiKey: + Type: AWS::Lambda::Function + DependsOn: ApiGatewayApiKey + Properties: + Code: + ZipFile: | + import boto3 + import cfnresponse + import traceback + + def lambda_handler(event, context): + try: + api_key_name = "AMSAPIKey" + + client = boto3.client('apigateway') + + response = client.get_api_keys( + nameQuery=api_key_name, + includeValues=True + ) + + if response['ResponseMetadata']['HTTPStatusCode'] == 200: + api_key_value = response['items'][0]['value'] + print("API Key Value:", api_key_value) + cfnresponse.send(event, context, cfnresponse.SUCCESS, {"APIKeyValue": api_key_value}) + return {"APIKeyValue": api_key_value} + else: + cfnresponse.send(event, context, cfnresponse.FAILED, {"Error": "Failed to retrieve API key"}) + return {"Error": "Failed to retrieve API key"} + except Exception as e: + traceback.print_exc() + cfnresponse.send(event, context, cfnresponse.FAILED, {"Error": str(e)}) + return {"Error": str(e)} + #Environment: + # Variables: + # API_KEY_NAME: !Sub "${AWS::StackName}-OriginGroup" + Description: AWS Lambda function + Handler: "index.lambda_handler" + MemorySize: 256 + Role: !GetAtt LambdaIamRole.Arn + Runtime: python3.12 + Timeout: 60 + + LambdaApiValue: + Type: Custom::LambdaApiKey + DependsOn: LambdaGetApiKey + Properties: + ServiceToken: !Sub "${LambdaGetApiKey.Arn}" + + # General IAM Role for Lambda Functions + LambdaIamRole: + Type: "AWS::IAM::Role" + Properties: + RoleName: !Sub "MyLambdaExecutionRole-${AWS::StackName}" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: "lambda.amazonaws.com" + Action: "sts:AssumeRole" + Policies: + - PolicyName: "EC2FullAccessPolicy1" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Action: + - "ec2:RunInstances" + - "ec2:CreateTags" + - "ec2:DescribeInstances" + - "autoscaling:UpdateAutoScalingGroup" + - "autoscaling:DescribeAutoScalingGroups" + - "ec2:CreateNetworkInterface" + - "ec2:DescribeNetworkInterfaces" + - "ec2:DeleteNetworkInterface" + - "elasticloadbalancing:DescribeRules" + - "elasticloadbalancing:DeleteRule" + - "elasticloadbalancing:DescribeTargetGroups" + - "elasticloadbalancing:CreateRule" + - "apigateway:GET" + - "acm:DescribeCertificate" + Resource: "*" + - PolicyName: "CloudWatchLogsPolicy" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Action: + - "logs:CreateLogGroup" + - "logs:CreateLogStream" + - "logs:PutLogEvents" + Resource: "*" + Path: '/' + + # Lambda Resource Based Policy for Cloudwatch + LambdaInvokePermission: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: !Ref LambdaCloudwatchFunction + Principal: lambda.alarms.cloudwatch.amazonaws.com + SourceArn: !GetAtt AutoScalingGroupScaleDownAlarm.Arn + + ELBLambdaInvokePermission: + Type: AWS::Lambda::Permission + Properties: + FunctionName: !GetAtt LambdaFunctionTrigger.Arn + Action: lambda:InvokeFunction + Principal: elasticloadbalancing.amazonaws.com + + # Cloudwatch rule to set 0 in Autoscale + AutoScalingGroupScaleDownAlarm: + Type: 'AWS::CloudWatch::Alarm' + Properties: + AlarmName: AutoScalingGroupScaleDownAlarm + ComparisonOperator: LessThanOrEqualToThreshold + EvaluationPeriods: 3 + MetricName: CPUUtilization + Namespace: AWS/EC2 + Period: 120 + Statistic: Average + Threshold: 10 + ActionsEnabled: true + AlarmActions: + - !GetAtt LambdaCloudwatchFunction.Arn + Dimensions: + - Name: AutoScalingGroupName + Value: !Ref OriginGroup +Outputs: + EndPoint: + Description: Load Balancer Address of Ant Media Server + Value: !Join + - '' + - - 'https://' + - !GetAtt + - ApplicationLoadBalancer + - DNSName +# ApiKey: +# Description: API Key +# Value: !GetAtt LambdaApiValue.APIKeyValue diff --git a/aws-custom-cluster/template.yaml b/aws-custom-cluster/template.yaml new file mode 100644 index 00000000..b0176c7c --- /dev/null +++ b/aws-custom-cluster/template.yaml @@ -0,0 +1,1149 @@ +AWSTemplateFormatVersion: '2010-09-09' + +Description: Ant Media Server - Self-Hosted Solution +Parameters: + KeyName: + Description: Name of an existing EC2 KeyPair + Type: AWS::EC2::KeyPair::KeyName + ConstraintDescription: must be the name of an existing EC2 KeyPair. + LoadBalancerCertificateArn: + Description: 'Amazon Resource Name (ARN) of the certificate to associate with the load balancer. If you do not have the SSL certificate, please check this guide: https://antmedia.io/ant-media-server-cloudformation-installation/ ' + Type: String + Default: '' + InstanceType: + Description: Ant Media Server Edge EC2 instance type + Type: String + Default: c5.xlarge + AllowedValues: + - t2.large + - t2.xlarge + - t2.2xlarge + - m3.large + - m3.xlarge + - m3.2xlarge + - m4.large + - m4.xlarge + - m4.2xlarge + - m4.4xlarge + - m4.10xlarge + - m4.16xlarge + - m5.large + - m5.xlarge + - m5.2xlarge + - m5.4xlarge + - m5.12xlarge + - m5.24xlarge + - c3.large + - c3.xlarge + - c3.2xlarge + - c3.4xlarge + - c3.8xlarge + - c4.large + - c4.xlarge + - c4.2xlarge + - c4.4xlarge + - c4.8xlarge + - c5.large + - c5.xlarge + - c5.2xlarge + - c5.4xlarge + - c5.9xlarge + - c5.12xlarge + - c5.18xlarge + - c5.24xlarge + - c5d.large + - c5d.xlarge + - c5d.2xlarge + - c5d.4xlarge + - c5d.9xlarge + - c5d.18xlarge + - c5n.large + - c5n.xlarge + - c5n.2xlarge + - c5n.4xlarge + - c5n.9xlarge + - c5n.18xlarge + - r3.large + - r3.xlarge + - r3.2xlarge + - r3.4xlarge + - r3.8xlarge + ConstraintDescription: must be a valid EC2 instance type. + +Resources: + + DescribeImagesRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: lambda.amazonaws.com + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: DescribeImages + PolicyDocument: + Version: '2012-10-17' + Statement: + - Action: ec2:DescribeImages + Effect: Allow + Resource: "*" + AMSGetLatestAMI: + Type: AWS::Lambda::Function + Properties: + Runtime: python3.11 + Handler: index.handler + Role: !Sub ${DescribeImagesRole.Arn} + Timeout: 60 + Code: + ZipFile: | + import boto3 + import cfnresponse + import json + import traceback + + def handler(event, context): + try: + response = boto3.client('ec2').describe_images( + Filters=[ + {'Name': 'product-code', 'Values': [event['ResourceProperties']['ProductId']]}, + {'Name': 'name', 'Values': [event['ResourceProperties']['Name']]}, + {'Name': 'architecture', 'Values': [event['ResourceProperties']['Architecture']]}, + {'Name': 'root-device-type', 'Values': ['ebs']}, + ], + ) + + amis = sorted(response['Images'], + key=lambda x: x['CreationDate'], + reverse=True) + id = amis[0]['ImageId'] + + cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, id) + except: + traceback.print_last() + cfnresponse.send(event, context, cfnresponse.FAIL, {}, "ok") + AntMediaAmi: + Type: Custom::FindAMI + Properties: + ServiceToken: !Sub ${AMSGetLatestAMI.Arn} + ProductId: "4wh7rhpic3wfwamyp5905tsbt" + Name: "AntMedia-AWS-Marketplace-EE-*" + Architecture: "x86_64" + + AntMediaVPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: 10.0.0.0/16 + EnableDnsHostnames: true + EnableDnsSupport: true + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-AntMedia-VPC + + OriginZone: + Type: AWS::EC2::Subnet + DependsOn: AntMediaVPC + Properties: + VpcId: !Ref AntMediaVPC + CidrBlock: 10.0.1.0/24 + MapPublicIpOnLaunch: true + AvailabilityZone: + Fn::Select: + - 0 + - Fn::GetAZs: "" + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-AntMedia-Origin-Subnet + + EdgeZone: + Type: AWS::EC2::Subnet + DependsOn: AntMediaVPC + Properties: + VpcId: !Ref AntMediaVPC + CidrBlock: 10.0.2.0/24 + MapPublicIpOnLaunch: true + AvailabilityZone: + Fn::Select: + - 1 + - Fn::GetAZs: "" + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-AntMedia-Edge-Subnet + + DefaultGateway: + Type: AWS::EC2::InternetGateway + + InternetGatewayAttachment: + Type: AWS::EC2::VPCGatewayAttachment + Properties: + InternetGatewayId: !Ref DefaultGateway + VpcId: !Ref AntMediaVPC + + RouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref AntMediaVPC + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-AntMedia-Route-Table + + DefaultRoute: + Type: AWS::EC2::Route + DependsOn: InternetGatewayAttachment + Properties: + RouteTableId: !Ref RouteTable + GatewayId: !Ref DefaultGateway + DestinationCidrBlock: 0.0.0.0/0 + + SubnetRouteTableAssociationOrigin: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref RouteTable + SubnetId: !Ref OriginZone + + SubnetRouteTableAssociationEdge: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref RouteTable + SubnetId: !Ref EdgeZone + + ELBSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupDescription: Allows access + VpcId: !Ref AntMediaVPC + SecurityGroupIngress: + - CidrIp: 0.0.0.0/0 + IpProtocol: tcp + FromPort: 80 + ToPort: 80 + Description: Allow 80. Port for Origin Instances + - CidrIp: 0.0.0.0/0 + IpProtocol: tcp + FromPort: 443 + ToPort: 443 + Description: Allow 443. Port for Origin Instances + - CidrIp: 0.0.0.0/0 + IpProtocol: tcp + FromPort: 5080 + ToPort: 5080 + Description: Allow 5080. Port for Edge Instances + - CidrIp: 0.0.0.0/0 + IpProtocol: tcp + FromPort: 5443 + ToPort: 5443 + Description: Allow 5443. Port for Edge Instances + - CidrIp: 0.0.0.0/0 + IpProtocol: tcp + FromPort: 4444 + ToPort: 4444 + Description: Allow 4444. Port for accessing Dashboard + + ApplicationLoadBalancer: + Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer' + Properties: + Subnets: + - !Ref OriginZone + - !Ref EdgeZone + SecurityGroups: + - !GetAtt [ ELBSecurityGroup, GroupId ] + + ALBTargetGroupOrigin: + Type: 'AWS::ElasticLoadBalancingV2::TargetGroup' + Properties: + HealthCheckIntervalSeconds: 30 + HealthCheckTimeoutSeconds: 5 + HealthyThresholdCount: 3 + Port: 5080 + Protocol: HTTP + UnhealthyThresholdCount: 5 + VpcId: !Ref AntMediaVPC + TargetGroupAttributes: + - Key: stickiness.enabled + Value: 'true' + - Key: stickiness.type + Value: lb_cookie + - Key: stickiness.lb_cookie.duration_seconds + Value: '30' + - Key: deregistration_delay.timeout_seconds + Value: 0 + + ALBTargetGroupLambda: + Type: 'AWS::ElasticLoadBalancingV2::TargetGroup' + Properties: + # Name: ALBTargetGroupLambda + TargetType: lambda + Targets: + - Id: !GetAtt LambdaFunctionTrigger.Arn + + ALBListener443: + Type: 'AWS::ElasticLoadBalancingV2::Listener' + Properties: + Certificates: + - CertificateArn: !Ref LoadBalancerCertificateArn + DefaultActions: + - Type: forward + TargetGroupArn: !Ref ALBTargetGroupOrigin + LoadBalancerArn: !Ref ApplicationLoadBalancer + Port: '443' + Protocol: HTTPS + + ALBListenerRuleWithPath: + Type: AWS::ElasticLoadBalancingV2::ListenerRule + Properties: + Actions: + - Type: forward + TargetGroupArn: !Ref ALBTargetGroupLambda + Conditions: + - Field: path-pattern + Values: [ "*" ] + ListenerArn: !Ref ALBListener443 + Priority: 1 + + RedisServerlessCache: + Type: AWS::ElastiCache::ServerlessCache + Properties: + DailySnapshotTime: "03:00" + Description: "Ant Media Server - Redis Serverless Cache" + Engine: "Redis" + SecurityGroupIds: + - !GetAtt "InstanceSecurityGroup.GroupId" + ServerlessCacheName: !Sub ${AWS::StackName}-ServerlessRedis + SubnetIds: + - !Ref OriginZone + - !Ref EdgeZone + Tags: + - Key: Name + Value: RedisCache + + OriginGroup: + Type: 'AWS::AutoScaling::AutoScalingGroup' + DependsOn: + - LaunchTemplateOrigin + Properties: + VPCZoneIdentifier: + - !Ref OriginZone + LaunchTemplate: + LaunchTemplateName: !Sub ${AWS::StackName}-AntMedia-LaunchTemplateOrigin + Version: !GetAtt 'LaunchTemplateOrigin.LatestVersionNumber' + MinSize: 0 + MaxSize: 100 + DesiredCapacity: 0 + TargetGroupARNs: + - !Ref ALBTargetGroupOrigin + Tags: + - Key: Name + Value: Ant-Media-Server + PropagateAtLaunch: 'true' + + LaunchTemplateOrigin: + Type: 'AWS::EC2::LaunchTemplate' + Properties: + LaunchTemplateName: !Sub ${AWS::StackName}-AntMedia-LaunchTemplateOrigin + LaunchTemplateData: + InstanceType: !Ref InstanceType + KeyName: !Ref KeyName + ImageId: !Ref AntMediaAmi + SecurityGroupIds: + - !GetAtt "InstanceSecurityGroup.GroupId" + IamInstanceProfile: + Arn: !GetAtt InstanceProfile.Arn + BlockDeviceMappings: + - DeviceName: /dev/sda1 + Ebs: + VolumeSize: 10 + VolumeType: gp2 + DeleteOnTermination: true + UserData: + Fn::Base64: !Sub | + #!/bin/bash + sudo apt-get update -y + sudo apt-get install stunnel python3-pip jq -y + sudo tee /etc/stunnel/stunnel.conf > /dev/null < { + console.log('Request successfully sent.'); + console.log(api); + console.log(api_key) + }) + .catch(error => { + console.error('Error:', error); + }); + + }; + + var onExpire = function(response) { + var logEl = document.querySelector(".hcaptcha-success"); + if (!logEl) { + logEl = document.createElement("div"); + logEl.className = "hcaptcha-success"; + document.body.appendChild(logEl); + } + logEl.innerHTML = "Token expired."; + }; + """ + + # if response.status_code == 200: + html_content = f""" + + + + + Ant Media Server will be starting + + + +

Get Ready!

+

Ant Media Server will be starting

+
+ + +
+
+
+ +
+ + + + """ + + # else: + # html_content = "Waiting for the server to be ready" + + response = { + "statusCode": 200, + "headers": { + "Content-Type": "text/html", + }, + "body": html_content + } + + return response + + Handler: "index.lambda_handler" + Role: !GetAtt LambdaIamRole.Arn + Runtime: python3.12 + Timeout: 60 + MemorySize: 128 + Environment: + Variables: + API_URL: !Sub "https://${ApiGatewayRestApi}.execute-api.${AWS::Region}.amazonaws.com/" + #TARGETGROUP_ARN: !Ref ALBTargetGroupLambda + + + + LambdaCloudwatchFunction: + Type: AWS::Lambda::Function + Properties: + Code: + ZipFile: | + import boto3, os, time, json + + def lambda_handler(event, context): + + listener_arn = os.environ['LISTENER_ARN'] + origin_asg = os.environ['ORIGIN_ASG'] + target_group_arn = os.environ['TARGETGROUP_ARN'] + alarm_name = 'AutoScalingGroupScaleDownAlarm' + autoscaling_client = boto3.client('autoscaling') + elb_client = boto3.client('elbv2') + ec2_client = boto3.client("ec2") + ssm_client = boto3.client('ssm') + cloudwatch_client = boto3.client('cloudwatch') + + script = """ + #/usr/bin/env bash -x + sed -i 's|INSTALL_DIRECTORY="$1"|INSTALL_DIRECTORY="/usr/local/antmedia"|g' /usr/local/antmedia/conf/jwt_generator.sh + . /usr/local/antmedia/conf/jwt_generator.sh + generate_jwt + REST_URL="http://localhost:5080/rest/v2/applications-info" + curl -s -L "$REST_URL" --header "ProxyAuthorization: $JWT_KEY" -o /tmp/curl_output.txt + jq '[.[] | select(.liveStreamCount > 0)] | length > 0' /tmp/curl_output.txt + """ + + asg_names = autoscaling_client.describe_auto_scaling_groups() + asg_origin_name = [group for group in asg_names['AutoScalingGroups'] if + origin_asg in group['AutoScalingGroupName']] + asg_origin_group_names = [group['AutoScalingGroupName'] for group in asg_origin_name][0] + + print(f"Auto Scaling Group name: {asg_origin_group_names}") + origin_calculate_total_instance = autoscaling_client.describe_auto_scaling_groups(AutoScalingGroupNames=[asg_origin_group_names]) + origin_current_capacity = len(origin_calculate_total_instance['AutoScalingGroups'][0]['Instances']) + instance = origin_calculate_total_instance['AutoScalingGroups'][0]['Instances'] + try: + instance_id = instance[0]['InstanceId'] + print("Current capacity and Instance ID", {"origin_current_capacity": origin_current_capacity, "instance_id": instance_id}) + except IndexError: + cloudwatch_set_ok(alarm_name) + print("No instances found in Auto Scaling Group", asg_origin_group_names) + return { + 'statusCode': 200, + 'body': json.dumps({"message": "No instances found in Auto Scaling Group"}) + } + + smm_response = ssm_client.send_command( + DocumentName ='AWS-RunShellScript', + Parameters = {'commands': [script]}, + InstanceIds = [instance_id] + ) + + command_id = smm_response['Command']['CommandId'] + + while True: + time.sleep(2) + invocation_response = ssm_client.get_command_invocation( + CommandId=command_id, + InstanceId=instance_id, + ) + if invocation_response['Status'] not in ['Pending', 'InProgress']: + break + + smm_output = invocation_response['StandardOutputContent'].strip() + print(f"SSM command output: {smm_output}") + #debug + #print(invocation_response['StandardOutputContent']) + #error = invocation_response['StandardErrorContent'].strip() + + if origin_current_capacity == 1: + if smm_output == 'false': + print("No live streams found. Updating Auto Scaling Group.") + origin_response = autoscaling_client.update_auto_scaling_group( + AutoScalingGroupName=asg_origin_group_names, + MinSize=0, + DesiredCapacity=0 + ) + + create_rule = elb_client.create_rule( + Actions=[ + { + 'Type': 'forward', + 'TargetGroupArn': target_group_arn + } + ], + Conditions=[ + { + 'Field': 'path-pattern', + 'Values': ['*'] + } + ], + ListenerArn=listener_arn, + Priority=1 + ) + cloudwatch_set_ok(alarm_name) + print(origin_response) + return { + 'statusCode': 200, + 'body': 'Auto Scaling Group updated successfully!' + } + else: + print("Live streams found.") + else: + print(f"Current capacity is not 1, it is {origin_current_capacity}. No updates needed.") + + cloudwatch_set_ok(alarm_name) + return { + 'statusCode': 200, + 'body': 'Auto Scaling Group does not require update.' + } + + + def cloudwatch_set_ok(alarm_name): + try: + cloudwatch_client = boto3.client('cloudwatch') + cloudwatch_response = cloudwatch_client.set_alarm_state( + AlarmName=alarm_name, + StateValue='OK', + StateReason='Updating alarm state to OK' + ) + print("Alarm state updated to OK", cloudwatch_response) + return cloudwatch_response + except Exception as e: + error_message = { + "error": str(e), + "function": "cloudwatch_set_ok", + "alarm_name": alarm_name + } + print(json.dumps(error_message)) + raise e + + #FunctionName: InstanceDeleteFunction + Handler: "index.lambda_handler" + Role: !GetAtt LambdaIamRole.Arn + Runtime: python3.12 + Timeout: 60 + MemorySize: 128 + Environment: + Variables: + ORIGIN_ASG: !Sub "${AWS::StackName}-OriginGroup" + LISTENER_ARN: !Ref ALBListener443 + TARGETGROUP_ARN: !Ref ALBTargetGroupLambda + LambdaGetApiKey: + Type: AWS::Lambda::Function + DependsOn: ApiGatewayApiKey + Properties: + Code: + ZipFile: | + import boto3 + import cfnresponse + import traceback + + def lambda_handler(event, context): + try: + api_key_name = "AMSAPIKey" + + client = boto3.client('apigateway') + + response = client.get_api_keys( + nameQuery=api_key_name, + includeValues=True + ) + + if response['ResponseMetadata']['HTTPStatusCode'] == 200: + api_key_value = response['items'][0]['value'] + print("API Key Value:", api_key_value) + cfnresponse.send(event, context, cfnresponse.SUCCESS, {"APIKeyValue": api_key_value}) + return {"APIKeyValue": api_key_value} + else: + cfnresponse.send(event, context, cfnresponse.FAILED, {"Error": "Failed to retrieve API key"}) + return {"Error": "Failed to retrieve API key"} + except Exception as e: + traceback.print_exc() + cfnresponse.send(event, context, cfnresponse.FAILED, {"Error": str(e)}) + return {"Error": str(e)} + #Environment: + # Variables: + # API_KEY_NAME: !Sub "${AWS::StackName}-OriginGroup" + Description: AWS Lambda function + Handler: "index.lambda_handler" + MemorySize: 256 + Role: !GetAtt LambdaIamRole.Arn + Runtime: python3.12 + Timeout: 60 + + LambdaApiValue: + Type: Custom::LambdaApiKey + DependsOn: LambdaGetApiKey + Properties: + ServiceToken: !Sub "${LambdaGetApiKey.Arn}" + + # General IAM Role for Lambda Functions + LambdaIamRole: + Type: "AWS::IAM::Role" + Properties: + RoleName: !Sub "MyLambdaExecutionRole-${AWS::StackName}" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Principal: + Service: "lambda.amazonaws.com" + Action: "sts:AssumeRole" + Policies: + - PolicyName: "AccessPolicy" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Action: + - "ec2:RunInstances" + - "ec2:CreateTags" + - "ec2:DescribeInstances" + - "autoscaling:UpdateAutoScalingGroup" + - "autoscaling:DescribeAutoScalingGroups" + - "ec2:CreateNetworkInterface" + - "ec2:DescribeNetworkInterfaces" + - "ec2:DeleteNetworkInterface" + - "elasticloadbalancing:DescribeRules" + - "elasticloadbalancing:DeleteRule" + - "elasticloadbalancing:DescribeTargetGroups" + - "elasticloadbalancing:CreateRule" + - "apigateway:GET" + - "acm:DescribeCertificate" + - "ssm:GetCommandInvocation" + - "ssm:SendCommand" + - "cloudwatch:SetAlarmState" + Resource: "*" + - PolicyName: "CloudWatchLogsPolicy" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: "Allow" + Action: + - "logs:CreateLogGroup" + - "logs:CreateLogStream" + - "logs:PutLogEvents" + Resource: "*" + Path: '/' + + # Lambda Resource Based Policy for Cloudwatch + LambdaInvokePermission: + Type: AWS::Lambda::Permission + Properties: + Action: lambda:InvokeFunction + FunctionName: !Ref LambdaCloudwatchFunction + Principal: lambda.alarms.cloudwatch.amazonaws.com + SourceArn: !GetAtt AutoScalingGroupScaleDownAlarm.Arn + + ELBLambdaInvokePermission: + Type: AWS::Lambda::Permission + Properties: + FunctionName: !GetAtt LambdaFunctionTrigger.Arn + Action: lambda:InvokeFunction + Principal: elasticloadbalancing.amazonaws.com + + InstanceProfile: + Type: 'AWS::IAM::InstanceProfile' + Properties: + Roles: + - !Ref EC2Role + + EC2Role: + Type: 'AWS::IAM::Role' + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: [ec2.amazonaws.com] + Action: ['sts:AssumeRole'] + Policies: + - PolicyName: AllowSSM + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - "ssm:*" + - "cloudwatch:PutMetricData" + - "ds:CreateComputer" + - "ds:DescribeDirectories" + - "ec2:DescribeInstanceStatus" + - "logs:*" + - "ssm:*" + - "ec2messages:*" + Resource: '*' + + # Cloudwatch rule to set 0 in Autoscale + AutoScalingGroupScaleDownAlarm: + Type: 'AWS::CloudWatch::Alarm' + Properties: + AlarmName: AutoScalingGroupScaleDownAlarm + ComparisonOperator: LessThanOrEqualToThreshold + EvaluationPeriods: 3 + MetricName: CPUUtilization + Namespace: AWS/EC2 + Period: 120 + Statistic: Average + Threshold: 10 + ActionsEnabled: true + AlarmActions: + - !GetAtt LambdaCloudwatchFunction.Arn + Dimensions: + - Name: AutoScalingGroupName + Value: !Ref OriginGroup +Outputs: + EndPoint: + Description: Load Balancer Address of Ant Media Server + Value: !Join + - '' + - - 'https://' + - !GetAtt + - ApplicationLoadBalancer + - DNSName +# ApiKey: +# Description: API Key +# Value: !GetAtt LambdaApiValue.APIKeyValue