├── .gitignore ├── fargate ├── Dockerfile ├── destroy.sh ├── main.py ├── README.md └── deploy.sh ├── lambda-api ├── .gitignore ├── handler.py ├── serverless.yml └── README.md ├── locustfile.py ├── apigw-service-proxy ├── deploy.sh ├── README.md └── template.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | -------------------------------------------------------------------------------- /fargate/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/meinheld-gunicorn-flask:python3.7 2 | 3 | RUN pip install boto3 4 | 5 | COPY . /app 6 | -------------------------------------------------------------------------------- /lambda-api/.gitignore: -------------------------------------------------------------------------------- 1 | # Distribution / packaging 2 | .Python 3 | env/ 4 | build/ 5 | develop-eggs/ 6 | dist/ 7 | downloads/ 8 | eggs/ 9 | .eggs/ 10 | lib/ 11 | lib64/ 12 | parts/ 13 | sdist/ 14 | var/ 15 | *.egg-info/ 16 | .installed.cfg 17 | *.egg 18 | 19 | # Serverless directories 20 | .serverless -------------------------------------------------------------------------------- /fargate/destroy.sh: -------------------------------------------------------------------------------- 1 | aws iam delete-role-policy \ 2 | --policy-name 'sns-publish' \ 3 | --role-name fargate-ingest-role 4 | 5 | aws iam delete-role \ 6 | --role-name fargate-ingest-role 7 | 8 | fargate service scale sns-ingest 0 9 | 10 | fargate service destroy sns-ingest 11 | 12 | fargate lb destroy flask-lb 13 | -------------------------------------------------------------------------------- /locustfile.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from locust import HttpLocust, TaskSet, task 4 | 5 | class Root(TaskSet): 6 | 7 | @task(2) 8 | def ingest(self): 9 | payload = { "message": "Hello" } 10 | headers = { "Content-Type": "application/json" } 11 | self.client.post("/ingest", data=json.dumps(payload), headers=headers) 12 | 13 | class Locust(HttpLocust): 14 | task_set = Root 15 | min_wait = 1000 16 | max_wait = 9000 17 | -------------------------------------------------------------------------------- /lambda-api/handler.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import boto3 5 | 6 | TOPIC_ARN = os.environ['TOPIC_ARN'] 7 | sns = boto3.client('sns') 8 | 9 | 10 | def ingest(event, context): 11 | payload = event['body'] 12 | 13 | sns.publish( 14 | TopicArn=TOPIC_ARN, 15 | Message=payload 16 | ) 17 | 18 | response = { 19 | "statusCode": 200, 20 | "body": json.dumps({ "message": "ok" }) 21 | } 22 | 23 | return response 24 | -------------------------------------------------------------------------------- /apigw-service-proxy/deploy.sh: -------------------------------------------------------------------------------- 1 | aws cloudformation deploy \ 2 | --stack-name service-proxy \ 3 | --template-file template.yml \ 4 | --capabilities CAPABILITY_IAM 5 | 6 | HOST=$(aws cloudformation describe-stacks \ 7 | --stack-name service-proxy \ 8 | --output text \ 9 | --query 'Stacks[0].Outputs[?OutputKey==`ServiceEndpoint`].OutputValue') 10 | 11 | echo '' 12 | echo "Your service proxy endpoint is ready! 🎉" 13 | echo '' 14 | echo "Run locust load testing with the following command:" 15 | echo '' 16 | echo " locust --host=$HOST" 17 | -------------------------------------------------------------------------------- /fargate/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | import boto3 5 | from flask import Flask, request 6 | app = Flask(__name__) 7 | 8 | sns = boto3.client('sns', region_name='us-east-1') 9 | TOPIC_ARN=os.environ['TOPIC_ARN'] 10 | 11 | 12 | @app.route("/ingest", methods=['POST']) 13 | def ingest(): 14 | resp = sns.publish( 15 | TopicArn=TOPIC_ARN, 16 | Message=request.get_data(as_text=True) 17 | ) 18 | return json.dumps({ "message": "ok" }) 19 | 20 | if __name__ == "__main__": 21 | app.run(host="0.0.0.0", port=80) 22 | -------------------------------------------------------------------------------- /lambda-api/serverless.yml: -------------------------------------------------------------------------------- 1 | service: apigw-test 2 | 3 | provider: 4 | name: aws 5 | runtime: python3.7 6 | stage: dev 7 | region: us-east-1 8 | memorySize: 3008 9 | iamRoleStatements: 10 | - Effect: Allow 11 | Action: sns:Publish 12 | Resource: { Ref: IngestTopic } 13 | environment: 14 | TOPIC_ARN: { Ref: IngestTopic } 15 | 16 | functions: 17 | ingest: 18 | handler: handler.ingest 19 | events: 20 | - http: 21 | path: ingest 22 | method: post 23 | 24 | resources: 25 | Resources: 26 | IngestTopic: 27 | Type: AWS::SNS::Topic 28 | -------------------------------------------------------------------------------- /fargate/README.md: -------------------------------------------------------------------------------- 1 | ## Fargate API 2 | 3 | The `deploy.sh` script in this directory will deploy a Fargate service with five instances fronted by an Application Load Balancer. The service exposes a single endpoint which accepts POST requests and forwards the payload body to SNS. 4 | 5 | ![Fargate to SNS](https://user-images.githubusercontent.com/6509926/53013070-6f1d0180-340a-11e9-860a-4f9962a04792.png) 6 | 7 | ### Usage 8 | 9 | 1. Install and configure the [AWS CLI](https://aws.amazon.com/cli/) and the [fargate CLI](http://somanymachines.com/fargate/). 10 | 11 | 2. Run the `deploy.sh` script in this repository to deploy the Fargate service. 12 | 13 | When the deploy is complete, it will print out the command needed to run load testing with Locust. 14 | -------------------------------------------------------------------------------- /apigw-service-proxy/README.md: -------------------------------------------------------------------------------- 1 | ## API Gateway Service Proxy 2 | 3 | The CloudFormation template in this directory will configure an API Gateway REST API with a single endpoint. The endpoint will accept POST requests and forward the payload body to SNS. 4 | 5 | ![APIG Service Proxy](https://user-images.githubusercontent.com/6509926/53012281-249a8580-3408-11e9-91e6-c64cfc82a434.png) 6 | 7 | For more information, check out this post for a [full walkthrough on an API Gateway service proxy](https://www.alexdebrie.com/posts/aws-api-gateway-service-proxy/). 8 | 9 | ### Usage 10 | 11 | 1. Install and configure the [AWS CLI](https://aws.amazon.com/cli/). 12 | 13 | 2. Run the `deploy.sh` script in this repository to deploy the CloudFormation template. 14 | 15 | When the deploy is complete, it will print out the command needed to run load testing with Locust. -------------------------------------------------------------------------------- /lambda-api/README.md: -------------------------------------------------------------------------------- 1 | ## Serverless architecture with AWS Lambda 2 | 3 | The code in this directory will deploy a Serverless application. It has a single HTTP endpoint, `/ingest` which receives POST requests and forwards the payload to an SNS topic. 4 | 5 | ![SNS Publish with Lambda](https://user-images.githubusercontent.com/6509926/52906603-cbb6cb80-3214-11e9-8a97-a5ea2d4036d3.png) 6 | 7 | ### Usage 8 | 9 | 1. Install the [Serverless Framework](www.serverless.com): 10 | 11 | ```bash 12 | npm install -g serverless 13 | ``` 14 | 15 | 2. Create a service by pulling down this directory: 16 | 17 | ```bash 18 | sls create --template-url https://github.com/alexdebrie/aws-api-performance-bakeoff/lambda-api 19 | cd lambda-api 20 | ``` 21 | 22 | 3. Deploy your service: 23 | 24 | ```bash 25 | serverless deploy 26 | ``` 27 | 28 | At the end, it will print out your endpoint which you can use for load testing with Locust. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AWS API Performance Bakeoff 2 | 3 | This repository includes scripts for deploying an API that exposes a single endpoint. The endpoint accepts a payload via POST request and forwards the payload to an SNS topic. 4 | 5 | ### Architectures 6 | 7 | There are three different architectures for deploying this service: 8 | 9 | 1. [Using Fargate and an Application Load Balancer](./fargate/README.md) 10 | 11 | 2. [Using the Serverless Framework and AWS Lambda](./lambda-api/README.md) 12 | 13 | 3. [Using an API Gateway service proxy integration directly to SNS](./apigw-service-proxy/README.md) 14 | 15 | ### Testing 16 | 17 | For load testing, I used [Locust](https://locust.io/). There's a locustfile in this directory. 18 | 19 | To use, first install Locust: 20 | 21 | ```bash 22 | pip install locustio 23 | ``` 24 | 25 | Then, run locust with the host of your deployed API: 26 | 27 | ```bash 28 | locust --host= 29 | ``` 30 | 31 | Navigate to localhost:8089 to kick off your load test. -------------------------------------------------------------------------------- /fargate/deploy.sh: -------------------------------------------------------------------------------- 1 | TOPIC_ARN=$(aws sns create-topic \ 2 | --name fargate-ingest-topic \ 3 | --output text \ 4 | --query 'TopicArn') 5 | 6 | ROLE_ARN=$(aws iam create-role \ 7 | --role-name fargate-ingest-role \ 8 | --assume-role-policy-document '{ 9 | "Version": "2012-10-17", 10 | "Statement": { 11 | "Effect": "Allow", 12 | "Principal": {"Service": "ecs-tasks.amazonaws.com"}, 13 | "Action": "sts:AssumeRole" 14 | } 15 | }' \ 16 | --output text \ 17 | --query 'Role.Arn') 18 | 19 | aws iam put-role-policy \ 20 | --role-name fargate-ingest-role \ 21 | --policy-name 'sns-publish' \ 22 | --policy-document '{ 23 | "Version": "2012-10-17", 24 | "Statement": { 25 | "Effect": "Allow", 26 | "Action": "sns:Publish", 27 | "Resource": "'$TOPIC_ARN'" 28 | } 29 | }' 30 | 31 | fargate lb create 'flask-lb' \ 32 | --port HTTP:80 33 | 34 | fargate service create sns-ingest \ 35 | --task-role $ROLE_ARN \ 36 | --env TOPIC_ARN=$TOPIC_ARN \ 37 | --lb flask-lb \ 38 | --port HTTP:80 \ 39 | --memory 8192 \ 40 | --cpu 4096 \ 41 | --num 50 42 | 43 | DNS_NAME=$(fargate lb info flask-lb | grep 'DNS Name' | cut -f 3 -d " ") 44 | 45 | echo '' 46 | echo "Your Fargate endpoint is ready! 🎉" 47 | echo '' 48 | echo "Run locust load testing with the following command:" 49 | echo '' 50 | echo " locust --host=http://$DNS_NAME" 51 | -------------------------------------------------------------------------------- /apigw-service-proxy/template.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: API Gateway service proxy to an SNS Topic 3 | Resources: 4 | IngestTopic: 5 | Type: AWS::SNS::Topic 6 | APIGatewayIamRole: 7 | Type: "AWS::IAM::Role" 8 | Properties: 9 | AssumeRolePolicyDocument: 10 | Version: "2012-10-17" 11 | Statement: 12 | - 13 | Effect: "Allow" 14 | Principal: 15 | Service: 16 | - "apigateway.amazonaws.com" 17 | Action: 18 | - "sts:AssumeRole" 19 | RolePolicies: 20 | Type: "AWS::IAM::Policy" 21 | Properties: 22 | PolicyName: "sns-publish" 23 | PolicyDocument: 24 | Version: "2012-10-17" 25 | Statement: 26 | - 27 | Effect: "Allow" 28 | Action: "sns:Publish" 29 | Resource: 30 | Ref: IngestTopic 31 | Roles: 32 | - 33 | Ref: "APIGatewayIamRole" 34 | ApiGatewayRestApi: 35 | Type: AWS::ApiGateway::RestApi 36 | Properties: 37 | Name: "Service Proxy" 38 | ApiGatewayResourceIngest: 39 | Type: AWS::ApiGateway::Resource 40 | Properties: 41 | ParentId: 42 | Fn::GetAtt: 43 | - ApiGatewayRestApi 44 | - RootResourceId 45 | PathPart: "ingest" 46 | RestApiId: 47 | Ref: ApiGatewayRestApi 48 | ApiGatewayMethodIngestPost: 49 | Type: AWS::ApiGateway::Method 50 | Properties: 51 | HttpMethod: "POST" 52 | ResourceId: 53 | Ref: ApiGatewayResourceIngest 54 | RestApiId: 55 | Ref: ApiGatewayRestApi 56 | AuthorizationType: "NONE" 57 | Integration: 58 | Type: "AWS" 59 | IntegrationHttpMethod: "POST" 60 | Credentials: 61 | Fn::GetAtt: 62 | - APIGatewayIamRole 63 | - Arn 64 | Uri: 65 | Fn::Join: 66 | - "" 67 | - - "arn:" 68 | - Ref: AWS::Partition 69 | - ":apigateway:" 70 | - Ref: AWS::Region 71 | - ":sns:path//" 72 | RequestParameters: 73 | "integration.request.header.Content-Type": "'application/x-www-form-urlencoded'" 74 | RequestTemplates: 75 | "application/json": 76 | Fn::Join: 77 | - "" 78 | - - "Action=Publish&TopicArn=$util.urlEncode('" 79 | - Ref: IngestTopic 80 | - "')&Message=$util.urlEncode($input.body)" 81 | IntegrationResponses: 82 | - StatusCode: 200 83 | SelectionPattern: "" 84 | ResponseTemplates: 85 | "application/json": '{"body": "Message received"}' 86 | MethodResponses: 87 | - ResponseModels: 88 | "application/json": "Empty" 89 | StatusCode: 200 90 | ApiGatewayDeployment: 91 | Type: AWS::ApiGateway::Deployment 92 | Properties: 93 | RestApiId: 94 | Ref: ApiGatewayRestApi 95 | StageName: "prod" 96 | DependsOn: 97 | ApiGatewayMethodIngestPost 98 | Outputs: 99 | ServiceEndpoint: 100 | Description: "URL of the service proxy endpoint" 101 | Value: 102 | Fn::Join: 103 | - "" 104 | - - "https://" 105 | - Ref: ApiGatewayRestApi 106 | - ".execute-api." 107 | - Ref: AWS::Region 108 | - "." 109 | - Ref: AWS::URLSuffix 110 | - "/prod" 111 | --------------------------------------------------------------------------------