Skip to content

Commit

Permalink
add deployement
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentsarago committed Apr 3, 2020
1 parent 565faa3 commit 89d8d64
Show file tree
Hide file tree
Showing 23 changed files with 1,034 additions and 23 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,6 @@ ENV/
/site

# mypy
.mypy_cache/
.mypy_cache/

cdk.out/
7 changes: 5 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7
FROM laurents/uvicorn-gunicorn-fastapi:python3.7-slim
# Ref https://github.com/tiangolo/uvicorn-gunicorn-fastapi-docker/issues/15
# Cuts image size by 50%
# FROM tiangolo/uvicorn-gunicorn-fastapi:python3.7

ENV CURL_CA_BUNDLE /etc/ssl/certs/ca-certificates.crt

COPY README.md /app/README.md
COPY titiler/ /app/titiler/
COPY setup.py /app/setup.py

RUN pip install /app/.
RUN pip install -e /app/. --no-cache-dir
123 changes: 115 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,120 @@ A lightweight Cloud Optimized GeoTIFF tile server.

# Deployment

**To Do**
The stack is deployed by the [aws cdk](https://aws.amazon.com/cdk/) utility. It will handle tasks such as generating a docker image and packaging handlers automatically.

1. Instal cdk and set up CDK in your AWS account - Only need once per account
```bash
$ npm install cdk -g

$ cdk bootstrap # Deploys the CDK toolkit stack into an AWS environment
```

2. Install dependencies

```bash
# Note: it's recommanded to use virtualenv
$ git clone https://github.com/developmentseed/titiler.git
$ cd titiler && pip install -e .[deploy]
```

3. Pre-Generate CFN template
```bash
$ cdk synth # Synthesizes and prints the CloudFormation template for this stack
```

4. Edit [stack/config.py](stack/config.py)

```python
PROJECT_NAME = "titiler"
STAGE = os.environ.get("STAGE", "dev")

# // Service config
# Min/Max Number of ECS images
MIN_ECS_INSTANCES = 2
MAX_ECS_INSTANCES = 50

# CPU value | Memory value
# 256 (.25 vCPU) | 0.5 GB, 1 GB, 2 GB
# 512 (.5 vCPU) | 1 GB, 2 GB, 3 GB, 4 GB
# 1024 (1 vCPU) | 2 GB, 3 GB, 4 GB, 5 GB, 6 GB, 7 GB, 8 GB
# 2048 (2 vCPU) | Between 4 GB and 16 GB in 1-GB increments
# 4096 (4 vCPU) | Between 8 GB and 30 GB in 1-GB increments
TASK_CPU = 1024
TASK_MEMORY = 2048
```

5. Deploy
```bash
$ cdk deploy # Deploys the stack(s) named STACKS into your AWS account
```

# Test locally
```bash
$ git clone https://github.com/developmentseed/titiler.git

$ pip install -e .
$ uvicorn titiler.main:app --reload
```

### Docker
Or with Docker
```
$ docker-compose build
$ docker-compose up
```

## Authors
Created by [Development Seed](<http://developmentseed.org>)
# API

### Doc

`:endpoint:/docs`
![](https://user-images.githubusercontent.com/10407788/78325903-011c9680-7547-11ea-853f-50e0fb0f4d92.png)

### Tiles

`:endpoint:/v1/{z}/{x}/{y}[@{scale}x][.{ext}]`
- **z**: Mercator tiles's zoom level.
- **x**: Mercator tiles's column.
- **y**: Mercator tiles's row.
- **scale**: Tile size scale, default is set to 1 (256x256). OPTIONAL
- **ext**: Output image format, default is set to None and will be either JPEG or PNG depending on masked value. OPTIONAL
- **url**: Cloud Optimized GeoTIFF URL. **REQUIRED**
- **bidx**: Coma (',') delimited band indexes. OPTIONAL
- **nodata**: Overwrite internal Nodata value. OPTIONAL
- **rescale**: Coma (',') delimited Min,Max bounds. OPTIONAL
- **color_formula**: rio-color formula. OPTIONAL
- **color_map**: rio-tiler color map name. OPTIONAL

### Metadata

`:endpoint:/v1/tilejson.json` - Get tileJSON document
- **url**: Cloud Optimized GeoTIFF URL. **REQUIRED**
- **tile_format**: Output image format, default is set to None and will be either JPEG or PNG depending on masked value.
- **tile_scale**: Tile size scale, default is set to 1 (256x256). OPTIONAL
- **kwargs**: Other options will be forwarded to the `tiles` url.

`:endpoint:/v1/bounds` - Get general image bounds
- **url**: Cloud Optimized GeoTIFF URL. **REQUIRED**

`:endpoint:/v1/info` - Get general image info
- **url**: Cloud Optimized GeoTIFF URL. **REQUIRED**

`:endpoint:/v1/metadata` - Get image statistics
- **url**: Cloud Optimized GeoTIFF URL. **REQUIRED**
- **bidx**: Coma (',') delimited band indexes. OPTIONAL
- **nodata**: Overwrite internal Nodata value. OPTIONAL
- **pmin**: min percentile, default is 2. OPTIONAL
- **pmax**: max percentile, default is 98. OPTIONAL
- **max_size**: Max image size from which to calculate statistics, default is 1024. OPTIONAL
- **histogram_bins**: Histogram bins, default is 20. OPTIONAL
- **histogram_range**: Coma (',') delimited histogram bounds. OPTIONAL

## UI

## Project structure
`:endpoint:/index.html` - Full UI (histogram, predefined rescaling, ...)

`:endpoint:/simple_viewer.html` - Simple UI (no histogram, manual rescaling, ...)

# Project structure

```
titiler/ - titiler python module.
Expand All @@ -44,9 +140,16 @@ titiler/ - titiler python module.
├── ressources/ - application ressources (enums, constants, ...).
├── templates/ - html/xml models.
├── main.py - FastAPI application creation and configuration.
└── utils.py - utility functions.
```
├── utils.py - utility functions.
stack/
├── app.py - AWS Stack definition (vpc, cluster, ecs, alb ...)
├── config.py - Optional parameters for the stack definition [EDIT THIS]
OpenAPI/
└── openapi.json - OpenAPI document.
```

## Contribution & Development

Expand All @@ -67,3 +170,7 @@ This repo is set to use `pre-commit` to run *my-py*, *flake8*, *pydocstring* and
```bash
$ pre-commit install
```

## Authors
Created by [Development Seed](<http://developmentseed.org>)

3 changes: 3 additions & 0 deletions cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"app": "python3 stack/app.py"
}
8 changes: 8 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,17 @@
extra_reqs = {
"dev": ["pytest", "pytest-cov", "pytest-asyncio", "pre-commit"],
"server": ["uvicorn", "click==7.0"],
"deploy": [
"aws-cdk.core",
"aws-cdk.aws_ecs",
"aws-cdk.aws_ec2",
"aws-cdk.aws_autoscaling",
"aws-cdk.aws_ecs_patterns",
],
"test": ["mock", "pytest", "pytest-cov", "pytest-asyncio", "requests"],
}


setup(
name="titiler",
version="0.1.0",
Expand Down
1 change: 1 addition & 0 deletions stack/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""AWS App."""
118 changes: 118 additions & 0 deletions stack/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Construct App."""

from typing import Any, Union

import os

from aws_cdk import (
core,
aws_ec2 as ec2,
aws_ecs as ecs,
aws_ecs_patterns as ecs_patterns,
)

import config


class titilerStack(core.Stack):
"""Titiler ECS Fargate Stack."""

def __init__(
self,
scope: core.Construct,
id: str,
cpu: Union[int, float] = 256,
memory: Union[int, float] = 512,
mincount: int = 1,
maxcount: int = 50,
code_dir: str = "./",
**kwargs: Any,
) -> None:
"""Define stack."""
super().__init__(scope, id, *kwargs)

vpc = ec2.Vpc(self, f"{id}-vpc", max_azs=2)

cluster = ecs.Cluster(self, f"{id}-cluster", vpc=vpc)

fargate_service = ecs_patterns.ApplicationLoadBalancedFargateService(
self,
f"{id}-service",
cluster=cluster,
cpu=cpu,
memory_limit_mib=memory,
desired_count=mincount,
public_load_balancer=True,
listener_port=80,
task_image_options=dict(
image=ecs.ContainerImage.from_asset(
code_dir, exclude=["cdk.out", ".git"]
),
container_port=80,
environment=dict(
CPL_TMPDIR="/tmp",
GDAL_CACHEMAX="25%",
GDAL_DISABLE_READDIR_ON_OPEN="EMPTY_DIR",
GDAL_HTTP_MERGE_CONSECUTIVE_RANGES="YES",
GDAL_HTTP_MULTIPLEX="YES",
GDAL_HTTP_VERSION="2",
MODULE_NAME="titiler.main",
PYTHONWARNINGS="ignore",
VARIABLE_NAME="app",
VSI_CACHE="TRUE",
VSI_CACHE_SIZE="1000000",
WORKERS_PER_CORE="5",
LOG_LEVEL="error",
),
),
)

scalable_target = fargate_service.service.auto_scale_task_count(
min_capacity=mincount, max_capacity=maxcount
)

# https://github.com/awslabs/aws-rails-provisioner/blob/263782a4250ca1820082bfb059b163a0f2130d02/lib/aws-rails-provisioner/scaling.rb#L343-L387
scalable_target.scale_on_request_count(
"RequestScaling",
requests_per_target=50,
scale_in_cooldown=core.Duration.seconds(240),
scale_out_cooldown=core.Duration.seconds(30),
target_group=fargate_service.target_group,
)

# scalable_target.scale_on_cpu_utilization(
# "CpuScaling", target_utilization_percent=70,
# )

fargate_service.service.connections.allow_from_any_ipv4(
port_range=ec2.Port(
protocol=ec2.Protocol.ALL,
string_representation="All port 80",
from_port=80,
),
description="Allows traffic on port 80 from NLB",
)


app = core.App()

# Tag infrastructure
for key, value in {
"Project": config.PROJECT_NAME,
"Stack": config.STAGE,
"Owner": os.environ.get("OWNER"),
"Client": os.environ.get("CLIENT"),
}.items():
if value:
core.Tag.add(app, key, value)

stackname = f"{config.PROJECT_NAME}-{config.STAGE}"
titilerStack(
app,
stackname,
cpu=config.TASK_CPU,
memory=config.TASK_MEMORY,
mincount=config.MIN_ECS_INSTANCES,
maxcount=config.MAX_ECS_INSTANCES,
)
app.synth()
20 changes: 20 additions & 0 deletions stack/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""STACK Configs."""

import os

PROJECT_NAME = "titiler"
STAGE = os.environ.get("STAGE", "dev")

# // Service config
# Min/Max Number of ECS images
MIN_ECS_INSTANCES = 2
MAX_ECS_INSTANCES = 50

# CPU value | Memory value
# 256 (.25 vCPU) | 0.5 GB, 1 GB, 2 GB
# 512 (.5 vCPU) | 1 GB, 2 GB, 3 GB, 4 GB
# 1024 (1 vCPU) | 2 GB, 3 GB, 4 GB, 5 GB, 6 GB, 7 GB, 8 GB
# 2048 (2 vCPU) | Between 4 GB and 16 GB in 1-GB increments
# 4096 (4 vCPU) | Between 8 GB and 30 GB in 1-GB increments
TASK_CPU = 1024
TASK_MEMORY = 2048
2 changes: 1 addition & 1 deletion titiler/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"""titiler."""
"""titiler.api"""
2 changes: 1 addition & 1 deletion titiler/api/api_v1/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"""titiler."""
"""titiler.api.api_v1"""
2 changes: 1 addition & 1 deletion titiler/api/api_v1/endpoints/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"""titiler."""
"""titiler.api.api_v1.endpoints"""
13 changes: 12 additions & 1 deletion titiler/api/api_v1/endpoints/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
from titiler.core import config
from titiler.models.mapbox import TileJSON
from titiler.ressources.enums import ImageType
from titiler.api.utils import info as cogInfo


_info = partial(run_in_threadpool, cogInfo)
_bounds = partial(run_in_threadpool, cogeo.bounds)
_metadata = partial(run_in_threadpool, cogeo.metadata)
_spatial_info = partial(run_in_threadpool, cogeo.spatial_info)
Expand Down Expand Up @@ -96,6 +97,16 @@ async def bounds(
return await _bounds(url)


@router.get("/info", responses={200: {"description": "Return basic info on COG."}})
async def info(
response: Response,
url: str = Query(..., description="Cloud Optimized GeoTIFF URL."),
):
"""Handle /info requests."""
response.headers["Cache-Control"] = "max-age=3600"
return await _info(url)


@router.get(
"/metadata", responses={200: {"description": "Return the metadata of the COG."}}
)
Expand Down
Loading

0 comments on commit 89d8d64

Please sign in to comment.