aws static web cloudfront

If you want to know how to deploy a static web in Cloudfront, using S3 to store your files and with your own domain this is your site

AWS Services and Tools

First of all, let’s do a little recap of the services that we will use

AWS S3

Amazon Simple Storage Service (Amazon S3) is an object storage service that offers industry-leading scalability, data availability, security, and performance

AWS S3 page – https://aws.amazon.com/s3/

This service offers buckets where you can put files. We will use this service to storage our web files

AWS Cloudfront

Amazon CloudFront is a fast content delivery network (CDN) service that securely delivers data, videos, applications, and APIs to customers globally with low latency, high transfer speeds, all within a developer-friendly environment

AWS Cloudfront page – https://aws.amazon.com/cloudfront/

With Cloudfront we can distribute our files all over the world. We will use this service to create and endpoint connected to our S3 bucket to serve our web

AWS Certificate Manager

AWS Certificate Manager is a service that lets you easily provision, manage, and deploy public and private Secure Sockets Layer/Transport Layer Security (SSL/TLS) certificates for use with AWS services and your internal connected resources

AWS Certificate Manager page – https://aws.amazon.com/certificate-manager/

With Certificate Manager you can create SSL Certificates to serve our web with https and with a nice url

AWS CDK

The AWS Cloud Development Kit (AWS CDK) is an open source software development framework to model and provision your cloud application resources using familiar programming languages

AWS CDK page – https://aws.amazon.com/cdk/

With the Cloud development Kit developed by AWS you can define your infrastructure in several programming languages with handy features like loops, conditionals, etc…

Prerequisites

  • An AWS account: If you don’t have one you can check this page to create a new one
  • AWS credentials created: You must setup your AWS credentials in your account. See this link to get more information on how to create them. Then use this link to know how to config your credentials in your environment
  • AWS CDK installed: You can see how to install the CDK in this page
  • An internet domain: You can buy a domain in many pages. Use your favorite one

Let’s start

Create a new directory and config your environment. In this tutorial we will use Python as the language

mkdir static-example
cd static-example
cdk init --language python
python3 -m venv .env
source .env/bin/activate
pip install -r requirements.txt

With this command we will have the environment with the python modules installed and with an empty cdk project like this:

.
├── README.md
├── app.py
├── cdk.json
├── requirements.txt
├── setup.py
└── static_example
    ├── __init__.py
    └── static_example_stack.py

Let me point the important files:

  • app.py: Entrypoint to our infrastructure. See Apps on the CDK Documentation for more information
  • static_example/static_example_stack.py: Our stack. See Stacks on the CDK Documentation for more information
  • setup.py: Setup configurations of the project

In the static_example.py file we will define our AWS resources

Creating the S3 bucket

We need to install the aws-cdk.aws-s3 module. Edit setup.py and add the module in the install_requires variable

install_requires=[
  "aws-cdk.core==1.23.0",
  "aws-cdk.aws-s3==1.23.0"
]

Install the new modules from the console

pip install -r requirements.txt

Modify static_example_stack.py

from aws_cdk import (
    core,
    aws_s3 as s3
)

class StaticExampleStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        bucket = s3.Bucket(self, "MyBucket",
                           access_control=s3.BucketAccessControl.PUBLIC_READ,
                           website_index_document="index.html"
                           )
        bucket.grant_public_access()

In line 3 we are importing the s3 module
In line 11 we create our bucket. We must set PUBLIC_READ in order to let anyone from internet access the bucket
In line 15 we grant public access to make this bucket public

Synthesize an AWS Cloudformation template by running

cdk synth

This command will create an AWS Cloudformation and store it in a new directory cdk.out

.
├── README.md
├── app.py
├── cdk.context.json
├── cdk.json
├── cdk.out
│   ├── cdk.out
│   ├── manifest.json
│   ├── static-example.template.json
│   └── tree.json
├── requirements.txt
├── setup.py
└── static_example
    ├── __init__.py
    └── static_example_stack.py

You can see the template opening the file static-example.template.json

Now we can deploy our stack to AWS

cdk deploy

Congratulations you’ve created your first bucket with CDK

Create the certificate

We need to create a certificate in order to serve our page over https with our own domain

There is a caveat when creating the certificate. If you want to use it with cloudformation you must create the certificate in the us-east-1 region. If you’re deploying your website to this region there is no problem, but if you’re trying to deploy in other region you have to do a little trick in order to make the stack works

First we will need the aws-cdk.aws-ssm and aws-cdk.aws-certificatemanager modules. Edit the setup.py file and add them to the install_requires variable

install_requires=[
  "aws-cdk.core==1.23.0",
  "aws-cdk.aws-s3==1.23.0",
  "aws-cdk.aws-certificatemanager==1.23.0",
  "aws-cdk.aws-ssm==1.23.0"
]

Install the new requirements

pip install -r requirements.txt

Create a file called certificate_stack.py in the static_example directory like this

from aws_cdk import (
    core,
    aws_certificatemanager as cm,
    aws_ssm as ssm,
)

class CertificateStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        certificate = cm.Certificate(self, "MyCertificate",
                                     domain_name="static.rubenjgarcia.es",
                                     validation_method=cm.ValidationMethod.DNS
                                     )

        ssm.StringParameter(self, "MyCertificateARNParameter",
                            parameter_name="certificate-arn",
                            string_value=certificate.certificate_arn
        )

In line 12 we create the certificate. Put your domain name like I’ve done in line 13
In line 17 we create a parameter in the AWS Systems Manager Parameter Store. We need to create this parameter to store the ARN of our certificate because we can’t access to the information of one stack from another in other region in Cloudformation

Now we have to edit the app.py file to create another stack. We have to do this because we are creating this stack in other region so we have to change the environment of the stack

#!/usr/bin/env python3

