UNPKG

ness

Version:

✪ No-effort static sites deployed to your AWS account.

828 lines (815 loc) 28.1 kB
AWSTemplateFormatVersion: '2010-09-09' Parameters: AccessLogStack: Description: 'Optional stack name of s3 stack to store access logs.' Type: String Default: '' WAFStack: Description: 'Optional stack name of WAF stack.' Type: String Default: '' DomainName: Description: 'Optional domain.' Type: String Default: '' SubDomainNameWithDot: Description: 'Primary name that is used to create the DNS entry with trailing dot. Leave blank for naked (or apex and bare) domain.' Type: String Default: '' RedirectSubDomainNameWithDot: Description: 'Optional sub domain name redirecting to DomainName (e.g. www.).' Type: String Default: '' DefaultRootObject: Description: 'Optional name of the index document for the website (e.g., index.html).' Type: String Default: 'index.html' DefaultErrorObject: Description: 'Optional name of the error document for the website (e.g. error.html).' Type: String Default: '' DefaultErrorResponseCode: Description: 'The HTTP status code that you want to return along with the error page (requires DefaultErrorObject).' Type: String Default: '404' AllowedValues: ['200', '404'] ContentSecurityPolicy: Description: 'Optional content-security-policy to be served with the site. Defaults to HTTPS only.' Type: String Default: 'default-src https:; script-src https:; style-src https:' ExistingCertificate: Description: 'Optional ACM Certificate ARN.' Type: String Default: '' IncludeCloudFrontAlias: Description: 'Whether to include the domain as an alias for the CloudFront distribution. If an existing distribution already has the Alias, we need to delete it first.' Type: String Default: 'true' AllowedValues: ['true', 'false'] LambdaBucket: Description: 'Bucket for deploying lambdas.' Type: String Default: '' DefaultCachePolicyArn: Description: 'Default cache policy ARN.' Type: String Default: '' ImageCachePolicyArn: Description: 'Image cache policy ARN.' Type: String Default: '' StaticCachePolicyArn: Description: 'Static cache policy ARN.' Type: String Default: '' ImageOriginRequestPolicyArn: Description: 'Image origin request policy ARN.' Type: String Default: '' IsNextJs: Description: 'Whether this is a Next.js deployment.' Type: String Default: 'false' AllowedValues: ['true', 'false'] NextJsDefaultLambdaKey: Description: 'Next.js default lambda key.' Type: String Default: '' NextJsImageLambdaKey: Description: 'Next.js image lambda key.' Type: String Default: '' NextJsApiLambdaKey: Description: 'Next.js api lambda key.' Type: String Default: '' NextJsRegenerationLambdaKey: Description: 'Next.js regeneration lambda key.' Type: String Default: '' NextJsImagePath: Description: 'Next.js image path.' Type: String Default: '' NextJsDataPath: Description: 'Next.js data path.' Type: String Default: '' NextJsBasePath: Description: 'Next.js base path.' Type: String Default: '' NextJsStaticPath: Description: 'Next.js static path.' Type: String Default: '' NextJsApiPath: Description: 'Next.js api path.' Type: String Default: '' LogsRetentionInDays: Description: 'Specifies the number of days you want to retain log events in the specified log group.' Type: Number Default: 14 AllowedValues: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653] Conditions: HasDomain: !Not [!Equals [!Ref DomainName, '']] IsNextJs: !Equals [!Ref IsNextJs, 'true'] HasDomainOrIsNextJs: !Or [!Condition HasDomain, !Condition IsNextJs] NoDomain: !Not [!Condition HasDomainOrIsNextJs] HasRedirectDomainName: !Not [!Equals [!Ref RedirectSubDomainNameWithDot, '']] HasBucket: !Not [!Equals [!Ref AccessLogStack, '']] HasWAF: !Not [!Equals [!Ref WAFStack, '']] HasDefaultRootObject: !Not [!Equals [!Ref DefaultRootObject, '']] HasDefaultErrorObject: !Not [!Equals [!Ref DefaultErrorObject, '']] HasExistingCertificate: !Not [!Equals [!Ref ExistingCertificate, '']] ShouldIncludeCloudFrontAlias: !Equals [!Ref IncludeCloudFrontAlias, 'true'] HasNextJsImagePath: !Not [!Equals [!Ref NextJsImagePath, '']] HasNextJsDataPath: !Not [!Equals [!Ref NextJsDataPath, '']] HasNextJsBasePath: !Not [!Equals [!Ref NextJsBasePath, '']] HasNextJsStaticPath: !Not [!Equals [!Ref NextJsStaticPath, '']] HasNextJsApiPath: !Not [!Equals [!Ref NextJsApiPath, '']] HasNextJsRegenerationLambda: !Not [!Equals [!Ref NextJsRegenerationLambdaKey, '']] Resources: Bucket: Type: 'AWS::S3::Bucket' UpdateReplacePolicy: Delete DeletionPolicy: Delete Properties: AccelerateConfiguration: AccelerationStatus: Enabled WebsiteConfiguration: !If [ NoDomain, { IndexDocument: !If [HasDefaultRootObject, !Ref DefaultRootObject, !Ref 'AWS::NoValue'], ErrorDocument: !If [HasDefaultErrorObject, !Ref DefaultErrorObject, !Ref 'AWS::NoValue'], }, !Ref 'AWS::NoValue', ] BucketPolicyPublic: Condition: NoDomain Type: 'AWS::S3::BucketPolicy' Properties: Bucket: !Ref Bucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${Bucket}/*' Principal: '*' BucketPolicy: Condition: HasDomainOrIsNextJs Type: 'AWS::S3::BucketPolicy' Properties: Bucket: !Ref Bucket PolicyDocument: Statement: - Action: 's3:GetObject' Effect: Allow Resource: !Sub 'arn:aws:s3:::${Bucket}/*' Principal: CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId - Sid: AllowSSLRequestsOnly # AWS Foundational Security Best Practices v1.0.0 S3.5 Effect: Deny Principal: '*' Action: 's3:*' Resource: - !GetAtt 'Bucket.Arn' - !Sub '${Bucket.Arn}/*' Condition: Bool: 'aws:SecureTransport': false NextJsDefaultLambdaRole: Condition: IsNextJs Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com - edgelambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: S3Policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - s3:GetObject* - s3:GetBucket* - s3:List* - s3:DeleteObject* - s3:PutObject* - s3:Abort* Resource: !Sub 'arn:aws:s3:::${Bucket}/*' - PolicyName: SQSPolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - sqs:SendMessage - sqs:GetQueueAttributes - sqs:GetQueueUrl Resource: - !If - HasNextJsRegenerationLambda - !GetAtt NextJsRegenerationQueue.Arn - '*' - PolicyName: InvokePolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - lambda:InvokeFunction Resource: - !If - HasNextJsRegenerationLambda - !GetAtt NextJsRegenerationLambda.Arn - '*' NextJsDefaultLambda: Condition: IsNextJs Type: AWS::Lambda::Function Properties: Code: S3Bucket: !Ref LambdaBucket S3Key: !Ref NextJsDefaultLambdaKey Role: !GetAtt NextJsDefaultLambdaRole.Arn Description: Default Lambda@Edge function for Next.js Handler: index.handler Runtime: nodejs12.x Timeout: 10 MemorySize: 512 DependsOn: - NextJsDefaultLambdaRole NextJsDefaultLambdaCurrentVersion: Condition: IsNextJs Type: AWS::Lambda::Version Properties: FunctionName: !Ref NextJsDefaultLambda UpdateReplacePolicy: Delete DeletionPolicy: Delete NextJsImageLambdaRole: Condition: HasNextJsImagePath Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com - edgelambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: S3Policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - s3:GetObject - s3:PutObject Resource: !Sub 'arn:aws:s3:::${Bucket}/*' NextJsImageLambda: Condition: HasNextJsImagePath Type: AWS::Lambda::Function Properties: Code: S3Bucket: !Ref LambdaBucket S3Key: !Ref NextJsImageLambdaKey Role: !GetAtt NextJsImageLambdaRole.Arn Description: Image Lambda@Edge function for Next.js Handler: index.handler Runtime: nodejs12.x Timeout: 10 MemorySize: 512 DependsOn: - NextJsImageLambdaRole NextJsImageLambdaCurrentVersion: Condition: HasNextJsImagePath Type: AWS::Lambda::Version Properties: FunctionName: !Ref NextJsImageLambda UpdateReplacePolicy: Delete DeletionPolicy: Delete NextJsImageLambdaCurrentVersionEventInvokeConfig: Condition: HasNextJsImagePath Type: AWS::Lambda::EventInvokeConfig Properties: FunctionName: !Ref NextJsImageLambda Qualifier: !GetAtt NextJsImageLambdaCurrentVersion.Version MaximumRetryAttempts: 1 NextJsApiLambdaRole: Condition: HasNextJsApiPath Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com - edgelambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: S3Policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - s3:GetObject - s3:PutObject Resource: !Sub 'arn:aws:s3:::${Bucket}/*' NextJsApiLambda: Condition: HasNextJsApiPath Type: AWS::Lambda::Function Properties: Code: S3Bucket: !Ref LambdaBucket S3Key: !Ref NextJsApiLambdaKey Role: !GetAtt NextJsApiLambdaRole.Arn Description: API Lambda@Edge function for Next.js Handler: index.handler Runtime: nodejs12.x Timeout: 10 MemorySize: 512 DependsOn: - NextJsApiLambdaRole NextJsApiLambdaCurrentVersion: Condition: HasNextJsApiPath Type: AWS::Lambda::Version Properties: FunctionName: !Ref NextJsApiLambda UpdateReplacePolicy: Delete DeletionPolicy: Delete NextJsApiLambdaCurrentVersionEventInvokeConfig: Condition: HasNextJsApiPath Type: AWS::Lambda::EventInvokeConfig Properties: FunctionName: !Ref NextJsApiLambda Qualifier: !GetAtt NextJsApiLambdaCurrentVersion.Version MaximumRetryAttempts: 1 NextJsRegenerationQueue: Condition: HasNextJsRegenerationLambda Type: AWS::SQS::Queue DeletionPolicy: Delete Properties: QueueName: !Sub '${Bucket}.fifo' FifoQueue: true NextJsRegenerationLambdaRole: Condition: HasNextJsRegenerationLambda Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies: - PolicyName: S3Policy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - s3:GetObject* - s3:GetBucket* - s3:List* - s3:DeleteObject* - s3:PutObject* - s3:Abort* Resource: - !Sub 'arn:aws:s3:::${Bucket}' - !Sub 'arn:aws:s3:::${Bucket}/*' - PolicyName: SQSPolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - sqs:ReceiveMessage - sqs:ChangeMessageVisibility - sqs:GetQueueUrl - sqs:DeleteMessage - sqs:GetQueueAttributes Resource: !GetAtt NextJsRegenerationQueue.Arn NextJsRegenerationLambda: Condition: HasNextJsRegenerationLambda Type: AWS::Lambda::Function Properties: Code: S3Bucket: !Ref LambdaBucket S3Key: !Ref NextJsRegenerationLambdaKey Role: !GetAtt NextJsRegenerationLambdaRole.Arn Description: Regeneration Lambda function for Next.js Handler: index.handler Runtime: nodejs14.x Timeout: 30 MemorySize: 512 DependsOn: - NextJsRegenerationLambdaRole NextJsRegenerationLambdaCurrentVersion: Condition: HasNextJsRegenerationLambda Type: AWS::Lambda::Version Properties: FunctionName: !Ref NextJsRegenerationLambda UpdateReplacePolicy: Delete DeletionPolicy: Delete NextJsRegenerationLambdaEventSourceMapping: Condition: HasNextJsRegenerationLambda Type: AWS::Lambda::EventSourceMapping Properties: Enabled: true EventSourceArn: !GetAtt NextJsRegenerationQueue.Arn FunctionName: !GetAtt NextJsRegenerationLambda.Arn ViewerRequestRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - 'lambda.amazonaws.com' - 'edgelambda.amazonaws.com' Action: 'sts:AssumeRole' ViewerRequestLambdaPolicy: Type: 'AWS::IAM::Policy' Properties: PolicyDocument: Statement: - Effect: Allow Action: - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: !GetAtt 'ViewerRequestLogGroup.Arn' PolicyName: lambda Roles: - !Ref ViewerRequestRole ViewerRequestLambdaEdgePolicy: Type: 'AWS::IAM::Policy' Properties: PolicyDocument: Statement: - Effect: Allow Action: 'logs:CreateLogGroup' Resource: !Sub 'arn:${AWS::Partition}:logs:*:${AWS::AccountId}:log-group:/aws/lambda/us-east-1.${ViewerRequestFunction}:log-stream:' - Effect: Allow Action: - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: !Sub 'arn:${AWS::Partition}:logs:*:${AWS::AccountId}:log-group:/aws/lambda/us-east-1.${ViewerRequestFunction}:log-stream:*' PolicyName: 'lambda-edge' Roles: - !Ref ViewerRequestRole ViewerRequestFunction: Type: 'AWS::Lambda::Function' Properties: Code: ZipFile: !Sub | const regex = /\.[A-Za-z0-9]+$/; const indexDocument = '${DefaultRootObject}'; const domainName = '${DomainName}'.toLowerCase(); const redirectSubDomainNameWithDot = '${RedirectSubDomainNameWithDot}'.toLowerCase(); const redirectDomainName = redirectSubDomainNameWithDot ? `${!redirectSubDomainNameWithDot}${!domainName}` : ''; exports.handler = async function(event) { const cf = event.Records[0].cf; if (cf.request.headers.host[0].value.toLowerCase() === redirectDomainName) { return { status: '301', statusDescription: 'Moved Permanently', headers: { location: [{ key: 'Location', value: `https://${!domainName}${!cf.request.uri}`, }], } }; } if (cf.request.uri.endsWith('/')) { return Object.assign({}, cf.request, {uri: `${!cf.request.uri}${!indexDocument}`}); } if (cf.request.uri.endsWith(`/${!indexDocument}`)) { return { status: '302', statusDescription: 'Found', headers: { location: [{ key: 'Location', value: cf.request.uri.substr(0, cf.request.uri.length - indexDocument.length), }], } }; } if (!regex.test(cf.request.uri)) { return { status: '302', statusDescription: 'Found', headers: { location: [{ key: 'Location', value: `${!cf.request.uri}/`, }], } }; } return cf.request; }; Handler: 'index.handler' MemorySize: 128 Role: !GetAtt 'ViewerRequestRole.Arn' Runtime: 'nodejs12.x' Timeout: 5 ViewerRequestVersion: Type: 'AWS::Lambda::Version' Properties: FunctionName: !Ref ViewerRequestFunction ViewerRequestLogGroup: Type: 'AWS::Logs::LogGroup' Properties: LogGroupName: !Sub '/aws/lambda/${ViewerRequestFunction}' RetentionInDays: !Ref LogsRetentionInDays ViewerResponseFunction: Type: 'AWS::CloudFront::Function' Properties: AutoPublish: true Name: !Sub '${AWS::StackName}-CfFunction' FunctionConfig: Comment: CloudFront Function to add security headers to objects Runtime: cloudfront-js-1.0 FunctionCode: !Sub | function handler(event) { var response = event.response; var headers = response.headers; // Set HTTP security headers // Since JavaScript doesn't allow for hyphens in variable names, we use the dict["key"] notation headers['strict-transport-security'] = { value: 'max-age=63072000; includeSubdomains; preload'}; headers['content-security-policy'] = { value: "${ContentSecurityPolicy}"}; headers['x-content-type-options'] = { value: 'nosniff'}; headers['x-frame-options'] = {value: 'DENY'}; headers['x-xss-protection'] = {value: '1; mode=block'}; headers['referrer-policy'] = {value: 'same-origin'}; // Return the response to viewers return response; } CloudFrontOriginAccessIdentity: Condition: HasDomainOrIsNextJs Type: 'AWS::CloudFront::CloudFrontOriginAccessIdentity' Properties: CloudFrontOriginAccessIdentityConfig: Comment: !Sub - '${SubDomainNameWithDot}${DomainName}' - SubDomainNameWithDot: !Ref SubDomainNameWithDot DomainName: !Ref DomainName CloudFrontDistribution: Condition: HasDomainOrIsNextJs Type: 'AWS::CloudFront::Distribution' Properties: DistributionConfig: Aliases: !If - ShouldIncludeCloudFrontAlias - !If - HasRedirectDomainName - - !Sub - '${SubDomainNameWithDot}${DomainName}' - SubDomainNameWithDot: !Ref SubDomainNameWithDot DomainName: !Ref DomainName - !Sub - '${RedirectSubDomainNameWithDot}${DomainName}' - RedirectSubDomainNameWithDot: !Ref RedirectSubDomainNameWithDot DomainName: !Ref DomainName - - !Sub - '${SubDomainNameWithDot}${DomainName}' - SubDomainNameWithDot: !Ref SubDomainNameWithDot DomainName: !Ref DomainName - !Ref 'AWS::NoValue' Comment: Created by Ness CustomErrorResponses: !If - HasDefaultErrorObject - - ErrorCode: 403 # 403 from S3 indicates that the file does not exists ResponseCode: !Ref DefaultErrorResponseCode ResponsePagePath: !Sub - '/${DefaultErrorObject}' - DefaultErrorObject: !Ref DefaultErrorObject - [] CacheBehaviors: - !If - HasNextJsImagePath - AllowedMethods: - GET - HEAD - OPTIONS - PUT - PATCH - POST - DELETE CachePolicyId: !Ref ImageCachePolicyArn CachedMethods: - GET - HEAD - OPTIONS Compress: true LambdaFunctionAssociations: - EventType: origin-request LambdaFunctionARN: !Ref NextJsImageLambdaCurrentVersion OriginRequestPolicyId: !Ref ImageOriginRequestPolicyArn PathPattern: !Ref NextJsImagePath TargetOriginId: s3origin ViewerProtocolPolicy: redirect-to-https - !Ref AWS::NoValue - !If - HasNextJsDataPath - AllowedMethods: - GET - HEAD - OPTIONS CachePolicyId: !Ref DefaultCachePolicyArn CachedMethods: - GET - HEAD - OPTIONS Compress: true LambdaFunctionAssociations: - EventType: origin-request IncludeBody: true LambdaFunctionARN: !Ref NextJsDefaultLambdaCurrentVersion - EventType: origin-response LambdaFunctionARN: !Ref NextJsDefaultLambdaCurrentVersion PathPattern: !Ref NextJsDataPath TargetOriginId: s3origin ViewerProtocolPolicy: redirect-to-https - !Ref AWS::NoValue - !If - HasNextJsBasePath - AllowedMethods: - GET - HEAD - OPTIONS CachePolicyId: !Ref StaticCachePolicyArn CachedMethods: - GET - HEAD - OPTIONS Compress: true PathPattern: !Ref NextJsBasePath TargetOriginId: s3origin ViewerProtocolPolicy: redirect-to-https - !Ref AWS::NoValue - !If - HasNextJsStaticPath - AllowedMethods: - GET - HEAD - OPTIONS CachePolicyId: !Ref StaticCachePolicyArn CachedMethods: - GET - HEAD - OPTIONS Compress: true PathPattern: !Ref NextJsStaticPath TargetOriginId: s3origin ViewerProtocolPolicy: redirect-to-https - !Ref AWS::NoValue - !If - HasNextJsApiPath - AllowedMethods: - GET - HEAD - OPTIONS - PUT - PATCH - POST - DELETE CachePolicyId: !Ref DefaultCachePolicyArn CachedMethods: - GET - HEAD - OPTIONS Compress: true LambdaFunctionAssociations: - EventType: origin-request IncludeBody: true LambdaFunctionARN: !Ref NextJsApiLambdaCurrentVersion PathPattern: !Ref NextJsApiPath TargetOriginId: s3origin ViewerProtocolPolicy: redirect-to-https - !Ref AWS::NoValue DefaultCacheBehavior: AllowedMethods: - GET - HEAD - OPTIONS CachePolicyId: !Ref DefaultCachePolicyArn CachedMethods: - GET - HEAD - OPTIONS Compress: true LambdaFunctionAssociations: !If - IsNextJs - - EventType: origin-request IncludeBody: true LambdaFunctionARN: !Ref NextJsDefaultLambdaCurrentVersion - EventType: origin-response LambdaFunctionARN: !Ref NextJsDefaultLambdaCurrentVersion - - EventType: viewer-request LambdaFunctionARN: !Ref ViewerRequestVersion FunctionAssociations: - EventType: viewer-response FunctionARN: !GetAtt ViewerResponseFunction.FunctionMetadata.FunctionARN TargetOriginId: s3origin ViewerProtocolPolicy: redirect-to-https DefaultRootObject: !If [HasDefaultRootObject, !Ref DefaultRootObject, !Ref 'AWS::NoValue'] Enabled: true HttpVersion: http2 IPV6Enabled: true Logging: !If [ HasBucket, { Bucket: {'Fn::ImportValue': !Sub '${AccessLogStack}-BucketDomainName'}, Prefix: !Ref 'AWS::StackName', }, !Ref 'AWS::NoValue', ] Origins: - DomainName: !GetAtt 'Bucket.RegionalDomainName' Id: s3origin S3OriginConfig: OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}' PriceClass: 'PriceClass_All' ViewerCertificate: !If [ HasExistingCertificate, { AcmCertificateArn: !Ref ExistingCertificate, MinimumProtocolVersion: 'TLSv1.1_2016', SslSupportMethod: 'sni-only', }, !Ref 'AWS::NoValue', ] WebACLId: !If - HasWAF - {'Fn::ImportValue': !Sub '${WAFStack}-WebACL'} - !Ref 'AWS::NoValue' Outputs: StackName: Description: 'Stack name.' Value: !Sub '${AWS::StackName}' BucketName: Description: 'Name of the S3 bucket storing the static files.' Value: !Ref Bucket Export: Name: !Sub '${AWS::StackName}-BucketName' URL: Description: 'URL to static website.' Value: !If - HasDomain - !Sub - 'https://${SubDomainNameWithDot}${DomainName}' - SubDomainNameWithDot: !Ref SubDomainNameWithDot DomainName: !Ref DomainName - !If - IsNextJs - !Sub - 'https://${DomainName}' - DomainName: !GetAtt 'CloudFrontDistribution.DomainName' - !GetAtt 'Bucket.WebsiteURL' Export: Name: !Sub '${AWS::StackName}-URL' DistributionId: Condition: HasDomainOrIsNextJs Description: 'CloudFront distribution ID' Value: !Ref CloudFrontDistribution Export: Name: !Sub '${AWS::StackName}-DistributionId' DistributionDomainName: Condition: HasDomainOrIsNextJs Description: 'CloudFront distribution domain name' Value: !GetAtt 'CloudFrontDistribution.DomainName' Export: Name: !Sub '${AWS::StackName}-DistributionDomainName'