| 
1 |  | -"""AWS CDK application for the stac-fastapi-geoparquet Stack  | 
 | 1 | +"""AWS CDK application for the stac-fastapi-geoparquet stack, with a pgstac  | 
 | 2 | +database to compare."""  | 
2 | 3 | 
 
  | 
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 |  | -Also includes a pgstac for side-by-side testing.  | 
9 |  | -"""  | 
10 |  | - | 
11 |  | -import os  | 
12 |  | -from typing import Any  | 
13 |  | - | 
14 |  | -from aws_cdk import (  | 
15 |  | -    App,  | 
16 |  | -    CfnOutput,  | 
17 |  | -    Duration,  | 
18 |  | -    RemovalPolicy,  | 
19 |  | -    Stack,  | 
20 |  | -    Tags,  | 
21 |  | -)  | 
22 |  | -from aws_cdk.aws_apigatewayv2 import HttpApi, HttpStage, ThrottleSettings  | 
23 |  | -from aws_cdk.aws_apigatewayv2_integrations import HttpLambdaIntegration  | 
24 |  | -from aws_cdk.aws_ec2 import (  | 
25 |  | -    GatewayVpcEndpointAwsService,  | 
26 |  | -    InstanceType,  | 
27 |  | -    InterfaceVpcEndpointAwsService,  | 
28 |  | -    Peer,  | 
29 |  | -    Port,  | 
30 |  | -    SubnetConfiguration,  | 
31 |  | -    SubnetSelection,  | 
32 |  | -    SubnetType,  | 
33 |  | -    Vpc,  | 
34 |  | -)  | 
35 |  | -from aws_cdk.aws_iam import AnyPrincipal, Effect, PolicyStatement  | 
36 |  | -from aws_cdk.aws_lambda import Code, Function, Runtime  | 
37 |  | -from aws_cdk.aws_logs import RetentionDays  | 
38 |  | -from aws_cdk.aws_rds import DatabaseInstanceEngine, PostgresEngineVersion  | 
39 |  | -from aws_cdk.aws_s3 import BlockPublicAccess, Bucket  | 
40 |  | -from aws_cdk.custom_resources import (  | 
41 |  | -    AwsCustomResource,  | 
42 |  | -    AwsCustomResourcePolicy,  | 
43 |  | -    AwsSdkCall,  | 
44 |  | -    PhysicalResourceId,  | 
45 |  | -)  | 
 | 4 | +from aws_cdk import App  | 