from aws_cdk import core

from static_example.static_example_stack import StaticExampleStack
from static_example.certificate_stack import CertificateStack

app = core.App()
CertificateStack(app, "certificate", env=core.Environment(region="us-east-1"))
StaticExampleStack(app, "static-example")

app.synth()

In line 6 we import our new stack
In line 9 we create the new stack with a new environment pointing to us-east-1 region

Now we can deploy our new stack

cdk deploy certificate

AWS must validate that you are the owner of the domain so when the stack will be deploying you have to add a DNS in your server. You will see something like this while the stack is deploying

Content of DNS Record is: {Name: _xxx.static.rubenjgarcia.es.,Type: CNAME,Value: _xxx.acm-validations.aws.}

Add the CNAME in your server. After a few seconds the deployment will continue

Create Cloudfront distribution

The last step is create the Cloudfront distribution. We need to add 2 more modules in setup.py, aws-cdk.aws-cloudfront and aws-cdk.custom-resources

install_requires=[
  "aws-cdk.core==1.23.0",
  "aws-cdk.aws-s3==1.23.0",
  "aws-cdk.aws-certificatemanager==1.23.0",
  "aws-cdk.aws-ssm==1.23.0",
  "aws-cdk.aws-cloudfront==1.23.0",
  "aws-cdk.custom-resources==1.23.0"
]

Install the new requirements again

pip install -r requirements.txt

Modify the static_example_stack.py like this

import time

from aws_cdk import (
    core,
    aws_s3 as s3,
    aws_cloudfront as cf,
    custom_resources as cr
)

class StaticExampleStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        bucket = s3.Bucket(self, "MyBucket",
                           access_control=s3.BucketAccessControl.PUBLIC_READ,
                           website_index_document="index.html"
                           )
        bucket.grant_public_access()

        parameter = cr.AwsCustomResource(self, "GetCertificateArn",
                                         on_update=cr.AwsSdkCall(
                                             service="SSM",
                                             action="getParameter",
                                             parameters={
                                                 "Name": "certificate-arn"
                                             },
                                             region="us-east-1",
                                             physical_resource_id=str(time.time)
                                         )
                                         )

        cf.CloudFrontWebDistribution(self, "CDN",
                                     price_class=cf.PriceClass.PRICE_CLASS_100,
                                     alias_configuration=cf.AliasConfiguration(
                                         names=["static.rubenjgarcia.es"],
                                         acm_cert_ref=parameter.get_data_string("Parameter.Value"),
                                         ssl_method=cf.SSLMethod.SNI,
                                         security_policy=cf.SecurityPolicyProtocol.TLS_V1_1_2016
                                     ),
                                     origin_configs=[
                                         cf.SourceConfiguration(
                                             behaviors=[
                                                 cf.Behavior(
                                                     is_default_behavior=True)
                                             ],
                                             s3_origin_source=cf.S3OriginConfig(
                                                 s3_bucket_source=bucket
                                             )
                                         )
                                     ]
                                     )

In line 21 we get the parameter that stores the ARN of the certificate using a Custom Resource
In line 33 we create the Cloudfront distribution pointing to our bucket as you can see in line 48

As we are using an agnostic environment to deploy our stack we have to bootstrap it in order to use custom resources

cdk bootstrap

Now you can deploy the static-example to AWS

cdk deploy static-example

Once is deployed we need to create another CNAME pointing our web to the Cloudfront endpoint

Open the manifest.json file in the cdk.out directory. We need the logicalId of our cloudfront distribution. We will find it under the artifacts > static-example > metadata > /static-example/Cloudfront/CFDistribution > data path. You will see something like CloudfrontCFDistribution3AF3BD0B

Now use the AWS CLI to get the resource information from Cloudformation

aws cloudformation describe-stack-resource --stack-name static-example --logical-resource-id CloudfrontCFDistribution3AF3BD0B

This is the output that you will get

{
    "StackResourceDetail": {
        "StackName": "static-example",
        "StackId": "arn:aws:cloudformation:eu-west-1:637673531073:stack/static-example/29e92d90-4aa8-11ea-a6d4-0297e5fc50ee",
        "LogicalResourceId": "CloudfrontCFDistribution3AF3BD0B",
        "PhysicalResourceId": "D3BO1RFLKIX8XA",
        "ResourceType": "AWS::CloudFront::Distribution",
        "LastUpdatedTimestamp": "2020-01-01T00:00:00.000Z",
        "ResourceStatus": "CREATE_COMPLETE",
        "Metadata": "{\"aws:cdk:path\":\"static-example/Cloudfront/CFDistribution\"}",
        "DriftInformation": {
            "StackResourceDriftStatus": "NOT_CHECKED"
        }
    }
}

The value that we are looking for is under the StackResourceDetail > PhysicalResourceId path

Use the CLI to get the domain name of the Cloudfront distribution

aws cloudfront get-distribution --id D3BO1RFLKIX8XA

The value that you are looking is under the path Distribution > DomainName

Create a CNAME of your domain pointing to the value of the DomainName

Upload content to your bucket

Now that we have everything up and running we can upload our files to the bucket and see it in our web browser

First create an index.html file

<!DOCTYPE html>
<html>
  <body>
    <h1>I'm a static web page served from Cloudfront. I live in a S3 bucket</h1>
  </body>
</html>

We need the physical name of our bucket. Use the CLI to get that information like we did with the Cloudfront distribution. The path in the manifest.json file is artifacts > static-example > metadata > /static-example/MyBucket/Resource > data

Once you get the resource details, use the PhysicalResourceId in the path that you pass to the CLI to upload the index.html file

aws s3 cp index.html s3://static-example-mybucketa78f4fb1-1utievfrkntla

And that’s all. You can navigate to your web and you will see the content in the browser