Skip to content

Commit 8965651

Browse files
authored
feat: add configuration for deploying a cloudfront distribution for the veda-backend (#229)
2 parents 7b54953 + dd1d675 commit 8965651

File tree

9 files changed

+228
-25
lines changed

9 files changed

+228
-25
lines changed

.example.env

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,11 @@ VEDA_RASTER_DATA_ACCESS_ROLE_ARN=[OPTIONAL ARN OF IAM ROLE TO BE ASSUMED BY RAST
2020
VEDA_RASTER_EXPORT_ASSUME_ROLE_CREDS_AS_ENVS=False
2121

2222
VEDA_DB_PUBLICLY_ACCESSIBLE=TRUE
23+
24+
VEDA_RASTER_PATH_PREFIX=
25+
VEDA_STAC_PATH_PREFIX=
26+
27+
STAC_BROWSER_BUCKET=
28+
STAC_URL=
29+
CERT_ARN=
30+
VEDA_CLOUDFRONT=

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ The constructs and applications in this project are configured using pydantic. T
5353
| Domain | `VEDA_DOMAIN` | [domain/infrastructure/config.py](domain/infrastructure/config.py) |
5454
| Network | `N/A` | [network/infrastructure/config.py](network/infrastructure/config.py) |
5555
| Raster API (TiTiler) | `VEDA_RASTER` | [raster_api/infrastructure/config.py](raster_-_api/infrastructure/config.py) |
56-
| STAC API | `VEDA_STAC` | [stac_api/infrastructure/config.py](stac_api/infrastructure/config.py) |
56+
| STAC API | `VEDA` | [stac_api/infrastructure/config.py](stac_api/infrastructure/config.py) |
57+
| Routes | `VEDA` | [routes/infrastructure/config.py](routes/infrastructure/config.py) |
5758

5859
### Deploying to the cloud
5960

app.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from network.infrastructure.construct import VpcConstruct
1111
from permissions_boundary.infrastructure.construct import PermissionsBoundaryAspect
1212
from raster_api.infrastructure.construct import RasterApiLambdaConstruct
13+
from routes.infrastructure.construct import CloudfrontDistributionConstruct
1314
from stac_api.infrastructure.construct import StacApiLambdaConstruct
1415

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

75+
veda_routes = CloudfrontDistributionConstruct(
76+
veda_stack,
77+
"routes",
78+
raster_api_id=raster_api.raster_api.api_id,
79+
stac_api_id=stac_api.stac_api.api_id,
80+
region=veda_app_settings.cdk_default_region,
81+
)
82+
7483
# TODO this conditional supports deploying a second set of APIs to a separate custom domain and should be removed if no longer necessary
7584
if veda_app_settings.alt_domain():
7685
alt_domain = DomainConstruct(

raster_api/infrastructure/config.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,40 +45,50 @@ class vedaRasterSettings(BaseSettings):
4545
timeout: int = 30 # seconds
4646
memory: int = 8000 # Mb
4747

48-
enable_mosaic_search: bool = Field(
48+
raster_enable_mosaic_search: bool = Field(
4949
False,
5050
description="Deploy the raster API with the mosaic/list endpoint TRUE/FALSE",
5151
)
52-
pgstac_secret_arn: Optional[str] = Field(
52+
raster_pgstac_secret_arn: Optional[str] = Field(
5353
None,
5454
description="Name or ARN of the AWS Secret containing database connection parameters",
5555
)
5656

57-
data_access_role_arn: Optional[str] = Field(
57+
raster_data_access_role_arn: Optional[str] = Field(
5858
None,
5959
description="Resource name of role permitting access to specified external S3 buckets",
6060
)
6161

62-
export_assume_role_creds_as_envs: Optional[bool] = Field(
62+
raster_export_assume_role_creds_as_envs: Optional[bool] = Field(
6363
False,
6464
description="enables 'get_gdal_config' flow to export AWS credentials as os env vars",
6565
)
6666

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

72-
path_prefix: Optional[str] = Field(
72+
raster_path_prefix: Optional[str] = Field(
7373
"",
7474
description="Optional path prefix to add to all api endpoints",
7575
)
7676

77+
domain_hosted_zone_name: Optional[str] = Field(
78+
None,
79+
description="Domain name for the cloudfront distribution",
80+
)
81+
82+
cloudfront: Optional[bool] = Field(
83+
False,
84+
description="Boolean if Cloudfront Distribution should be deployed",
85+
)
86+
7787
class Config:
7888
"""model config"""
7989

8090
env_file = ".env"
81-
env_prefix = "VEDA_RASTER_"
91+
env_prefix = "VEDA_"
8292

8393

8494
veda_raster_settings = vedaRasterSettings()

raster_api/infrastructure/construct.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,26 +62,41 @@ def __init__(
6262

6363
veda_raster_function.add_environment(
6464
"VEDA_RASTER_ENABLE_MOSAIC_SEARCH",
65-
str(veda_raster_settings.enable_mosaic_search),
65+
str(veda_raster_settings.raster_enable_mosaic_search),
6666
)
6767

6868
veda_raster_function.add_environment(
6969
"VEDA_RASTER_PGSTAC_SECRET_ARN", database.pgstac.secret.secret_full_arn
7070
)
7171

7272
veda_raster_function.add_environment(
73-
"VEDA_RASTER_PATH_PREFIX", veda_raster_settings.path_prefix
73+
"VEDA_RASTER_PATH_PREFIX", veda_raster_settings.raster_path_prefix
7474
)
7575

7676
# Optional AWS S3 requester pays global setting
77-
if veda_raster_settings.aws_request_payer:
77+
if veda_raster_settings.raster_aws_request_payer:
7878
veda_raster_function.add_environment(
79-
"AWS_REQUEST_PAYER", veda_raster_settings.aws_request_payer
79+
"AWS_REQUEST_PAYER", veda_raster_settings.raster_aws_request_payer
80+
)
81+
82+
integration_kwargs = dict(handler=veda_raster_function)
83+
if (
84+
veda_raster_settings.domain_hosted_zone_name
85+
and veda_raster_settings.cloudfront
86+
):
87+
integration_kwargs[
88+
"parameter_mapping"
89+
] = aws_apigatewayv2_alpha.ParameterMapping().overwrite_header(
90+
"host",
91+
aws_apigatewayv2_alpha.MappingValue(
92+
veda_raster_settings.domain_hosted_zone_name
93+
),
8094
)
8195

8296
raster_api_integration = (
8397
aws_apigatewayv2_integrations_alpha.HttpLambdaIntegration(
84-
construct_id, veda_raster_function
98+
construct_id,
99+
**integration_kwargs,
85100
)
86101
)
87102

@@ -112,12 +127,12 @@ def __init__(
112127
)
113128

114129
# Optional use sts assume role with GetObject permissions for external S3 bucket(s)
115-
if veda_raster_settings.data_access_role_arn:
130+
if veda_raster_settings.raster_data_access_role_arn:
116131
# Get the role for external data access
117132
data_access_role = aws_iam.Role.from_role_arn(
118133
self,
119134
"data-access-role",
120-
veda_raster_settings.data_access_role_arn,
135+
veda_raster_settings.raster_data_access_role_arn,
121136
)
122137

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

129144
veda_raster_function.add_environment(
130145
"VEDA_RASTER_DATA_ACCESS_ROLE_ARN",
131-
veda_raster_settings.data_access_role_arn,
146+
veda_raster_settings.raster_data_access_role_arn,
132147
)
133148

134149
# Optional configuration to export assume role session into lambda function environment
135-
if veda_raster_settings.export_assume_role_creds_as_envs:
150+
if veda_raster_settings.raster_export_assume_role_creds_as_envs:
136151
veda_raster_function.add_environment(
137152
"VEDA_RASTER_EXPORT_ASSUME_ROLE_CREDS_AS_ENVS",
138-
str(veda_raster_settings.export_assume_role_creds_as_envs),
153+
str(veda_raster_settings.raster_export_assume_role_creds_as_envs),
139154
)

routes/infrastructure/config.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
"""Settings for Cloudfront distribution - any environment variables starting with
2+
`VEDA_` will overwrite the values of variables in this file
3+
"""
4+
from typing import Optional
5+
6+
from pydantic import BaseSettings, Field
7+
8+
9+
class vedaRouteSettings(BaseSettings):
10+
"""Veda Route settings"""
11+
12+
cloudfront: Optional[bool] = Field(
13+
False,
14+
description="Boolean if Cloudfront Distribution should be deployed",
15+
)
16+
17+
# STAC S#3 browser bucket name
18+
stac_browser_bucket: Optional[str] = Field(
19+
"", description="STAC browser S3 bucket name"
20+
)
21+
22+
# API Gateway URLs
23+
ingest_url: Optional[str] = Field(
24+
"",
25+
description="URL of ingest API",
26+
)
27+
28+
domain_hosted_zone_name: Optional[str] = Field(
29+
None,
30+
description="Domain name for the cloudfront distribution",
31+
)
32+
33+
domain_hosted_zone_id: Optional[str] = Field(
34+
None, description="Domain ID for the cloudfront distribution"
35+
)
36+
37+
cert_arn: Optional[str] = Field(
38+
None,
39+
description="Certificate’s ARN",
40+
)
41+
42+
class Config:
43+
"""model config"""
44+
45+
env_prefix = "VEDA_"
46+
case_sentive = False
47+
env_file = ".env"
48+
49+
50+
veda_route_settings = vedaRouteSettings()

routes/infrastructure/construct.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
"""CDK Construct for a Cloudfront Distribution."""
2+
from typing import Optional
3+
from urllib.parse import urlparse
4+
5+
from aws_cdk import CfnOutput, Stack
6+
from aws_cdk import aws_certificatemanager as certificatemanager
7+
from aws_cdk import aws_cloudfront as cf
8+
from aws_cdk import aws_cloudfront_origins as origins
9+
from aws_cdk import aws_s3 as s3
10+
from constructs import Construct
11+
12+
from .config import veda_route_settings
13+
14+
15+
class CloudfrontDistributionConstruct(Construct):
16+
"""CDK Construct for a Cloudfront Distribution."""
17+
18+
def __init__(
19+
self,
20+
scope: Construct,
21+
construct_id: str,
22+
raster_api_id: str,
23+
stac_api_id: str,
24+
region: Optional[str],
25+
**kwargs,
26+
) -> None:
27+
"""."""
28+
super().__init__(scope, construct_id)
29+
30+
stack_name = Stack.of(self).stack_name
31+
32+
if veda_route_settings.cloudfront:
33+
s3Bucket = s3.Bucket.from_bucket_name(
34+
self,
35+
"stac-browser-bucket",
36+
bucket_name=veda_route_settings.stac_browser_bucket,
37+
)
38+
39+
# Certificate must be in zone us-east-1
40+
domain_cert = (
41+
certificatemanager.Certificate.from_certificate_arn(
42+
self, "domainCert", veda_route_settings.cert_arn
43+
)
44+
if veda_route_settings.cert_arn
45+
else None
46+
)
47+
48+
self.distribution = cf.Distribution(
49+
self,
50+
stack_name,
51+
comment=stack_name,
52+
default_behavior=cf.BehaviorOptions(
53+
origin=origins.HttpOrigin(
54+
s3Bucket.bucket_website_domain_name,
55+
protocol_policy=cf.OriginProtocolPolicy.HTTP_ONLY,
56+
),
57+
cache_policy=cf.CachePolicy.CACHING_DISABLED,
58+
),
59+
certificate=domain_cert,
60+
domain_names=[veda_route_settings.domain_hosted_zone_name]
61+
if veda_route_settings.domain_hosted_zone_name
62+
else None,
63+
additional_behaviors={
64+
"/api/stac*": cf.BehaviorOptions(
65+
origin=origins.HttpOrigin(
66+
f"{stac_api_id}.execute-api.{region}.amazonaws.com"
67+
),
68+
cache_policy=cf.CachePolicy.CACHING_DISABLED,
69+
allowed_methods=cf.AllowedMethods.ALLOW_ALL,
70+
),
71+
"/api/raster*": cf.BehaviorOptions(
72+
origin=origins.HttpOrigin(
73+
f"{raster_api_id}.execute-api.{region}.amazonaws.com"
74+
),
75+
cache_policy=cf.CachePolicy.CACHING_DISABLED,
76+
allowed_methods=cf.AllowedMethods.ALLOW_ALL,
77+
),
78+
"/api/ingest*": cf.BehaviorOptions(
79+
origin=origins.HttpOrigin(
80+
urlparse(veda_route_settings.ingest_url).hostname
81+
),
82+
cache_policy=cf.CachePolicy.CACHING_DISABLED,
83+
allowed_methods=cf.AllowedMethods.ALLOW_ALL,
84+
),
85+
},
86+
)
87+
88+
CfnOutput(self, "Endpoint", value=self.distribution.domain_name)

stac_api/infrastructure/config.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,31 @@ class vedaSTACSettings(BaseSettings):
1313
memory: int = 8000 # Mb
1414

1515
# Secret database credentials
16-
pgstac_secret_arn: Optional[str] = Field(
16+
stac_pgstac_secret_arn: Optional[str] = Field(
1717
None,
1818
description="Name or ARN of the AWS Secret containing database connection parameters",
1919
)
2020

21-
path_prefix: Optional[str] = Field(
21+
stac_path_prefix: Optional[str] = Field(
2222
"",
2323
description="Optional path prefix to add to all api endpoints",
2424
)
2525

26+
domain_hosted_zone_name: Optional[str] = Field(
27+
None,
28+
description="Domain name for the cloudfront distribution",
29+
)
30+
31+
cloudfront: Optional[bool] = Field(
32+
False,
33+
description="Boolean if Cloudfront Distribution should be deployed",
34+
)
35+
2636
class Config:
2737
"""model config"""
2838

2939
env_file = ".env"
30-
env_prefix = "VEDA_STAC_"
40+
env_prefix = "VEDA_"
3141

3242

3343
veda_stac_settings = vedaSTACSettings()

stac_api/infrastructure/construct.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,24 @@ def __init__(
7171
)
7272

7373
lambda_function.add_environment(
74-
"VEDA_STAC_PATH_PREFIX", veda_stac_settings.path_prefix
74+
"VEDA_STAC_PATH_PREFIX", veda_stac_settings.stac_path_prefix
7575
)
7676

77+
integration_kwargs = dict(handler=lambda_function)
78+
if veda_stac_settings.domain_hosted_zone_name and veda_stac_settings.cloudfront:
79+
integration_kwargs[
80+
"parameter_mapping"
81+
] = aws_apigatewayv2_alpha.ParameterMapping().overwrite_header(
82+
"host",
83+
aws_apigatewayv2_alpha.MappingValue(
84+
veda_stac_settings.domain_hosted_zone_name
85+
),
86+
)
87+
7788
stac_api_integration = (
7889
aws_apigatewayv2_integrations_alpha.HttpLambdaIntegration(
79-
construct_id, handler=lambda_function
90+
construct_id,
91+
**integration_kwargs,
8092
)
8193
)
8294

@@ -86,11 +98,11 @@ def __init__(
8698
domain_name=domain_name
8799
)
88100

89-
stac_api = aws_apigatewayv2_alpha.HttpApi(
101+
self.stac_api = aws_apigatewayv2_alpha.HttpApi(
90102
self,
91103
f"{stack_name}-{construct_id}",
92104
default_integration=stac_api_integration,
93105
default_domain_mapping=domain_mapping,
94106
)
95107

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

0 commit comments

Comments
 (0)