46 | 5 | from config import Config  | 
47 |  | -from constructs import Construct  | 
48 |  | -from eoapi_cdk import PgStacApiLambda, PgStacDatabase  | 
49 |  | - | 
50 |  | - | 
51 |  | -class VpcStack(Stack):  | 
52 |  | -    def __init__(  | 
53 |  | -        self, scope: Construct, config: Config, id: str, **kwargs: Any  | 
54 |  | -    ) -> None:  | 
55 |  | -        super().__init__(scope, id=id, tags=config.tags, **kwargs)  | 
56 |  | - | 
57 |  | -        self.vpc = Vpc(  | 
58 |  | -            self,  | 
59 |  | -            "vpc",  | 
60 |  | -            subnet_configuration=[  | 
61 |  | -                SubnetConfiguration(  | 
62 |  | -                    name="ingress", subnet_type=SubnetType.PUBLIC, cidr_mask=24  | 
63 |  | -                ),  | 
64 |  | -                SubnetConfiguration(  | 
65 |  | -                    name="application",  | 
66 |  | -                    subnet_type=SubnetType.PRIVATE_WITH_EGRESS,  | 
67 |  | -                    cidr_mask=24,  | 
68 |  | -                ),  | 
69 |  | -                SubnetConfiguration(  | 
70 |  | -                    name="rds",  | 
71 |  | -                    subnet_type=SubnetType.PRIVATE_ISOLATED,  | 
72 |  | -                    cidr_mask=24,  | 
73 |  | -                ),  | 
74 |  | -            ],  | 
75 |  | -            nat_gateways=config.nat_gateway_count,  | 
76 |  | -        )  | 
77 |  | -        self.vpc.add_interface_endpoint(  | 
78 |  | -            "SecretsManagerEndpoint",  | 
79 |  | -            service=InterfaceVpcEndpointAwsService.SECRETS_MANAGER,  | 
80 |  | -        )  | 
81 |  | -        self.vpc.add_interface_endpoint(  | 
82 |  | -            "CloudWatchEndpoint",  | 
83 |  | -            service=InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS,  | 
84 |  | -        )  | 
85 |  | -        self.vpc.add_gateway_endpoint("S3", service=GatewayVpcEndpointAwsService.S3)  | 
86 |  | -        self.export_value(  | 
87 |  | -            self.vpc.select_subnets(subnet_type=SubnetType.PUBLIC).subnets[0].subnet_id  | 
88 |  | -        )  | 
89 |  | -        self.export_value(  | 
90 |  | -            self.vpc.select_subnets(subnet_type=SubnetType.PUBLIC).subnets[1].subnet_id  | 
91 |  | -        )  | 
92 |  | - | 
93 |  | - | 
94 |  | -class StacFastApiGeoparquetStack(Stack):  | 
95 |  | -    def __init__(  | 
96 |  | -        self,  | 
97 |  | -        scope: Construct,  | 
98 |  | -        construct_id: str,  | 
99 |  | -        config: Config,  | 
100 |  | -        runtime: Runtime = Runtime.PYTHON_3_12,  | 
101 |  | -        **kwargs: Any,  | 
102 |  | -    ) -> None:  | 
103 |  | -        super().__init__(scope, construct_id, **kwargs)  | 
104 |  | - | 
105 |  | -        for key, value in config.tags.items():  | 
106 |  | -            Tags.of(self).add(key, value)  | 
107 |  | - | 
108 |  | -        bucket = Bucket(  | 
109 |  | -            scope=self,  | 
110 |  | -            id="bucket",  | 
111 |  | -            bucket_name=config.bucket_name,  | 
112 |  | -            versioned=True,  | 
113 |  | -            removal_policy=RemovalPolicy.RETAIN  | 
114 |  | -            if config.stage != "test"  | 
115 |  | -            else RemovalPolicy.DESTROY,  | 
116 |  | -            public_read_access=True,  | 
117 |  | -            block_public_access=BlockPublicAccess(  | 
118 |  | -                block_public_acls=False,  | 
119 |  | -                block_public_policy=False,  | 
120 |  | -                ignore_public_acls=False,  | 
121 |  | -                restrict_public_buckets=False,  | 
122 |  | -            ),  | 
123 |  | -        )  | 
124 |  | - | 
125 |  | -        # make the bucket public, requester-pays  | 
126 |  | -        bucket.add_to_resource_policy(  | 
127 |  | -            PolicyStatement(  | 
128 |  | -                actions=["s3:GetObject"],  | 
129 |  | -                resources=[bucket.arn_for_objects("*")],  | 
130 |  | -                principals=[AnyPrincipal()],  | 
131 |  | -                effect=Effect.ALLOW,  | 
132 |  | -            )  | 
133 |  | -        )  | 
134 |  | - | 
135 |  | -        add_request_pay = AwsSdkCall(  | 
136 |  | -            action="putBucketRequestPayment",  | 
137 |  | -            service="S3",  | 
138 |  | -            region=self.region,  | 
139 |  | -            parameters={  | 
140 |  | -                "Bucket": bucket.bucket_name,  | 
141 |  | -                "RequestPaymentConfiguration": {"Payer": "Requester"},  | 
142 |  | -            },  | 
143 |  | -            physical_resource_id=PhysicalResourceId.of(bucket.bucket_name),  | 
144 |  | -        )  | 
145 |  | - | 
146 |  | -        aws_custom_resource = AwsCustomResource(  | 
147 |  | -            self,  | 
148 |  | -            "RequesterPaysCustomResource",  | 
149 |  | -            policy=AwsCustomResourcePolicy.from_sdk_calls(  | 
150 |  | -                resources=[bucket.bucket_arn]  | 
151 |  | -            ),  | 
152 |  | -            on_create=add_request_pay,  | 
153 |  | -            on_update=add_request_pay,  | 
154 |  | -        )  | 
155 |  | - | 
156 |  | -        aws_custom_resource.node.add_dependency(bucket)  | 
157 |  | - | 
158 |  | -        CfnOutput(self, "BucketName", value=bucket.bucket_name)  | 
159 |  | - | 
160 |  | -        api_lambda = Function(  | 
161 |  | -            scope=self,  | 
162 |  | -            id="lambda",  | 
163 |  | -            runtime=runtime,  | 
164 |  | -            handler="handler.handler",  | 
165 |  | -            memory_size=config.memory,  | 
166 |  | -            log_retention=RetentionDays.ONE_WEEK,  | 
167 |  | -            timeout=Duration.seconds(config.timeout),  | 
168 |  | -            code=Code.from_docker_build(  | 
169 |  | -                path=os.path.abspath("../.."),  | 
170 |  | -                file="infrastructure/aws/lambda/Dockerfile",  | 
171 |  | -                build_args={  | 
172 |  | -                    "PYTHON_VERSION": runtime.to_string().replace("python", ""),  | 
173 |  | -                },  | 
174 |  | -            ),  | 
175 |  | -            environment={  | 
176 |  | -                "STAC_FASTAPI_GEOPARQUET_HREF": f"s3://{bucket.bucket_name}/{config.geoparquet_key}",  | 
177 |  | -                # find pre-fetched extensions  | 
178 |  | -                "STAC_FASTAPI_DUCKDB_EXTENSION_DIRECTORY": "/tmp/duckdb-extensions",  | 
179 |  | -                "HOME": "/tmp",  # for duckdb's home_directory  | 
180 |  | -            },  | 
181 |  | -        )  | 
182 |  | - | 
183 |  | -        bucket.grant_read(api_lambda)  | 
184 |  | - | 
185 |  | -        api = HttpApi(  | 
186 |  | -            scope=self,  | 
187 |  | -            id="api",  | 
188 |  | -            default_integration=HttpLambdaIntegration(  | 
189 |  | -                "api-integration",  | 
190 |  | -                handler=api_lambda,  | 
191 |  | -            ),  | 
192 |  | -            default_domain_mapping=None,  # TODO: enable custom domain name  | 
193 |  | -            create_default_stage=False,  # Important: disable default stage creation  | 
194 |  | -        )  | 
195 |  | - | 
196 |  | -        stage = HttpStage(  | 
197 |  | -            self,  | 
198 |  | -            "api-stage",  | 
199 |  | -            http_api=api,  | 
200 |  | -            auto_deploy=True,  | 
201 |  | -            stage_name="$default",  | 
202 |  | -            throttle=ThrottleSettings(  | 
203 |  | -                rate_limit=config.rate_limit,  | 
204 |  | -                burst_limit=config.rate_limit * 2,  | 
205 |  | -            )  | 
206 |  | -            if config.rate_limit  | 
207 |  | -            else None,  | 
208 |  | -        )  | 
209 |  | - | 
210 |  | -        assert stage.url  | 
211 |  | -        CfnOutput(self, "ApiURL", value=stage.url)  | 
212 |  | - | 
213 |  | - | 
214 |  | -class StacFastApiPgstacStack(Stack):  | 
215 |  | -    def __init__(  | 
216 |  | -        self,  | 
217 |  | -        scope: Construct,  | 
218 |  | -        vpc: Vpc,  | 
219 |  | -        id: str,  | 
220 |  | -        config: Config,  | 
221 |  | -        **kwargs: Any,  | 
222 |  | -    ) -> None:  | 
223 |  | -        super().__init__(  | 
224 |  | -            scope,  | 
225 |  | -            id=id,  | 
226 |  | -            tags=config.tags,  | 
227 |  | -            **kwargs,  | 
228 |  | -        )  | 
229 |  | -        pgstac_db = PgStacDatabase(  | 
230 |  | -            self,  | 
231 |  | -            "pgstac-db",  | 
232 |  | -            vpc=vpc,  | 
233 |  | -            engine=DatabaseInstanceEngine.postgres(  | 
234 |  | -                version=PostgresEngineVersion.VER_16  | 
235 |  | -            ),  | 
236 |  | -            vpc_subnets=SubnetSelection(subnet_type=(SubnetType.PUBLIC)),  | 
237 |  | -            allocated_storage=config.pgstac_db_allocated_storage,  | 
238 |  | -            instance_type=InstanceType(config.pgstac_db_instance_type),  | 
239 |  | -            removal_policy=RemovalPolicy.DESTROY,  | 
240 |  | -        )  | 
241 |  | -        # allow connections from any ipv4 to pgbouncer instance security group  | 
242 |  | -        assert pgstac_db.security_group  | 
243 |  | -        pgstac_db.security_group.add_ingress_rule(Peer.any_ipv4(), Port.tcp(5432))  | 
244 |  | -        pgstac_api = PgStacApiLambda(  | 
245 |  | -            self,  | 
246 |  | -            "stac-api",  | 
247 |  | -            api_env={  | 
248 |  | -                "NAME": "stac-fastapi-pgstac",  | 
249 |  | -                "description": f"{config.stage} STAC API",  | 
250 |  | -            },  | 
251 |  | -            db=pgstac_db.connection_target,  | 
252 |  | -            db_secret=pgstac_db.pgstac_secret,  | 
253 |  | -            stac_api_domain_name=None,  | 
254 |  | -        )  | 
255 |  | - | 
256 |  | -        assert pgstac_api.url  | 
257 |  | -        CfnOutput(self, "ApiURL", value=pgstac_api.url)  | 
258 |  | - | 
 | 6 | +from stacks.app import AppStack  | 
 | 7 | +from stacks.infra import InfraStack  | 
259 | 8 | 
 
  | 
260 | 9 | app = App()  | 
261 | 10 | config = Config()  | 
262 |  | -vpc_stack = VpcStack(scope=app, config=config, id=f"vpc-{config.name}")  | 
263 |  | -StacFastApiPgstacStack(  | 
264 |  | -    scope=app, vpc=vpc_stack.vpc, config=config, id=f"{config.name}-pgstac"  | 
 | 11 | +infra_stack = InfraStack(  | 
 | 12 | +    scope=app,  | 
 | 13 | +    id=config.stack_name("infra"),  | 
 | 14 | +    config=config,  | 
265 | 15 | )  | 
266 |  | -StacFastApiGeoparquetStack(  | 
267 |  | -    app,  | 
268 |  | -    config.stack_name,  | 
 | 16 | +AppStack(  | 
 | 17 | +    scope=app,  | 
 | 18 | +    id=config.stack_name("app"),  | 
 | 19 | +    pgstac_db=infra_stack.pgstac_db,  | 
 | 20 | +    bucket=infra_stack.bucket,  | 
269 | 21 |     config=config,  | 
270 | 22 | )  | 
271 | 23 | app.synth()  | 
0 commit comments