· engineering  · 4 min read

Cost-Control "Kill Switch" for AWS CloudFront?

Trying to use AWS Cloudwatch to implement a "kill switch" that automatically disables CloudFront distribution if the bandwidth cost exceeds a specified threshold.

Trying to use AWS Cloudwatch to implement a "kill switch" that automatically disables CloudFront distribution if the bandwidth cost exceeds a specified threshold.

A client has an AWS setup that serves a static site from S3 via CloudFront, including some large files.
This configuration presents a risk: a bug, or a malicious actor could exploit this by frequently requesting these files, rapidly inflating our AWS bill.
Given that AWS’s EstimatedCharges metric updates every six hours and resets monthly, monitoring this is not suitable for immediate responses required by our scenario.

AWS CloudFront does contain some mitigation against DDOS attacks, but not at the application layer.
The client would rather interrupt the service for everyone, than face extremely high bills.

Cost Calculation

We have configured our CloudFront with PriceClass_100 to ensure content delivery from American and European edge locations, minimizing costs from more expensive regions.
Serving under 10TB, our costs are approximately $0.085 per GB, as bandwidth is the primary cost factor. Our objective is to monitor this and trigger protective measures if costs risk exceeding our budget.

Max Bytes=(BudgetCost per GB)×10243\text{Max Bytes} = \left( \frac{\text{Budget}}{\text{Cost per GB}} \right) \times 1024^3

Ideally we would be able to trigger an alarm based on a rolling total. But AWS explicitly advises against using either SUM or ROLLING_SUM metric math in alarms.
So realistically, we need to evaluate what the most we are willing to spend in any given hour is, and disable the distribution if that threshold is reached.

Solution Overview

Our solution involves creating a CloudWatch alarm that monitors the BytesDownloaded metric from the CloudFront distribution, coupled with a Lambda function that disables the distribution when the bandwidth cost crosses our set limit. This mechanism acts swiftly, independently of standard billing alerts, providing an additional layer of financial safety.

It should be noted that billing alerts were also enabled for this client account, and the notification of the team via eventBridge when the Alarm state changes was handled separately.

CloudFormation Implementation

Below is the CloudFormation template to set up the necessary components:

AWSTemplateFormatVersion: 2010-09-09
Description: Disable cloudfront if it has served more tan X bytes in the past 30 days
Parameters:
  ThresholdInBytes:
    Type: Number
    Description: The threshold in bytes for the CloudFront cost alarm. If the cost exceeds this threshold, the distribution will be disabled.
  DistributionId:
    Type: String
    Description: The ID of the CloudFront distribution to disable.
 
Resources:
  CloudFrontDisableFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Runtime: nodejs18.x
      Environment:
        Variables:
          DISTRIBUTION_ID: !Ref DistributionId
      Code:
        ZipFile: |
          // Import the CloudFront client and the UpdateDistributionCommand
          const { CloudFrontClient, GetDistributionConfigCommand, UpdateDistributionCommand } = require('@aws-sdk/client-cloudfront');
 
          // Initialize the CloudFront client
          const client = new CloudFrontClient({ region: 'us-east-1' }); // Change this as per your distribution's region
 
          const distributionId = process.env.DISTRIBUTION_ID;
 
          exports.handler = async (event, context) => {
 
              try {
                  // First, get the current distribution configuration
                  const getParams = { Id: distributionId };
                  const getConfigCommand = new GetDistributionConfigCommand(getParams);
                  const getConfigResponse = await client.send(getConfigCommand);
 
                  // Prepare parameters for updating the distribution
                  const updateParams = {
                      Id: distributionId,
                      IfMatch: getConfigResponse.ETag, // ETag is required for the update
                      DistributionConfig: {
                          ...getConfigResponse.DistributionConfig,
                          Enabled: false
                      }
                  };
 
                  // Create and send the update command
                  const updateCommand = new UpdateDistributionCommand(updateParams);
                  const updateResponse = await client.send(updateCommand);
                  
                  console.log("Distribution Updated Successfully:", updateResponse);
                  return updateResponse;
              } catch (error) {
                  console.error("Error updating CloudFront distribution:", error);
                  throw error;
              }
          };
 
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      Description: Role for use cloudfront kill switch lambda
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: 'CloudFrontAccess'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - 'cloudfront:UpdateDistribution'
                  - 'cloudfront:GetDistributionConfig'
                Resource: !Sub 'arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${DistributionId}'
 
  CloudFrontCostAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmDescription: 'Alarm when CloudFront costs exceed threshold'
      MetricName: BytesDownloaded
      Namespace: AWS/CloudFront
      Statistic: Sum
      Period: 3600 #1 hour
      EvaluationPeriods: 1
      Threshold: !Ref ThresholdInBytes
      ComparisonOperator: GreaterThanOrEqualToThreshold
      AlarmActions:
        - !GetAtt CloudFrontDisableFunction.Arn

Explanation:

Lambda Function: Disables the CloudFront distribution when triggered.
IAM Role: Grants the necessary permissions to the Lambda function to modify CloudFront configurations.
CloudWatch Alarm: Monitors data transfer and triggers the Lambda function if the transferred data cost exceeds our preset threshold. Since December 2023 it has been possible to trigger a lambda function directly from the alarm, without SNS or something as an intermediary.

Conclusion

Implementing a CloudFront kill switch via AWS CloudFormation can add a simple layer of reactive cost control to help ensure your AWS spending remains within budget without manual intervention, particularly for applications serving large files, where traffic spikes can quickly lead to unexpectedly high charges.

If we were able to take advanatage of metric math in a Cloudwatch Alarm, this solution would be much better than it currently is, as we could set a monthly budget and react immediately when it is reached. As it is this approach only allows us to react to the last hours worth of data, and thus we need to toe the line between reacting to legitimate interest and a DDOS attack.
Nonetheless it might still be useful, given that it could take up to 6 hours to receive a billing alert (and maybe longer to act on it).

James Babington

About James Babington

A cloud architect and engineer with a wealth of experience across AWS, web development, and security, James enjoys writing about the technical challenges and solutions he's encountered, but most of all he loves it when a plan comes together and it all just works.

Comments

No comments yet. Be the first to comment!

Leave a Comment

Check this box if you don't want your comment to be displayed publicly.

Back to Blog

Related Posts

View All Posts »
S3: Getting data in and out

S3: Getting data in and out

Manage your S3 data transfers efficiently; ways to copy and sync from various sources, using the AWS CLI and DataSync.