Static Website Hosting/Publishing using AWS S3, AWS CloudFront, SSL Certificate and Custom FQDN

AWS Cloud

Amazon CloudFront is a content delivery network operated by Amazon Web Services. Content delivery networks provide a globally-distributed network of proxy servers to cache content, such as web videos or other bulky media, more locally to consumers, to improve access speed for downloading the content (citation).

Although AWS CloudFront can be used as a CDN (caching layer) for dynamic websites, it also allows for the hosting and publishing for a static website without the use of web servers in the situation where the website needs no server side functionality, or the server-side functionality can be provided by servers, but there is significant static content that can be delivered too.

Static Website Example

In this example we have a static website called mysite.domain.com that hosts an index.html page with an image, the site content is uploaded to an AWS S3 bucket, whereby it is published via AWS CloudFront (with an attached SSL Certificate) by use of the custom domain https://mysite.domain.com

The FQDN mysite.domain.com is configured as a CNAME that points at the AWS CloudFront CDN, which is in the format <randomalphanumericstring>.cloudfront.net, when a user queries for the mysite.domain.com this CNAME points at the CloudFront CDN FQDN (as shown earlier), this is held by the AWS DNS Name Servers that then respond with an IP address of the nearest endpoint whereby they can access the CloudFront CDN for this particular site, the user is delivered the content. If the content does not happen to be in that local “edge location” i.e. not cached, a request is made to the S3 bucket via AWS’s network to retrieve the resource(s). It is possible to add additional security by use of the AWS Shield and AWS WAF, it is also possible to add additional resilience by deploying a second S3 bucket with the content and set AWS CloudFront to use the primary S3 bucket, unless that is unavailable.

AWS CloudFormation Template

The following AWS CloudFormation template provides an example template which will deploy the required AWS resources, all you need to do is upload your content and it is then published with high availability across the globe within a few minutes; this without the need for multiple datacentres and the supporting hardware/software to complete this.

You can find the example template here:

