Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
emileten committed Jun 30, 2023
1 parent d5c9d1d commit f6d3daa
Show file tree
Hide file tree
Showing 9 changed files with 381 additions and 0 deletions.
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
cdk.out
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(
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,
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
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,
)

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(
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}"
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: "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"
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.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

0 comments on commit f6d3daa

Please sign in to comment.