Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial PR : Create a public demonstration repository named cdk-eoapi #1

Merged
merged 11 commits into from
Jul 12, 2023
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# CDK
emileten marked this conversation as resolved.
Show resolved Hide resolved
cdk.out
emileten marked this conversation as resolved.
Show resolved Hide resolved
44 changes: 44 additions & 0 deletions app.py
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(
emileten marked this conversation as resolved.
Show resolved Hide resolved
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()
32 changes: 32 additions & 0 deletions cdk.json
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 added cdk_eoapi/__init__.py
Empty file.
167 changes: 167 additions & 0 deletions cdk_eoapi/pgStacInfra.py
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,
emileten marked this conversation as resolved.
Show resolved Hide resolved
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(
emileten marked this conversation as resolved.
Show resolved Hide resolved
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",
),
emileten marked this conversation as resolved.
Show resolved Hide resolved
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(
emileten marked this conversation as resolved.
Show resolved Hide resolved
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(
emileten marked this conversation as resolved.
Show resolved Hide resolved
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"*{self.stack_name}*ingestor*"
) # beware, there is a limit in the number of characters a role can have (64) and AWS automatically truncates the role ARN if it's too long.

stac_ingestor_env = {"REQUESTER_PAYS": "True"}
emileten marked this conversation as resolved.
Show resolved Hide resolved

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
50 changes: 50 additions & 0 deletions cdk_eoapi/vpc.py
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,
emileten marked this conversation as resolved.
Show resolved Hide resolved
)

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
)
51 changes: 51 additions & 0 deletions config.py
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(
emileten marked this conversation as resolved.
Show resolved Hide resolved
description="Database instance type", default="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(
emileten marked this conversation as resolved.
Show resolved Hide resolved
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}"
21 changes: 21 additions & 0 deletions config.yaml
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: "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"
13 changes: 13 additions & 0 deletions requirements.txt
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.3
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