├── terraform ├── wms_api │ ├── cog_renderer │ │ ├── .gitignore │ │ ├── src │ │ │ ├── styles │ │ │ │ ├── aerosols.csv │ │ │ │ └── cloud_moisture.csv │ │ │ └── main.py │ │ ├── iam.tf │ │ └── main.tf │ ├── list_layers │ │ ├── .gitignore │ │ ├── iam.tf │ │ ├── main.tf │ │ └── src │ │ │ └── main.py │ ├── main.tf │ ├── api.tf │ ├── iam.tf │ └── api.yaml ├── netcdf_datasource │ ├── process_netcdf │ │ ├── .gitignore │ │ ├── src │ │ │ ├── main.py │ │ │ └── netcdf_to_geotiff.py │ │ ├── main.tf │ │ └── iam.tf │ ├── filter_subscription │ │ ├── .gitignore │ │ ├── src │ │ │ └── main.py │ │ ├── iam.tf │ │ └── main.tf │ ├── sns.tf │ └── main.tf ├── .gitignore ├── variables.tf ├── s3 │ └── main.tf ├── main.tf └── webui │ └── index.html ├── .flake8 └── README.md /terraform/wms_api/cog_renderer/.gitignore: -------------------------------------------------------------------------------- 1 | src.zip 2 | -------------------------------------------------------------------------------- /terraform/wms_api/list_layers/.gitignore: -------------------------------------------------------------------------------- 1 | src.zip 2 | -------------------------------------------------------------------------------- /terraform/netcdf_datasource/process_netcdf/.gitignore: -------------------------------------------------------------------------------- 1 | src.zip 2 | -------------------------------------------------------------------------------- /terraform/netcdf_datasource/filter_subscription/.gitignore: -------------------------------------------------------------------------------- 1 | src.zip 2 | -------------------------------------------------------------------------------- /terraform/.gitignore: -------------------------------------------------------------------------------- 1 | .terraform/ 2 | terraform.tfvars 3 | terraform.tfstate.backup 4 | -------------------------------------------------------------------------------- /terraform/wms_api/cog_renderer/src/styles/aerosols.csv: -------------------------------------------------------------------------------- 1 | nv,0,0,0,0 2 | 0,0,0,0,0 3 | 0.25,100,100,100,100 4 | 0.5,175,175,175,255 5 | 0.75,200,200,200,255 6 | 1.10,255,255,255,255 7 | -------------------------------------------------------------------------------- /terraform/wms_api/cog_renderer/src/styles/cloud_moisture.csv: -------------------------------------------------------------------------------- 1 | nv,0,0,0,0 2 | 300,0,0,0,0 3 | 240,175,175,175,100 4 | 230,255,255,255,200 5 | 210,31,144,184,255 6 | 180,0,0,255,255 7 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | docstring-convention = google 4 | docstring-quotes = " 5 | inline-quotes = " 6 | enable-extensions = G, SC, M 7 | ignore = D212, D300, 8 | import-order-style = smarkets 9 | -------------------------------------------------------------------------------- /terraform/netcdf_datasource/sns.tf: -------------------------------------------------------------------------------- 1 | resource "aws_sns_topic_subscription" "new_goes_object" { 2 | topic_arn = var.sns_topic_arn 3 | protocol = "lambda" 4 | endpoint = module.filter_subscription.lambda_arn 5 | } 6 | -------------------------------------------------------------------------------- /terraform/wms_api/main.tf: -------------------------------------------------------------------------------- 1 | /* 2 | * The purpose of this module is to implement a WMS API 3 | */ 4 | 5 | # Name and Project Tags 6 | variable "name" {} 7 | variable "project" {} 8 | 9 | # Parameters 10 | variable "region" {} 11 | variable "source_bucket" {} 12 | 13 | # Lambda functions 14 | module "cog_renderer" { 15 | source = "./cog_renderer" 16 | name = "${var.name}_cog_renderer" 17 | project = var.project 18 | source_bucket = var.source_bucket 19 | api_gateway_arn = aws_api_gateway_rest_api.tile.execution_arn 20 | } 21 | 22 | module "list_layers" { 23 | source = "./list_layers" 24 | name = "${var.name}_list_layers" 25 | project = var.project 26 | source_bucket = var.source_bucket 27 | api_gateway_arn = aws_api_gateway_rest_api.tile.execution_arn 28 | } 29 | -------------------------------------------------------------------------------- /terraform/variables.tf: -------------------------------------------------------------------------------- 1 | # AWS Credentials 2 | variable "access_key" { 3 | type = string 4 | description = "AWS Access Key" 5 | } 6 | variable "secret_key" { 7 | type = string 8 | description = "AWS Secret Key" 9 | } 10 | variable "aws_region" { 11 | type = string 12 | description = "AWS Region" 13 | default = "us-east-1" 14 | } 15 | 16 | # Name and Project Tags 17 | variable "name" { 18 | type = string 19 | description = "Name prefix used by all resources" 20 | } 21 | variable "project" { 22 | type = string 23 | description = "A 'project' tag is added to all resources to track cost" 24 | } 25 | 26 | # Main S3 Bucket name 27 | variable "s3_bucket_name" { 28 | type = string 29 | description = "Name for the S3 bucket where data is stored. S3 bucket names must be unique" 30 | } 31 | -------------------------------------------------------------------------------- /terraform/wms_api/api.tf: -------------------------------------------------------------------------------- 1 | # Tiling API 2 | 3 | resource "aws_api_gateway_rest_api" "tile" { 4 | name = var.name 5 | body = data.template_file.openapi.rendered 6 | } 7 | 8 | # Open API definition 9 | data "template_file" "openapi" { 10 | template = file("${path.module}/api.yaml") 11 | 12 | vars = { 13 | name = var.name 14 | cog_renderer = module.cog_renderer.lambda_arn 15 | list_layers = module.list_layers.lambda_arn 16 | region = var.region 17 | credentials = aws_iam_role.api_gw.arn 18 | } 19 | } 20 | 21 | # API Deployment 22 | resource "aws_api_gateway_deployment" "demo" { 23 | rest_api_id = aws_api_gateway_rest_api.tile.id 24 | stage_name = "demo" 25 | 26 | # Set the description to a hash of the OpenAPI file to force updates on changes 27 | stage_description = md5(file("${path.module}/api.yaml")) 28 | } 29 | -------------------------------------------------------------------------------- /terraform/s3/main.tf: -------------------------------------------------------------------------------- 1 | /* 2 | * The purpose of this module is to manage all S3 resources 3 | */ 4 | 5 | # Name and Project Tags 6 | variable "name" {} 7 | variable "project" {} 8 | 9 | # S3 Bucket Name 10 | variable "s3_bucket_name" {} 11 | 12 | # Main S3 bucket 13 | resource "aws_s3_bucket" "data" { 14 | bucket = var.s3_bucket_name 15 | acl = "private" 16 | tags = { 17 | Name = var.name 18 | Project = var.project 19 | } 20 | 21 | # Apply default encryption for all objects 22 | server_side_encryption_configuration { 23 | rule { 24 | apply_server_side_encryption_by_default { 25 | sse_algorithm = "AES256" 26 | } 27 | } 28 | } 29 | 30 | # Expire items older than 1 day 31 | lifecycle_rule { 32 | id = "expire" 33 | enabled = true 34 | 35 | expiration { 36 | days = 1 37 | } 38 | } 39 | } 40 | 41 | # block all public access 42 | resource "aws_s3_bucket_public_access_block" "data" { 43 | bucket = aws_s3_bucket.data.id 44 | 45 | block_public_acls = true 46 | block_public_policy = true 47 | ignore_public_acls = true 48 | restrict_public_buckets = true 49 | } 50 | -------------------------------------------------------------------------------- /terraform/netcdf_datasource/process_netcdf/src/main.py: -------------------------------------------------------------------------------- 1 | """Ingest a new NetCDF File.""" 2 | 3 | # System Imports 4 | import logging 5 | import os 6 | 7 | # External Imports 8 | import boto3 9 | import netcdf_to_geotiff 10 | 11 | # Set environment variables for GDAL and Proj 12 | os.environ["PROJ_LIB"] = "/opt/share/proj" 13 | os.environ["GDAL_DATA"] = "/opt/share/gdal" 14 | 15 | # Configure Logging 16 | logger = logging.getLogger() 17 | logger.setLevel(logging.INFO) 18 | 19 | # Get S3 Client 20 | client = boto3.client("s3") 21 | 22 | # Get environment variables 23 | SOURCE_BUCKET = os.environ["source_bucket"] 24 | DEST_BUCKET = os.environ["dest_bucket"] 25 | 26 | 27 | def handler(event, context): 28 | """AWS Lambda handler.""" 29 | logger.info(f"Processing File: {event['key']}") 30 | 31 | data = netcdf_to_geotiff.convert( 32 | SOURCE_BUCKET, 33 | event["key"], 34 | event["definition"]["parameter_name"], 35 | event["definition"]["band"], 36 | client, 37 | ) 38 | 39 | # Upload to S3 40 | client.upload_file(data["local_file"], DEST_BUCKET, data["key"]) 41 | 42 | # Cleanup 43 | os.remove(data["local_file"]) 44 | -------------------------------------------------------------------------------- /terraform/wms_api/iam.tf: -------------------------------------------------------------------------------- 1 | /* 2 | * IAM Role assumed by API Gateway 3 | */ 4 | 5 | # Role 6 | resource "aws_iam_role" "api_gw" { 7 | name = "${var.name}_api_gw" 8 | path = "/" 9 | assume_role_policy = data.aws_iam_policy_document.api_gw.json 10 | 11 | tags = { 12 | Name = "${var.name}_api_gw" 13 | Project = var.project 14 | } 15 | } 16 | 17 | # Service using role 18 | data "aws_iam_policy_document" "api_gw" { 19 | statement { 20 | effect = "Allow" 21 | actions = ["sts:AssumeRole"] 22 | principals { 23 | type = "Service" 24 | identifiers = ["apigateway.amazonaws.com"] 25 | } 26 | } 27 | } 28 | 29 | # In-line policy attachment 30 | resource "aws_iam_role_policy" "api_gw" { 31 | name = "${var.name}_api_gw" 32 | role = aws_iam_role.api_gw.id 33 | policy = data.aws_iam_policy_document.lambda_invoke.json 34 | } 35 | 36 | # In-line policy definition 37 | data "aws_iam_policy_document" "lambda_invoke" { 38 | statement { 39 | sid = "InvokeLambda" 40 | effect = "Allow" 41 | actions = [ 42 | "lambda:InvokeFunction" 43 | ] 44 | resources = [ 45 | module.cog_renderer.lambda_arn, 46 | module.list_layers.lambda_arn 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | # Specify the provider and access details 2 | provider "aws" { 3 | access_key = var.access_key 4 | secret_key = var.secret_key 5 | region = var.aws_region 6 | } 7 | 8 | # Configure terraform to store its state on S3 9 | terraform { 10 | backend "s3" {} 11 | } 12 | 13 | # Modules 14 | module "geos16" { 15 | source = "./netcdf_datasource" 16 | name = "${var.name}_goes16" 17 | project = var.project 18 | sns_topic_arn = "arn:aws:sns:us-east-1:123901341784:NewGOES16Object" 19 | source_bucket = "noaa-goes16" 20 | dest_bucket = var.s3_bucket_name 21 | data_definitions = [{ 22 | filter_regex = "ABI-L2-CMIPF\\/[0-9]{4}\\/[0-9]{3}/[0-9]{2}/OR_ABI-L2-CMIPF-M6C09.*.nc" 23 | parameter_name = "CMI" 24 | band = 1 25 | }, { 26 | filter_regex = "ABI-L2-CMIPC\\/[0-9]{4}\\/[0-9]{3}/[0-9]{2}/OR_ABI-L2-CMIPC-M6C09.*.nc" 27 | parameter_name = "CMI" 28 | band = 1 29 | }] 30 | } 31 | 32 | module "s3" { 33 | source = "./s3" 34 | name = var.name 35 | project = var.project 36 | s3_bucket_name = var.s3_bucket_name 37 | } 38 | 39 | module "wms_api" { 40 | source = "./wms_api" 41 | name = var.name 42 | project = var.project 43 | source_bucket = var.s3_bucket_name 44 | region = var.aws_region 45 | } 46 | -------------------------------------------------------------------------------- /terraform/wms_api/list_layers/iam.tf: -------------------------------------------------------------------------------- 1 | /* 2 | * IAM Role used by this lambda function 3 | */ 4 | 5 | # Role 6 | resource "aws_iam_role" "lambda" { 7 | name = "${var.name}_lambda" 8 | path = "/" 9 | assume_role_policy = data.aws_iam_policy_document.lambda.json 10 | 11 | tags = { 12 | Name = "${var.name}_lambda" 13 | Project = var.project 14 | } 15 | } 16 | 17 | # Service using role 18 | data "aws_iam_policy_document" "lambda" { 19 | statement { 20 | effect = "Allow" 21 | actions = ["sts:AssumeRole"] 22 | principals { 23 | type = "Service" 24 | identifiers = ["lambda.amazonaws.com"] 25 | } 26 | } 27 | } 28 | 29 | # In-line policy attachment 30 | resource "aws_iam_role_policy" "s3" { 31 | name = "${var.name}_s3" 32 | role = aws_iam_role.lambda.id 33 | policy = data.aws_iam_policy_document.s3.json 34 | } 35 | 36 | # In-line policy definition 37 | data "aws_iam_policy_document" "s3" { 38 | statement { 39 | sid = "S3ListBucket" 40 | effect = "Allow" 41 | actions = [ 42 | "s3:ListBucket" 43 | ] 44 | resources = ["arn:aws:s3:::${var.source_bucket}"] 45 | } 46 | } 47 | 48 | # AWS CloudWatch policy attachment 49 | resource "aws_iam_role_policy_attachment" "cloudwatch" { 50 | role = aws_iam_role.lambda.id 51 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 52 | } 53 | -------------------------------------------------------------------------------- /terraform/netcdf_datasource/filter_subscription/src/main.py: -------------------------------------------------------------------------------- 1 | """Parse SNS event notification.""" 2 | 3 | # System Imports 4 | import json 5 | import logging 6 | import os 7 | import re 8 | 9 | # External Imports 10 | import boto3 11 | 12 | # Configure Logging 13 | logger = logging.getLogger() 14 | logger.setLevel(logging.INFO) 15 | 16 | # Get environment variables 17 | PROCESSING_FUNCTION = os.environ["processing_function"] 18 | DATA_DEFINITIONS = json.loads(os.environ["data_definitions"]) 19 | 20 | # Get Lambda Client 21 | client = boto3.client("lambda") 22 | 23 | 24 | def handler(event, context): 25 | """AWS Lambda handler.""" 26 | sns_record = event["Records"][0]["Sns"] 27 | 28 | # Pull the fine name 29 | goes16_event = json.loads(sns_record["Message"]) 30 | file_name = goes16_event["Records"][0]["s3"]["object"]["key"] 31 | logger.info(file_name) 32 | 33 | for definition in DATA_DEFINITIONS: 34 | file_pattern = re.compile(definition["filter_regex"]) 35 | if file_pattern.fullmatch(file_name): 36 | logger.info(f"Processing: {file_name}") 37 | client = boto3.client("lambda") 38 | client.invoke( 39 | FunctionName=PROCESSING_FUNCTION, 40 | InvocationType="Event", 41 | Payload=json.dumps({"key": file_name, "definition": definition}), 42 | ) 43 | 44 | return 45 | -------------------------------------------------------------------------------- /terraform/netcdf_datasource/filter_subscription/iam.tf: -------------------------------------------------------------------------------- 1 | /* 2 | * IAM Role used by this lambda function 3 | */ 4 | 5 | # Role 6 | resource "aws_iam_role" "lambda" { 7 | name = "${var.name}_lambda" 8 | path = "/" 9 | assume_role_policy = data.aws_iam_policy_document.lambda.json 10 | 11 | tags = { 12 | Name = "${var.name}_lambda" 13 | Project = var.project 14 | } 15 | } 16 | 17 | # Service using role 18 | data "aws_iam_policy_document" "lambda" { 19 | statement { 20 | effect = "Allow" 21 | actions = ["sts:AssumeRole"] 22 | principals { 23 | type = "Service" 24 | identifiers = ["lambda.amazonaws.com"] 25 | } 26 | } 27 | } 28 | 29 | # In-line policy attachment 30 | resource "aws_iam_role_policy" "lambda" { 31 | name = "${var.name}_lambda" 32 | role = aws_iam_role.lambda.id 33 | policy = data.aws_iam_policy_document.lambda_invoke.json 34 | } 35 | 36 | # In-line policy definition 37 | data "aws_iam_policy_document" "lambda_invoke" { 38 | statement { 39 | sid = "InvokeLambda" 40 | effect = "Allow" 41 | actions = [ 42 | "lambda:InvokeFunction" 43 | ] 44 | resources = [var.processing_function] 45 | } 46 | } 47 | 48 | # AWS CloudWatch policy attachment 49 | resource "aws_iam_role_policy_attachment" "cloudwatch" { 50 | role = aws_iam_role.lambda.id 51 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 52 | } 53 | -------------------------------------------------------------------------------- /terraform/webui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | OpenLayers example 13 | 14 | 15 |
16 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /terraform/netcdf_datasource/process_netcdf/main.tf: -------------------------------------------------------------------------------- 1 | /* 2 | * Lambda function that converts a NetCDF file to a COG 3 | */ 4 | 5 | # Name and Project Tags 6 | variable "name" {type = string} 7 | variable "project" {type = string} 8 | 9 | # Parameters 10 | variable "source_bucket" {type = string} 11 | variable "dest_bucket" {type = string} 12 | 13 | # Archive Source Code 14 | data "archive_file" "process_netcdf" { 15 | type = "zip" 16 | source_dir = "${path.module}/src/" 17 | output_path = "${path.module}/src.zip" 18 | } 19 | 20 | resource "aws_lambda_function" "process_netcdf" { 21 | filename = "${path.module}/src.zip" 22 | source_code_hash = data.archive_file.process_netcdf.output_base64sha256 23 | layers = [ 24 | "arn:aws:lambda:us-east-1:552188055668:layer:geolambda-python:3", 25 | "arn:aws:lambda:us-east-1:552188055668:layer:geolambda:4" 26 | ] 27 | function_name = var.name 28 | description = "Convert a NetCDF file to a COG" 29 | role = aws_iam_role.lambda.arn 30 | handler = "main.handler" 31 | runtime = "python3.7" 32 | timeout = 900 33 | memory_size = 384 34 | 35 | environment { 36 | variables = { 37 | source_bucket = var.source_bucket 38 | dest_bucket = var.dest_bucket 39 | } 40 | } 41 | 42 | tags = { 43 | Name = var.name 44 | Project = var.project 45 | } 46 | } 47 | 48 | # Return Lambda ARN 49 | output "lambda_arn" { 50 | value = aws_lambda_function.process_netcdf.arn 51 | } 52 | -------------------------------------------------------------------------------- /terraform/netcdf_datasource/main.tf: -------------------------------------------------------------------------------- 1 | /* 2 | * The purpose of this module is to define a goes16 datasource 3 | */ 4 | 5 | # Name and Project Tags 6 | variable "name" {type = string} 7 | variable "project" {type = string} 8 | 9 | # Parameters 10 | variable "sns_topic_arn" { 11 | type = string 12 | default = "arn:aws:sns:us-east-1:123901341784:NewGOES16Object" 13 | } 14 | variable "source_bucket" { 15 | type = string 16 | default = "noaa-goes16" 17 | } 18 | variable "dest_bucket" {type = string} 19 | variable "data_definitions" { 20 | type = list(object({ 21 | filter_regex = string 22 | parameter_name = string 23 | band = number 24 | })) 25 | default = [ 26 | { 27 | filter_regex = "ABI-L2-CMIPF\\/[0-9]{4}\\/[0-9]{3}/[0-9]{2}/OR_ABI-L2-CMIPF-M6C09.*.nc" 28 | parameter_name = "CMI" 29 | band = 1 30 | } 31 | ] 32 | } 33 | 34 | # Lambda functions 35 | module "filter_subscription" { 36 | source = "./filter_subscription" 37 | name = "${var.name}_filter_subscription" 38 | project = var.project 39 | sns_topic_arn = var.sns_topic_arn 40 | processing_function = module.process_netcdf.lambda_arn 41 | data_definitions = var.data_definitions 42 | } 43 | 44 | module "process_netcdf" { 45 | source = "./process_netcdf" 46 | name = "${var.name}_process_netcdf" 47 | project = var.project 48 | source_bucket = var.source_bucket 49 | dest_bucket = var.dest_bucket 50 | } 51 | -------------------------------------------------------------------------------- /terraform/wms_api/cog_renderer/iam.tf: -------------------------------------------------------------------------------- 1 | /* 2 | * IAM Role used by this lambda function 3 | */ 4 | 5 | # Role 6 | resource "aws_iam_role" "lambda" { 7 | name = "${var.name}_lambda" 8 | path = "/" 9 | assume_role_policy = data.aws_iam_policy_document.lambda.json 10 | 11 | tags = { 12 | Name = "${var.name}_lambda" 13 | Project = var.project 14 | } 15 | } 16 | 17 | # Service using role 18 | data "aws_iam_policy_document" "lambda" { 19 | statement { 20 | effect = "Allow" 21 | actions = ["sts:AssumeRole"] 22 | principals { 23 | type = "Service" 24 | identifiers = ["lambda.amazonaws.com"] 25 | } 26 | } 27 | } 28 | 29 | # In-line policy attachment 30 | resource "aws_iam_role_policy" "s3" { 31 | name = "${var.name}_s3" 32 | role = aws_iam_role.lambda.id 33 | policy = data.aws_iam_policy_document.s3.json 34 | } 35 | 36 | # In-line policy definition 37 | data "aws_iam_policy_document" "s3" { 38 | statement { 39 | sid = "S3ListBucket" 40 | effect = "Allow" 41 | actions = [ 42 | "s3:ListBucket" 43 | ] 44 | resources = ["arn:aws:s3:::${var.source_bucket}"] 45 | } 46 | 47 | statement { 48 | sid = "S3GetObject" 49 | effect = "Allow" 50 | actions = [ 51 | "s3:GetObject" 52 | ] 53 | resources = ["arn:aws:s3:::${var.source_bucket}/*"] 54 | } 55 | } 56 | 57 | # AWS CloudWatch policy attachment 58 | resource "aws_iam_role_policy_attachment" "cloudwatch" { 59 | role = aws_iam_role.lambda.id 60 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 61 | } 62 | -------------------------------------------------------------------------------- /terraform/wms_api/list_layers/main.tf: -------------------------------------------------------------------------------- 1 | /* 2 | * Lambda function that lists available layers 3 | */ 4 | 5 | # Name and Project Tags 6 | variable "name" {} 7 | variable "project" {} 8 | 9 | # Parameters 10 | variable "source_bucket" {} 11 | variable "api_gateway_arn" {} 12 | 13 | # Archive Source Code 14 | data "archive_file" "list_layers" { 15 | type = "zip" 16 | source_dir = "${path.module}/src/" 17 | output_path = "${path.module}/src.zip" 18 | } 19 | 20 | resource "aws_lambda_function" "list_layers" { 21 | filename = "${path.module}/src.zip" 22 | source_code_hash = data.archive_file.list_layers.output_base64sha256 23 | 24 | function_name = var.name 25 | description = "Lists layers in an S3 Bucket" 26 | role = aws_iam_role.lambda.arn 27 | handler = "main.handler" 28 | runtime = "python3.7" 29 | timeout = 300 30 | memory_size = 128 31 | 32 | environment { 33 | variables = { 34 | source_bucket = var.source_bucket 35 | } 36 | } 37 | 38 | tags = { 39 | Name = var.name 40 | Project = var.project 41 | } 42 | } 43 | 44 | # Allow API Gateway to invoke this lambda function 45 | resource "aws_lambda_permission" "allow_API_GW" { 46 | statement_id = "AllowExecutionFromAPIGW" 47 | action = "lambda:InvokeFunction" 48 | function_name = aws_lambda_function.list_layers.function_name 49 | principal = "apigateway.amazonaws.com" 50 | source_arn = "${var.api_gateway_arn}/*/GET/layers" 51 | } 52 | 53 | # Return Lambda ARN 54 | output "lambda_arn" { 55 | value = aws_lambda_function.list_layers.arn 56 | } 57 | -------------------------------------------------------------------------------- /terraform/netcdf_datasource/process_netcdf/iam.tf: -------------------------------------------------------------------------------- 1 | /* 2 | * IAM Role used by this lambda function 3 | */ 4 | 5 | # Role 6 | resource "aws_iam_role" "lambda" { 7 | name = "${var.name}_lambda" 8 | path = "/" 9 | assume_role_policy = data.aws_iam_policy_document.lambda.json 10 | 11 | tags = { 12 | Name = "${var.name}_lambda" 13 | Project = var.project 14 | } 15 | } 16 | 17 | # Service using role 18 | data "aws_iam_policy_document" "lambda" { 19 | statement { 20 | effect = "Allow" 21 | actions = ["sts:AssumeRole"] 22 | principals { 23 | type = "Service" 24 | identifiers = ["lambda.amazonaws.com"] 25 | } 26 | } 27 | } 28 | 29 | # In-line policy attachments 30 | resource "aws_iam_role_policy" "s3" { 31 | name = "${var.name}_s3" 32 | role = aws_iam_role.lambda.id 33 | policy = data.aws_iam_policy_document.s3.json 34 | } 35 | 36 | # In-line policy definition 37 | data "aws_iam_policy_document" "s3" { 38 | statement { 39 | sid = "S3ListBucket" 40 | effect = "Allow" 41 | actions = [ 42 | "s3:ListBucket" 43 | ] 44 | resources = ["arn:aws:s3:::${var.source_bucket}"] 45 | } 46 | 47 | statement { 48 | sid = "S3GetObject" 49 | effect = "Allow" 50 | actions = [ 51 | "s3:GetObject" 52 | ] 53 | resources = ["arn:aws:s3:::${var.source_bucket}/*"] 54 | } 55 | 56 | statement { 57 | sid = "S3PutObject" 58 | effect = "Allow" 59 | actions = [ 60 | "s3:PutObject" 61 | ] 62 | resources = ["arn:aws:s3:::${var.dest_bucket}/*"] 63 | } 64 | } 65 | 66 | # AWS CloudWatch policy attachment 67 | resource "aws_iam_role_policy_attachment" "cloudwatch" { 68 | role = aws_iam_role.lambda.id 69 | policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 70 | } 71 | -------------------------------------------------------------------------------- /terraform/wms_api/cog_renderer/main.tf: -------------------------------------------------------------------------------- 1 | /* 2 | * Lambda function that converts a COG to an image 3 | */ 4 | 5 | # Name and Project Tags 6 | variable "name" {} 7 | variable "project" {} 8 | 9 | # Parameters 10 | variable "source_bucket" {} 11 | variable "api_gateway_arn" {} 12 | 13 | # Archive Source Code 14 | data "archive_file" "cog_renderer" { 15 | type = "zip" 16 | source_dir = "${path.module}/src/" 17 | output_path = "${path.module}/src.zip" 18 | } 19 | 20 | resource "aws_lambda_function" "cog_renderer" { 21 | filename = "${path.module}/src.zip" 22 | source_code_hash = data.archive_file.cog_renderer.output_base64sha256 23 | layers = [ 24 | "arn:aws:lambda:us-east-1:710449791192:layer:geolambda-python:3", 25 | "arn:aws:lambda:us-east-1:710449791192:layer:geolambda:9" 26 | ] 27 | function_name = var.name 28 | description = "Convert a COG to an image" 29 | role = aws_iam_role.lambda.arn 30 | handler = "main.handler" 31 | runtime = "python3.8" 32 | timeout = 300 33 | memory_size = 128 34 | 35 | environment { 36 | variables = { 37 | source_bucket = var.source_bucket 38 | } 39 | } 40 | 41 | tags = { 42 | Name = var.name 43 | Project = var.project 44 | } 45 | } 46 | 47 | # Allow API Gateway to invoke this lambda function 48 | resource "aws_lambda_permission" "allow_API_GW" { 49 | statement_id = "AllowExecutionFromAPIGW" 50 | action = "lambda:InvokeFunction" 51 | function_name = aws_lambda_function.cog_renderer.function_name 52 | principal = "apigateway.amazonaws.com" 53 | source_arn = "${var.api_gateway_arn}/*/GET/wms" 54 | } 55 | 56 | # Return Lambda ARN 57 | output "lambda_arn" { 58 | value = aws_lambda_function.cog_renderer.arn 59 | } 60 | -------------------------------------------------------------------------------- /terraform/netcdf_datasource/filter_subscription/main.tf: -------------------------------------------------------------------------------- 1 | /* 2 | * Lambda function filters an SNS subscription 3 | */ 4 | 5 | # Name and Project Tags 6 | variable "name" {type = string} 7 | variable "project" {type = string} 8 | 9 | # Parameters 10 | variable "sns_topic_arn" {type = string} 11 | variable "processing_function" {type = string} 12 | variable "data_definitions" { 13 | type = list(object({ 14 | filter_regex = string 15 | parameter_name = string 16 | band = number 17 | })) 18 | } 19 | 20 | # Archive Source Code 21 | data "archive_file" "filter_subscription" { 22 | type = "zip" 23 | source_dir = "${path.module}/src/" 24 | output_path = "${path.module}/src.zip" 25 | } 26 | 27 | resource "aws_lambda_function" "filter_subscription" { 28 | filename = "${path.module}/src.zip" 29 | source_code_hash = data.archive_file.filter_subscription.output_base64sha256 30 | function_name = var.name 31 | description = "Filter the sns topic" 32 | role = aws_iam_role.lambda.arn 33 | handler = "main.handler" 34 | runtime = "python3.7" 35 | timeout = 120 36 | 37 | environment { 38 | variables = { 39 | processing_function = var.processing_function 40 | data_definitions = jsonencode(var.data_definitions) 41 | } 42 | } 43 | 44 | tags = { 45 | Name = var.name 46 | Project = var.project 47 | } 48 | } 49 | 50 | # Allow SNS to invoke this lambda function 51 | resource "aws_lambda_permission" "allow_SNS" { 52 | statement_id = "AllowExecutionFromSNS" 53 | action = "lambda:InvokeFunction" 54 | function_name = aws_lambda_function.filter_subscription.function_name 55 | principal = "sns.amazonaws.com" 56 | source_arn = var.sns_topic_arn 57 | } 58 | 59 | # Return Lambda ARN 60 | output "lambda_arn" { 61 | value = aws_lambda_function.filter_subscription.arn 62 | } 63 | -------------------------------------------------------------------------------- /terraform/wms_api/cog_renderer/src/main.py: -------------------------------------------------------------------------------- 1 | """Convert a COG to png.""" 2 | # System Imports 3 | import base64 4 | import logging 5 | import os 6 | import sys 7 | 8 | # update path to gdal libraries 9 | sys.path.append("/opt/python/osgeo") 10 | 11 | # External Imports 12 | from osgeo import gdal 13 | 14 | # Set environment variables for GDAL and Proj 15 | os.environ["PROJ_LIB"] = "/opt/share/proj" 16 | os.environ["GDAL_DATA"] = "/opt/share/gdal" 17 | 18 | # Configure Logging 19 | logger = logging.getLogger() 20 | logger.setLevel(logging.INFO) 21 | 22 | # Get environment variables 23 | SOURCE_BUCKET = os.environ["source_bucket"] 24 | 25 | 26 | def handler(event, context): 27 | """AWS Lambda handler.""" 28 | layername = event["queryStringParameters"]["LAYERS"] 29 | width = event["queryStringParameters"]["WIDTH"] 30 | height = event["queryStringParameters"]["HEIGHT"] 31 | bounding_box = event["queryStringParameters"]["BBOX"].split(",") 32 | style = event["queryStringParameters"]["STYLES"] 33 | 34 | # Convert to GeoTIFF 35 | gdal_raster = gdal.Translate( 36 | "", 37 | f"/vsis3/{SOURCE_BUCKET}/{layername}", 38 | format="MEM", 39 | width=width, 40 | height=height, 41 | projWin=[bounding_box[0], bounding_box[3], bounding_box[2], bounding_box[1]], 42 | projWinSRS="EPSG:3857", 43 | scaleParams=[], 44 | ) 45 | 46 | # Add color 47 | gdal.DEMProcessing( 48 | os.path.join("/tmp", "file.png"), 49 | gdal_raster, 50 | "color-relief", 51 | addAlpha=True, 52 | colorFilename=os.path.join("styles", f"{style}.csv"), 53 | ) 54 | gdal_raster = None 55 | 56 | with open(os.path.join("/tmp", "file.png"), "rb") as image_file: 57 | encoded_image = base64.b64encode(image_file.read()).decode("utf-8") 58 | 59 | return { 60 | "isBase64Encoded": True, 61 | "statusCode": 200, 62 | "headers": { 63 | "Content-Type": "image/png", 64 | "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,access-control-allow-origin", 65 | "Access-Control-Allow-Methods": "*", 66 | "Access-Control-Allow-Origin": "*", 67 | }, 68 | "body": encoded_image, 69 | } 70 | -------------------------------------------------------------------------------- /terraform/wms_api/list_layers/src/main.py: -------------------------------------------------------------------------------- 1 | """List layers in an S3 Bucket.""" 2 | # System Imports 3 | import json 4 | import logging 5 | import os 6 | 7 | # External Imports 8 | import boto3 9 | 10 | # Configure Logging 11 | logger = logging.getLogger() 12 | logger.setLevel(logging.INFO) 13 | 14 | # Get environment variables 15 | SOURCE_BUCKET = os.environ["source_bucket"] 16 | 17 | # Get S3 Client 18 | client = boto3.client("s3") 19 | 20 | 21 | def handler(event, context): 22 | """AWS Lambda handler.""" 23 | layers = list_bucket(SOURCE_BUCKET, "", continuation_token=None, objects=None) 24 | 25 | return { 26 | "statusCode": 200, 27 | "headers": { 28 | "Content-Type": "application/json", 29 | "Access-Control-Allow-Headers": "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,access-control-allow-origin", 30 | "Access-Control-Allow-Methods": "*", 31 | "Access-Control-Allow-Origin": "*", 32 | }, 33 | "body": json.dumps(layers), 34 | } 35 | 36 | 37 | def list_bucket(bucket, prefix, continuation_token=None, objects=None): 38 | """ 39 | Recursively lists contents on an S3 bucket. 40 | 41 | Args: 42 | bucket: S3 Bucket Name 43 | prefix: Prefix to use for objects 44 | continuation_token: used for pagination 45 | objects: running list of objects 46 | Returns: 47 | list of objects in the bucket with the specified prefix 48 | 49 | """ 50 | if objects is None: 51 | objects = [] 52 | 53 | if not continuation_token: 54 | response = client.list_objects_v2(Bucket=bucket, MaxKeys=1000, Prefix=prefix) 55 | else: 56 | response = client.list_objects_v2( 57 | Bucket=bucket, 58 | MaxKeys=1000, 59 | Prefix=prefix, 60 | ContinuationToken=continuation_token, 61 | ) 62 | 63 | if "Contents" in response: 64 | objects.extend( 65 | [ 66 | s3_object["Key"] 67 | for s3_object in response["Contents"] 68 | if ".tif" in s3_object["Key"] 69 | ] 70 | ) 71 | 72 | if "NextContinuationToken" in response: 73 | return list_bucket( 74 | bucket, 75 | prefix, 76 | continuation_token=response["NextContinuationToken"], 77 | objects=objects, 78 | ) 79 | 80 | return objects 81 | -------------------------------------------------------------------------------- /terraform/netcdf_datasource/process_netcdf/src/netcdf_to_geotiff.py: -------------------------------------------------------------------------------- 1 | """Convert NetCDF to a geoTIFF.""" 2 | 3 | # System Imports 4 | import logging 5 | import os 6 | import uuid 7 | 8 | # External Imports 9 | from osgeo import gdal 10 | 11 | # Configure Logging 12 | logger = logging.getLogger() 13 | logger.setLevel(logging.INFO) 14 | 15 | 16 | def convert(netcdf_bucket, netcdf_key, parameter_name, band, s3_client): 17 | """ 18 | Convert a NetCDF File to a Cloud Optimzed GeoTIFF (COG). 19 | 20 | Args: 21 | netcdf_bucket: Name of the NetCDF source S3 Bucket 22 | netcdf_key: Key to the NetCDF File 23 | parameter_name: Name of the NetCDF Parameter to extract 24 | band: Band number to extract 25 | s3_client: Boto3 S3 client 26 | 27 | Returns: 28 | data: Object containing information about the new COG 29 | 30 | """ 31 | input_file = os.path.join("/tmp", "file.nc") 32 | 33 | # Download file 34 | s3_client.download_file(netcdf_bucket, netcdf_key, input_file) 35 | 36 | # Convert to GeoTIFF 37 | temp_file_1 = os.path.join("/tmp", f"{uuid.uuid1()}_1.tif") 38 | gdal.Translate( 39 | temp_file_1, 40 | f"NETCDF:{input_file}:{parameter_name}", 41 | format="GTiff", 42 | bandList=[band], 43 | outputType=gdal.GDT_Float32, 44 | creationOptions=["COMPRESS=DEFLATE"], 45 | unscale=True, # GDAL warp cannot handle scaled values 46 | ) 47 | os.remove(input_file) 48 | 49 | # Warp to EPSG:3857 50 | temp_file_2 = os.path.join("/tmp", f"{uuid.uuid1()}_2.tif") 51 | gdal.Warp( 52 | temp_file_2, 53 | temp_file_1, 54 | format="GTiff", 55 | dstSRS="EPSG:3857", 56 | workingType=gdal.GDT_Float32, 57 | outputType=gdal.GDT_Float32, 58 | creationOptions=["COMPRESS=DEFLATE"], 59 | resampleAlg="cubicspline", 60 | ) 61 | os.remove(temp_file_1) 62 | 63 | # Apply compression and tiling 64 | output_file = os.path.join("/tmp", f"{uuid.uuid1()}_final.tif") 65 | dataset = gdal.Translate( 66 | output_file, 67 | temp_file_2, 68 | format="GTiff", 69 | creationOptions=["TILED=YES", "COMPRESS=DEFLATE"], 70 | ) 71 | os.remove(temp_file_2) 72 | 73 | # Add overviews 74 | dataset.BuildOverviews("NEAREST", [2, 4, 8, 16, 32, 64]) 75 | 76 | # Close dataset 77 | dataset = None 78 | 79 | return { 80 | "local_file": output_file, 81 | "key": f'{netcdf_key.split(".")[0]}.tif', 82 | "bbox": [0, 0, 0, 0], 83 | "band": band, 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WMS Serverless 2 | 3 | A Serverless implementation of WMS on AWS. 4 | 5 | This repository contains a [terraform](https://www.terraform.io/) project that implements a 6 | serverless version of [WMS](https://www.opengeospatial.org/standards/wms) on 7 | [AWS](https://aws.amazon.com/). 8 | 9 | A live demo can be found here: https://nathancalandra.cloud/goes16-serverless 10 | 11 | ## Components 12 | 13 | This project consists of three modules. 14 | 15 | ### S3 16 | 17 | This modules contains the main S3 bucket used in this project. It's purpose is store layers that 18 | will be served by the API. The data source module can be used to automatically add files to this 19 | bucket. To prevent storing large amounts of data, a life-cycle policy is applied to delete any 20 | object older than one day. 21 | 22 | ### NetCDF Data Source 23 | 24 | This module contains lambda functions and an SNS subscription that are used to convert NetCDF files 25 | on the `noaa-goes16` public S3 bucket to [COGs](https://www.cogeo.org/). It can be configured to 26 | pull as much data as desired from the source bucket. It's default configuration pulls a single 27 | product. To add more products edit the `data_definitions` variable in `terraform/main.tf`. For 28 | example, the following configuration processes bands 2 and 9 from the Cloud Moisture Imagery (CMI) 29 | product. See the [documentation](https://docs.opendata.aws/noaa-goes16/cics-readme.html) on the 30 | public goes16 S3 bucket for a description of the available data. 31 | 32 | ```hcl 33 | [{ 34 | filter_regex = "ABI-L2-CMIPF\\/[0-9]{4}\\/[0-9]{3}/[0-9]{2}/OR_ABI-L2-CMIPF-M6C09.*.nc" 35 | parameter_name = "CMI" 36 | band = 1 37 | }, { 38 | filter_regex = "ABI-L2-CMIPF\\/[0-9]{4}\\/[0-9]{3}/[0-9]{2}/OR_ABI-L2-CMIPF-M6C02.*.nc" 39 | parameter_name = "CMI" 40 | band = 1 41 | }] 42 | ``` 43 | 44 | ### WMS API 45 | 46 | The WMS API module uses API Gateway and lambda functions to operate on files stored in the S3 47 | bucket. Currently there are two endpoints: 48 | 49 | #### wms 50 | 51 | This is endpoint supports a partial implementation of the WMS "GetMap" request. 52 | 53 | Supported parameters: 54 | 55 | | Name | Supported (yes/no) | Notes | 56 | |-------------|:--------------------:|--------------------------------------------------------| 57 | | VERSION | yes | Only 1.3.0 | 58 | | REQUEST | yes | Only "GetMap" | 59 | | LAYERS | yes | Only one layer at a time | 60 | | STYLES | yes | Name of a csv file in the cog renderer lambda function | 61 | | CRS | no | | 62 | | BBOX | yes | | 63 | | WIDTH | yes | | 64 | | HEIGHT | yes | | 65 | | FORMAT | yes | Only PNG | 66 | | TRANSPARENT | yes | | 67 | | BGCOLOR | no | | 68 | | EXCEPTIONS | no | | 69 | | TIME | no | | 70 | | ELEVATION | no | | 71 | 72 | #### layers 73 | 74 | This endpoint simply lists layers in the main S3 bucket. It is a placeholder until WMS 75 | "GetCapabilities" is supported. 76 | 77 | ## Installation 78 | 79 | 1. Install [terraform](https://www.terraform.io/downloads.html) 80 | 1. Open a terminal inside the terraform directory 81 | 1. Run `terraform init` 82 | 1. Provide variables for s3 backend storage 83 | 1. Run `terraform apply` 84 | 85 | ## AWS Services used by this project 86 | 87 | - [S3](https://aws.amazon.com/s3/) 88 | - [Lambda](https://aws.amazon.com/lambda/) 89 | - [SNS](https://aws.amazon.com/sns/) 90 | - [API Gateway](https://aws.amazon.com/api-gateway/) 91 | 92 | 93 | ## Lambda, GDAL, and Python 94 | 95 | https://github.com/developmentseed/geolambda 96 | -------------------------------------------------------------------------------- /terraform/wms_api/api.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.2 2 | info: 3 | title: ${name} 4 | description: WMS API 5 | version: 0.0.0 6 | servers: 7 | - url: 'localhost' 8 | description: 'tester' 9 | x-amazon-apigateway-request-validators: 10 | all: 11 | validateRequestBody: true 12 | validateRequestParameters: true 13 | validateRequestHeaders: true 14 | params-only: 15 | validateRequestBody: false 16 | validateRequestParameters: true 17 | validateRequestHeaders: true 18 | x-amazon-apigateway-binary-media-types: 19 | - image/png 20 | - image/jpeg 21 | - image/webp 22 | paths: 23 | /layers: 24 | get: 25 | summary: Get Available Layers 26 | description: Returns a list of available layers 27 | tags: 28 | - Info 29 | responses: 30 | 200: 31 | description: OK 32 | headers: 33 | Access-Control-Allow-Headers: 34 | type: string 35 | Access-Control-Allow-Methods: 36 | type: string 37 | Access-Control-Allow-Origin: 38 | type: string 39 | content: 40 | application/json: 41 | $ref: '#/components/schemas/layers' 42 | x-amazon-apigateway-integration: 43 | type: aws_proxy 44 | uri: arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${list_layers}/invocations 45 | httpMethod: POST 46 | credentials: ${credentials} 47 | responses: 48 | default: 49 | statusCode: 200 50 | options: 51 | summary: Enable CORS 52 | description: Enable CORS by returning the correct headers 53 | responses: 54 | 200: 55 | description: Default response for CORS method 56 | headers: 57 | Access-Control-Allow-Headers: 58 | type: string 59 | Access-Control-Allow-Methods: 60 | type: string 61 | Access-Control-Allow-Origin: 62 | type: string 63 | x-amazon-apigateway-integration: 64 | type: mock 65 | responses: 66 | default: 67 | statusCode: 200 68 | responseParameters: 69 | method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,access-control-allow-origin'" 70 | method.response.header.Access-Control-Allow-Methods: "'*'" 71 | method.response.header.Access-Control-Allow-Origin: "'*'" 72 | responseTemplates: 73 | application/json: | 74 | {} 75 | passthroughBehavior: when_no_match 76 | requestTemplates: 77 | application/json: | 78 | {"statusCode" : 200} 79 | /wms: 80 | get: 81 | summary: Web Map Service 82 | description: OGC compliant WMS feed 83 | tags: 84 | - Tile 85 | x-amazon-apigateway-request-validator: params-only 86 | parameters: 87 | - name: VERSION 88 | description: WMS Version (1.3.0) 89 | in: query 90 | required: true 91 | schema: 92 | type: string 93 | example: 1.3.0 94 | - name: REQUEST 95 | description: WMS request type 96 | in: query 97 | required: true 98 | schema: 99 | type: string 100 | example: GetMap 101 | - name: LAYERS 102 | description: layer name 103 | in: query 104 | required: true 105 | schema: 106 | type: string 107 | example: layername 108 | - name: STYLES 109 | description: style name 110 | in: query 111 | required: true 112 | schema: 113 | type: string 114 | example: style 115 | - name: CRS 116 | description: Coordinate Reference System (CRS) 117 | in: query 118 | required: true 119 | schema: 120 | type: string 121 | example: epsg:4326 122 | - name: BBOX 123 | description: Boundingbox corners (minx,miny,maxx,maxy) 124 | in: query 125 | required: true 126 | schema: 127 | type: array 128 | items: 129 | type: number 130 | example: [0,0,0,0] 131 | - name: WIDTH 132 | description: Output image width 133 | in: query 134 | required: true 135 | schema: 136 | type: integer 137 | example: 256 138 | - name: HEIGHT 139 | description: Output image height 140 | in: query 141 | required: true 142 | schema: 143 | type: integer 144 | example: 256 145 | - name: FORMAT 146 | description: Output image format 147 | in: query 148 | required: true 149 | schema: 150 | type: string 151 | example: image/png 152 | - name: TRANSPARENT 153 | description: Background map transparency 154 | in: query 155 | required: false 156 | schema: 157 | type: boolean 158 | default: false 159 | example: false 160 | - name: BGCOLOR 161 | description: Background color 162 | in: query 163 | required: false 164 | schema: 165 | type: string 166 | default: "0xFFFFFF" 167 | example: "0xFFFFFF" 168 | - name: EXCEPTIONS 169 | description: exception format 170 | in: query 171 | required: false 172 | schema: 173 | type: string 174 | default: json 175 | example: json 176 | - name: TIME 177 | description: Time value of the layer 178 | in: query 179 | required: false 180 | schema: 181 | type: string 182 | example: "2019-11-01T01:15:07Z" 183 | - name: ELEVATION 184 | description: Elevation value of the layer 185 | in: query 186 | required: false 187 | schema: 188 | type: string 189 | example: "0" 190 | responses: 191 | 200: 192 | description: OK 193 | headers: 194 | Access-Control-Allow-Headers: 195 | type: string 196 | Access-Control-Allow-Methods: 197 | type: string 198 | Access-Control-Allow-Origin: 199 | type: string 200 | content: 201 | image/png: 202 | $ref: '#/components/schemas/binary' 203 | x-amazon-apigateway-integration: 204 | type: aws_proxy 205 | uri: arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${cog_renderer}/invocations 206 | httpMethod: POST 207 | credentials: ${credentials} 208 | requestParameters: 209 | # TODO: maybe not needed? 210 | integration.request.header.Accept: method.request.querystring.FORMAT 211 | responses: 212 | default: 213 | statusCode: 200 214 | options: 215 | summary: Enable CORS 216 | description: Enable CORS by returning the correct headers 217 | responses: 218 | 200: 219 | description: Default response for CORS method 220 | headers: 221 | Access-Control-Allow-Headers: 222 | type: string 223 | Access-Control-Allow-Methods: 224 | type: string 225 | Access-Control-Allow-Origin: 226 | type: string 227 | x-amazon-apigateway-integration: 228 | type: mock 229 | responses: 230 | default: 231 | statusCode: 200 232 | responseParameters: 233 | method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,access-control-allow-origin'" 234 | method.response.header.Access-Control-Allow-Methods: "'*'" 235 | method.response.header.Access-Control-Allow-Origin: "'*'" 236 | responseTemplates: 237 | application/json: | 238 | {} 239 | passthroughBehavior: when_no_match 240 | requestTemplates: 241 | application/json: | 242 | {"statusCode" : 200} 243 | components: 244 | schemas: 245 | binary: 246 | type: string 247 | format: binary 248 | layers: 249 | type: array 250 | items: 251 | type: string 252 | --------------------------------------------------------------------------------