AWSTemplateFormatVersion: '2010-09-09' Description: Serve Z/X/Y tiles through CloudFront + Lambda from an existing S3 bucket. Parameters: BucketName: Description: 'Name of an existing S3 bucket with .pmtiles tilesets. Should be in the same region as your CloudFormation stack.' Type: String MinLength: 1 AllowedOrigins: Description: 'Comma-separated list of domains (e.g. example.com) allowed by browser CORS policy, or * for all origins.' Type: List Default: "*" PublicHostname: Description: 'Optional. Replace *.cloudfront.net in TileJSON with a custom hostname (e.g. example.com). See docs on how this value is cached.' Type: String Outputs: CloudFrontDistributionUrl: Description: 'URL of the CloudFront distribution' Value: !Sub "https://${CloudFrontDistribution.DomainName}" Export: Name: !Sub "${AWS::StackName}-CloudFrontDistributionURL" Conditions: IsPublicHostnameProvided: Fn::Not: - Fn::Equals: - Ref: PublicHostname - '' Resources: LogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${AWS::StackName}" RetentionInDays: 7 LambdaExecutionRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole Policies: - PolicyName: !Sub "${AWS::StackName}-LambdaLoggingPolicy" PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogStream - logs:PutLogEvents Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${AWS::StackName}:log-stream:*" - PolicyName: !Sub "${AWS::StackName}-LambdaS3AccessPolicy" PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: s3:GetObject Resource: !Sub arn:aws:s3:::${BucketName}/* LambdaFunctionUrl: Type: 'AWS::Lambda::Url' Properties: AuthType: NONE TargetFunctionArn: !GetAtt LambdaFunction.Arn InvokeMode: BUFFERED LambdaFunctionUrlPermission: Type: 'AWS::Lambda::Permission' Properties: Action: lambda:InvokeFunctionUrl FunctionName: !Ref LambdaFunction Principal: '*' FunctionUrlAuthType: NONE ViewerRequestCloudFrontFunction: Type: AWS::CloudFront::Function Properties: Name: !Sub "${AWS::StackName}-ViewerRequestCloudFrontFunction" AutoPublish: true FunctionCode: | function handler(event) { const request = event.request; request.headers['x-distribution-domain-name'] = { value: event.context.distributionDomainName }; return request; } FunctionConfig: Comment: 'Add x-distribution-domain header.' Runtime: cloudfront-js-2.0 CloudFrontDistribution: Type: 'AWS::CloudFront::Distribution' DeletionPolicy: Delete Properties: DistributionConfig: Origins: - Id: LambdaOrigin DomainName: !Select [2, !Split ["/", !GetAtt LambdaFunctionUrl.FunctionUrl]] CustomOriginConfig: OriginProtocolPolicy: https-only DefaultCacheBehavior: TargetOriginId: LambdaOrigin ViewerProtocolPolicy: redirect-to-https CachePolicyId: !Ref CachePolicyId ResponseHeadersPolicyId: !Ref ResponseHeadersPolicyId OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac FunctionAssociations: Fn::If: - IsPublicHostnameProvided - !Ref "AWS::NoValue" - - EventType: viewer-request FunctionARN: !GetAtt ViewerRequestCloudFrontFunction.FunctionARN Enabled: true HttpVersion: http2and3 Comment: "CloudFront Distribution" PriceClass: PriceClass_All # Change this to save cost and distribute to fewer countries. Check https://aws.amazon.com/cloudfront/pricing/ CachePolicyId: Type: 'AWS::CloudFront::CachePolicy' Properties: CachePolicyConfig: Name: !Sub "${AWS::StackName}-CachePolicyConfig" DefaultTTL: 86400 MaxTTL: 31536000 MinTTL: 0 ParametersInCacheKeyAndForwardedToOrigin: EnableAcceptEncodingBrotli: true EnableAcceptEncodingGzip: true HeadersConfig: HeaderBehavior: none CookiesConfig: CookieBehavior: none QueryStringsConfig: QueryStringBehavior: none ResponseHeadersPolicyId: Type: 'AWS::CloudFront::ResponseHeadersPolicy' # DeletionPolicy: Delete Properties: ResponseHeadersPolicyConfig: Name: !Sub "${AWS::StackName}-ResponseHeadersPolicyConfig" CorsConfig: AccessControlAllowOrigins: Items: !Ref AllowedOrigins AccessControlAllowHeaders: Items: - '*' AccessControlAllowMethods: Items: - GET - HEAD - OPTIONS AccessControlExposeHeaders: Items: - ETag AccessControlAllowCredentials: false # Set to true if you want to include credentials OriginOverride: true Comment: 'CORS policy for Protomaps' LambdaFunction: Type: 'AWS::Lambda::Function' Properties: FunctionName: !Sub "${AWS::StackName}-LambdaFunction" Runtime: nodejs20.x Architectures: [arm64] Role: !GetAtt LambdaExecutionRole.Arn Handler: index.handler MemorySize: 512 LoggingConfig: LogGroup: !Ref LogGroup Environment: Variables: BUCKET: !Ref BucketName PUBLIC_HOSTNAME: !Ref PublicHostname Code: S3Bucket: !Ref BucketName S3Key: lambda_function.zip