Skip to content

Commit

Permalink
Build and deploy with SAM
Browse files Browse the repository at this point in the history
  • Loading branch information
mojodna committed Dec 16, 2018
1 parent 57bb59d commit e16d539
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 53 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -17,4 +17,5 @@ out.zip
package-lock.json
.venv/
.vscode/
.aws-sam/
.aws-sam/
packaged.yaml
14 changes: 14 additions & 0 deletions Makefile
@@ -1,4 +1,18 @@
PATH := node_modules/.bin:$(PATH)
STACK_NAME ?= "marblecutter-virtual"

deploy: packaged.yaml
sam deploy \
--template-file $< \
--stack-name $(STACK_NAME) \
--capabilities CAPABILITY_IAM \
--parameter-overrides DomainName=$(DOMAIN_NAME)

packaged.yaml: .aws-sam/build/template.yaml
sam package --s3-bucket $(S3_BUCKET) --output-template-file $@

.aws-sam/build/template.yaml: template.yaml requirements.txt virtual/*.py
sam build --use-container

deploy-apex: project.json deps/deps.tgz
apex deploy -l debug -E environment.json
Expand Down
51 changes: 38 additions & 13 deletions README.md
Expand Up @@ -136,26 +136,51 @@ See tile parameters.

`http://localhost:8000/preview?url=https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fplanet-disaster-data%2Fhurricane-harvey%2FSkySat_Freeport_s03_20170831T162740Z3.tif`

## Deploying to AWS Lambda
## Deploying to AWS

tk
marblecutter-virtual is deployed using the [AWS Serverless Application Model
(SAM)](https://github.com/awslabs/serverless-application-model).

Once you have the [SAM CLI](https://github.com/awslabs/aws-sam-cli) installed, you can build with:

```bash
sam build --use-container
```

You can then test it locally as though it's running on Lambda + API Gateway
(it will be _really_ slow, as function invocations are not re-used in the
same way as on Lambda proper):

```bash
sam local start-api
```

To deploy, first package the application:

```bash
sam package --s3-bucket <staging-bucket> --output-template-file packaged.yaml
```

Once staged, it can be deployed:

```bash
make deploy-up
sam deploy \
--template-file packaged.yaml \
--stack-name marblecutter-virtual \
--capabilities CAPABILITY_IAM \
--parameter-overrides DomainName=<hostname>
```

or
These commands are wrapped as a `deploy` target, so this can be done more
simply with:

```bash
make deploy-apex
S3_BUCKET=<staging-bucket> DOMAIN_NAME=<hostname> make deploy
```

NOTE: when setting up a Cloudfront distribution in front of a regional API
Gateway endpoint, ensure that `Origin Protocol Policy` is `HTTPS Only` (API
Gateway doesn't support HTTP) and add an `Origin Custom Header`:
`X-Forwarded-Host` should be the hostname used for your Cloudfront distribution
(otherwise auto-generated tile URLs will use the API Gateway domain; CF sends a
`Host` header corresponding to the origin, not the CDN endpoint).

NOTE: reading `s3://` URLs from Lambda requires that the IAM role created by Up
be granted S3 access (not to specific buckets, just generally).
Gateway endpoint (which is what this process does), an `Origin Custom Header`
will be added: `X-Forwarded-Host` should be the hostname used for your
Cloudfront distribution (otherwise auto-generated tile URLs will use the API
Gateway domain; CF sends a `Host` header corresponding to the origin, not the
CDN endpoint).
1 change: 1 addition & 0 deletions requirements.txt
Expand Up @@ -3,3 +3,4 @@ cachetools ~= 2.0.0
https://github.com/mojodna/marblecutter/archive/ee1ce6f.tar.gz#egg=marblecutter[web]
rasterio[s3] ~= 1.0
numpy
serverless-wsgi
3 changes: 3 additions & 0 deletions sample.env
@@ -1,3 +1,6 @@
AWS_ACCESS_KEY_ID=<redacted>
AWS_SECRET_ACCESS_KEY=<redacted>
AWS_REGION=us-east-1
S3_BUCKET=<redacted>
STACK_NAME=marblecutter-virtual
DOMAIN_NAME=<redacted>
116 changes: 116 additions & 0 deletions template.yaml
@@ -0,0 +1,116 @@
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
marblecutter-virtual
SAM Template for marblecutter-virtual
Parameters:
DomainName:
Type: String
Description: Endpoint name
AcmCertificateArn:
Type: String
Description: ACM Certificate ARN (must have been created in us-east-1)
Default: ""

Globals:
Api:
# API Gateway regional endpoints
EndpointConfiguration: REGIONAL

# enable CORS; to make more specific, change the origin wildcard
# to a particular domain name, e.g. "'www.example.com'"
Cors:
AllowMethods: "'*'"
AllowHeaders: "'*'"
AllowOrigin: "'*'"

# Send/receive binary data through the APIs
BinaryMediaTypes:
# This is equivalent to */* when deployed
- "*~1*"

Resources:
# Lambda function
MarblecutterVirtualFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .
Policies: AmazonS3ReadOnlyAccess
Handler: virtual.lambda.handle
Runtime: python3.6
Environment:
Variables:
AWS_REQUEST_PAYER: requester
CPL_TMPDIR: /tmp
GDAL_CACHEMAX: 75%
GDAL_DISABLE_READDIR_ON_OPEN: TRUE
GDAL_HTTP_MERGE_CONSECUTIVE_RANGES: YES
# requires nghttp2 support
# GDAL_HTTP_VERSION: 2
VSI_CACHE: TRUE
VSI_CACHE_SIZE: 500000000
API_GATEWAY_BASE_PATH: Prod
Timeout: 15
MemorySize: 1536
Events:
# API Gateway routes
ProxyApiRoot:
Type: Api
Properties:
Path: /
Method: ANY
ProxyApiGreedy:
Type: Api
Properties:
Path: /{proxy+}
Method: ANY

