DEV Community

Cover image for Poor man's kill switch for your demo applications
Nicola Apicella
Nicola Apicella

Posted on

Poor man's kill switch for your demo applications

Today I want to share with you a little trick I learned a while back.

You have just finished building your website or service and it is ready to be demo-ed. Maybe you want to present it to your colleagues, write a blog post about it or just get feedback.
Whatever the reason, you need to have the prototype publicly available on the Internet, but you are worried that could be misused.

All it takes is someone who forgets to define the exit condition in the for loop. And...also, you know...not everybody on the internet is well intended.

for {
  response, e := http.Get("https://my-demo-service/")
}
Enter fullscreen mode Exit fullscreen mode

Ofcourse, you do not want to be broke just to show your service.
To prevent the service to be overwhelmed by requests and save a few bucks, we can use a kill switch, or as I like to call it: the poor man's kill switch.

It allows to expose a public endpoint for a live demo, while giving you the peace of mind that in case of misuse, the api will kill the requests keeping the your costs very low. This is possible thanks to API gateway cost model: throttle requested are not considered in the billing :)


Throttling is a process which limits the number of requests directed to an Api by consumers during a given period.
A common algorithm used to throttle request is the token bucket algorithm. In a nutshell, you have an initial bucket with tokens.
The amount of tokens represents the number of concurrent requests allowed in the system at any given moment. When a new request arrives, the algorithm removes a token from the bucket.
When the request has been processed (i.e. response has been sent back to the caller), the algorithm inserts back the token in the bucket. If the bucket has no token, requests are denied immediately.

Throttling is the first step to protect our API. But for a demo application, this is not enough.

For example, you might set the throttling limit to 1000 requests/second. If anyone abuses the Api, you might still end up paying quite a few bucks.
AWS Api Gateway for example, you would pay about $30 per month. Of course you will have also all the costs associated with your backend (EC2, Lambda, or any other compute option), the storage, etc, thus in reality the cost might higher than that.

I say it's too much to show a live demo in a Github project.

Let's see how can we use throttling to shut down our api when we detect that something fishy is going on.

The service

In the following example, I am going to pretend I have a service built on AWS. If you you use a different cloud provider, you need to check if they bill for throttled requests.

The service use AWS Api gateway to expose a Lambda function backend. All the code in this example is in github.com/napicella/examples/kill-switch-lambda

Kill switch mechanics

We will create an alarm on the number of requests received by the Api Gateway. The alarm triggers a lambda function which sets the throttling rate to zero, thus denying all the requests. As mentioned, Api Gateway does not charge for throttle requests, thus we won't pay for any further request.
The alarm is set to also send us an email to notify the event.
The CloudWatch alarm and the Lambda are be defined in a Cloud Formation template.

Cloudwatch alarm

The alarm triggers if the Api Calls are above the limit we provide (in the example we set it to be 100).
We also add a default subscription which is responsible to send an email to our email address.

Parameters:
  ApiGatewayApiName:
    Type: String
    Description: Exported key for the Api Gateway ApiName to use as metric source for the alarm(s)

Resources:
  LambdaErrorAlarm:
    Type: 'AWS::CloudWatch::Alarm'
    Properties:
      AlarmDescription: 'API Gateway calls'
      Namespace: 'AWS/ApiGateway'
      MetricName: Count
      Dimensions:
      - Name: ApiName
        Value:
          Fn::ImportValue:
            !Sub "${ApiGatewayApiName}"
      Statistic: Sum
      Period: 60
      EvaluationPeriods: 1
      Threshold: 100
      ComparisonOperator: GreaterThanOrEqualToThreshold
      AlarmActions:
        - Ref: "AlarmNotificationTopic"
      TreatMissingData: notBreaching

  AlarmNotificationTopic:
    Type: AWS::SNS::Topic
    Properties:
      Subscription:
      - Endpoint: "some-email@gmail.com"
        Protocol: email
Enter fullscreen mode Exit fullscreen mode

Lambda function definition

In another template we define the Lambda function and we subscribe it to the topic created earlier; this is why the template takes as parameter the TooManyRequestAlarmTopic.
We could have defined everything in a single template, but it's common practice to have alarms in their own template.

Parameters:
  TooManyRequestAlarmTopic:
    Type: String
  ApiGatewayId:
    Type: String
  ApiGatewayStage:
    Type: String

Resources:
  apigatewayauthorizernodejs:
    Type: 'AWS::Serverless::Function'
    Properties:
      Handler: index.handler
      Runtime: nodejs12.x
      CodeUri: ./src
      Description: 'Kill switch'
      MemorySize: 256
      Timeout: 5
      Role: !GetAtt apigatewayauthorizernodejsRole.Arn
      Environment:
        Variables:
          API_GW_ID: !Sub "${ApiGatewayId}"
          API_GW_STAGE: !Sub "${ApiGatewayStage}"
      Events:
        TooManyRequestAlarm:
          Type: SNS
          Properties:
            Topic: !Sub "${TooManyRequestAlarmTopic}"


  apigatewayauthorizernodejsRole:
    Type: AWS::IAM::Role
    ##  omitted for brevity, see Github for the template
Enter fullscreen mode Exit fullscreen mode

Finally, the code for the Lambda function which updates the Api Gateway with the new throttling parameters

const AWS = require('aws-sdk'); 

const apigateway = new AWS.APIGateway({apiVersion: '2015-07-09', region: process.env.AWS_REGION});
var params = {
    restApiId: process.env.API_GW_ID,
    stageName: process.env.API_GW_STAGE,
    patchOperations: [{
        op: 'replace',
        path: '/~1*/*/throttling/rateLimit',
        value: '0'
    }]
};
console.log(`Kill switch for ${params.restApiId} - ${params.stageName}`);

const handler = (event, context, callback) => {

    apigateway.updateStage(params, function(err, data) {
        if (err) {
            console.log(err, err.stack);
            callback(err);
        } else {
            console.log("All the request will be throttled");
            callback(null, {
                statusCode: 200,
                body: JSON.stringify({message : "All the request will be throttled!!"})
            });
        }
    });

}

module.exports = { handler };
Enter fullscreen mode Exit fullscreen mode

Conclusions

This is all for now. Remember to set the default throttling limits and add a kill switch to your next demo project! Cheers!

Liked the article? Share it on Twitter


Credit for the image to TheDigitalArtist

Top comments (0)