There is a one-to-one relationship implied by the of the CloudFront CDN to the origin (S3 Bucket(s) of the website. i.e. You need one distribution per website. To this end, the use of a CloudFormation Stack per website seems logical, although not necessary it gives the option for either hard-coding the variables of the site into the Stack template, or passing them in at runtime. In this particular configuration, having one Stack Template per website and not passing in parameters to create or update means the entire “configuration” of a website can be held within the Stack Template, the website resource files are of course held within the relevant S3 bucket.

CloudFront allows for publishing a website via an auto-generated endpoint FQDN (e.g. d2vjgket99pivd.cloudfront.net) out across the whole world without the need for infrastructure to support this massive scale and availability. All requests are sourced from the origin S3 bucket (or cache depending). You can specify multiple origin S3 buckets (within an origin group), it is therefore recommended to have at least two S3 Buckets located in different AWS Regions and a failover rule configured, so that the prolonged loss of a particular S3 bucket does not result in the loss of a website’s availability.

The CloudFront allows for use of the Web Application Firewall and AWS Shield to provide additional protection of your resources from attack and also excessive service/bandwidth charges.

A very simple example that deploys a static website using a (origin) S3 bucket for the website files, CloudFront (CDN) with SSL Certificate, with web access logging enabled into another separate S3 bucket for logging purposes.

Usage Steps

To deploy your static website you should follow this process, you must ensure that you store the template of any deployed website within version control, so that the current configuration can be seen and any changes may be made at a future date as required.

1. Make a copy of the CloudFormation template file and name to your site’s FQDN, e.g. “mysite.domain.com.yaml”.

2. Create SSL Certificate that references the FQDN(s) (of the site). Once created get the ARN of this certificate for use within this template. The certificate must be created in us-east-1 even if the CloudFront Distribution is not being used in us-east-1.

3. Cutomise your template file as follows:

  • 3.1. Update the “Description” (line 2) with the primary FQDN of the website.
  • 3.2. Update the parameter “DomainName” with the site’s primary FQDN, e.g. “mysite.domain.com”.
  • 3.3. Update the parameter “CertificateARN” with the ARN of the certificate you just created.

4. Deploy the template by running the following, the AWS Region to which are you logged into is where your stack will be deployed.

aws cloudformation create-stack --template-body file://<Site FQDN>.yaml --stack-name <Site FQDN>

(info) The Stack-Name must only contain letters, numbers and hyphens only.

5. Using the output CNAME for the CloudFront Distribution create a CNAME to point your site FQDN(s) at this CloudFront Distribution CNAME.

6. Upload content to the site S3 Bucket for presentation.

7. Test and verify operation of the website and verify logging is working as expected.

AWS CloudFront Deployment Deep-Dive and Analysis

The first step is to create your SSL Certificate, you do this using the AWS Certificate Manager to create a certificate, then you validate it via, in this example DNS; which requires you create a CNAME within the domain that allows AWS to verify you own the domain.

Once created you need to retrieve the ARN (AWS Resource Name) of the certificate for you’ll need this in your template to refer to the certificate from your AWS CloudFront resource.

(tick) The certificate must be created in us-east-1 even if the CloudFront Distribution is not being used in us-east-1.

Parameters

The parameters are the variables that are going to be used by CloudFormation when you deploy the template, in this case there are two you need to enter:

Parameters:
   
  DomainName:
    Description: The primary FQDN of the website, it will also become the prefix for all Origin and Logging bucket names to ensure uniqueness and ease of identification.
    Type: String
    Default: "<Site FQDN>"
 
  CertificateARN:
    Description: The ARN of the certificate to be used by the CloudFront Distribution. Certificate must be in us-east-1.
    Type: String
    Default: "<SSL Certificate ARN>"

NOTE: It is possible to have multiple FQDNs that point/resolve to a single website and in this case CloudFront distribution, however you will need to adjust the template to allow this to work, with multiple DomainName parameters and also adding multiple “Aliases” to both your SSL Certificate and your DNS.

AWS S3 Bucket (Origin Bucket)

The following defines the AWS S3 bucket where you’d upload your website resource files. The configuration ensures that the bucket is not accessible directly from the outside world, this is by design. The only way to retrieve objects from the bucket (as an end-user) would be via the AWS CloudFront distribution endpoint.

##################################################
# S3 Origin Bucket Configuration (Website Source)
##################################################

  OriginBucket1:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Join ['-',[!Ref 'DomainName', 'originbucket1']]
      PublicAccessBlockConfiguration: 
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      Tags:
        - Key: Stack
          Value: !Sub '${AWS::StackName}'
        - Key: Name
          Value: !Join ['-',[!Ref 'DomainName', 'originbucket1']]

AWS CloudFront Distribution

The meat of the AWS CloudFront Distribution is shown below, there is a one to one mapping between a Distribution and a Website, so the below creates a CloudFront Distribution, it then sets the aliases, i.e. the DomainName passed from the parameter, in this example that is: mysite.domain.com. You then specify the default root object, e.g. index.html, so when a user queries for https://mysite.domain.com they get the default index page returned. 

You then specify the origin S3 bucket where the resource is held, this can be extended to have multiple S3 buckets as the origin to improve availability, but this is beyond the scope of this document.

The certificate is then specified by the Parameter that contains the Certificate ARN you created earlier.

The distribution configuration also declares where the web logs should go, i.e. another S3 bucket, but this is covered later.

##########################################################################
# CloudFront Distribution
##########################################################################

  OriginDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Aliases:
          - !Ref 'DomainName'
        Comment: !Ref 'DomainName'
        DefaultCacheBehavior:
          Compress: true
          ForwardedValues:
            QueryString: false
          TargetOriginId: OriginBucket1 #S3Origin
          ViewerProtocolPolicy: redirect-to-https
        DefaultRootObject: index.html
        CustomErrorResponses:
          - ErrorCachingMinTTL: 300
            ErrorCode: 403
            ResponseCode: 200
            ResponsePagePath: /error.html
          - ErrorCachingMinTTL: 300
            ErrorCode: 404
            ResponseCode: 200
            ResponsePagePath: /error.html
        Enabled: true
        HttpVersion: http2
        IPV6Enabled: true
        Origins:
          - Id: OriginBucket1
            DomainName: !GetAtt OriginBucket1.DomainName # Refers to the FQDN of the S3 Bucket
            S3OriginConfig:
              OriginAccessIdentity: '' #old way
            OriginAccessControlId: !GetAtt CloudFrontOriginAccessControl.Id
        PriceClass: PriceClass_All # Global Distribution 'PriceClass_All', 'PriceClass_200' US and EU, 'PriceClass_100' Asia.
        ViewerCertificate:
          AcmCertificateArn: !Ref CertificateARN
          MinimumProtocolVersion: TLSv1
          SslSupportMethod: sni-only
        Logging:
          Bucket: !GetAtt AccessLogBucket.DomainName # Refers to the FQDN of the S3 Bucket
          IncludeCookies: false
          Prefix: cloudfront/
      Tags:
        - Key: Stack
          Value: !Sub '${AWS::StackName}'
        - Key: Name
          Value: !Join ['-',[!Ref 'DomainName', 'cloudfront-distribution']]

S3 Origin Bucket Policy Configuration

To ensure your website resources can be distributed by CloudFront, you need to create an Access Policy that allows the AWS CloudFront resource to access the S3 Origin bucket that contains the website, this grants the CloudFront “getObject” permissions to retrieve the website resource files from the bucket to be presented to the user.

######################################################################################################
# S3 Origin Bucket Policy Configuration - Allows access to Origin Buckets from CloudFront Distribution
######################################################################################################

  OriginBucket1Policy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref OriginBucket1
      PolicyDocument:
        Statement:
        - Action: s3:GetObject
          Effect: Allow
          Resource: !Sub ${OriginBucket1.Arn}/*
          Principal:
            Service: cloudfront.amazonaws.com
          Condition:
            StringEquals:
              AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${OriginDistribution}

  CloudFrontOriginAccessControl:
    Type: AWS::CloudFront::OriginAccessControl
    Properties: 
      OriginAccessControlConfig:
        Description: !Join [' - ',[!Ref 'DomainName', 'Origin Access Control']]
        Name: !Join ['-',[!Sub '${AWS::StackName}', 'oac']]
        OriginAccessControlOriginType: s3
        SigningBehavior: always
        SigningProtocol: sigv4

S3 Logging Bucket

The final part is declaring the logging bucket. Again you create a bucket, you specify some retention information and then set an Access Policy that allows the CloudFront Distribution to be able to write access log files into the bucket. When a user accesses the website, CloudFront logs the website access logs into this bucket (akin to what you’d have on a normal web server). It is also possible to add additional logging, but this is beyond the scope of this document/example.

#######################################################
# S3 Logging Bucket Configuration (Logging Destination)
#######################################################

  AccessLogBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Join ['-',[!Ref 'DomainName', 'accesslogbucket']]
      OwnershipControls:
        Rules:
          - ObjectOwnership: ObjectWriter # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket-ownershipcontrolsrule.html
      PublicAccessBlockConfiguration: 
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      LifecycleConfiguration:
        Rules:
          - Id: Retain2yrs
            Status: Enabled
            ExpirationInDays: 730
            Transitions:
              - StorageClass: STANDARD_IA
                TransitionInDays: 30
      Tags:
      - Key: Stack
        Value: !Sub '${AWS::StackName}'
      - Key: Name
        Value: !Join ['-',[!Ref 'DomainName', 'accesslogbucket']]

  AccessLogBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref AccessLogBucket
      PolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: cloudfront.amazonaws.com
            Action: s3:* # Bit open, don't you think?
            Resource:
              - !Sub ${AccessLogBucket.Arn}/*
              - !GetAtt AccessLogBucket.Arn
            Condition:
              StringEquals:
                AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${OriginDistribution}

Outputs

There is an outputs section, you can specify additional information if you so wish, in this example it provides the Origin Distribution Domain Name, which is the autogenerated FQDN in the format: <randomalphanumericstring>.cloudfront.net which you’ll need to point your website’s FQDN(s) at for them to be able to access the website.

Outputs:

  CloudFrontDomainName:
    Description: "FQDN to point CNAMEs(that clients will use) to access your website."
    Value: !GetAtt OriginDistribution.DomainName

Invalidations

An invalidation sounds more serious than it really is, in essence an Invalidation is a way to expire resources within the AWS CloudFront cache before they would normally expire (or refresh) by themselves, you can find more details in: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html, however the key take away would be if you are adding, updating or removing resource files from your origin S3 bucket, to ensure your site reflects the changes quickly you may want to create an invalidation that triggers a refresh of all the objects. Of course this generates traffic and therefore cost, but you may want to do this to ensure changed objects are reflected as soon as possible on your website and deleted objects are purged as soon as required.

Additional Information

Image Attribution

1 thought on “Static Website Hosting/Publishing using AWS S3, AWS CloudFront, SSL Certificate and Custom FQDN

Leave a Reply

Your email address will not be published. Required fields are marked *