Deep Dive Into Serverless

February 7, 2023
Ryan Jones
5 minutes to read

Cloudfront can be simply defined as a CDN (Content Delivery Network), caching your static assets in a datacenter nearer to your viewers. But Cloudfront is a lot more complex and versatile than this simple definition.
Cloudfront is a “pull” CDN, which means that you don’t push your content to the CDN. The content is pulled into the CDN Edge from the origin at the first request of any piece of content.

In addition to the traditional pull and cache usage, Cloudfront can also be used as:

  • A Networking Router
  • A Firewall
  • A Web Server
  • An Application Server

Why is using a CDN relevant?

The main reason is to improve the speed of delivery of static content. By caching the content on the CDN edge, you not only reduce the download time from a few seconds to a few milliseconds, but you also reduce the load and amount of requests on your backend (Network, IO, CPU, Memory, …).

Static content can be defined as content not changing between two identical requests done in the same time frame.

Identical can be as simple as the same URI, or as fine grained as down to the authentication header. The time frame can range between 1 second to 1 year.
The most common case is caching resources like Javascript or CSS and serving the same file to all users forever. But caching a JSON response tailored to a user (Authentication header) for a few seconds reduces the backend calls when the user has the well-known “frenetic browser reload syndrome”.

Edges, Mid-Tier Caches, and Origins

Cloudfront isn’t “just” some servers in datacenters around the world. The service is a layered network of Edge Locations and Regional Edge Caches (or Mid-Tier Caches).

Edge Locations are distributed around the globe with more than 400 points of presence in over 90 cities across 48 countries. Each Edge Location is connected to one of the 13 Regional Edge Caches.

Regional Edge Caches are transparent to you and your visitors, you can’t configure them or access them directly. Your visitors will interact with the nearest Edge Location, which will connect to the attached Regional Edge Cache and finally to your origin. Therefore, in this article, we will refer to Cloudfront as the combination of Edge Locations and Region Edge Caches.

What Have We Learned?

Cloudfront is more than just a simple “pull-cache-serve” service

  • You improve delivery speed to your visitors
  • You can increase resilience by always using a healthy backend
  • You improve overall speed to your backend by leveraging AWS’s backbone
  • You can modify any request to tailor the response to your visitor’s device or region
  • You don’t always need a backend
  • You protect your backend by reducing the number of calls reaching it

Access free book

More from Serverless Guru

Building Serverless REST APIs for a Meal Prep Service with CloudGTO

October 31, 2023
Learn More

How to build an AWS AppSync GraphQL API with multiple data sources

October 26, 2023
Learn More

Building a Secure Serverless API with Lambda Function URL and CloudFront — Part 1

October 17, 2023
Learn More

Performing tests on your IaC code with Pulumi

Let's Talk

This is continuing from Part One of getting set up using Pulumi.

Software testing is generally meant to verify if one’s system is working as intended without any gaps or errors.

When it comes to the deployment of cloud infrastructure, there are quite a number of things that can go wrong. For instance, provisioning resources that handle sensitive data without specific access restrictions, or creating a resource with the intention of performing one task but not having it quite work as intended.

Thankfully, it is possible to perform tests in such situations with Pulumi.

Pulumi is an Infrastructure as Code (IaC) tool that leverages general-purpose programming languages for provisioning cloud resources with multi-cloud support. Owing to its conducive implementation, integrating with other infrastructure or configuration management tools is a simple task.

In this article, we’ll look into creating a serverless function with Pulumi’s SDK and then explore how to perform tests on the program.

Pulumi program setup and deployment.

The function we’ll create will get invoked when new entries are added to a DynamoDB table. On invocation, the function will get the text content and call the AWS Polly service for converting the text-to-speech, and then store the audio file in an S3 bucket.

Before we can write the program, the SDK needs to be installed. Follow their quick start guide here.

Python is the language we will use to write the program and perform tests. To install python if you don’t have it already, the instructions can be followed from this link.

Once the SDK and the language runtime is installed, inside an empty directory execute this command

pulumi new aws-python

If there are multiple AWS profiles configured with your AWS CLI, you can specify the profile that should be used by Pulumi for deployments by running,

pulumi config set aws:profile 

The Pulumi program goes inside the ‘’ file. Replace the contents of that file with this code.

"""An AWS Python Pulumi program"""

import pulumi
from pulumi_aws import s3,lambda_,iam,dynamodb

