Migrating from Lambda@Edge to CloudFront Functions

I have been using Lambda@Edge for several years now, first time I used it was back in 2018, wrote this post back then. I was always under the impression that the Lambda function was running in the edge cache, hence the name. But I was apparently wrong, it runs in the region cache and not in the edge cache. That became clear when AWS, back in May 2021, release the new and shiny CloudFront Functions that was going to run in true edge cache.

So what are the differences? How do we go about to migrate from Lambda@Edge to CloudFront Functions. In this post I will try to explain the differences and talk about my experience migrating my blog infrastructure to CloudFront functions.

Lambda@Edge vs CloudFront Functions

So what are the difference between the two then? First of all Lambda@Edge run at the 13 (at the time of writing this post) Regional Caches. CloudFront Functions run at the 200+ Edge Caches. That mean that the your code will run closer to your users.

The other major differences are around runtime support, memory usages, execution time, and more. Here is a table showing the differences.

Feature CloudFront Functions Lambda@Edge
Runtime JavaScript
(ECMAScript 5.1 compliant)
Node.js, Python
CloudFront triggers Viewer request Viewer response Viewer request
Viewer response
Origin request
Origin response
Execution time 1 millisecond 5 seconds (viewer triggers)
30 seconds (origin triggers)
Memory 2MB 128MB (viewer triggers)
10GB (origin triggers)
Package size 10 KB 1 MB (viewer triggers)
50 MB (origin triggers)
Network access No Yes
File system access No Yes
Access to the request body No Yes

That was a lot of differences, but what does it mean? How do I know which to choose?

Pricing

So what about pricing? Is CloudFront Functions or Lambda@Edge more expensive?
With Lambda@Edge you pay $0.60 per 1 million requests plus the execution time ($0.00000625125 FOR EVERY 128MB-SECOND).
With CloudFront Functions you pay $0.10 per 1 million Invocations and nothing for execution time.
That mean that CloudFront Functions is the cheapest option of the two.

Choosing Lambda@Edge or CloudFront Functions

As we just saw there are several differences between Lambda@Edge and CloudFront functions. Which should I use?
I would say that if you don't need to react on origin triggers, have access to the request body, need file or network access, can finish execution within 1 ms and have really small deployment packages, then CloudFront Functions is probably a great match for you. Just remember that no network access mean no access to any other AWS service.

Migrating

CloudFront Functions are deployed with the CloudFront distribution in the region of your choice. This truly made deployment much easier, Lambda@Edge always had to be deployed in us-east-1 region, with CloudFront Functions there was no need to target different regions during deployment.

I have been using Lambda@Edge to make CloudFront act more like a webserver, so if a link was pointing to a folder I use Lambda@edge to append index.html to the path, so the correct file is requested from S3. Since I didn't use any network access, my Lambda@edge function was rather fast, and written in pure Javascript the migration process was rather easy.

Create the Function

First of all I created a AWS::CloudFront::Function and deployed that.

CloudFrontFunction:
    Type: AWS::CloudFront::Function
    Properties:
      Name: UpdatePath
      AutoPublish: true
      FunctionCode: !Sub |
        function handler(event) {
          var request = event.request;
          var uri = request.uri;
          uri = uri.replace(/\/$/, '\/index.html');
          request.uri = uri;
          return request;
        }
      FunctionConfig:
        Comment: !Sub Append index.html to folder paths
        Runtime: cloudfront-js-1.0

The Runtime of the function must always be cloudfront-js-1.0. I could not find a good way to keep the code in an external source file and not inline it in the CloudFormation template. This is a rather clunky way to add the code for the function. I would love to see a deployment similar to how Lambda is deployed. Since my code was rather short it is manageable to have it inlined.

Testing and debugging

When the function was deployed I could turn to the console to test it out, which was a rather nice experience. You can select the trigger and what stage, Development or Live, you like to use for the test.
image

It is also possible to perform tests using the CLI in an automatic way. This is done using the test-function command.

aws cloudfront test-function \
    --name ExampleFunction \
    --if-match ETVABCEXAMPLE \
    --event-object fileb://event-object.json
    --stage DEVELOPMENT

The event-object is Json with the information about stage etc.

{
    "version": "1.0",
    "context": {
        "eventType": "viewer-request"
    },
    "viewer": {
        "ip": "1.2.3.4"
    },
    "request": {
        "method": "GET",
        "uri": "/mytest/",
        "headers": {
            "host": {"value": "example.org"}
        }
    }
}

The deployment and testing is really nice, you can easily deploy a new development version and test it out before going live.
Logs, printed using console.log() is always sent to CloudWatch logs in us-east-1, doesn't matter what edge location the Function runs in, logs are in us-east-1

So after I had tested the Function it was time to update the CloudFormation template.

Doing the actual migration

The last step is to do the actual update of CloudFormation and do the migration. I commented out the LambdaFunctionAssociations section and added the FunctionAssociations instead.

CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
        DistributionConfig:
            ....
            FunctionAssociations:
                - EventType: viewer-request
                FunctionARN: !GetAtt CloudFrontFunction.FunctionMetadata.FunctionARN
            #LambdaFunctionAssociations:
            #    - EventType: viewer-request
            #    LambdaFunctionARN: !Ref LambdaARN

The deployment took a couple of minutes but was not that bad. After the deployment was done the page was working perfect and I could delete the Lambda@Edge function.

Conclusion

Migrating to CloudFront Functions from Lambda@Edge was really easy, and was accomplished with just a few steps. So what did the migration bring? I feel that it was easier to debug and test CloudFront Functions. Testing was nice and straight forward. That logs are always in us-east-1 and not as with Lambda@Edge where they end up in the region the Lambda@Edge function runs in, is a killer feature. My cost was down slightly, but not much, I'm not using that much. I can see cost to be a driver for migration for heavy usage. For some reason the page felt more responsive, even though it's the same code that is running. Can be due to CloudFront Functions running closer to the client, can be my imagination, I have not done any proper measurements yet.

In short, I would say that if you can you should migrate.

24