-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
381 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import os | ||
from config import Config | ||
from cdk_eoapi import pgStacInfra, vpc | ||
from aws_cdk import App | ||
import yaml | ||
|
||
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( | ||
scope=app, | ||
id=config.build_service_name("pgSTAC-vpc"), | ||
nat_gateway_count=config.nat_gateway_count, | ||
) | ||
|
||
|
||
pgstac_infra_stack = pgStacInfra.pgStacInfraStack( | ||
scope=app, | ||
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.synth() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
{ | ||
"app": "python3 app.py", | ||
"watch": { | ||
"include": [ | ||
"**" | ||
], | ||
"exclude": [ | ||
"README.md", | ||
"cdk*.json", | ||
"requirements*.txt", | ||
"source.bat", | ||
"**/*.pyc", | ||
"**/*.tmp", | ||
"**/__pycache__", | ||
"tests", | ||
"scripts", | ||
"*venv" | ||
] | ||
}, | ||
"context": { | ||
"@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, | ||
"@aws-cdk/core:stackRelativeExports": true, | ||
"@aws-cdk/aws-rds:lowercaseDbIdentifier": true, | ||
"@aws-cdk/aws-lambda:recognizeVersionProps": true, | ||
"@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, | ||
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, | ||
"@aws-cdk/core:target-partitions": [ | ||
"aws", | ||
"aws-cn" | ||
] | ||
} | ||
} |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
from aws_cdk import ( | ||
Stack, | ||
aws_iam, | ||
aws_ec2, | ||
aws_rds, | ||
) | ||
from constructs import Construct | ||
from cdk_pgstac import ( | ||
BastionHost, | ||
PgStacApiLambda, | ||
PgStacDatabase, | ||
StacIngestor, | ||
TitilerPgstacApiLambda, | ||
) | ||
from typing import Union, Optional | ||
import boto3 | ||
|
||
|
||
class pgStacInfraStack(Stack): | ||
def __init__( | ||
self, | ||
scope: Construct, | ||
id: str, | ||
vpc: aws_ec2.Vpc, | ||
stage: str, | ||
db_allocated_storage: int, | ||
public_db_subnet: bool, | ||
db_instance_type: str, | ||
stac_api_lambda_name: str, | ||
titiler_pgstac_api_lambda_name: str, | ||
bastion_host_allow_ip_list: list, | ||
bastion_host_create_elastic_ip: bool, | ||
titiler_buckets: list, | ||
data_access_role_arn: Optional[str], | ||
auth_provider_jwks_url: Optional[str], | ||
bastion_host_user_data: Union[str, aws_ec2.UserData], | ||
**kwargs, | ||
) -> None: | ||
super().__init__(scope, id, **kwargs) | ||
|
||
pgstac_db = PgStacDatabase( | ||
self, | ||
"pgstac-db", | ||
vpc=vpc, | ||
engine=aws_rds.DatabaseInstanceEngine.postgres( | ||
version=aws_rds.PostgresEngineVersion.VER_14 | ||
), | ||
vpc_subnets=aws_ec2.SubnetSelection( | ||
subnet_type=aws_ec2.SubnetType.PUBLIC | ||
if public_db_subnet | ||
else aws_ec2.SubnetType.PRIVATE_ISOLATED | ||
), | ||
allocated_storage=db_allocated_storage, | ||
instance_type=aws_ec2.InstanceType(db_instance_type), | ||
) | ||
|
||
stac_api_lambda = PgStacApiLambda( | ||
self, | ||
"pgstac-api", | ||
api_env=dict(NAME=stac_api_lambda_name, description=f"{stage} STAC API"), | ||
vpc=vpc, | ||
db=pgstac_db.db, | ||
db_secret=pgstac_db.pgstac_secret, | ||
subnet_selection=aws_ec2.SubnetSelection( | ||
subnet_type=aws_ec2.SubnetType.PUBLIC | ||
if public_db_subnet | ||
else aws_ec2.SubnetType.PRIVATE_WITH_EGRESS | ||
), | ||
) | ||
|
||
titiler_pgstac_api_lambda = TitilerPgstacApiLambda( | ||
self, | ||
"titiler-pgstac-api", | ||
api_env=dict( | ||
NAME=titiler_pgstac_api_lambda_name, | ||
description=f"{stage} titiler pgstac API", | ||
), | ||
vpc=vpc, | ||
db=pgstac_db.db, | ||
db_secret=pgstac_db.pgstac_secret, | ||
subnet_selection=aws_ec2.SubnetSelection( | ||
subnet_type=aws_ec2.SubnetType.PUBLIC | ||
if public_db_subnet | ||
else aws_ec2.SubnetType.PRIVATE_WITH_EGRESS | ||
), | ||
buckets=titiler_buckets, | ||
) | ||
|
||
bastion_host = BastionHost( | ||
self, | ||
"bastion-host", | ||
vpc=vpc, | ||
db=pgstac_db.db, | ||
ipv4_allowlist=bastion_host_allow_ip_list, | ||
user_data=aws_ec2.UserData.custom(bastion_host_user_data) | ||
if bastion_host_user_data | ||
else aws_ec2.UserData.for_linux(), | ||
create_elastic_ip=bastion_host_create_elastic_ip, | ||
) | ||
|
||
if data_access_role_arn: | ||
data_access_role = aws_iam.Role.from_role_arn( | ||
self, | ||
"data-access-role", | ||
role_arn=data_access_role_arn, | ||
) | ||
else: | ||
data_access_role = self._create_data_access_role() | ||
data_access_role = self._grant_assume_role_with_principal_pattern( | ||
data_access_role, f"*{id}*stacingestorexecutionrole*" | ||
) # we expect the role to have the stack id along with the role id (which we expect from cdk-pgstac) in its name | ||
|
||
stac_ingestor_env = {"REQUESTER_PAYS": "True"} | ||
|
||
if auth_provider_jwks_url: | ||
stac_ingestor_env["JWKS_URL"] = auth_provider_jwks_url | ||
|
||
stac_ingestor = StacIngestor( | ||
self, | ||
"stac-ingestor", | ||
stac_url=stac_api_lambda.url, | ||
stage=stage, | ||
vpc=vpc, | ||
data_access_role=data_access_role, | ||
stac_db_secret=pgstac_db.pgstac_secret, | ||
stac_db_security_group=pgstac_db.db.connections.security_groups[0], | ||
subnet_selection=aws_ec2.SubnetSelection( | ||
subnet_type=aws_ec2.SubnetType.PRIVATE_WITH_EGRESS | ||
), | ||
api_env=stac_ingestor_env, | ||
) | ||
|
||
def _create_data_access_role(self) -> aws_iam.Role: | ||
""" | ||
Creates basic data access role | ||
""" | ||
|
||
return aws_iam.Role( | ||
self, | ||
"data-access-role", | ||
assumed_by=aws_iam.ServicePrincipal("lambda.amazonaws.com"), | ||
) | ||
|
||
def _grant_assume_role_with_principal_pattern( | ||
self, role_to_assume: aws_iam.Role, principal_pattern: str | ||
) -> aws_iam.Role: | ||
""" | ||
Grants assume role permissions to the role with the given pattern in the current account | ||
""" | ||
account_id = boto3.client("sts").get_caller_identity().get("Account") | ||
|
||
role_to_assume.assume_role_policy.add_statements( | ||
aws_iam.PolicyStatement( | ||
effect=aws_iam.Effect.ALLOW, | ||
principals=[aws_iam.AnyPrincipal()], | ||
actions=["sts:AssumeRole"], | ||
conditions={ | ||
"StringLike": { | ||
"aws:PrincipalArn": [ | ||
f"arn:aws:iam::{account_id}:role/{principal_pattern}" | ||
] | ||
} | ||
}, | ||
) | ||
) | ||
|
||
return role_to_assume |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
from aws_cdk import Stack, aws_ec2 | ||
from constructs import Construct | ||
|
||
|
||
class VpcStack(Stack): | ||
def __init__( | ||
self, scope: Construct, id: str, nat_gateway_count: int, **kwargs | ||
) -> None: | ||
super().__init__(scope, id, **kwargs) | ||
|
||
self.vpc = aws_ec2.Vpc( | ||
self, | ||
"vpc", | ||
subnet_configuration=[ | ||
aws_ec2.SubnetConfiguration( | ||
name="ingress", subnet_type=aws_ec2.SubnetType.PUBLIC, cidr_mask=24 | ||
), | ||
aws_ec2.SubnetConfiguration( | ||
name="application", | ||
subnet_type=aws_ec2.SubnetType.PRIVATE_WITH_EGRESS, | ||
cidr_mask=24, | ||
), | ||
aws_ec2.SubnetConfiguration( | ||
name="rds", | ||
subnet_type=aws_ec2.SubnetType.PRIVATE_ISOLATED, | ||
cidr_mask=24, | ||
), | ||
], | ||
nat_gateways=nat_gateway_count, | ||
) | ||
|
||
self.vpc.add_gateway_endpoint( | ||
"DynamoDbEndpoint", service=aws_ec2.GatewayVpcEndpointAwsService.DYNAMODB | ||
) | ||
|
||
self.vpc.add_interface_endpoint( | ||
"SecretsManagerEndpoint", | ||
service=aws_ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER, | ||
) | ||
|
||
self.export_value( | ||
self.vpc.select_subnets(subnet_type=aws_ec2.SubnetType.PUBLIC) | ||
.subnets[0] | ||
.subnet_id | ||
) | ||
self.export_value( | ||
self.vpc.select_subnets(subnet_type=aws_ec2.SubnetType.PUBLIC) | ||
.subnets[1] | ||
.subnet_id | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import pydantic | ||
from typing import Optional, List, Dict, Union, Any | ||
from aws_cdk import aws_ec2 | ||
|
||
|
||
class Config(pydantic.BaseSettings): | ||
project_id: Optional[str] = pydantic.Field( | ||
description="Project ID", default="cdk-eoapi-demo" | ||
) | ||
stage: Optional[str] = pydantic.Field( | ||
description="Stage of deployment", default="test" | ||
) | ||
auth_provider_jwks_url: Optional[str] = pydantic.Field( | ||
description="Auth Provider JSON Web Key Set URL for ingestion authentication. If not provided, no authentication will be required." | ||
) | ||
data_access_role_arn: Optional[str] = pydantic.Field( | ||
description="Role ARN for data access, if none will be created at runtime.", | ||
) | ||
db_instance_type: Optional[str] = pydantic.Field( | ||
description="Database instance type", default="db.t3.micro" | ||
) | ||
db_allocated_storage: Optional[int] = pydantic.Field( | ||
description="Allocated storage for the database", default=5 | ||
) | ||
public_db_subnet: Optional[bool] = pydantic.Field( | ||
description="Whether to put the database in a public", default=False | ||
) | ||
nat_gateway_count: Optional[int] = pydantic.Field( | ||
description="Number of NAT gateways to create", default=1 | ||
) | ||
bastion_host_create_elastic_ip: Optional[bool] = pydantic.Field( | ||
description="Whether to create an elastic IP for the bastion host", | ||
default=False, | ||
) | ||
bastion_host_allow_ip_list: Optional[List[str]] = pydantic.Field( | ||
description="YAML file containing list of IP addresses to allow SSH access to the bastion host", | ||
default=[], | ||
) | ||
bastion_host_user_data: Optional[ | ||
Union[Dict[str, Any], aws_ec2.UserData] | ||
] = pydantic.Field( | ||
description="Path to file containing user data for the bastion host", | ||
default=aws_ec2.UserData.for_linux(), | ||
) | ||
titiler_buckets: Optional[List[str]] = pydantic.Field( | ||
description="Path to YAML file containing list of buckets to grant access to the titiler API", | ||
default=[], | ||
) | ||
|
||
def build_service_name(self, service_id: str) -> str: | ||
return f"{self.project_id}-{self.stage}-{service_id}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
project_id: "my-project-id" | ||
stage: "test" | ||
auth_provider_jwks_url: "https://example.com/jwks.json" | ||
data_access_role_arn: "arn:aws:iam::123456789012:role/my-role" | ||
db_instance_type: "db.t3.micro" | ||
db_allocated_storage: 5 | ||
public_db_subnet: false | ||
nat_gateway_count: 1 | ||
bastion_host_create_elastic_ip: false | ||
bastion_host_allow_ip_list: | ||
- "192.0.2.0/32" | ||
- "203.0.113.0/32" | ||
bastion_host_user_data: | ||
users: | ||
- name: USERNAME | ||
shell: /bin/bash | ||
ssh_authorized_keys: | ||
- PUBLIC_SSH_KEY | ||
titiler_buckets: | ||
- "bucket1" | ||
- "bucket2" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
aws-cdk-lib>=2.75.0 | ||
aws_cdk.aws_cognito_identitypool_alpha>=2.75.0a0 | ||
cdk-pgstac==4.2.2 | ||
constructs>=10.0.0,<11.0.0 | ||
pydantic==1.9.1 | ||
black==22.3.0 | ||
boto3==1.24.15 | ||
boto3-stubs[cognito-idp,cognito-identity] | ||
flake8==4.0.1 | ||
click==8.1.3 | ||
requests==2.28.0 | ||
python-dotenv==1.0.0 | ||
pyyaml==6.0 |