├── .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 | Test 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 |

Launch Stack

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 | --------------------------------------------------------------------------------