Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,33 @@
# eoapi-template

Demonstration application showing the use and configuration options of the [eoapi-cdk constructs](https://github.com/developmentseed/eoapi-cdk) on AWS.

## Requirements

- python
- docker
- the AWS CDK CLI
- AWS credentials environment variables configured to point to an account.
- **Optional** a `config.yaml` file to override the default deployment settings defined in `config.py`.

## Installation

```
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
```

## Deployment

First, synthesize the app

```
cdk synth --all
```

Then, deploy

```
cdk deploy --all --require-approval never
```
37 changes: 4 additions & 33 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,17 @@
import yaml
from aws_cdk import App

from config import Config
from config import build_app_config
from eoapi_template import pgStacInfra, vpc

app = App()

try:
with open("config.yaml") as f:
config = yaml.safe_load(f)
config = (
{} if config is None else config
) # if config is empty, set it to an empty dict
config = Config(**config)
except FileNotFoundError:
# if no config at the expected path, using defaults
config = Config()

vpc_stack = vpc.VpcStack(
tags=config.tags,
scope=app,
id=config.build_service_name("pgSTAC-vpc"),
nat_gateway_count=config.nat_gateway_count,
)
app_config = build_app_config()

vpc_stack = vpc.VpcStack(scope=app, app_config=app_config)

pgstac_infra_stack = pgStacInfra.pgStacInfraStack(
scope=app,
tags=config.tags,
id=config.build_service_name("pgSTAC-infra"),
vpc=vpc_stack.vpc,
stac_api_lambda_name=config.build_service_name("STAC API"),
titiler_pgstac_api_lambda_name=config.build_service_name("titiler pgSTAC API"),
stage=config.stage,
db_allocated_storage=config.db_allocated_storage,
public_db_subnet=config.public_db_subnet,
db_instance_type=config.db_instance_type,
bastion_host_allow_ip_list=config.bastion_host_allow_ip_list,
bastion_host_create_elastic_ip=config.bastion_host_create_elastic_ip,
bastion_host_user_data=yaml.dump(config.bastion_host_user_data),
titiler_buckets=config.titiler_buckets,
data_access_role_arn=config.data_access_role_arn,
auth_provider_jwks_url=config.auth_provider_jwks_url,
app_config=app_config,
)
app.synth()
87 changes: 86 additions & 1 deletion config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Any, Dict, List, Optional, Union

import pydantic
import yaml
from aws_cdk import aws_ec2
from pydantic_core.core_schema import FieldValidationInfo
from pydantic_settings import BaseSettings
Expand All @@ -10,7 +11,7 @@
DEFAULT_NAT_GATEWAY_COUNT = 1


class Config(BaseSettings):
class AppConfig(BaseSettings):
project_id: Optional[str] = pydantic.Field(
description="Project ID", default=DEFAULT_PROJECT_ID
)
Expand Down Expand Up @@ -54,6 +55,12 @@ class Config(BaseSettings):
description="Number of NAT gateways to create",
default=DEFAULT_NAT_GATEWAY_COUNT,
)
bastion_host: Optional[bool] = pydantic.Field(
description="""Whether to create a bastion host. It can typically
be used to make administrative connections to the database if
`public_db_subnet` is False""",
default=True,
)
bastion_host_create_elastic_ip: Optional[bool] = pydantic.Field(
description="Whether to create an elastic IP for the bastion host",
default=False,
Expand All @@ -74,6 +81,39 @@ class Config(BaseSettings):
buckets to grant access to the titiler API""",
default=[],
)
acm_certificate_arn: Optional[str] = pydantic.Field(
description="""ARN of ACM certificate to use for
custom domain names. If provided,
CDNs are created for all the APIs""",
default=None,
)
stac_api_custom_domain: Optional[str] = pydantic.Field(
description="""Custom domain name for the STAC API.
Must provide `acm_certificate_arn`""",
default=None,
)
titiler_pgstac_api_custom_domain: Optional[str] = pydantic.Field(
description="""Custom domain name for the titiler pgstac API.
Must provide `acm_certificate_arn`""",
default=None,
)
stac_ingestor_api_custom_domain: Optional[str] = pydantic.Field(
description="""Custom domain name for the STAC ingestor API.
Must provide `acm_certificate_arn`""",
default=None,
)
tipg_api_custom_domain: Optional[str] = pydantic.Field(
description="""Custom domain name for the tipg API.
Must provide `acm_certificate_arn`""",
default=None,
)
stac_browser_version: Optional[str] = pydantic.Field(
description="""Version of the Radiant Earth STAC browser to deploy.
If none provided, no STAC browser will be deployed.
If provided, `stac_api_custom_domain` must be provided
as it will be used as a backend.""",
default=None,
)

@pydantic.field_validator("tags")
def default_tags(cls, v, info: FieldValidationInfo):
Expand All @@ -91,5 +131,50 @@ def validate_nat_gateway_count(cls, v, info: FieldValidationInfo):
else:
return v

@pydantic.field_validator("stac_browser_version")
def validate_stac_browser_version(cls, v, info: FieldValidationInfo):
if v is not None and info.data["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"""
)
else:
return v

@pydantic.field_validator("acm_certificate_arn")
def validate_acm_certificate_arn(cls, v, info: FieldValidationInfo):
if v is None and any(
[
info.data["stac_api_custom_domain"],
info.data["titiler_pgstac_api_custom_domain"],
info.data["stac_ingestor_api_custom_domain"],
info.data["tipg_api_custom_domain"],
]
):
raise ValueError(
"""If any custom domain is provided,
an ACM certificate ARN must be provided"""
)
else:
return v

def build_service_name(self, service_id: str) -> str:
return f"{self.project_id}-{self.stage}-{service_id}"


def build_app_config() -> AppConfig:
"""Builds the AppConfig object from config.yaml file if exists,
otherwise use defaults"""
try:
with open("config.yaml") as f:
print("Loading config from config.yaml")
app_config = yaml.safe_load(f)
app_config = (
{} if app_config is None else app_config
) # if config is empty, set it to an empty dict
app_config = AppConfig(**app_config)
except FileNotFoundError:
# if no config at the expected path, using defaults
app_config = AppConfig()

return app_config
Loading