Skip to content

Commit

Permalink
feat: add configuration for deploying a cloudfront distribution for t…
Browse files Browse the repository at this point in the history
…he veda-backend (#229)
  • Loading branch information
smohiudd authored Oct 12, 2023
2 parents 7b54953 + dd1d675 commit 8965651
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 25 deletions.
8 changes: 8 additions & 0 deletions .example.env
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@ VEDA_RASTER_DATA_ACCESS_ROLE_ARN=[OPTIONAL ARN OF IAM ROLE TO BE ASSUMED BY RAST
VEDA_RASTER_EXPORT_ASSUME_ROLE_CREDS_AS_ENVS=False

VEDA_DB_PUBLICLY_ACCESSIBLE=TRUE

VEDA_RASTER_PATH_PREFIX=
VEDA_STAC_PATH_PREFIX=

STAC_BROWSER_BUCKET=
STAC_URL=
CERT_ARN=
VEDA_CLOUDFRONT=
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ The constructs and applications in this project are configured using pydantic. T
| Domain | `VEDA_DOMAIN` | [domain/infrastructure/config.py](domain/infrastructure/config.py) |
| Network | `N/A` | [network/infrastructure/config.py](network/infrastructure/config.py) |
| Raster API (TiTiler) | `VEDA_RASTER` | [raster_api/infrastructure/config.py](raster_-_api/infrastructure/config.py) |
| STAC API | `VEDA_STAC` | [stac_api/infrastructure/config.py](stac_api/infrastructure/config.py) |
| STAC API | `VEDA` | [stac_api/infrastructure/config.py](stac_api/infrastructure/config.py) |
| Routes | `VEDA` | [routes/infrastructure/config.py](routes/infrastructure/config.py) |

### Deploying to the cloud

Expand Down
9 changes: 9 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from network.infrastructure.construct import VpcConstruct
from permissions_boundary.infrastructure.construct import PermissionsBoundaryAspect
from raster_api.infrastructure.construct import RasterApiLambdaConstruct
from routes.infrastructure.construct import CloudfrontDistributionConstruct
from stac_api.infrastructure.construct import StacApiLambdaConstruct

app = App()
Expand Down Expand Up @@ -71,6 +72,14 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
domain_name=domain.stac_domain_name,
)

veda_routes = CloudfrontDistributionConstruct(
veda_stack,
"routes",
raster_api_id=raster_api.raster_api.api_id,
stac_api_id=stac_api.stac_api.api_id,
region=veda_app_settings.cdk_default_region,
)

# TODO this conditional supports deploying a second set of APIs to a separate custom domain and should be removed if no longer necessary
if veda_app_settings.alt_domain():
alt_domain = DomainConstruct(
Expand Down
24 changes: 17 additions & 7 deletions raster_api/infrastructure/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,40 +45,50 @@ class vedaRasterSettings(BaseSettings):
timeout: int = 30 # seconds
memory: int = 8000 # Mb

enable_mosaic_search: bool = Field(
raster_enable_mosaic_search: bool = Field(
False,
description="Deploy the raster API with the mosaic/list endpoint TRUE/FALSE",
)
pgstac_secret_arn: Optional[str] = Field(
raster_pgstac_secret_arn: Optional[str] = Field(
None,
description="Name or ARN of the AWS Secret containing database connection parameters",
)

data_access_role_arn: Optional[str] = Field(
raster_data_access_role_arn: Optional[str] = Field(
None,
description="Resource name of role permitting access to specified external S3 buckets",
)

export_assume_role_creds_as_envs: Optional[bool] = Field(
raster_export_assume_role_creds_as_envs: Optional[bool] = Field(
False,
description="enables 'get_gdal_config' flow to export AWS credentials as os env vars",
)

aws_request_payer: Optional[str] = Field(
raster_aws_request_payer: Optional[str] = Field(
None,
description="Set optional global parameter to 'requester' if the requester agrees to pay S3 transfer costs",
)

path_prefix: Optional[str] = Field(
raster_path_prefix: Optional[str] = Field(
"",
description="Optional path prefix to add to all api endpoints",
)

domain_hosted_zone_name: Optional[str] = Field(
None,
description="Domain name for the cloudfront distribution",
)

cloudfront: Optional[bool] = Field(
False,
description="Boolean if Cloudfront Distribution should be deployed",
)

class Config:
"""model config"""

env_file = ".env"
env_prefix = "VEDA_RASTER_"
env_prefix = "VEDA_"


veda_raster_settings = vedaRasterSettings()
35 changes: 25 additions & 10 deletions raster_api/infrastructure/construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,26 +62,41 @@ def __init__(

veda_raster_function.add_environment(
"VEDA_RASTER_ENABLE_MOSAIC_SEARCH",
str(veda_raster_settings.enable_mosaic_search),
str(veda_raster_settings.raster_enable_mosaic_search),
)

veda_raster_function.add_environment(
"VEDA_RASTER_PGSTAC_SECRET_ARN", database.pgstac.secret.secret_full_arn
)

veda_raster_function.add_environment(
"VEDA_RASTER_PATH_PREFIX", veda_raster_settings.path_prefix
"VEDA_RASTER_PATH_PREFIX", veda_raster_settings.raster_path_prefix
)

# Optional AWS S3 requester pays global setting
if veda_raster_settings.aws_request_payer:
if veda_raster_settings.raster_aws_request_payer:
veda_raster_function.add_environment(
"AWS_REQUEST_PAYER", veda_raster_settings.aws_request_payer
"AWS_REQUEST_PAYER", veda_raster_settings.raster_aws_request_payer
)

integration_kwargs = dict(handler=veda_raster_function)
if (
veda_raster_settings.domain_hosted_zone_name
and veda_raster_settings.cloudfront
):
integration_kwargs[
"parameter_mapping"
] = aws_apigatewayv2_alpha.ParameterMapping().overwrite_header(
"host",
aws_apigatewayv2_alpha.MappingValue(
veda_raster_settings.domain_hosted_zone_name
),
)

raster_api_integration = (
aws_apigatewayv2_integrations_alpha.HttpLambdaIntegration(
construct_id, veda_raster_function
construct_id,
**integration_kwargs,
)
)

Expand Down Expand Up @@ -112,12 +127,12 @@ def __init__(
)

# Optional use sts assume role with GetObject permissions for external S3 bucket(s)
if veda_raster_settings.data_access_role_arn:
if veda_raster_settings.raster_data_access_role_arn:
# Get the role for external data access
data_access_role = aws_iam.Role.from_role_arn(
self,
"data-access-role",
veda_raster_settings.data_access_role_arn,
veda_raster_settings.raster_data_access_role_arn,
)

# Allow this lambda to assume the data access role
Expand All @@ -128,12 +143,12 @@ def __init__(

veda_raster_function.add_environment(
"VEDA_RASTER_DATA_ACCESS_ROLE_ARN",
veda_raster_settings.data_access_role_arn,
veda_raster_settings.raster_data_access_role_arn,
)

# Optional configuration to export assume role session into lambda function environment
if veda_raster_settings.export_assume_role_creds_as_envs:
if veda_raster_settings.raster_export_assume_role_creds_as_envs:
veda_raster_function.add_environment(
"VEDA_RASTER_EXPORT_ASSUME_ROLE_CREDS_AS_ENVS",
str(veda_raster_settings.export_assume_role_creds_as_envs),
str(veda_raster_settings.raster_export_assume_role_creds_as_envs),
)
50 changes: 50 additions & 0 deletions routes/infrastructure/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Settings for Cloudfront distribution - any environment variables starting with
`VEDA_` will overwrite the values of variables in this file
"""
from typing import Optional

from pydantic import BaseSettings, Field


class vedaRouteSettings(BaseSettings):
"""Veda Route settings"""

cloudfront: Optional[bool] = Field(
False,
description="Boolean if Cloudfront Distribution should be deployed",
)

# STAC S#3 browser bucket name
stac_browser_bucket: Optional[str] = Field(
"", description="STAC browser S3 bucket name"
)

# API Gateway URLs
ingest_url: Optional[str] = Field(
"",
description="URL of ingest API",
)

domain_hosted_zone_name: Optional[str] = Field(
None,
description="Domain name for the cloudfront distribution",
)

domain_hosted_zone_id: Optional[str] = Field(
None, description="Domain ID for the cloudfront distribution"
)

cert_arn: Optional[str] = Field(
None,
description="Certificate’s ARN",
)

class Config:
"""model config"""

env_prefix = "VEDA_"
case_sentive = False
env_file = ".env"


veda_route_settings = vedaRouteSettings()
88 changes: 88 additions & 0 deletions routes/infrastructure/construct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""CDK Construct for a Cloudfront Distribution."""
from typing import Optional
from urllib.parse import urlparse

from aws_cdk import CfnOutput, Stack
from aws_cdk import aws_certificatemanager as certificatemanager
from aws_cdk import aws_cloudfront as cf
from aws_cdk import aws_cloudfront_origins as origins
from aws_cdk import aws_s3 as s3
from constructs import Construct

from .config import veda_route_settings


class CloudfrontDistributionConstruct(Construct):
"""CDK Construct for a Cloudfront Distribution."""

def __init__(
self,
scope: Construct,
construct_id: str,
raster_api_id: str,
stac_api_id: str,
region: Optional[str],
**kwargs,
) -> None:
"""."""
super().__init__(scope, construct_id)

stack_name = Stack.of(self).stack_name

if veda_route_settings.cloudfront:
s3Bucket = s3.Bucket.from_bucket_name(
self,
"stac-browser-bucket",
bucket_name=veda_route_settings.stac_browser_bucket,
)

# Certificate must be in zone us-east-1
domain_cert = (
certificatemanager.Certificate.from_certificate_arn(
self, "domainCert", veda_route_settings.cert_arn
)
if veda_route_settings.cert_arn
else None
)

self.distribution = cf.Distribution(
self,
stack_name,
comment=stack_name,
default_behavior=cf.BehaviorOptions(
origin=origins.HttpOrigin(
s3Bucket.bucket_website_domain_name,
protocol_policy=cf.OriginProtocolPolicy.HTTP_ONLY,
),
cache_policy=cf.CachePolicy.CACHING_DISABLED,
),
certificate=domain_cert,
domain_names=[veda_route_settings.domain_hosted_zone_name]
if veda_route_settings.domain_hosted_zone_name
else None,
additional_behaviors={
"/api/stac*": cf.BehaviorOptions(
origin=origins.HttpOrigin(
f"{stac_api_id}.execute-api.{region}.amazonaws.com"
),
cache_policy=cf.CachePolicy.CACHING_DISABLED,
allowed_methods=cf.AllowedMethods.ALLOW_ALL,
),
"/api/raster*": cf.BehaviorOptions(
origin=origins.HttpOrigin(
f"{raster_api_id}.execute-api.{region}.amazonaws.com"
),
cache_policy=cf.CachePolicy.CACHING_DISABLED,
allowed_methods=cf.AllowedMethods.ALLOW_ALL,
),
"/api/ingest*": cf.BehaviorOptions(
origin=origins.HttpOrigin(
urlparse(veda_route_settings.ingest_url).hostname
),
cache_policy=cf.CachePolicy.CACHING_DISABLED,
allowed_methods=cf.AllowedMethods.ALLOW_ALL,
),
},
)

CfnOutput(self, "Endpoint", value=self.distribution.domain_name)
16 changes: 13 additions & 3 deletions stac_api/infrastructure/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,31 @@ class vedaSTACSettings(BaseSettings):
memory: int = 8000 # Mb

# Secret database credentials
pgstac_secret_arn: Optional[str] = Field(
stac_pgstac_secret_arn: Optional[str] = Field(
None,
description="Name or ARN of the AWS Secret containing database connection parameters",
)

path_prefix: Optional[str] = Field(
stac_path_prefix: Optional[str] = Field(
"",
description="Optional path prefix to add to all api endpoints",
)

domain_hosted_zone_name: Optional[str] = Field(
None,
description="Domain name for the cloudfront distribution",
)

cloudfront: Optional[bool] = Field(
False,
description="Boolean if Cloudfront Distribution should be deployed",
)

class Config:
"""model config"""

env_file = ".env"
env_prefix = "VEDA_STAC_"
env_prefix = "VEDA_"


veda_stac_settings = vedaSTACSettings()
20 changes: 16 additions & 4 deletions stac_api/infrastructure/construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,24 @@ def __init__(
)

lambda_function.add_environment(
"VEDA_STAC_PATH_PREFIX", veda_stac_settings.path_prefix
"VEDA_STAC_PATH_PREFIX", veda_stac_settings.stac_path_prefix
)

integration_kwargs = dict(handler=lambda_function)
if veda_stac_settings.domain_hosted_zone_name and veda_stac_settings.cloudfront:
integration_kwargs[
"parameter_mapping"
] = aws_apigatewayv2_alpha.ParameterMapping().overwrite_header(
"host",
aws_apigatewayv2_alpha.MappingValue(
veda_stac_settings.domain_hosted_zone_name
),
)

stac_api_integration = (
aws_apigatewayv2_integrations_alpha.HttpLambdaIntegration(
construct_id, handler=lambda_function
construct_id,
**integration_kwargs,
)
)

Expand All @@ -86,11 +98,11 @@ def __init__(
domain_name=domain_name
)

stac_api = aws_apigatewayv2_alpha.HttpApi(
self.stac_api = aws_apigatewayv2_alpha.HttpApi(
self,
f"{stack_name}-{construct_id}",
default_integration=stac_api_integration,
default_domain_mapping=domain_mapping,
)

CfnOutput(self, "stac-api", value=stac_api.url)
CfnOutput(self, "stac-api", value=self.stac_api.url)

0 comments on commit 8965651

Please sign in to comment.