Skip to content

Commit a02acef

Browse files
authored
feat(titiler-pgstac-api): add titiler-pgstac endpoint (#42)
* added a new feature, deploying a titiler-pgstac service with access to the pgstac database, and exposing its API.
1 parent 3b34fd1 commit a02acef

File tree

8 files changed

+191
-0
lines changed

8 files changed

+191
-0
lines changed

lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from "./bootstrapper";
33
export * from "./database";
44
export * from "./ingestor-api";
55
export * from "./stac-api";
6+
export * from "./titiler-pgstac-api";

lib/titiler-pgstac-api/index.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import {
2+
Stack,
3+
aws_iam as iam,
4+
aws_ec2 as ec2,
5+
aws_rds as rds,
6+
aws_lambda as lambda,
7+
aws_secretsmanager as secretsmanager,
8+
CfnOutput,
9+
Duration,
10+
aws_logs,
11+
} from "aws-cdk-lib";
12+
import { HttpApi } from "@aws-cdk/aws-apigatewayv2-alpha";
13+
import { HttpLambdaIntegration } from "@aws-cdk/aws-apigatewayv2-integrations-alpha";
14+
import { Construct } from "constructs";
15+
16+
export class TitilerPgstacApiLambda extends Construct {
17+
readonly url: string;
18+
public titilerPgstacLambdaFunction: lambda.Function;
19+
20+
constructor(scope: Construct, id: string, props: TitilerPgStacApiLambdaProps) {
21+
super(scope, id);
22+
23+
const titilerPgstacEnv = {
24+
"CPL_VSIL_CURL_ALLOWED_EXTENSIONS": ".tif,.TIF,.tiff",
25+
"GDAL_CACHEMAX": "200",
26+
"GDAL_DISABLE_READDIR_ON_OPEN": "EMPTY_DIR",
27+
"GDAL_INGESTED_BYTES_AT_OPEN": "32768",
28+
"GDAL_HTTP_MERGE_CONSECUTIVE_RANGES": "YES",
29+
"GDAL_HTTP_MULTIPLEX": "YES",
30+
"GDAL_HTTP_VERSION": "2",
31+
"PYTHONWARNINGS": "ignore",
32+
"VSI_CACHE": "TRUE",
33+
"VSI_CACHE_SIZE": "5000000",
34+
"DB_MIN_CONN_SIZE": "1",
35+
"DB_MAX_CONN_SIZE": "1",
36+
"PGSTAC_SECRET_ARN": props.dbSecret.secretArn,
37+
}
38+
39+
40+
this.titilerPgstacLambdaFunction = new lambda.Function(this, "lambda", {
41+
handler: "handler.handler",
42+
runtime: lambda.Runtime.PYTHON_3_8,
43+
code: lambda.Code.fromDockerBuild(__dirname, {
44+
file: "runtime/Dockerfile",
45+
buildArgs: { PYTHON_VERSION: '3.10' },
46+
}),
47+
timeout: Duration.seconds(30),
48+
vpc: props.vpc,
49+
vpcSubnets: props.subnetSelection,
50+
allowPublicSubnet: true,
51+
memorySize: 3008,
52+
logRetention: aws_logs.RetentionDays.ONE_WEEK,
53+
environment: titilerPgstacEnv,
54+
});
55+
56+
// grant access to buckets using addToRolePolicy
57+
if (props.buckets) {
58+
props.buckets.forEach(bucket => {
59+
this.titilerPgstacLambdaFunction.addToRolePolicy(new iam.PolicyStatement({
60+
actions: ["s3:GetObject"],
61+
resources: [`arn:aws:s3:::${bucket}/*`],
62+
}));
63+
});
64+
}
65+
66+
props.dbSecret.grantRead(this.titilerPgstacLambdaFunction);
67+
this.titilerPgstacLambdaFunction.connections.allowTo(props.db, ec2.Port.tcp(5432), "allow connections from titiler");
68+
69+
const stacApi = new HttpApi(this, `${Stack.of(this).stackName}-titiler-pgstac-api`, {
70+
defaultIntegration: new HttpLambdaIntegration("integration", this.titilerPgstacLambdaFunction),
71+
});
72+
73+
this.url = stacApi.url!;
74+
75+
new CfnOutput(this, "titiler-pgstac-api-output", {
76+
exportName: `${Stack.of(this).stackName}-titiler-pgstac-url`,
77+
value: this.url,
78+
});
79+
}
80+
}
81+
82+
export interface TitilerPgStacApiLambdaProps {
83+
84+
/**
85+
* VPC into which the lambda should be deployed.
86+
*/
87+
readonly vpc: ec2.IVpc;
88+
89+
/**
90+
* RDS Instance with installed pgSTAC.
91+
*/
92+
readonly db: rds.IDatabaseInstance;
93+
94+
/**
95+
* Subnet into which the lambda should be deployed.
96+
*/
97+
readonly subnetSelection: ec2.SubnetSelection;
98+
99+
/**
100+
* Secret containing connection information for pgSTAC database.
101+
*/
102+
readonly dbSecret: secretsmanager.ISecret;
103+
104+
/**
105+
* Customized environment variables to send to titiler-pgstac runtime.
106+
*/
107+
readonly apiEnv?: Record<string, string>;
108+
109+
/**
110+
* list of buckets the lambda will be granted access to.
111+
*/
112+
readonly buckets?: string[];
113+
114+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
ARG PYTHON_VERSION
2+
3+
FROM --platform=linux/amd64 public.ecr.aws/lambda/python:${PYTHON_VERSION}
4+
5+
WORKDIR /tmp
6+
RUN python -m pip install pip -U
7+
8+
COPY runtime/requirements.txt requirements.txt
9+
RUN python -m pip install -r requirements.txt "mangum>=0.14,<0.15" -t /asset
10+
11+
# Reduce package size and remove useless files
12+
RUN find /asset -type f -name '*.pyc' | while read f; do n=$(echo $f | sed 's/__pycache__\///' | sed 's/.cpython-[0-9]*//'); cp $f $n; done;
13+
RUN find /asset -type d -name '__pycache__' -print0 | xargs -0 rm -rf
14+
RUN find /asset -type f -name '*.py' -print0 | xargs -0 rm -f
15+
RUN find /asset -type d -name 'tests' -print0 | xargs -0 rm -rf
16+
RUN rm -rdf /asset/numpy/doc/ /asset/boto3* /asset/botocore* /asset/bin /asset/geos_license /asset/Misc
17+
18+
COPY runtime/src/*.py /asset
19+
20+
CMD ["echo", "hello world"]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
uvicorn
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
titiler.pgstac==0.3.3
2+
boto3>=1.26.139
3+
psycopg[binary, pool]
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""eoapi.raster."""
2+
3+
__version__ = "0.1.0"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
Handler for AWS Lambda.
3+
"""
4+
5+
import os
6+
from mangum import Mangum
7+
from utils import get_secret_dict
8+
from titiler.pgstac.main import app
9+
10+
pgstac_secret_arn = os.environ["PGSTAC_SECRET_ARN"]
11+
12+
secret = get_secret_dict(pgstac_secret_arn)
13+
os.environ.update(
14+
{
15+
"postgres_host": secret["host"],
16+
"postgres_dbname": secret["dbname"],
17+
"postgres_user": secret["username"],
18+
"postgres_pass": secret["password"],
19+
"postgres_port": str(secret["port"]),
20+
}
21+
)
22+
23+
handler = Mangum(app)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import base64
2+
import json
3+
import boto3
4+
5+
6+
def get_secret_dict(secret_name: str):
7+
"""Retrieve secrets from AWS Secrets Manager
8+
9+
Args:
10+
secret_name (str): name of aws secrets manager secret containing database connection secrets
11+
profile_name (str, optional): optional name of aws profile for use in debugger only
12+
13+
Returns:
14+
secrets (dict): decrypted secrets in dict
15+
"""
16+
17+
# Create a Secrets Manager client
18+
session = boto3.session.Session()
19+
client = session.client(service_name="secretsmanager")
20+
21+
get_secret_value_response = client.get_secret_value(SecretId=secret_name)
22+
23+
if "SecretString" in get_secret_value_response:
24+
return json.loads(get_secret_value_response["SecretString"])
25+
else:
26+
return json.loads(base64.b64decode(get_secret_value_response["SecretBinary"]))

0 commit comments

Comments
 (0)