def declare_domain(self, domain_name: str, hosted_zone_id: str, stage_name: str) -> list[AWSObject]: """Declare a custom domain for one of the API stage. Note that when a custom domain is created then a certificate is automatically created for that domain. :param domain_name: domain name :param hosted_zone_id: hosted zone in which the domain belongs to :param stage_name: stage that should be associated with that domain :return: a list of AWSObject """ result = [] certificate_id = name_to_id(self.name + domain_name + "Certificate") certificate = Certificate( certificate_id, DomainName=domain_name, DomainValidationOptions=[ DomainValidationOption(DomainName=domain_name, HostedZoneId=hosted_zone_id) ], ValidationMethod="DNS", ) result.append(certificate) domain = apigatewayv2.DomainName( name_to_id(self.name + domain_name + "Domain"), DomainName=domain_name, DomainNameConfigurations=[ apigatewayv2.DomainNameConfiguration( CertificateArn=certificate.ref()) ], ) result.append(domain) result.append( apigatewayv2.ApiMapping( name_to_id(self.name + domain_name + "ApiMapping"), DomainName=domain.ref(), ApiId=self.ref, Stage=self.stage_ref(stage_name), )) result.append( route53.RecordSetType( name_to_id(self.name + domain_name + "DNS"), Name=domain_name, Type="A", HostedZoneId=hosted_zone_id, AliasTarget=route53.AliasTarget( DNSName=GetAtt( name_to_id(self.name + domain_name + "Domain"), "RegionalDomainName", ), HostedZoneId=GetAtt( name_to_id(self.name + domain_name + "Domain"), "RegionalHostedZoneId", ), EvaluateTargetHealth=False, ), )) return result
def test_acm_non_alphanumeric(self): project_config = ProjectConfig( 'tests/cloudformation/sample-project_acm.yaml', 'dev') config_parser = ConfigParser(project_config.config, 'my-stack-name') certificate_name = 'mycert-dev.something.io' parsed_certificate_name = 'mycertdevsomethingio' domain_name = 'helloworld.test.dsd.io' tags = [{ 'Key': 'Name', 'Value': { 'Fn::Join': ['', [{ 'Ref': u'AWS::StackName' }, '-', u'acm']] } }] subject_alternative_names = [] domain_validation_options = DomainValidationOption( DomainName=domain_name, ValidationDomain=domain_name) ACMCertificate = Certificate( parsed_certificate_name, DomainName=domain_name, SubjectAlternativeNames=subject_alternative_names, DomainValidationOptions=[domain_validation_options], Tags=tags) certificate_cfg = [ config_parser._get_acm_certificate(certificate_name) ] expected = [ACMCertificate] compare(self._resources_to_dict(expected), self._resources_to_dict(certificate_cfg))
def create_acm_certificate(stack, domain_name, alternate_names=[]): """Add ACM Certificate Resource.""" return stack.stack.add_resource(Certificate( 'mycert', DomainName='{0}'.format(domain_name), SubjectAlternativeNames=alternate_names, DomainValidationOptions=[ DomainValidationOption( DomainName='{0}'.format(domain_name), ValidationDomain='{0}'.format(domain_name), ), ], ))
def test_acm(self): project_config = ProjectConfig( 'tests/cloudformation/sample-project_acm.yaml', 'dev') config_parser = ConfigParser(project_config.config, 'my-stack-name') certificate_name = 'mycert' domain_name = 'helloworld.test.dsd.io' validation_domain = 'dsd.io' tags = [{ 'Key': 'Name', 'Value': { 'Fn::Join': ['', [{ 'Ref': u'AWS::StackName' }, '-', u'acm']] } }, { 'Key': 'test_key1', 'Value': 'test_value_1' }, { 'Key': 'test_key2', 'Value': 'test_value_2' }] subject_alternative_names = [ 'goodbye.test.somewhere.io', 'hello_again.subdomain.dsd.io' ] # The main domain should use validation_domain, # 'goodbye.test.somewhere.io' should use the domain_validation_options # to validate on somewhere.io. # 'hello_again.subdomain.dsd.io' has no validation_options so should # default to the validation_domain domain_validation_options = [ DomainValidationOption(DomainName=domain_name, ValidationDomain=validation_domain), DomainValidationOption(DomainName='goodbye.test.somewhere.io', ValidationDomain='somewhere.io'), DomainValidationOption(DomainName='hello_again.subdomain.dsd.io', ValidationDomain=validation_domain) ] ACMCertificate = Certificate( certificate_name, DomainName=domain_name, SubjectAlternativeNames=subject_alternative_names, DomainValidationOptions=domain_validation_options, Tags=tags) certificate_cfg = [ config_parser._get_acm_certificate(certificate_name) ] expected = [ACMCertificate] compare(self._resources_to_dict(expected), self._resources_to_dict(certificate_cfg))
) custom_app_certificate_arn_condition = "CustomAppCertArnCondition" template.add_condition(custom_app_certificate_arn_condition, Not(Equals(Ref(custom_app_certificate_arn), ""))) stack_cert_condition = "StackCertificateCondition" template.add_condition(stack_cert_condition, Not(Equals(Ref(certificate_validation_method), dont_create_value))) cert_condition = "CertificateCondition" template.add_condition(cert_condition, Or( Not(Equals(Ref(custom_app_certificate_arn), "")), Not(Equals(Ref(certificate_validation_method), dont_create_value)) )) application = If(custom_app_certificate_arn_condition, Ref(custom_app_certificate_arn), Ref(template.add_resource( Certificate( 'Certificate', Condition=stack_cert_condition, DomainName=domain_name, SubjectAlternativeNames=If(no_alt_domains, Ref("AWS::NoValue"), domain_name_alternates), DomainValidationOptions=[ DomainValidationOption( DomainName=domain_name, ValidationDomain=domain_name, ), ], ValidationMethod=Ref(certificate_validation_method) ) )))
def __init__(self, utils, templatePath='./cloudformation/api.json', description='Top Level API Gateway Template for {App}', version='2010-09-09'): super(self.__class__, self).__init__() self.utils = utils self.templatePath = templatePath appName = self.utils.config['App'] domainName = self.utils.config['Domain'] tags = self.utils.config['Tags'] self.add_version(version) self.add_description(description.format(App=appName)) ################### # ACM Certificate # ################### self.certificate = self.add_resource( Certificate( '{App}Certificate'.format(App=appName), DomainName=domainName, SubjectAlternativeNames=[ '*.{Domain}'.format(Domain=domainName) ], Tags=Tags(tags), )) ##################### # Deployment Bucket # ##################### self.bucket = self.add_resource( Bucket( '{App}DeploymentBucket'.format(App=appName), DeletionPolicy='Retain', Tags=Tags(tags), VersioningConfiguration=VersioningConfiguration( Status='Enabled', ), )) ########### # RestApi # ########### self.api = self.add_resource( RestApi( '{App}Api'.format(App=appName), Name=appName + 'Api', Description='API for {App} AWS SAML Login Redirection'.format( App=appName), )) ################## # RestApi Domain # ################## self.apiDomain = self.add_resource( DomainName( '{App}ApiDomainName'.format(App=appName), CertificateArn=Ref(self.certificate), DomainName=domainName, )) self.apiDomainMapping = self.add_resource( BasePathMapping( '{App}ApiDomainNameMapping'.format(App=appName), DomainName=Ref(self.apiDomain), RestApiId=Ref(self.api), )) ########### # Outputs # ########### self.add_output( Output( '{App}Api'.format(App=appName), Value=Ref(self.api), Export=Export('{App}Api'.format(App=appName), ), )) self.add_output( Output( '{App}ApiDomainName'.format(App=appName), Value=Ref(self.apiDomain), Export=Export('{App}ApiDomainName'.format(App=appName), ), )) self.add_output( Output( '{App}ApiDomainDistribution'.format(App=appName), Value=GetAtt(self.apiDomain, 'DistributionDomainName'), Export=Export( '{App}ApiDomainDistribution'.format(App=appName), ), )) self.add_output( Output( '{App}ApiRoot'.format(App=appName), Value=GetAtt(self.api, 'RootResourceId'), Export=Export('{App}ApiRoot'.format(App=appName), ), )) self.add_output( Output( '{App}Certificate'.format(App=appName), Value=Ref(self.certificate), Export=Export('{App}Certificate'.format(App=appName), ), )) self.add_output( Output( '{App}DeploymentBucket'.format(App=appName), Value=Ref(self.bucket), Export=Export('{App}DeploymentBucket'.format(App=appName), ), )) ################## # Write Template # ################## with open(templatePath, 'w') as templateFile: templateFile.write(self.to_json())
# Currently, you can specify only certificates that are in the US East (N. Virginia) region. # http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distributionconfig-viewercertificate.html assets_custom_domain_and_us_east_1_condition = "AssetsCloudFrontDomainAndUsEast1Condition" template.add_condition( assets_custom_domain_and_us_east_1_condition, And(Not(Equals(Ref(assets_cloudfront_domain), "")), Equals(Ref(AWS_REGION), "us-east-1"))) assets_certificate = template.add_resource( Certificate( 'AssetsCertificate', Condition=assets_custom_domain_and_us_east_1_condition, DomainName=Ref(assets_cloudfront_domain), DomainValidationOptions=[ DomainValidationOption( DomainName=Ref(assets_cloudfront_domain), ValidationDomain=Ref(assets_cloudfront_domain), ), ], )) assets_certificate_arn = template.add_parameter( Parameter( "AssetsCloudFrontCertArn", Description= "If (1) you specified a custom static media domain, (2) your stack is NOT in the us-east-1 " "region, and (3) you wish to serve static media over HTTPS, you must manually create an " "ACM certificate in the us-east-1 region and provide its ARN here.", Type="String", ),
from troposphere import Template from troposphere.certificatemanager import Certificate, DomainValidationOption t = Template() t.add_resource( Certificate( 'mycert', DomainName='example.com', DomainValidationOptions=[ DomainValidationOption( DomainName='example.com', ValidationDomain='example.com', ), ], Tags=[ { 'Key': 'tag-key', 'Value': 'tag-value' }, ], )) print(t.to_json())
from troposphere import Ref from troposphere.certificatemanager import Certificate, DomainValidationOption from .domain import domain_name from .template import template application = Ref( template.add_resource( Certificate( 'Certificate', DomainName=domain_name, DomainValidationOptions=[ DomainValidationOption( DomainName=domain_name, ValidationDomain=domain_name, ), ], )))
def __init__(self, prefix: str, lb_security_groups: List[SecurityGroup], subnets: List[Subnet], vpc: VPC, desired_domain_name: str, healthy_http_codes: Optional[List[int]] = None): """ Constructor. :param prefix: A prefix for resource names. :param lb_security_groups: Security groups to attach to a loadbalancer. NOTE! when passing loadbalancer security groups - make sure the loadbalancer can communicate through ci/cd blue/green deployments opened ports. Usually they are 8000 and 44300. :param subnets: Subnets in which loadbalancer can exist. :param vpc: Virtual private cloud in which target groups and a loadbalancer exist. :param desired_domain_name: Domain name for using https. :param healthy_http_codes: The deployed instance is constantly pinged to determine if it is available (healthy) or not. Specify a list of http codes that your service can return and should be treated as healthy. """ # By default a healthy http code is considered to be 200. healthy_http_codes = healthy_http_codes or [200] # If your service's task definition uses the awsvpc network mode # (which is required for the Fargate launch type), you must choose ip as the target type, # not instance, when creating your target groups because # tasks that use the awsvpc network mode are associated with an elastic network interface, # not an Amazon EC2 instance. self.target_type = 'ip' # Certificate so a loadbalancer could communicate via HTTPS. self.certificate = Certificate( prefix + 'FargateEcsCertificate', DomainName=desired_domain_name, ValidationMethod='DNS', ) # A main target group to which a loadbalancer forwards a HTTP traffic. # This is the main group with which our ecs container is associated. self.target_group_1_http = TargetGroup( prefix + 'FargateEcsTargetGroup1', Name=prefix + 'FargateEcsTargetGroup1', Matcher=Matcher( HttpCode=','.join([str(code) for code in healthy_http_codes])), Port=self.TARGET_GROUP_PORT, Protocol='HTTP', VpcId=Ref(vpc), TargetType=self.target_type) # Second target group is usd for Blue/Green deployments. A new container (that should be deployed) # is associated with the second target group. self.target_group_2_http = TargetGroup( prefix + 'FargateEcsTargetGroup2', Name=prefix + 'FargateEcsTargetGroup2', Matcher=Matcher( HttpCode=','.join([str(code) for code in healthy_http_codes])), Port=self.TARGET_GROUP_PORT, Protocol='HTTP', VpcId=Ref(vpc), TargetType=self.target_type) self.load_balancer = LoadBalancer( prefix + 'FargateEcsLoadBalancer', Subnets=[Ref(sub) for sub in subnets], SecurityGroups=[Ref(group) for group in lb_security_groups], Name=prefix + 'FargateEcsLoadBalancer', Scheme='internet-facing', ) self.load_balancer_output = Output( prefix + 'FargateEcsLoadBalancerUrl', Description='The endpoint url of a loadbalancer.', Value=GetAtt(self.load_balancer, 'DNSName')) # Listener that listens to HTTP incoming traffic and redirects to other HTTPS listener. self.listener_http_1 = Listener( prefix + 'FargateEcsHttpListener1', Port=self.LISTENER_HTTP_PORT_1, Protocol='HTTP', LoadBalancerArn=Ref(self.load_balancer), DefaultActions=[ # Redirect to https. Action(Type='redirect', RedirectConfig=RedirectConfig( Host='#{host}', Path='/#{path}', Port=str(self.LISTENER_HTTPS_PORT_1), Query='#{query}', StatusCode='HTTP_301', Protocol='HTTPS')) ]) # Listener that listens to HTTPS traffic and forwards to a target group. self.listener_https_1 = Listener( prefix + 'FargateEcsHttpsListener1', Certificates=[LBCertificate(CertificateArn=Ref(self.certificate))], Port=self.LISTENER_HTTPS_PORT_1, Protocol='HTTPS', LoadBalancerArn=Ref(self.load_balancer), DefaultActions=[ Action(Type='forward', TargetGroupArn=Ref(self.target_group_1_http)) ]) # Second listener is usd for Blue/Green deployments (testing new instance). Test HTTP traffic is # redirected to test HTTPS traffic. self.listener_http_2 = Listener( prefix + 'FargateEcsHttpListener2', Port=self.LISTENER_HTTP_PORT_2, Protocol='HTTP', LoadBalancerArn=Ref(self.load_balancer), DefaultActions=[ # Redirect to https. Action(Type='redirect', RedirectConfig=RedirectConfig( Host='#{host}', Path='/#{path}', Port=str(self.LISTENER_HTTPS_PORT_2), Query='#{query}', StatusCode='HTTP_301', Protocol='HTTPS')) ]) # Listener that listens to test HTTP traffic and forwards to a secondary target group (new container). self.listener_https_2 = Listener( prefix + 'FargateEcsHttpsListener2', Certificates=[LBCertificate(CertificateArn=Ref(self.certificate))], Port=self.LISTENER_HTTPS_PORT_2, Protocol='HTTPS', LoadBalancerArn=Ref(self.load_balancer), DefaultActions=[ Action(Type='forward', TargetGroupArn=Ref(self.target_group_2_http)) ])
)) domain_name = template.add_parameter( Parameter( 'DomainName', Type=constants.STRING, Default='videos.spunt.be', )) cloudfront_certificate = template.add_resource( Certificate( "CloudFrontCertificate", DomainName=Ref(domain_name), DomainValidationOptions=[ DomainValidationOption( DomainName=Ref(domain_name), ValidationDomain=ImportValue( Join('-', [Ref(dns_stack), 'HostedZoneName'])), ) ], ValidationMethod='DNS', )) video_cdn = template.add_resource( Distribution( "VideoDistribution", DistributionConfig=DistributionConfig( Aliases=[Ref(domain_name)], Comment=Ref(AWS_STACK_NAME), DefaultCacheBehavior=DefaultCacheBehavior( TargetOriginId='S3', ViewerProtocolPolicy='redirect-to-https',
from troposphere import Template from troposphere.certificatemanager import Certificate, DomainValidationOption t = Template() t.add_resource( Certificate( "mycert", DomainName="example.com", DomainValidationOptions=[ DomainValidationOption( DomainName="example.com", ValidationDomain="example.com", ), ], Tags=[ { "Key": "tag-key", "Value": "tag-value" }, ], )) print(t.to_json())
zone_set_cond = t.add_condition( 'zoneIdSet', Not(Equals(Ref(zone_id), '')) ) ############# # Resources ############# # SSL cert for CloudFront ssl_certificate = t.add_resource(Certificate( 'myCert', DomainName = Ref(domain_name), DomainValidationOptions = [ DomainValidationOption( DomainName = Ref(domain_name), ValidationDomain = Ref(zone_apex), ), ], )) s3_bucket = t.add_resource(Bucket('myBucket')) bucket_policy = t.add_resource(BucketPolicy( 'myBucketPolicy', Bucket = Ref(s3_bucket), PolicyDocument = { 'Version' : '2012-10-17', 'Id' : 'PolicyForCloudFrontPrivateContent', 'Statement' : [ {
Default='rob-kenis-dot-com-dns', Description= 'Name of the CloudFormation stack that holds the HostedZone', )) cloudfront_certificate = template.add_resource( Certificate( "CloudFrontCertificate", DomainName='robkenis.com', SubjectAlternativeNames=['www.robkenis.com'], ValidationMethod='DNS', DomainValidationOptions=[ DomainValidationOption( DomainName='robkenis.com', HostedZoneId=ImportValue( Join('-', [Ref(dns_stack), 'HostedZoneId'])), ), DomainValidationOption( DomainName='www.robkenis.com', HostedZoneId=ImportValue( Join('-', [Ref(dns_stack), 'HostedZoneId'])), ), ], Tags=Tags({'Name': Ref(AWS_STACK_NAME)}), )) s3_website_origin = template.add_resource( Bucket( 'WebsiteOrigin', AccessControl='Private', ))
def create_template(): template = Template(Description=( "Static website hosted with S3 and CloudFront. " "https://github.com/schlarpc/overengineered-cloudfront-s3-static-website" )) partition_config = add_mapping( template, "PartitionConfig", { "aws": { # the region with the control plane for CloudFront, IAM, Route 53, etc "PrimaryRegion": "us-east-1", # assume that Lambda@Edge replicates to all default enabled regions, and that # future regions will be opt-in. generated with AWS CLI: # aws ec2 describe-regions --all-regions --query "Regions[?OptInStatus=='opt-in-not-required'].RegionName|sort(@)" "DefaultRegions": [ "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", "ap-south-1", "ap-southeast-1", "ap-southeast-2", "ca-central-1", "eu-central-1", "eu-north-1", "eu-west-1", "eu-west-2", "eu-west-3", "sa-east-1", "us-east-1", "us-east-2", "us-west-1", "us-west-2", ], }, # this doesn't actually work, because Lambda@Edge isn't supported in aws-cn "aws-cn": { "PrimaryRegion": "cn-north-1", "DefaultRegions": ["cn-north-1", "cn-northwest-1"], }, }, ) acm_certificate_arn = template.add_parameter( Parameter( "AcmCertificateArn", Description= "Existing ACM certificate to use for serving TLS. Overrides HostedZoneId.", Type="String", AllowedPattern="(arn:[^:]+:acm:[^:]+:[^:]+:certificate/.+|)", Default="", )) hosted_zone_id = template.add_parameter( Parameter( "HostedZoneId", Description= "Existing Route 53 zone to use for validating a new TLS certificate.", Type="String", AllowedPattern="(Z[A-Z0-9]+|)", Default="", )) dns_names = template.add_parameter( Parameter( "DomainNames", Description= "Comma-separated list of additional domain names to serve.", Type="CommaDelimitedList", Default="", )) tls_protocol_version = template.add_parameter( Parameter( "TlsProtocolVersion", Description= "CloudFront TLS security policy; see https://amzn.to/2DR91Xq for details.", Type="String", Default="TLSv1.2_2019", )) log_retention_days = template.add_parameter( Parameter( "LogRetentionDays", Description= "Days to keep CloudFront, S3, and Lambda logs. 0 means indefinite retention.", Type="Number", AllowedValues=[0] + CLOUDWATCH_LOGS_RETENTION_OPTIONS, Default=365, )) default_ttl_seconds = template.add_parameter( Parameter( "DefaultTtlSeconds", Description="Cache time-to-live when not set by S3 object headers.", Type="Number", Default=int(datetime.timedelta(minutes=5).total_seconds()), )) enable_price_class_hack = template.add_parameter( Parameter( "EnablePriceClassHack", Description="Cut your bill in half with this one weird trick.", Type="String", Default="false", AllowedValues=["true", "false"], )) retention_defined = add_condition(template, "RetentionDefined", Not(Equals(Ref(log_retention_days), 0))) using_price_class_hack = add_condition( template, "UsingPriceClassHack", Equals(Ref(enable_price_class_hack), "true")) using_acm_certificate = add_condition( template, "UsingAcmCertificate", Not(Equals(Ref(acm_certificate_arn), ""))) using_hosted_zone = add_condition(template, "UsingHostedZone", Not(Equals(Ref(hosted_zone_id), ""))) using_certificate = add_condition( template, "UsingCertificate", Or(Condition(using_acm_certificate), Condition(using_hosted_zone)), ) should_create_certificate = add_condition( template, "ShouldCreateCertificate", And(Condition(using_hosted_zone), Not(Condition(using_acm_certificate))), ) using_dns_names = add_condition(template, "UsingDnsNames", Not(Equals(Select(0, Ref(dns_names)), ""))) is_primary_region = "IsPrimaryRegion" template.add_condition( is_primary_region, Equals(Region, FindInMap(partition_config, Partition, "PrimaryRegion")), ) precondition_region_is_primary = template.add_resource( WaitConditionHandle( "PreconditionIsPrimaryRegionForPartition", Condition=is_primary_region, )) log_ingester_dlq = template.add_resource( Queue( "LogIngesterDLQ", MessageRetentionPeriod=int( datetime.timedelta(days=14).total_seconds()), KmsMasterKeyId="alias/aws/sqs", )) log_ingester_role = template.add_resource( Role( "LogIngesterRole", AssumeRolePolicyDocument=PolicyDocument( Version="2012-10-17", Statement=[ Statement( Effect="Allow", Principal=Principal("Service", "lambda.amazonaws.com"), Action=[sts.AssumeRole], ) ], ), Policies=[ PolicyProperty( PolicyName="DLQPolicy", PolicyDocument=PolicyDocument( Version="2012-10-17", Statement=[ Statement( Effect=Allow, Action=[sqs.SendMessage], Resource=[GetAtt(log_ingester_dlq, "Arn")], ) ], ), ) ], )) log_ingester = template.add_resource( Function( "LogIngester", Runtime=PYTHON_RUNTIME, Handler="index.{}".format(log_ingest.handler.__name__), Code=Code(ZipFile=inspect.getsource(log_ingest)), MemorySize=256, Timeout=300, Role=GetAtt(log_ingester_role, "Arn"), DeadLetterConfig=DeadLetterConfig( TargetArn=GetAtt(log_ingester_dlq, "Arn")), )) log_ingester_permission = template.add_resource( Permission( "LogIngesterPermission", FunctionName=GetAtt(log_ingester, "Arn"), Action="lambda:InvokeFunction", Principal="s3.amazonaws.com", SourceAccount=AccountId, )) log_bucket = template.add_resource( Bucket( "LogBucket", # S3 requires this ACL (regardless of bucket policy) or s3:PutBucketLogging fails. # When the CloudFront distribution is created, it adds an additional bucket ACL. # That ACL is not possible to model in CloudFormation. AccessControl="LogDeliveryWrite", LifecycleConfiguration=LifecycleConfiguration(Rules=[ LifecycleRule(ExpirationInDays=1, Status="Enabled"), LifecycleRule( AbortIncompleteMultipartUpload= AbortIncompleteMultipartUpload(DaysAfterInitiation=1), Status="Enabled", ), ]), NotificationConfiguration=NotificationConfiguration( LambdaConfigurations=[ LambdaConfigurations(Event="s3:ObjectCreated:*", Function=GetAtt(log_ingester, "Arn")) ]), BucketEncryption=BucketEncryption( ServerSideEncryptionConfiguration=[ ServerSideEncryptionRule( ServerSideEncryptionByDefault= ServerSideEncryptionByDefault( # if we use KMS, we can't read the logs SSEAlgorithm="AES256")) ]), OwnershipControls=OwnershipControls(Rules=[ OwnershipControlsRule(ObjectOwnership="BucketOwnerPreferred") ], ), PublicAccessBlockConfiguration=PublicAccessBlockConfiguration( BlockPublicAcls=True, BlockPublicPolicy=True, IgnorePublicAcls=True, RestrictPublicBuckets=True, ), DependsOn=[log_ingester_permission], )) log_ingester_log_group = template.add_resource( LogGroup( "LogIngesterLogGroup", LogGroupName=Join( "", ["/aws/lambda/", Ref(log_ingester)]), RetentionInDays=If(retention_defined, Ref(log_retention_days), NoValue), )) log_ingester_policy = template.add_resource( PolicyType( "LogIngesterPolicy", Roles=[Ref(log_ingester_role)], PolicyName="IngestLogPolicy", PolicyDocument=PolicyDocument( Version="2012-10-17", Statement=[ Statement( Effect=Allow, Action=[logs.CreateLogStream, logs.PutLogEvents], Resource=[ Join( ":", [ "arn", Partition, "logs", Region, AccountId, "log-group", "/aws/cloudfront/*", ], ), Join( ":", [ "arn", Partition, "logs", Region, AccountId, "log-group", "/aws/s3/*", ], ), GetAtt(log_ingester_log_group, "Arn"), ], ), Statement( Effect=Allow, Action=[s3.GetObject], Resource=[Join("", [GetAtt(log_bucket, "Arn"), "/*"])], ), ], ), )) bucket = template.add_resource( Bucket( "ContentBucket", LifecycleConfiguration=LifecycleConfiguration(Rules=[ # not supported by CFN yet: # LifecycleRule( # Transitions=[ # LifecycleRuleTransition( # StorageClass='INTELLIGENT_TIERING', # TransitionInDays=1, # ), # ], # Status="Enabled", # ), LifecycleRule( AbortIncompleteMultipartUpload= AbortIncompleteMultipartUpload(DaysAfterInitiation=7), Status="Enabled", ) ]), LoggingConfiguration=LoggingConfiguration( DestinationBucketName=Ref(log_bucket), LogFilePrefix="s3/"), BucketEncryption=BucketEncryption( ServerSideEncryptionConfiguration=[ ServerSideEncryptionRule( ServerSideEncryptionByDefault= ServerSideEncryptionByDefault( # Origin Access Identities can't use KMS SSEAlgorithm="AES256")) ]), OwnershipControls=OwnershipControls(Rules=[ OwnershipControlsRule(ObjectOwnership="BucketOwnerPreferred") ], ), PublicAccessBlockConfiguration=PublicAccessBlockConfiguration( BlockPublicAcls=True, BlockPublicPolicy=True, IgnorePublicAcls=True, RestrictPublicBuckets=True, ), )) origin_access_identity = template.add_resource( CloudFrontOriginAccessIdentity( "CloudFrontIdentity", CloudFrontOriginAccessIdentityConfig= CloudFrontOriginAccessIdentityConfig( Comment=GetAtt(bucket, "Arn")), )) bucket_policy = template.add_resource( BucketPolicy( "ContentBucketPolicy", Bucket=Ref(bucket), PolicyDocument=PolicyDocument( Version="2012-10-17", Statement=[ Statement( Effect=Allow, Principal=Principal( "CanonicalUser", GetAtt(origin_access_identity, "S3CanonicalUserId"), ), Action=[s3.GetObject], Resource=[Join("", [GetAtt(bucket, "Arn"), "/*"])], ), ], ), )) # Not strictly necessary, as ACLs should take care of this access. However, CloudFront docs # state "In some circumstances [...] S3 resets permissions on the bucket to the default value", # and this allows logging to work without any ACLs in place. log_bucket_policy = template.add_resource( BucketPolicy( "LogBucketPolicy", Bucket=Ref(log_bucket), PolicyDocument=PolicyDocument( Version="2012-10-17", Statement=[ Statement( Effect=Allow, Principal=Principal("Service", "delivery.logs.amazonaws.com"), Action=[s3.PutObject], Resource=[ Join( "/", [GetAtt(log_bucket, "Arn"), "cloudfront", "*"]) ], ), Statement( Effect=Allow, Principal=Principal("Service", "delivery.logs.amazonaws.com"), Action=[s3.ListBucket], Resource=[Join("/", [GetAtt(log_bucket, "Arn")])], ), Statement( Effect=Allow, Principal=Principal("Service", "s3.amazonaws.com"), Action=[s3.PutObject], Resource=[ Join("/", [GetAtt(log_bucket, "Arn"), "s3", "*"]) ], ), ], ), )) certificate_validator_dlq = template.add_resource( Queue( "CertificateValidatorDLQ", MessageRetentionPeriod=int( datetime.timedelta(days=14).total_seconds()), KmsMasterKeyId="alias/aws/sqs", Condition=should_create_certificate, )) certificate_validator_role = template.add_resource( Role( "CertificateValidatorRole", AssumeRolePolicyDocument=PolicyDocument( Version="2012-10-17", Statement=[ Statement( Effect="Allow", Principal=Principal("Service", "lambda.amazonaws.com"), Action=[sts.AssumeRole], ) ], ), Policies=[ PolicyProperty( PolicyName="DLQPolicy", PolicyDocument=PolicyDocument( Version="2012-10-17", Statement=[ Statement( Effect=Allow, Action=[sqs.SendMessage], Resource=[ GetAtt(certificate_validator_dlq, "Arn") ], ) ], ), ) ], # TODO scope down ManagedPolicyArns=[ "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", "arn:aws:iam::aws:policy/AmazonRoute53FullAccess", "arn:aws:iam::aws:policy/AWSCertificateManagerReadOnly", ], Condition=should_create_certificate, )) certificate_validator_function = template.add_resource( Function( "CertificateValidatorFunction", Runtime=PYTHON_RUNTIME, Handler="index.{}".format(certificate_validator.handler.__name__), Code=Code(ZipFile=inspect.getsource(certificate_validator)), MemorySize=256, Timeout=300, Role=GetAtt(certificate_validator_role, "Arn"), DeadLetterConfig=DeadLetterConfig( TargetArn=GetAtt(certificate_validator_dlq, "Arn")), Environment=Environment( Variables={ certificate_validator.EnvVars.HOSTED_ZONE_ID.name: Ref(hosted_zone_id) }), Condition=should_create_certificate, )) certificate_validator_log_group = template.add_resource( LogGroup( "CertificateValidatorLogGroup", LogGroupName=Join( "", ["/aws/lambda/", Ref(certificate_validator_function)]), RetentionInDays=If(retention_defined, Ref(log_retention_days), NoValue), Condition=should_create_certificate, )) certificate_validator_rule = template.add_resource( Rule( "CertificateValidatorRule", EventPattern={ "detail-type": ["AWS API Call via CloudTrail"], "detail": { "eventSource": ["acm.amazonaws.com"], "eventName": ["AddTagsToCertificate"], "requestParameters": { "tags": { "key": [certificate_validator_function.title], "value": [GetAtt(certificate_validator_function, "Arn")], } }, }, }, Targets=[ Target( Id="certificate-validator-lambda", Arn=GetAtt(certificate_validator_function, "Arn"), ) ], DependsOn=[certificate_validator_log_group], Condition=should_create_certificate, )) certificate_validator_permission = template.add_resource( Permission( "CertificateValidatorPermission", FunctionName=GetAtt(certificate_validator_function, "Arn"), Action="lambda:InvokeFunction", Principal="events.amazonaws.com", SourceArn=GetAtt(certificate_validator_rule, "Arn"), Condition=should_create_certificate, )) certificate = template.add_resource( Certificate( "Certificate", DomainName=Select(0, Ref(dns_names)), SubjectAlternativeNames=Ref( dns_names), # duplicate first name works fine ValidationMethod="DNS", Tags=Tags( **{ certificate_validator_function.title: GetAtt(certificate_validator_function, "Arn") }), DependsOn=[certificate_validator_permission], Condition=should_create_certificate, )) edge_hook_role = template.add_resource( Role( "EdgeHookRole", AssumeRolePolicyDocument=PolicyDocument( Version="2012-10-17", Statement=[ Statement( Effect="Allow", Principal=Principal( "Service", [ "lambda.amazonaws.com", "edgelambda.amazonaws.com" ], ), Action=[sts.AssumeRole], ) ], ), )) edge_hook_function = template.add_resource( Function( "EdgeHookFunction", Runtime=PYTHON_RUNTIME, Handler="index.handler", Code=Code(ZipFile=inspect.getsource(edge_hook)), MemorySize=128, Timeout=3, Role=GetAtt(edge_hook_role, "Arn"), )) edge_hook_function_hash = (hashlib.sha256( json.dumps(edge_hook_function.to_dict(), sort_keys=True).encode("utf-8")).hexdigest()[:10].upper()) edge_hook_version = template.add_resource( Version( "EdgeHookVersion" + edge_hook_function_hash, FunctionName=GetAtt(edge_hook_function, "Arn"), )) replica_log_group_name = Join( "/", [ "/aws/lambda", Join( ".", [ FindInMap(partition_config, Partition, "PrimaryRegion"), Ref(edge_hook_function), ], ), ], ) edge_hook_role_policy = template.add_resource( PolicyType( "EdgeHookRolePolicy", PolicyName="write-logs", PolicyDocument=PolicyDocument( Version="2012-10-17", Statement=[ Statement( Effect=Allow, Action=[logs.CreateLogStream, logs.PutLogEvents], Resource=[ Join( ":", [ "arn", Partition, "logs", "*", AccountId, "log-group", replica_log_group_name, "log-stream", "*", ], ), ], ), ], ), Roles=[Ref(edge_hook_role)], )) stack_set_administration_role = template.add_resource( Role( "StackSetAdministrationRole", AssumeRolePolicyDocument=PolicyDocument( Version="2012-10-17", Statement=[ Statement( Effect=Allow, Principal=Principal("Service", "cloudformation.amazonaws.com"), Action=[sts.AssumeRole], ), ], ), )) stack_set_execution_role = template.add_resource( Role( "StackSetExecutionRole", AssumeRolePolicyDocument=PolicyDocument( Version="2012-10-17", Statement=[ Statement( Effect=Allow, Principal=Principal( "AWS", GetAtt(stack_set_administration_role, "Arn")), Action=[sts.AssumeRole], ), ], ), Policies=[ PolicyProperty( PolicyName="create-stackset-instances", PolicyDocument=PolicyDocument( Version="2012-10-17", Statement=[ Statement( Effect=Allow, Action=[ cloudformation.DescribeStacks, logs.DescribeLogGroups, ], Resource=["*"], ), # stack instances communicate with the CFN service via SNS Statement( Effect=Allow, Action=[sns.Publish], NotResource=[ Join( ":", [ "arn", Partition, "sns", "*", AccountId, "*" ], ) ], ), Statement( Effect=Allow, Action=[ logs.CreateLogGroup, logs.DeleteLogGroup, logs.PutRetentionPolicy, logs.DeleteRetentionPolicy, ], Resource=[ Join( ":", [ "arn", Partition, "logs", "*", AccountId, "log-group", replica_log_group_name, "log-stream", "", ], ), ], ), Statement( Effect=Allow, Action=[ cloudformation.CreateStack, cloudformation.DeleteStack, cloudformation.UpdateStack, ], Resource=[ Join( ":", [ "arn", Partition, "cloudformation", "*", AccountId, Join( "/", [ "stack", Join( "-", [ "StackSet", StackName, "*" ], ), ], ), ], ) ], ), ], ), ), ], )) stack_set_administration_role_policy = template.add_resource( PolicyType( "StackSetAdministrationRolePolicy", PolicyName="assume-execution-role", PolicyDocument=PolicyDocument( Version="2012-10-17", Statement=[ Statement( Effect=Allow, Action=[sts.AssumeRole], Resource=[GetAtt(stack_set_execution_role, "Arn")], ), ], ), Roles=[Ref(stack_set_administration_role)], )) edge_log_groups = template.add_resource( StackSet( "EdgeLambdaLogGroupStackSet", AdministrationRoleARN=GetAtt(stack_set_administration_role, "Arn"), ExecutionRoleName=Ref(stack_set_execution_role), StackSetName=Join("-", [StackName, "EdgeLambdaLogGroup"]), PermissionModel="SELF_MANAGED", Description="Multi-region log groups for Lambda@Edge replicas", Parameters=[ StackSetParameter( ParameterKey="LogGroupName", ParameterValue=replica_log_group_name, ), StackSetParameter( ParameterKey="LogRetentionDays", ParameterValue=Ref(log_retention_days), ), ], OperationPreferences=OperationPreferences( FailureToleranceCount=0, MaxConcurrentPercentage=100, ), StackInstancesGroup=[ StackInstances( DeploymentTargets=DeploymentTargets(Accounts=[AccountId]), Regions=FindInMap(partition_config, Partition, "DefaultRegions"), ) ], TemplateBody=create_log_group_template().to_json(indent=None), DependsOn=[stack_set_administration_role_policy], )) price_class_distribution = template.add_resource( Distribution( "PriceClassDistribution", DistributionConfig=DistributionConfig( Comment="Dummy distribution used for price class hack", DefaultCacheBehavior=DefaultCacheBehavior( TargetOriginId="default", ViewerProtocolPolicy="allow-all", ForwardedValues=ForwardedValues(QueryString=False), ), Enabled=True, Origins=[ Origin(Id="default", DomainName=GetAtt(bucket, "DomainName")) ], IPV6Enabled=True, ViewerCertificate=ViewerCertificate( CloudFrontDefaultCertificate=True), PriceClass="PriceClass_All", ), Condition=using_price_class_hack, )) distribution = template.add_resource( Distribution( "ContentDistribution", DistributionConfig=DistributionConfig( Enabled=True, Aliases=If(using_dns_names, Ref(dns_names), NoValue), Logging=Logging(Bucket=GetAtt(log_bucket, "DomainName"), Prefix="cloudfront/"), DefaultRootObject="index.html", Origins=[ Origin( Id="default", DomainName=GetAtt(bucket, "DomainName"), S3OriginConfig=S3OriginConfig( OriginAccessIdentity=Join( "", [ "origin-access-identity/cloudfront/", Ref(origin_access_identity), ], )), ) ], DefaultCacheBehavior=DefaultCacheBehavior( TargetOriginId="default", Compress=True, ForwardedValues=ForwardedValues(QueryString=False), ViewerProtocolPolicy="redirect-to-https", DefaultTTL=Ref(default_ttl_seconds), LambdaFunctionAssociations=[ LambdaFunctionAssociation( EventType="origin-request", LambdaFunctionARN=Ref(edge_hook_version), ) ], ), HttpVersion="http2", IPV6Enabled=True, ViewerCertificate=ViewerCertificate( AcmCertificateArn=If( using_acm_certificate, Ref(acm_certificate_arn), If(using_hosted_zone, Ref(certificate), NoValue), ), SslSupportMethod=If(using_certificate, "sni-only", NoValue), CloudFrontDefaultCertificate=If(using_certificate, NoValue, True), MinimumProtocolVersion=Ref(tls_protocol_version), ), PriceClass=If(using_price_class_hack, "PriceClass_100", "PriceClass_All"), ), DependsOn=[ bucket_policy, log_ingester_policy, edge_log_groups, precondition_region_is_primary, ], )) distribution_log_group = template.add_resource( LogGroup( "DistributionLogGroup", LogGroupName=Join( "", ["/aws/cloudfront/", Ref(distribution)]), RetentionInDays=If(retention_defined, Ref(log_retention_days), NoValue), )) bucket_log_group = template.add_resource( LogGroup( "BucketLogGroup", LogGroupName=Join("", ["/aws/s3/", Ref(bucket)]), RetentionInDays=If(retention_defined, Ref(log_retention_days), NoValue), )) template.add_output(Output("DistributionId", Value=Ref(distribution))) template.add_output( Output("DistributionDomain", Value=GetAtt(distribution, "DomainName"))) template.add_output( Output( "DistributionDnsTarget", Value=If( using_price_class_hack, GetAtt(price_class_distribution, "DomainName"), GetAtt(distribution, "DomainName"), ), )) template.add_output( Output( "DistributionUrl", Value=Join("", ["https://", GetAtt(distribution, "DomainName"), "/"]), )) template.add_output(Output("ContentBucketArn", Value=GetAtt(bucket, "Arn"))) return template
template.add_condition( assets_create_certificate_condition, And( Not(Equals(Ref(assets_cloudfront_domain), "")), Equals(Ref(AWS_REGION), "us-east-1"), Equals(Ref(assets_certificate_arn), "") ) ) assets_certificate = template.add_resource( Certificate( 'AssetsCertificate', Condition=assets_create_certificate_condition, DomainName=Ref(assets_cloudfront_domain), DomainValidationOptions=[ DomainValidationOption( DomainName=Ref(assets_cloudfront_domain), ValidationDomain=Ref(assets_cloudfront_domain), ), ], ) ) # Create a CloudFront CDN distribution distribution = template.add_resource( Distribution( 'AssetsDistribution', Condition=assets_use_cloudfront_condition, DistributionConfig=DistributionConfig( Aliases=If(assets_custom_domain_condition, [Ref(assets_cloudfront_domain)], Ref("AWS::NoValue")), # use the ACM certificate we created (if any), otherwise fall back to the manually-supplied
from troposphere import If, Ref from troposphere.certificatemanager import Certificate, DomainValidationOption from .domain import domain_name, domain_name_alternates, no_alt_domains from .template import template application = Ref( template.add_resource( Certificate( 'Certificate', DomainName=domain_name, SubjectAlternativeNames=If(no_alt_domains, Ref("AWS::NoValue"), domain_name_alternates), DomainValidationOptions=[ DomainValidationOption( DomainName=domain_name, ValidationDomain=domain_name, ), ], )))
default_ttl = 86400, # If an alt_sub domain is not specified use empty string if domain_info['alt_sub'] != '': alternate_name = '{}.{}'.format(domain_info['alt_sub'], src_domain) else: alternate_name = '' # Provision certificate for CDN cdnCertificate = t.add_resource( Certificate('cdnCertificate{}'.format(src_domain.replace('.', '0')), DomainName=cdn_domain, DependsOn=redirectBucket, SubjectAlternativeNames=[alternate_name], DomainValidationOptions=[ DomainValidationOption(DomainName=cdn_domain, ValidationDomain=dns_domain) ], ValidationMethod='DNS', Tags=DefaultTags + Tags(Name='{}-{}'.format(env_l, app_group_l)))) # Provision the CDN Origin cdnOrigin = cf.Origin(Id='{}-{}-{}'.format(env_l, app_group_l, src_domain), DomainName=Select( 1, Split('//', GetAtt(redirectBucket, 'WebsiteURL'))), CustomOriginConfig=cf.CustomOriginConfig( HTTPPort=80, HTTPSPort=443,
'VIDEO_TABLE': Ref(video_table), } ), )) template.add_resource(LogGroup( "AllVideosFunctionLogGroup", LogGroupName=Join('/', ['/aws/lambda', Ref(all_videos_function)]), RetentionInDays=7, )) api_certificate = template.add_resource(Certificate( "ApiCertificate", DomainName=Ref(domain_name), DomainValidationOptions=[DomainValidationOption( DomainName=Ref(domain_name), ValidationDomain=ImportValue(Join('-', [Ref(dns_stack), 'HostedZoneName'])), )], ValidationMethod='DNS', )) api_domain_name = template.add_resource(DomainName( 'ApiDomainName', RegionalCertificateArn=Ref(api_certificate), DomainName=Ref(domain_name), EndpointConfiguration=EndpointConfiguration( Types=['REGIONAL'], ), )) template.add_resource(BasePathMapping(