logo

Using AWS CloudFormation to Deploy a Serverless Web Delivery Infrastructure

Deploy a Serverless Web Delivery Infrastructure using Amazon CloudFormation.

In this post, I will guide you through the steps to deploy a serverless Web Delivery Infrastructure using Amazon Web Services.

What is a Serverless Web Delivery Infrastructure?

Serverless CloudFormation Diagram I use Amazon Web Services for my blog, ptylr.com. I deploy it using a serverless configuration consisting of:

  • An Amazon S3 Bucket to store files;
  • Amazon CloudFront to provide edge-based caching of these files upon request;
  • AWS Lambda@Edge Functions to handle typical web-server based functions, such as generating search-optimised URIs, default documents and file restriction;
  • AWS Certificate Manager to provision and manage SSL/TLS certificates;
  • Amazon Route-53 for DNS management.

The result is a simple to manage, highly-available, highly-scalable delivery topology that is also very cost-efficient.

How Do I Deploy a Serverless Web Delivery Infrastructure?

To deploy and maintain the infrastructure for ptylr.com, I use AWS CloudFormation.

Per Amazon themselves (https://aws.amazon.com/cloudformation/),

> AWS CloudFormation allows you to use programming languages or a simple text file to model and provision, in an automated and secure manner, all the resources needed for your applications across all regions and accounts.

Could I manage a Serverless Web Delivery Infrastructure without using CloudFormation?

Yes, of course. You could configure each of the Amazon Services separately via the AWS CLI or Console. However, CloudFront is designed to take all the management effort away from you. It will automatically stand-up all of the infrastructure that you need, update it whenever you change the configuration and ultimately decommission it when no longer required. What’s more, there’s no additional charge for using CloudFormation - you pay only for the resources that it provisions.

What are the Steps to Deploy a Serverless Web Delivery Infrastructure, using AWS CloudFormation?

  1. Visit the (Amazon Web Services Console at (https://console.aws.amazon.com/cloudformation/ and choose Create Stack > With new resources (standard). Create new CloudFormation Stack - Screenshot

  2. Under “Prerequisite - Prepare template”, select Template is ready and choose Amazon S3 URL in “Template Source”. Enter the location of the CloudFormation Template that you want to use. You are welcome to use mine, which will create the Serverless Web Delivery Infrastructure in the diagram above (see below), then click Next. Choose Template - Screenshot

  3. Complete the following details and then click Next:

    • Stack Name - Give the Stack a memorable name - I normally use the FQDN and replace the dots with hyphens;
    • AcmCertificateArn - The ARN of the AWS Certificate Manager that has a SAN for your FQDN. Note: If your certificate does not have a SAN for your FQDN, CloudFormation will terminate with an error.
    • FQDN Specify Stack Details - Screenshot
  4. The next screen (“Configure stack options”) allows you to configure properties on the Stack that you might want. I do not change the defaults and simply click Next. Specify Stack Details - Screenshot

  5. Now review the deployment configuration that CloudFormation will build. You will need to check the box to confirm that “I acknowledge that AWS CloudFormation might create IAM resources.”. Once done, click Create Stack. Specify Stack Details - Screenshot

  6. Sit back and relax while CloudFormation builds your Stack, provisioning Amazon S3, CloudFront, IAM Roles, Bucket Policies, Lambda Functions and CloudFront Behaviors. Specify Stack Details - Screenshot Once your Stack has been provisioned, which will normally take around 15 minutes or so, you will be able to assign DNS Records to your CloudFront Distribution, upload some content into the S3 Bucket and start serving your content.

Example CloudFormation Template for Serverless Infrastructure

AWSTemplateFormatVersion: 2010-09-09
Description: >-
  AWS Resources to deliver a static website via S3, CloudFront & Lambda@Edge
  (with EdgeServices)
Parameters:
  FQDN:
    Type: String
    Description: FQDN of website.
    Default: ptylr.com
  AcmCertificateArn:
    Type: String
    Description: ARN of the certificate to use for the CloudFront distribution.
  RolePermissionBoundaryArn:
    Type: String
    Description: ARN of the permission boundary for IAM Role creation.
    Default: ''
Conditions:
  HasRolePermissionBoundaryArn:
    !Not [ !Equals [!Ref RolePermissionBoundaryArn, ''] ]
Resources:
  CloudFrontDistribution:
    Type: 'AWS::CloudFront::Distribution'
    Properties:
      DistributionConfig:
        Aliases:
          - !Ref FQDN
        DefaultCacheBehavior:
          Compress: true
          ForwardedValues:
            QueryString: true
            Headers:
              - Authorization
          TargetOriginId: !Sub '${S3Bucket}.s3.amazonaws.com'
          ViewerProtocolPolicy: redirect-to-https
          MinTTL: 0
          AllowedMethods:
            - HEAD
            - GET
          CachedMethods:
            - HEAD
            - GET
          LambdaFunctionAssociations:
            - EventType: origin-request
              LambdaFunctionARN: !Ref OriginRequestLambdaFunctionVersion
            - EventType: origin-response
              LambdaFunctionARN: !Ref OriginResponseLambdaFunctionVersion
        Enabled: true
        HttpVersion: http2
        Origins:
          - DomainName: !Sub '${S3Bucket}.s3.amazonaws.com'
            Id: !Sub '${S3Bucket}.s3.amazonaws.com'
            S3OriginConfig:
              OriginAccessIdentity:
                !Sub 'origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}'
        PriceClass: PriceClass_All
        ViewerCertificate:
          AcmCertificateArn: !Ref AcmCertificateArn
          MinimumProtocolVersion: TLSv1.1_2016
          SslSupportMethod: sni-only
      Tags:
        - Key: Domain
          Value: !Ref FQDN
  CloudFrontOriginAccessIdentity:
    Type: 'AWS::CloudFront::CloudFrontOriginAccessIdentity'
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: !Sub 'CloudFront Origin Access Identity for ${FQDN}'
  S3Bucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      AccessControl: Private
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      Tags:
        - Key: Domain
          Value: !Ref FQDN
  S3BucketPolicy:
    Type: 'AWS::S3::BucketPolicy'
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Statement:
          - Action:
              - 's3:GetObject'
            Effect: Allow
            Resource: !Sub 'arn:aws:s3:::${S3Bucket}/*'
            Principal:
              CanonicalUser: !GetAtt
                - CloudFrontOriginAccessIdentity
                - S3CanonicalUserId
          - Action:
              - 's3:GetObject'
            Effect: Allow
            Resource: !Sub 'arn:aws:s3:::${S3Bucket}/*'
            Principal: '*'
            Condition:
              StringLike:
                'aws:Referer':
                  - !Sub '${AWS::Region}.${OriginRequestLambdaFunction}'
                  - !Sub '${AWS::Region}.${OriginResponseLambdaFunction}'
  OriginRequestLambdaFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Description: >-
        EdgeServices - A Pair of Lambda@Edge Functions for Executing Common Operations on Request (see https://github.com/ptylr/Lambda-at-Edge/tree/master/EdgeServices)
      Code:
        S3Bucket: lambda-ptylr-com
        S3Key: EdgeServices_OriginRequest.zip
        S3ObjectVersion: WUceMAAQDakBdZl5rhHY82MaozSA2pld
      Handler: handler.handler
      MemorySize: 128
      Role: !Sub '${LambdaFunctionExecutionRole.Arn}'
      Runtime: nodejs12.x
      Tags:
        - Key: Domain
          Value: !Ref FQDN
  OriginRequestLambdaFunctionVersion:
    Type: 'AWS::Lambda::Version'
    Properties:
      FunctionName: !Ref OriginRequestLambdaFunction
      Description: !Sub 'EdgeServices-OriginRequest for ${FQDN}'
  OriginResponseLambdaFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Description: >-
        EdgeServices - A Pair of Lambda@Edge Functions for Executing Common Operations on Request (see https://github.com/ptylr/Lambda-at-Edge/tree/master/EdgeServices)
      Code:
        S3Bucket: lambda-ptylr-com
        S3Key: EdgeServices_OriginResponse.zip
        S3ObjectVersion: fmArTnlFsUZyZYhZl6Sok4H7JK6aw42h
      Handler: handler.handler
      MemorySize: 128
      Role: !Sub '${LambdaFunctionExecutionRole.Arn}'
      Runtime: nodejs12.x
      Tags:
        - Key: Domain
          Value: !Ref FQDN
  OriginResponseLambdaFunctionVersion:
    Type: 'AWS::Lambda::Version'
    Properties:
      FunctionName: !Ref OriginResponseLambdaFunction
      Description: !Sub 'EdgeServices-OriginResponse for ${FQDN}'
  LambdaFunctionExecutionRole:
    Type: 'AWS::IAM::Role'
    Properties:
      PermissionsBoundary:
        Fn::If:
          - HasRolePermissionBoundaryArn
          - Ref: RolePermissionBoundaryArn
          - Ref: AWS::NoValue
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - edgelambda.amazonaws.com
                - lambda.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
  LambdaFunctionExecutionPolicies:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: IAMP-LambdaFunctionExecutionPolicy
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action:
              - 's3:ListAllMyBuckets'
              - 's3:GetBucketLocation'
            Resource: '*'
          - Effect: Allow
            Action:
              - 's3:GetObject'
              - 's3:ListBucket'
            Resource:
              - !Sub 'arn:aws:s3:::${S3Bucket}'
              - !Sub 'arn:aws:s3:::${S3Bucket}/*'
      Roles:
        - !Ref LambdaFunctionExecutionRole