├── .gitignore
├── requirements.txt
├── scripts
├── build.sh
├── list.py
├── create-lambda-layer.sh
└── deploy.py
├── Dockerfile
├── .github
├── dependabot.yml
└── workflows
│ └── ci.yml
├── LICENSE
├── README.md
├── sam.yml
└── arns.json
/.gitignore:
--------------------------------------------------------------------------------
1 | *.zip
2 | __pycache__/
3 | .mypy_cache/
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | titiler.application==0.22.4
2 | mangum
3 | requests
4 | pyyaml
5 | jinja2
6 |
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | PYTHON_VERSION=$1
4 |
5 | # Base Image
6 | docker build \
7 | --platform linux/amd64 \
8 | --build-arg PYTHON_VERSION=${PYTHON_VERSION} \
9 | -t devseed/titiler-layer:latest .
10 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG PYTHON_VERSION=3.12
2 | FROM public.ecr.aws/lambda/python:${PYTHON_VERSION}
3 |
4 | ENV PREFIX /opt
5 | RUN mkdir ${PREFIX}/python
6 |
7 | RUN dnf install -y gcc-c++ && dnf clean all
8 |
9 | RUN python -m pip install pip -U
10 |
11 | COPY requirements.txt requirements.txt
12 | RUN python -m pip install \
13 | -r requirements.txt \
14 | --no-binary pydantic \
15 | -t $PREFIX/python
16 |
17 | ENV PYTHONPATH=$PYTHONPATH:$PREFIX/python
18 | ENV PATH=$PREFIX/python/bin:$PATH
19 |
20 | ENTRYPOINT bash
21 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "pip" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "daily"
12 | groups:
13 | titiler:
14 | patterns:
15 | - titiler*
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Development Seed
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/scripts/list.py:
--------------------------------------------------------------------------------
1 |
2 | import json
3 | from boto3.session import Session as boto3_session
4 |
5 | AWS_REGIONS = [
6 | "ap-northeast-1",
7 | "ap-northeast-2",
8 | "ap-south-1",
9 | "ap-southeast-1",
10 | "ap-southeast-2",
11 | "ca-central-1",
12 | "eu-central-1",
13 | "eu-north-1",
14 | "eu-west-1",
15 | "eu-west-2",
16 | "eu-west-3",
17 | "sa-east-1",
18 | "us-east-1",
19 | "us-east-2",
20 | "us-west-1",
21 | "us-west-2",
22 | ]
23 | layers = [
24 | "titiler",
25 | ]
26 |
27 |
28 | def main():
29 | results = []
30 | for region in AWS_REGIONS:
31 | res = {"region": region, "layers": []}
32 |
33 | session = boto3_session(region_name=region)
34 | client = session.client("lambda")
35 | for layer in layers:
36 | response = client.list_layer_versions(LayerName=layer)
37 | latest = response["LayerVersions"][0]
38 | res["layers"].append(dict(
39 | name=layer,
40 | arn=latest["LayerVersionArn"],
41 | version=latest["Version"]
42 | ))
43 | results.append(res)
44 |
45 | print(json.dumps(results))
46 |
47 |
48 | if __name__ == '__main__':
49 | main()
50 |
--------------------------------------------------------------------------------
/scripts/create-lambda-layer.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | echo "-----------------------"
3 | echo "Creating lambda layer"
4 | echo "-----------------------"
5 |
6 | #dnf install -y zip binutils
7 |
8 | echo "Remove lambda python packages"
9 | rm -rdf $PREFIX/python/boto3* \
10 | && rm -rdf $PREFIX/python/botocore* \
11 | && rm -rdf $PREFIX/python/docutils* \
12 | && rm -rdf $PREFIX/python/dateutil* \
13 | && rm -rdf $PREFIX/python/jmespath* \
14 | && rm -rdf $PREFIX/python/s3transfer* \
15 | && rm -rdf $PREFIX/python/numpy/doc/
16 |
17 | find $PREFIX/python -type d -a -name 'tests' -print0 | xargs -0 rm -rf
18 |
19 | echo "Remove uncompiled python scripts"
20 | find $PREFIX/python -type f -name '*.pyc' | while read f; do n=$(echo $f | sed 's/__pycache__\///' | sed 's/.cpython-[0-9]*//'); cp $f $n; done;
21 | find $PREFIX/python -type d -a -name '__pycache__' -print0 | xargs -0 rm -rf
22 | find $PREFIX/python -type f -a -name '*.py' -print0 | xargs -0 rm -f
23 |
24 | # Ref: https://github.com/developmentseed/titiler/discussions/1108#discussioncomment-13045681
25 | mkdir $PREFIX/lib/
26 | cp /usr/lib64/libexpat.so.1 $PREFIX/lib/
27 |
28 | echo "Create archives"
29 | cd $PREFIX && zip -r9q /tmp/package.zip python && zip -r9q /tmp/package.zip lib
30 |
31 | cp /tmp/package.zip /local/package.zip
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TiTiler AWS Lambda Layers
2 |
3 |
4 |
5 |
6 |
7 | ## Layers
8 |
9 | | Layer Version | TiTiler Version | Python Version |
10 | | --| --| --|
11 | | 17 | 0.22.4 | 3.12 |
12 |
13 | **Older version**
14 |
15 | | Layer Version | TiTiler Version | Python Version |
16 | | --| --| --|
17 | | 15 | 0.19.1 | 3.11 |
18 | | 14 | 0.18.5 | 3.11 |
19 | | 13 | 0.18.1 | 3.11 |
20 | | 12 | 0.17.3 | 3.11 |
21 | | 11 | 0.15.6 | 3.11 |
22 | | 10 | 0.15.0 | 3.11 |
23 | | 9 | 0.15.0 | 3.10 |
24 | | 8 | 0.14.0 | 3.10 |
25 | | 7 | 0.13.3 | 3.10 |
26 | | 6 | 0.13.1 | 3.10 |
27 | | 5 | 0.13.0 | 3.10 |
28 | | 4 | 0.12.0 | 3.10 |
29 | | 3 | 0.11.6 | 3.10 |
30 | | 2 | 0.11.6 | 3.10 |
31 | | 1 | 0.11.6 | 3.10 |
32 |
33 |
34 |
35 | #### Arns format
36 |
37 | - `arn:aws:lambda:${region}:552819999234:layer:titiler:${version}`
38 |
39 | #### Regions
40 | - ap-northeast-1
41 | - ap-northeast-2
42 | - ap-south-1
43 | - ap-southeast-1
44 | - ap-southeast-2
45 | - ca-central-1
46 | - eu-central-1
47 | - eu-north-1
48 | - eu-west-1
49 | - eu-west-2
50 | - eu-west-3
51 | - sa-east-1
52 | - us-east-1
53 | - us-east-2
54 | - us-west-1
55 | - us-west-2
56 |
57 | See [full list of ARN](/arns.json)
58 |
59 | ## SAM application
60 |
61 |

