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?
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?
-
Visit the (Amazon Web Services Console at (https://console.aws.amazon.com/cloudformation/ and choose Create Stack > With new resources (standard).
-
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.
-
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
-
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.
-
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.
-
Sit back and relax while CloudFormation builds your Stack, provisioning Amazon S3, CloudFront, IAM Roles, Bucket Policies, Lambda Functions and CloudFront Behaviors. 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