# CloudFront Distribution
CFDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Aliases:
- !Ref DomainName
Enabled: true
Comment: marblecutter-virtual
IPV6Enabled: true
Origins:
-
Id: MarblecutterVirtualApi
DomainName: !Sub "${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com"
CustomOriginConfig:
OriginProtocolPolicy: https-only
OriginCustomHeaders:
- HeaderName: X-Forwarded-Host
HeaderValue: !Ref DomainName
OriginPath: !Sub "/${ServerlessRestApi.Stage}"
DefaultCacheBehavior:
TargetOriginId: MarblecutterVirtualApi
ForwardedValues:
QueryString: true
Cookies:
Forward: none
ViewerProtocolPolicy: allow-all
# ViewerCertificate:
# AcmCertificateArn: !Ref AcmCertificateArn
# SslSupportMethod: sni-only

Outputs:
MarblecutterVirtualApi:
Description: "API Gateway endpoint URL for for marblecutter-virtual"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/${ServerlessRestApi.Stage}"

MarblecutterVirtualFunction:
Description: "marblecutter-virtual Lambda Function ARN"
Value: !GetAtt MarblecutterVirtualFunction.Arn

MarblecutterVirtualFunctionIamRole:
Description: "Implicit IAM Role created for marblecutter-virtual"
Value: !GetAtt MarblecutterVirtualFunctionRole.Arn

CFDistribution:
Description: Cloudfront Distribution Domain Name
Value: !GetAtt CFDistribution.DomainName
17 changes: 17 additions & 0 deletions virtual/lambda.py
@@ -0,0 +1,17 @@
# coding=utf-8
import logging
import os

from virtual.web import app
import serverless_wsgi

logging.getLogger("rasterio._base").setLevel(logging.WARNING)


def handle(event, context):
# transfer stage from event["requestContext"] to an X-Stage header
event["headers"]["X-Stage"] = event.get("requestContext", {}).pop("stage", None)
event["headers"]["Host"] = event["headers"].get(
"X-Forwarded-Host", event["headers"].get("Host")
)
return serverless_wsgi.handle_request(app, event, context)
58 changes: 19 additions & 39 deletions virtual/web.py
Expand Up @@ -4,11 +4,11 @@
import logging

from cachetools.func import lru_cache
from flask import Markup, jsonify, render_template, request, url_for
from flask import Markup, jsonify, render_template, request
from marblecutter import NoCatalogAvailable, tiling
from marblecutter.formats.optimal import Optimal
from marblecutter.transformations import Image
from marblecutter.web import app
from marblecutter.web import app, url_for
from mercantile import Tile

try:
Expand Down Expand Up @@ -43,17 +43,8 @@ def make_catalog(args):
raise NoCatalogAvailable()


def make_prefix():
host = request.headers.get("X-Forwarded-Host", request.headers.get("Host", ""))

# sniff for API Gateway
if ".execute-api." in host and ".amazonaws.com" in host:
return request.headers.get("X-Stage")


@app.route("/tiles/")
@app.route("/<prefix>/tiles/")
def meta(prefix=None):
def meta():
catalog = make_catalog(request.args)

meta = {
Expand All @@ -63,55 +54,44 @@ def meta(prefix=None):
"minzoom": catalog.minzoom,
"name": catalog.name,
"tilejson": "2.1.0",
}

with app.app_context():
meta["tiles"] = [
"tiles": [
"{}{{z}}/{{x}}/{{y}}?{}".format(
url_for("meta", prefix=make_prefix(), _external=True, _scheme=""),
urlencode(request.args),
url_for("meta", _external=True, _scheme=""), urlencode(request.args)
)
]
],
}

return jsonify(meta)


@app.route("/bounds/")
@app.route("/<prefix>/bounds/")
def bounds(prefix=None):
def bounds():
catalog = make_catalog(request.args)

return jsonify({"url": catalog.uri, "bounds": catalog.bounds})


@app.route("/preview")
@app.route("/<prefix>/preview")
def preview(prefix=None):
def preview():
# initialize the catalog so this route will fail if the source doesn't exist
make_catalog(request.args)

with app.app_context():
return render_template(
"preview.html",
tilejson_url=Markup(
url_for(
"meta",
prefix=make_prefix(),
_external=True,
_scheme="",
**request.args
)
return (
render_template(
"preview.html",
tilejson_url=Markup(
url_for("meta", _external=True, _scheme="", **request.args)
),
),
), 200, {
"Content-Type": "text/html"
}
200,
{"Content-Type": "text/html"},
)


@app.route("/tiles/<int:z>/<int:x>/<int:y>")
@app.route("/tiles/<int:z>/<int:x>/<int:y>@<int:scale>x")
@app.route("/<prefix>/tiles/<int:z>/<int:x>/<int:y>")
@app.route("/<prefix>/tiles/<int:z>/<int:x>/<int:y>@<int:scale>x")
def render_png(z, x, y, scale=1, prefix=None):
def render_png(z, x, y, scale=1):
catalog = make_catalog(request.args)
tile = Tile(x, y, z)

Expand Down

0 comments on commit e16d539

Please sign in to comment.