|
| 1 | +"""AWS CDK application for the stac-fastapi-geoparquet Stack |
| 2 | +
|
| 3 | +Generates a Lambda function with an API Gateway trigger and an S3 bucket. |
| 4 | +
|
| 5 | +After deploying the stack you will need to make sure the geoparquet file |
| 6 | +specified in the config gets uploaded to the bucket associated with this stack! |
| 7 | +""" |
| 8 | + |
| 9 | +import os |
| 10 | +from typing import Any |
| 11 | + |
| 12 | +from aws_cdk import ( |
| 13 | + App, |
| 14 | + CfnOutput, |
| 15 | + Duration, |
| 16 | + RemovalPolicy, |
| 17 | + Stack, |
| 18 | + Tags, |
| 19 | +) |
| 20 | +from aws_cdk.aws_apigatewayv2 import HttpApi, HttpStage, ThrottleSettings |
| 21 | +from aws_cdk.aws_apigatewayv2_integrations import HttpLambdaIntegration |
| 22 | +from aws_cdk.aws_iam import AnyPrincipal, Effect, PolicyStatement |
| 23 | +from aws_cdk.aws_lambda import Code, Function, Runtime |
| 24 | +from aws_cdk.aws_logs import RetentionDays |
| 25 | +from aws_cdk.aws_s3 import BlockPublicAccess, Bucket |
| 26 | +from aws_cdk.custom_resources import ( |
| 27 | + AwsCustomResource, |
| 28 | + AwsCustomResourcePolicy, |
| 29 | + AwsSdkCall, |
| 30 | + PhysicalResourceId, |
| 31 | +) |
| 32 | +from config import Config |
| 33 | +from constructs import Construct |
| 34 | + |
| 35 | + |
| 36 | +class StacFastApiGeoparquetStack(Stack): |
| 37 | + def __init__( |
| 38 | + self, |
| 39 | + scope: Construct, |
| 40 | + construct_id: str, |
| 41 | + config: Config, |
| 42 | + runtime: Runtime = Runtime.PYTHON_3_12, |
| 43 | + **kwargs: Any, |
| 44 | + ) -> None: |
| 45 | + super().__init__(scope, construct_id, **kwargs) |
| 46 | + |
| 47 | + for key, value in config.tags.items(): |
| 48 | + Tags.of(self).add(key, value) |
| 49 | + |
| 50 | + bucket = Bucket( |
| 51 | + scope=self, |
| 52 | + id="bucket", |
| 53 | + bucket_name=config.bucket_name, |
| 54 | + versioned=True, |
| 55 | + removal_policy=RemovalPolicy.RETAIN |
| 56 | + if config.stage != "test" |
| 57 | + else RemovalPolicy.DESTROY, |
| 58 | + public_read_access=True, |
| 59 | + block_public_access=BlockPublicAccess( |
| 60 | + block_public_acls=False, |
| 61 | + block_public_policy=False, |
| 62 | + ignore_public_acls=False, |
| 63 | + restrict_public_buckets=False, |
| 64 | + ), |
| 65 | + ) |
| 66 | + |
| 67 | + # make the bucket public, requester-pays |
| 68 | + bucket.add_to_resource_policy( |
| 69 | + PolicyStatement( |
| 70 | + actions=["s3:GetObject"], |
| 71 | + resources=[bucket.arn_for_objects("*")], |
| 72 | + principals=[AnyPrincipal()], |
| 73 | + effect=Effect.ALLOW, |
| 74 | + ) |
| 75 | + ) |
| 76 | + |
| 77 | + add_request_pay = AwsSdkCall( |
| 78 | + action="putBucketRequestPayment", |
| 79 | + service="S3", |
| 80 | + region=self.region, |
| 81 | + parameters={ |
| 82 | + "Bucket": bucket.bucket_name, |
| 83 | + "RequestPaymentConfiguration": {"Payer": "Requester"}, |
| 84 | + }, |
| 85 | + physical_resource_id=PhysicalResourceId.of(bucket.bucket_name), |
| 86 | + ) |
| 87 | + |
| 88 | + aws_custom_resource = AwsCustomResource( |
| 89 | + self, |
| 90 | + "RequesterPaysCustomResource", |
| 91 | + policy=AwsCustomResourcePolicy.from_sdk_calls( |
| 92 | + resources=[bucket.bucket_arn] |
| 93 | + ), |
| 94 | + on_create=add_request_pay, |
| 95 | + on_update=add_request_pay, |
| 96 | + ) |
| 97 | + |
| 98 | + aws_custom_resource.node.add_dependency(bucket) |
| 99 | + |
| 100 | + CfnOutput(self, "BucketName", value=bucket.bucket_name) |
| 101 | + |
| 102 | + api_lambda = Function( |
| 103 | + scope=self, |
| 104 | + id="lambda", |
| 105 | + runtime=runtime, |
| 106 | + handler="handler.handler", |
| 107 | + memory_size=config.memory, |
| 108 | + log_retention=RetentionDays.ONE_WEEK, |
| 109 | + timeout=Duration.seconds(config.timeout), |
| 110 | + code=Code.from_docker_build( |
| 111 | + path=os.path.abspath("../.."), |
| 112 | + file="infrastructure/aws/lambda/Dockerfile", |
| 113 | + build_args={ |
| 114 | + "PYTHON_VERSION": runtime.to_string().replace("python", ""), |
| 115 | + }, |
| 116 | + ), |
| 117 | + environment={ |
| 118 | + "STAC_FASTAPI_GEOPARQUET_HREF": f"s3://{bucket.bucket_name}/{config.geoparquet_key}", |
| 119 | + "HOME": "/tmp", # for duckdb's home_directory |
| 120 | + }, |
| 121 | + ) |
| 122 | + |
| 123 | + bucket.grant_read(api_lambda) |
| 124 | + |
| 125 | + api = HttpApi( |
| 126 | + scope=self, |
| 127 | + id="api", |
| 128 | + default_integration=HttpLambdaIntegration( |
| 129 | + "api-integration", |
| 130 | + handler=api_lambda, |
| 131 | + ), |
| 132 | + default_domain_mapping=None, # TODO: enable custom domain name |
| 133 | + create_default_stage=False, # Important: disable default stage creation |
| 134 | + ) |
| 135 | + |
| 136 | + stage = HttpStage( |
| 137 | + self, |
| 138 | + "api-stage", |
| 139 | + http_api=api, |
| 140 | + auto_deploy=True, |
| 141 | + stage_name="$default", |
| 142 | + throttle=ThrottleSettings( |
| 143 | + rate_limit=config.rate_limit, |
| 144 | + burst_limit=config.rate_limit * 2, |
| 145 | + ) |
| 146 | + if config.rate_limit |
| 147 | + else None, |
| 148 | + ) |
| 149 | + |
| 150 | + assert stage.url |
| 151 | + CfnOutput(self, "ApiURL", value=stage.url) |
| 152 | + |
| 153 | + |
| 154 | +app = App() |
| 155 | +config = Config() |
| 156 | +StacFastApiGeoparquetStack( |
| 157 | + app, |
| 158 | + config.stack_name, |
| 159 | + config=config, |
| 160 | +) |
| 161 | +app.synth() |
0 commit comments