diff --git a/.gitignore b/.gitignore index 95640297c..4dabfc96f 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,4 @@ ENV/ cdk.out/ deployment/k8s/titiler/values-test.yaml docs/src/api/ +.idea diff --git a/deployment/aws/cdk.json b/deployment/aws/cdk.json index 57cc60ce6..bcc9682df 100644 --- a/deployment/aws/cdk.json +++ b/deployment/aws/cdk.json @@ -1,3 +1,3 @@ { - "app": "python3 cdk/app.py" + "app": "python cdk/app.py" } diff --git a/deployment/aws/cdk/__init__.py b/deployment/aws/cdk/__init__.py index 4955682f0..82806b399 100644 --- a/deployment/aws/cdk/__init__.py +++ b/deployment/aws/cdk/__init__.py @@ -1 +1 @@ -"""AWS App.""" +"""AWS App""" diff --git a/deployment/aws/cdk/app.py b/deployment/aws/cdk/app.py index 3317d8424..a082e8aab 100644 --- a/deployment/aws/cdk/app.py +++ b/deployment/aws/cdk/app.py @@ -13,6 +13,7 @@ from aws_cdk import aws_logs as logs from aws_cdk.aws_apigatewayv2_integrations_alpha import HttpLambdaIntegration from config import StackSettings +from construct.private_api import TitilerPrivateApiStack from constructs import Construct settings = StackSettings() @@ -175,6 +176,16 @@ def __init__( ) ) +private_api = TitilerPrivateApiStack( + app, + f"{settings.name}-private-api-{settings.stage}", + vpc_endpoint_id=settings.vpc_endpoint_id, + memory=settings.memory, + timeout=settings.timeout, + concurrent=settings.max_concurrent, + permissions=perms, + environment=settings.env, +) ecs_stack = titilerECSStack( app, @@ -197,6 +208,7 @@ def __init__( environment=settings.env, ) + # Tag infrastructure for key, value in { "Project": settings.name, @@ -207,6 +219,7 @@ def __init__( if value: Tags.of(ecs_stack).add(key, value) Tags.of(lambda_stack).add(key, value) + Tags.of(private_api).add(key, value) app.synth() diff --git a/deployment/aws/cdk/config.py b/deployment/aws/cdk/config.py index d3fe0f151..a57b12b15 100644 --- a/deployment/aws/cdk/config.py +++ b/deployment/aws/cdk/config.py @@ -85,4 +85,7 @@ class StackSettings(BaseSettings): # Default: - No specific limit - account limit. max_concurrent: Optional[int] = None + # The VPC Endpoint ID + vpc_endpoint_id: None | str = None + model_config = SettingsConfigDict(env_prefix="TITILER_STACK_", env_file=".env") diff --git a/deployment/aws/cdk/construct/__init__.py b/deployment/aws/cdk/construct/__init__.py new file mode 100644 index 000000000..878c11c3f --- /dev/null +++ b/deployment/aws/cdk/construct/__init__.py @@ -0,0 +1 @@ +"""Constructs Custom Package""" diff --git a/deployment/aws/cdk/construct/private_api.py b/deployment/aws/cdk/construct/private_api.py new file mode 100644 index 000000000..92b1562b5 --- /dev/null +++ b/deployment/aws/cdk/construct/private_api.py @@ -0,0 +1,116 @@ +"""Private API Construct""" + +import os +from typing import Any, Dict, List, Optional, cast + +from aws_cdk import CfnOutput, Duration, Stack +from aws_cdk.aws_apigateway import ( + EndpointConfiguration, + EndpointType, + LambdaIntegration, + RestApi, +) +from aws_cdk.aws_iam import AnyPrincipal, Effect, PolicyDocument, PolicyStatement +from aws_cdk.aws_lambda import Code, Function, IFunction, Runtime +from aws_cdk.aws_logs import RetentionDays +from constructs import Construct + + +class TitilerPrivateApiStack(Stack): + """ + Titiler Private API Stack + + Private api configuration for titiler. + + author: @jeandsmith + """ + + def __init__( + self, + scope: Construct, + id: str, + vpc_endpoint_id: None | str, + memory: int = 1024, + timeout: int = 30, + runtime: Runtime = Runtime.PYTHON_3_11, + code_dir: str = "./", + concurrent: Optional[int] = None, + permissions: Optional[List[PolicyStatement]] = None, + environment: Optional[Dict] = None, + **kwargs: Any, + ) -> None: + """Define the stack""" + super().__init__(scope, id, **kwargs) + + permissions = permissions or [] + environment = environment or {} + + lambda_function = Function( + self, + f"{id}-lambda", + runtime=runtime, + code=Code.from_docker_build( + path=os.path.abspath(code_dir), + file="lambda/Dockerfile", + ), + handler="handler.handler", + memory_size=memory, + reserved_concurrent_executions=concurrent, + timeout=Duration.seconds(timeout), + environment=environment, + log_retention=RetentionDays.ONE_WEEK, + ) + + for perm in permissions: + lambda_function.add_to_role_policy(perm) + + policy = ( + PolicyDocument( + statements=[ + PolicyStatement( + principals=[AnyPrincipal()], + effect=Effect.DENY, + actions=["execute-api:Invoke"], + resources=[ + Stack.of(self).format_arn( + service="execute-api", resource="*" + ) + ], + conditions={ + "StringNotEquals": {"aws:SourceVpce": vpc_endpoint_id} + }, + ), + PolicyStatement( + principals=[AnyPrincipal()], + effect=Effect.ALLOW, + actions=["execute-api:Invoke"], + resources=[ + Stack.of(self).format_arn( + service="execute-api", resource="*" + ) + ], + ), + ] + ) + if vpc_endpoint_id + else None + ) + + endpoint_config = ( + EndpointConfiguration(types=[EndpointType.PRIVATE]) + if vpc_endpoint_id + else EndpointConfiguration(types=[EndpointType.REGIONAL]) + ) + + api = RestApi( + self, + f"{id}-endpoint", + default_integration=LambdaIntegration( + handler=cast(IFunction, lambda_function) + ), + policy=policy, + endpoint_configuration=endpoint_config, + ) + api.root.add_proxy() + + CfnOutput(self, "Endpoint", value=api.url)