# Create bucket to store transcribed audio file
bucket_tts = s3.Bucket(

# Create dynamoDB table containing text entries
ddb = dynamodb.Table(
    name="ttsslsguru", # this disables auto-naming
                    "name": "Id",
                    "type": "N",

# Create Lambda IAM lambda_role
lambda_role = iam.Role(
        "Version": "2012-10-17",
        "Statement": [
                "Action": "sts:AssumeRole",
                "Principal": {
                    "Service": ""
                "Effect": "Allow",
                "Sid": ""

# Attach policy to the role
lambda_role_policy = iam.RolePolicy(
        "Version": "2012-10-17",
        "Statement": [
            "Effect": "Allow",
            "Action": [
            "Resource": "*"

# Create Lambda function and attach the created role 
lambda_fn = lambda_.Function(
        '.': pulumi.FileArchive('.')

# Allow lambda to get events
lambda_event = lambda_.EventSourceMapping(

# Export created assets (buckets, lambda function)
pulumi.export('table', ddb.arn)
pulumi.export('lambda function', lambda_fn.arn)

In the root of your directory, create a python file that will contain the code for the function’s handler. (Be sure to match the handler value for the lambda object in the above program to the file name you create).

import boto3
import os
from contextlib import closing

def lambda_handler(event, context):

    for record in event['Records']:

        dynamodb_record = record["dynamodb"]
        textID = dynamodb_record["Keys"]["Id"]["N"]
        text_content = dynamodb_record["NewImage"]["text"]
        print("Text to Speech function. Text ID in DynamoDB: {}".format(textID))

        #For each block, invoke Polly API, which will transform text into audio
        polly = boto3.client('polly')
        response = polly.synthesize_speech(
            Text = text_content["S"],
            VoiceId = "Joey")
        #Save the audio stream returned by Amazon Polly on Lambda's temp directory.
        if "AudioStream" in response:
            with closing(response["AudioStream"]) as stream:
                output = os.path.join("/tmp/", textID)
                with open(output, "wb") as file:

        s3 = boto3.client('s3')

        s3.upload_file('/tmp/' + textID, 
        textID + ".mp3")


You can deploy the program to AWS by just executing

pulumi up

Based on how the program is written, Pulumi will automatically provision to reach the resource’s desired state.

Once the deployment is successful, insert any random text data into the dynamoDB table, ensuring that the content is entered against a ‘text’ attribute because the handler function is programmed to look for one. The converted text-to-speech mp3 file will be present in the bucket specified when the function finishes executing.

Testing with Pulumi:

Since these programs can be written in general-purpose programming languages, Pulumi makes it possible to take advantage of native testing frameworks for running automated tests. Pulumi supports three styles of performing automated tests for cloud programs.

Unit testing:
The nature of these tests is to evaluate the behavior of your code in isolation. Running in-memory, the external dependencies for these tests are replaced by mocks. They are authored in the same language as the Pulumi program. The focus of these tests is not on the behavior of the cloud resources but rather on their inputs.

For instance, we can create unit tests for the above program that verify three things.

  • Lambdas should have a minimum timeout of 30 sec.
  • Buckets to have ‘bucket’ parameter. Meaning that bucket’s auto-naming by Pulumi should be disabled.
  • DynamoDB table should have ‘stream_view_type’ attribute value as ‘NEW_IMAGE’

At the project root, make a copy of the above Pulumi program into another file. Inside a new python file place the following unit tests code.

import unittest
import pulumi

class MyMocks(pulumi.runtime.Mocks):
    def new_resource(self, type_, name, inputs, provider, id_):
        return [name + '_id', inputs]
    def call(self, token, args, provider):
        return {}


# It's important to import the program after the mocks are defined.
import  as infra

class TestingWithMocks(unittest.TestCase):
    # check 1: Lambdas should have a minimum timeout of 30sec.
    def test_lambda_timeout(self):
        def check_timeout(args):
            urn, timeout = args
            self.assertIsNotNone(timeout, f'lambda {urn} must have timeout')
            self.assertGreaterEqual(timeout,30,f'lambda {urn} must have a timeout greater or equal to 30')
        return pulumi.Output.all(infra.lambda_fn.urn, infra.lambda_fn.timeout).apply(check_timeout)

    # check 2: Bucket to have 'bucket' parameter.
    def test_bucket_name(self):
        def check_name(args):
            urn, bucket = args
            self.assertIsNotNone(bucket, f'Bucket {urn} must have a bucket parameter with a name')
        return pulumi.Output.all(infra.bucket_tts.urn, infra.bucket_tts.bucket).apply(check_name)    

    # check 3: DynamoDB table should have stream_view_type as NEW_IMAGE
    def test_dynamodb_stream_type(self):
        def check_stream_view(args):
            urn, stream_view_type = args
            self.assertIsNotNone(stream_view_type, f'Table {urn} should have stream_view_type parameter')
            self.assertEqual('NEW_IMAGE',stream_view_type,f'Table should have NEW_IMAGE as stream view type value')
        return pulumi.Output.all(infra.ddb.urn, infra.ddb.stream_view_type).apply(check_stream_view)

Alter the program’s values such that tests fail. Utilizing the python’s built-in test framework, unit test, we’ll execute these tests by running the command

python -m unittest 
Pulumi unit tests

Property testing:
These tests are executed while the resources are being deployed. They are based on Pulumi’s offering called CrossGuard (Policy as code). While Policy as Code and Property Testing both use the same technology, the goals and workflows are different. As opposed to unit testing, property tests can evaluate real values returned from the cloud provider instead of the mocked ones. Property tests are limited to Node.js and Python only.

To implement property testing, create a folder ‘tests’ at project root. Create ‘’ and ‘PulumiPolicy.yaml’ inside that folder. Install pulumi’s policy pack library by running

pip install pulumi-policy

Add this to PulumiPolicy.yaml.

description: A minimal Policy Pack for AWS using Python.
runtime: python
version: 0.0.1

Place this inside the of the ‘tests’ folder.

from pulumi_policy import (
import json

# Check for DynamoDB stream enabled value
def dynamodb_stream_validation(args: ResourceValidationArgs, report_violation: ReportViolation):
    if args.resource_type == "aws:dynamodb/table:Table" and "streamEnabled" in args.props:
        stream_value = args.props["streamEnabled"]
        if stream_value != True:
            report_violation("For this program to work DynamoDB streams need to be enabled")

stream_check = ResourceValidationPolicy(
    description="Check if streams are enabled.",

# Check if policies are included
def iam_policy_validation(args: ResourceValidationArgs, report_violation: ReportViolation):
    if args.resource_type == "aws:iam/rolePolicy:RolePolicy" and "policy" in args.props:
        policy = json.loads(args.props["policy"])
        policies = policy["Statement"][0]["Action"]
        if "s3:PutObject" not in policies or "polly:SynthesizeSpeech" not in policies:
            report_violation("For this program to work S3 and Polly policies needed")

policy_check = ResourceValidationPolicy(
    description="Check if permissions are present.",


These tests verify two items: the dynamoDB table must have streams enabled for the lambda function to get invoked, and the policy attached to the lambda role must have permissions for S3 and Polly services.

To run these tests, execute

pulumi up --policy-pack tests
Pulumi property testing

It’s only when the tests pass that deployment will proceed. This way you can ensure the created resources on the cloud are compliant with your project’s requirements.

Integration testing:
This form of testing runs the program in combination with Pulumi CLI to deploy the resources to an ephemeral environment. After running tests on the created infrastructure, the resources are destroyed. The prime responsibility of these tests is to ensure that the project is deployed to the expected state, and the programmed logic works as intended. The duration for these tests is considerably longer (in minutes) in comparison to the above variations.

Pulumi has an extensive set of integration tests written in Go. You can utilize their Go test framework irrespective of the language your Pulumi program is written in. Although, presently, support for integration tests for other languages from Pulumi aren’t available. If you would like to explore, there is a community developed framework for python called Pitfall.


By utilizing Pulumi’s testing concepts, you can ensure that the provisioning of resources is in line with your project’s requirements and the system works as intended. Migration to Pulumi for programs created with other tools is certainly possible by following their strategy from this doc. Not only that, but Pulumi also has a tool, tf2pulumi, that converts the IaC code written for Terraform in HCL (HashiCorp Configuration Language) to a Pulumi program. They also have integrations for any continuous integration/continuous delivery (CI/CD) system. Writing infrastructure deployment programs with Pulumi is simplified because you have the option to choose the language for creating the program.

More from Serverless Guru

Join the Community

Gather, share, and learn about AWS and serverless with enthusiasts worldwide in our open and free community.