From 4b74ea86ebeeb0d417ca47d5b27ccce21e1ec4bb Mon Sep 17 00:00:00 2001 From: hrodmn Date: Thu, 20 Feb 2025 06:22:37 -0600 Subject: [PATCH 1/3] Add CloudFront distribution for STAC Browser --- browser_config.js | 34 ++++++++++++++++++ infrastructure/app.py | 77 +++++++++++++++++++++++++++++++++++----- infrastructure/config.py | 12 +++++++ 3 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 browser_config.js diff --git a/browser_config.js b/browser_config.js new file mode 100644 index 0000000..a3631f9 --- /dev/null +++ b/browser_config.js @@ -0,0 +1,34 @@ +module.exports = { + catalogUrl: "https://stac.eoapi.dev", + catalogTitle: "eoAPI STAC Browser", + allowExternalAccess: true, // Must be true if catalogUrl is not given + allowedDomains: [], + detectLocaleFromBrowser: true, + storeLocale: true, + locale: "en", + fallbackLocale: "en", + supportedLocales: ["de", "es", "en", "fr", "it", "ro"], + apiCatalogPriority: null, + useTileLayerAsFallback: true, + displayGeoTiffByDefault: false, + buildTileUrlTemplate: ({ href, asset }) => + "https://raster.eoapi.dev/external/tiles/WebMercatorQuad/{z}/{x}/{y}@2x?url=" + + encodeURIComponent(asset.href.startsWith("/vsi") ? asset.href : href), + stacProxyUrl: null, + pathPrefix: "/", + historyMode: "history", + cardViewMode: "cards", + cardViewSort: "asc", + showThumbnailsAsAssets: false, + stacLint: true, + geoTiffResolution: 128, + redirectLegacyUrls: false, + itemsPerPage: 12, + defaultThumbnailSize: null, + maxPreviewsOnMap: 50, + crossOriginMedia: null, + requestHeaders: {}, + requestQueryParameters: {}, + preprocessSTAC: null, + authConfig: null, +}; diff --git a/infrastructure/app.py b/infrastructure/app.py index 2ff3edb..eb1de79 100644 --- a/infrastructure/app.py +++ b/infrastructure/app.py @@ -7,10 +7,14 @@ RemovalPolicy, Stack, aws_certificatemanager, + aws_cloudfront, + aws_cloudfront_origins, aws_ec2, aws_iam, aws_lambda, aws_rds, + aws_route53, + aws_route53_targets, aws_s3, ) from aws_cdk.aws_apigateway import DomainNameOptions @@ -352,22 +356,76 @@ def __init__( ) if app_config.stac_browser_version: + if not ( + app_config.hosted_zone_id + and app_config.stac_browser_custom_domain + and app_config.stac_browser_certificate_arn + ): + raise ValueError( + "to deploy STAC browser you must provide config parameters for hosted_zone_id and stac_browser_custom_domain and stac_browser_certificate_arn" + ) + stac_browser_bucket = aws_s3.Bucket( self, "stac-browser-bucket", bucket_name=app_config.build_service_name("stac-browser"), removal_policy=RemovalPolicy.DESTROY, auto_delete_objects=True, - website_index_document="index.html", - public_read_access=True, - block_public_access=aws_s3.BlockPublicAccess( - block_public_acls=False, - block_public_policy=False, - ignore_public_acls=False, - restrict_public_buckets=False, + block_public_access=aws_s3.BlockPublicAccess.BLOCK_ALL, + enforce_ssl=True, + ) + + distribution = aws_cloudfront.Distribution( + self, + "stac-browser-distribution", + default_behavior=aws_cloudfront.BehaviorOptions( + origin=aws_cloudfront_origins.S3Origin(stac_browser_bucket), + viewer_protocol_policy=aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + allowed_methods=aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD, + cached_methods=aws_cloudfront.CachedMethods.CACHE_GET_HEAD, + ), + default_root_object="index.html", + error_responses=[ + aws_cloudfront.ErrorResponse( + http_status=404, + response_http_status=200, + response_page_path="/index.html", + ) + ], + certificate=aws_certificatemanager.Certificate.from_certificate_arn( + self, + "stac-browser-certificate", + app_config.stac_browser_certificate_arn, + ), + domain_names=[app_config.stac_browser_custom_domain], + ) + + account_id = Stack.of(self).account + distribution_arn = f"arn:aws:cloudfront::${account_id}:distribution/${distribution.distribution_id}" + + stac_browser_bucket.add_to_resource_policy( + aws_iam.PolicyStatement( + actions=["s3:GetObject"], + resources=[stac_browser_bucket.arn_for_objects("*")], + principals=[aws_iam.ServicePrincipal("cloudfront.amazonaws.com")], + conditions={"StringEquals": {"AWS:SourceArn": distribution_arn}}, + ) + ) + + hosted_zone = aws_route53.HostedZone.from_hosted_zone_id( + self, "stac-browser-hosted-zone", app_config.hosted_zone_id + ) + + aws_route53.ARecord( + self, + "stac-browser-alias", + zone=hosted_zone, + target=aws_route53.RecordTarget.from_alias( + aws_route53_targets.CloudFrontTarget(distribution) ), - object_ownership=aws_s3.ObjectOwnership.OBJECT_WRITER, + record_name=app_config.stac_browser_custom_domain, ) + StacBrowser( self, "stac-browser", @@ -375,6 +433,9 @@ def __init__( stac_catalog_url=f"https://{app_config.stac_api_custom_domain}", website_index_document="index.html", bucket_arn=stac_browser_bucket.bucket_arn, + config_file_path=os.path.join( + os.path.abspath(context_dir), "browser_config.js" + ), ) def _create_data_access_role(self) -> aws_iam.Role: diff --git a/infrastructure/config.py b/infrastructure/config.py index 1342991..76c6f83 100644 --- a/infrastructure/config.py +++ b/infrastructure/config.py @@ -116,6 +116,18 @@ class AppConfig(BaseSettings): as it will be used as a backend.""", default=None, ) + stac_browser_custom_domain: Optional[str] = Field( + description="Custom domain name for the STAC Browser site", + default=None, + ) + stac_browser_certificate_arn: Optional[str] = Field( + description="Arn for the STAC Browser custom domain name (must be in us-east-1)", + default=None, + ) + hosted_zone_id: Optional[str] = Field( + description="Hosted Zone ID for custom domains", + default=None, + ) model_config = SettingsConfigDict( env_file=".env-cdk", yaml_file="config.yaml", extra="allow" From 1c8abd6a7f471ca5be885f75c2f41419ec28eefc Mon Sep 17 00:00:00 2001 From: hrodmn Date: Thu, 20 Feb 2025 06:34:53 -0600 Subject: [PATCH 2/3] add config.yaml to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0889d95..54110c9 100644 --- a/.gitignore +++ b/.gitignore @@ -169,3 +169,4 @@ node_modules/ .ruff_cache/ .env-cdk +config.yaml From 0d86184a9ae2b781a02eebbaabcb5ac1d52ecfad Mon Sep 17 00:00:00 2001 From: hrodmn Date: Thu, 20 Feb 2025 06:45:53 -0600 Subject: [PATCH 3/3] fix hosted zone lookup --- infrastructure/app.py | 8 ++++++-- infrastructure/config.py | 20 ++++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/infrastructure/app.py b/infrastructure/app.py index eb1de79..40fe0d9 100644 --- a/infrastructure/app.py +++ b/infrastructure/app.py @@ -358,6 +358,7 @@ def __init__( if app_config.stac_browser_version: if not ( app_config.hosted_zone_id + and app_config.hosted_zone_name and app_config.stac_browser_custom_domain and app_config.stac_browser_certificate_arn ): @@ -412,8 +413,11 @@ def __init__( ) ) - hosted_zone = aws_route53.HostedZone.from_hosted_zone_id( - self, "stac-browser-hosted-zone", app_config.hosted_zone_id + hosted_zone = aws_route53.HostedZone.from_hosted_zone_attributes( + self, + "stac-browser-hosted-zone", + hosted_zone_id=app_config.hosted_zone_id, + zone_name=app_config.hosted_zone_name, ) aws_route53.ARecord( diff --git a/infrastructure/config.py b/infrastructure/config.py index 76c6f83..2ff0d3b 100644 --- a/infrastructure/config.py +++ b/infrastructure/config.py @@ -128,6 +128,10 @@ class AppConfig(BaseSettings): description="Hosted Zone ID for custom domains", default=None, ) + hosted_zone_name: Optional[str] = Field( + description="Hosted Zone Name for custom domains", + default=None, + ) model_config = SettingsConfigDict( env_file=".env-cdk", yaml_file="config.yaml", extra="allow" @@ -149,14 +153,14 @@ def validate_model(self) -> Self: and therefore `nat_gateway_count` has to be > 0.""" ) - if ( - self.stac_browser_version is not None - and self.stac_api_custom_domain is None - ): - raise ValueError( - """If a STAC browser version is provided, - a custom domain must be provided for the STAC API""" - ) + # if ( + # self.stac_browser_version is not None + # and self.stac_api_custom_domain is None + # ): + # raise ValueError( + # """If a STAC browser version is provided, + # a custom domain must be provided for the STAC API""" + # ) if self.acm_certificate_arn is None and any( [