62 |
63 | Link: https://serverlessrepo.aws.amazon.com/applications/us-east-1/552819999234/TiTiler
64 |
65 | > **Note**
66 | > You can change the `TiTiler` version by changing the Lambda Layer version `LayerVersion` parameter before deploying.
67 |
68 | see: [SAM Application template](/sam.yml)
69 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | paths:
6 | - 'Dockerfile'
7 | - 'requirements.txt'
8 | - 'scripts/create-lambda-layer.sh'
9 | - 'scripts/deploy.py'
10 | - '.github/workflows/ci.yml'
11 |
12 | env:
13 | PYTHON_VERSION: '3.12'
14 |
15 | jobs:
16 | build:
17 | runs-on: ubuntu-latest
18 | if: "!contains(github.event.head_commit.message, '[skip ci]')"
19 | steps:
20 | - uses: actions/checkout@v4
21 | - name: Set up Python 3.12
22 | uses: actions/setup-python@v5
23 | with:
24 | python-version: 3.12
25 |
26 | - name: Set up Docker Buildx
27 | uses: docker/setup-buildx-action@v2
28 |
29 | - name: Login to DockerHub
30 | uses: docker/login-action@v2
31 | with:
32 | registry: ghcr.io
33 | username: ${{ github.actor }}
34 | password: ${{ secrets.GITHUB_TOKEN }}
35 |
36 | - name: Build
37 | uses: docker/build-push-action@v3
38 | with:
39 | platforms: linux/amd64
40 | context: .
41 | load: true
42 | push: false
43 | tags: devseed/titiler-layer:latest
44 | cache-from: type=gha
45 | cache-to: type=gha,mode=max
46 | build-args: |
47 | PYTHON_VERSION=${{ env.PYTHON_VERSION }}
48 |
49 | - name: Create Package
50 | run: |
51 | docker run \
52 | --platform=linux/amd64 \
53 | --entrypoint bash \
54 | -v ${{ github.workspace }}:/local \
55 | --rm devseed/titiler-layer:latest \
56 | /local/scripts/create-lambda-layer.sh
57 |
58 | - name: Configure AWS Credentials
59 | uses: aws-actions/configure-aws-credentials@v1
60 | with:
61 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
62 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
63 | aws-region: us-east-1
64 |
65 | - name: Set module version
66 | id: titiler_version
67 | run: |
68 | echo version=$(docker run --platform=linux/amd64 --entrypoint '' --rm devseed/titiler-layer:latest python -c'import titiler.core; print(titiler.core.__version__)') >> $GITHUB_OUTPUT
69 |
70 | - name: Print Version
71 | run: |
72 | echo "${{ steps.titiler_version.outputs.version }}"
73 |
74 | - name: Install dependencies
75 | run: |
76 | python -m pip install --upgrade pip
77 | python -m pip install boto3 click
78 |
79 | - name: Deploy layers
80 | if: github.ref == 'refs/heads/main'
81 | run: python scripts/deploy.py ${{ env.PYTHON_VERSION }} ${{ steps.titiler_version.outputs.version }}
82 |
--------------------------------------------------------------------------------
/scripts/deploy.py:
--------------------------------------------------------------------------------
1 | import click
2 |
3 | from boto3.session import Session as boto3_session
4 | from botocore.client import Config
5 |
6 | AWS_REGIONS = [
7 | "ap-northeast-1",
8 | "ap-northeast-2",
9 | "ap-south-1",
10 | "ap-southeast-1",
11 | "ap-southeast-2",
12 | "ca-central-1",
13 | "eu-central-1",
14 | "eu-north-1",
15 | "eu-west-1",
16 | "eu-west-2",
17 | "eu-west-3",
18 | "sa-east-1",
19 | "us-east-1",
20 | "us-east-2",
21 | "us-west-1",
22 | "us-west-2",
23 | ]
24 |
25 |
26 | @click.command()
27 | @click.argument("runtime", type=str)
28 | @click.argument("version", type=str)
29 | def main(runtime, version):
30 | """Build and Deploy Layers."""
31 | version_nodot = version.replace(".", "")
32 | runtime_nodot = runtime.replace(".", "")
33 |
34 | session = boto3_session()
35 |
36 | click.echo("Deploying titiler layer", err=True)
37 | for region in AWS_REGIONS:
38 | click.echo(f"AWS Region: {region}", err=True)
39 |
40 |
41 | # upload the package to s3
42 | s3_client = session.client("s3", region_name=region)
43 |
44 | s3_bucket = f"titiler-layers-{region}"
45 | s3_key = f"titiler{version_nodot}-py{runtime_nodot}.zip"
46 |
47 | click.echo(f"Uploading package to S3 s3://{s3_bucket}/{s3_key}", err=True)
48 |
49 | try:
50 | s3_client.head_bucket(Bucket=s3_bucket)
51 | except s3_client.exceptions.ClientError:
52 | ops = {}
53 | if region != "us-east-1":
54 | ops["CreateBucketConfiguration"] = {"LocationConstraint": region}
55 |
56 | s3_client.create_bucket(Bucket=s3_bucket, **ops)
57 |
58 | with open("package.zip", "rb") as data:
59 | s3_client.upload_fileobj(data, s3_bucket, s3_key)
60 |
61 | click.echo("Publishing new version", err=True)
62 |
63 | # Increase connection timeout to work around timeout errors
64 | config = Config(connect_timeout=6000, retries={"max_attempts": 5})
65 | lambda_client = session.client("lambda", region_name=region, config=config)
66 |
67 | res = lambda_client.publish_layer_version(
68 | LayerName="titiler",
69 | Content={"S3Bucket": s3_bucket, "S3Key": s3_key},
70 | CompatibleRuntimes=[f"python{runtime}"],
71 | Description=f"TiTiler Lambda Layer ({version}) - for Python {runtime}",
72 | LicenseInfo="MIT",
73 | )
74 |
75 | click.echo("Adding permission", err=True)
76 | lambda_client.add_layer_version_permission(
77 | LayerName="titiler",
78 | VersionNumber=res["Version"],
79 | StatementId="make_public",
80 | Action="lambda:GetLayerVersion",
81 | Principal="*",
82 | )
83 |
84 |
85 | if __name__ == "__main__":
86 | main()
87 |
--------------------------------------------------------------------------------
/sam.yml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: '2010-09-09'
2 | Transform: AWS::Serverless-2016-10-31
3 |
4 | Parameters:
5 | Bucket:
6 | Type: CommaDelimitedList
7 | Default: "*"
8 |
9 | DisableCOG:
10 | Type: String
11 | Default: "false"
12 | AllowedValues:
13 | - "true"
14 | - "false"
15 |
16 | DisableMosaic:
17 | Type: String
18 | Default: "true"
19 | AllowedValues:
20 | - "true"
21 | - "false"
22 |
23 | DisableSTAC:
24 | Type: String
25 | Default: "true"
26 | AllowedValues:
27 | - "true"
28 | - "false"
29 |
30 | LayerVersion:
31 | Type: String
32 | Default: 17
33 |
34 | Resources:
35 | TiTiler:
36 | Type: AWS::Serverless::Function
37 | Properties:
38 | Runtime: python3.12
39 | Handler: index.handler
40 | Description: 'Titiler: Dynamic tiler'
41 | Layers:
42 | - !Sub arn:aws:lambda:${AWS::Region}:552819999234:layer:titiler:${LayerVersion}
43 |
44 | InlineCode: |
45 | import logging
46 | from mangum import Mangum
47 | from titiler.application.main import app
48 |
49 | logging.getLogger("mangum.lifespan").setLevel(logging.ERROR)
50 | logging.getLogger("mangum.http").setLevel(logging.ERROR)
51 |
52 | handler = Mangum(app, lifespan="auto")
53 |
54 | MemorySize: 1024
55 | Timeout: 10
56 | Policies:
57 | - AWSLambdaExecute # Managed Policy
58 | - Version: '2012-10-17' # Policy Document
59 | Statement:
60 | - Effect: Allow
61 | Action:
62 | - s3:GetObject
63 | - s3:HeadObject
64 | Resource:
65 | !Split
66 | - ','
67 | - !Join
68 | - ''
69 | - - 'arn:aws:s3:::'
70 | - !Join
71 | - '/*,arn:aws:s3:::'
72 | - !Ref Bucket
73 | - '/*'
74 |
75 | Environment:
76 | Variables:
77 | CPL_VSIL_CURL_ALLOWED_EXTENSIONS: '.tif,.TIF,.tiff'
78 | GDAL_CACHEMAX: 200
79 | GDAL_DISABLE_READDIR_ON_OPEN: EMPTY_DIR
80 | GDAL_HTTP_MERGE_CONSECUTIVE_RANGES: YES
81 | GDAL_HTTP_MULTIPLEX: YES
82 | GDAL_HTTP_VERSION: 2
83 | VSI_CACHE: TRUE
84 | VSI_CACHE_SIZE: 536870912
85 | CPL_VSIL_CURL_CACHE_SIZE: 200000000
86 | GDAL_INGESTED_BYTES_AT_OPEN: 32768
87 | TITILER_API_DISABLE_COG: !Ref DisableCOG
88 | TITILER_API_DISABLE_STAC: !Ref DisableSTAC
89 | TITILER_API_DISABLE_MOSAIC: !Ref DisableMosaic
90 |
91 | Events:
92 | API:
93 | Type: HttpApi
94 |
95 | Outputs:
96 | LambdaFunc:
97 | Description: Lambda Fucntion ARN
98 | Value: !GetAtt TiTiler.Arn
99 |
100 | Api:
101 | Description: "Endpoint URL"
102 | Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/"
103 |
--------------------------------------------------------------------------------
/arns.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "region": "ap-northeast-1",
4 | "layers": [
5 | {
6 | "name": "titiler",
7 | "arn": "arn:aws:lambda:ap-northeast-1:552819999234:layer:titiler:17",
8 | "version": 17
9 | }
10 | ]
11 | },
12 | {
13 | "region": "ap-northeast-2",
14 | "layers": [
15 | {
16 | "name": "titiler",
17 | "arn": "arn:aws:lambda:ap-northeast-2:552819999234:layer:titiler:17",
18 | "version": 17
19 | }
20 | ]
21 | },
22 | {
23 | "region": "ap-south-1",
24 | "layers": [
25 | {
26 | "name": "titiler",
27 | "arn": "arn:aws:lambda:ap-south-1:552819999234:layer:titiler:17",
28 | "version": 17
29 | }
30 | ]
31 | },
32 | {
33 | "region": "ap-southeast-1",
34 | "layers": [
35 | {
36 | "name": "titiler",
37 | "arn": "arn:aws:lambda:ap-southeast-1:552819999234:layer:titiler:17",
38 | "version": 17
39 | }
40 | ]
41 | },
42 | {
43 | "region": "ap-southeast-2",
44 | "layers": [
45 | {
46 | "name": "titiler",
47 | "arn": "arn:aws:lambda:ap-southeast-2:552819999234:layer:titiler:17",
48 | "version": 17
49 | }
50 | ]
51 | },
52 | {
53 | "region": "ca-central-1",
54 | "layers": [
55 | {
56 | "name": "titiler",
57 | "arn": "arn:aws:lambda:ca-central-1:552819999234:layer:titiler:17",
58 | "version": 17
59 | }
60 | ]
61 | },
62 | {
63 | "region": "eu-central-1",
64 | "layers": [
65 | {
66 | "name": "titiler",
67 | "arn": "arn:aws:lambda:eu-central-1:552819999234:layer:titiler:17",
68 | "version": 17
69 | }
70 | ]
71 | },
72 | {
73 | "region": "eu-north-1",
74 | "layers": [
75 | {
76 | "name": "titiler",
77 | "arn": "arn:aws:lambda:eu-north-1:552819999234:layer:titiler:17",
78 | "version": 17
79 | }
80 | ]
81 | },
82 | {
83 | "region": "eu-west-1",
84 | "layers": [
85 | {
86 | "name": "titiler",
87 | "arn": "arn:aws:lambda:eu-west-1:552819999234:layer:titiler:17",
88 | "version": 17
89 | }
90 | ]
91 | },
92 | {
93 | "region": "eu-west-2",
94 | "layers": [
95 | {
96 | "name": "titiler",
97 | "arn": "arn:aws:lambda:eu-west-2:552819999234:layer:titiler:17",
98 | "version": 17
99 | }
100 | ]
101 | },
102 | {
103 | "region": "eu-west-3",
104 | "layers": [
105 | {
106 | "name": "titiler",
107 | "arn": "arn:aws:lambda:eu-west-3:552819999234:layer:titiler:17",
108 | "version": 17
109 | }
110 | ]
111 | },
112 | {
113 | "region": "sa-east-1",
114 | "layers": [
115 | {
116 | "name": "titiler",
117 | "arn": "arn:aws:lambda:sa-east-1:552819999234:layer:titiler:17",
118 | "version": 17
119 | }
120 | ]
121 | },
122 | {
123 | "region": "us-east-1",
124 | "layers": [
125 | {
126 | "name": "titiler",
127 | "arn": "arn:aws:lambda:us-east-1:552819999234:layer:titiler:17",
128 | "version": 17
129 | }
130 | ]
131 | },
132 | {
133 | "region": "us-east-2",
134 | "layers": [
135 | {
136 | "name": "titiler",
137 | "arn": "arn:aws:lambda:us-east-2:552819999234:layer:titiler:17",
138 | "version": 17
139 | }
140 | ]
141 | },
142 | {
143 | "region": "us-west-1",
144 | "layers": [
145 | {
146 | "name": "titiler",
147 | "arn": "arn:aws:lambda:us-west-1:552819999234:layer:titiler:17",
148 | "version": 17
149 | }
150 | ]
151 | },
152 | {
153 | "region": "us-west-2",
154 | "layers": [
155 | {
156 | "name": "titiler",
157 | "arn": "arn:aws:lambda:us-west-2:552819999234:layer:titiler:17",
158 | "version": 17
159 | }
160 | ]
161 | }
162 | ]
163 |
--------------------------------------------------------------------------------