Hosting a Static Website in AWS

Jul 8, 2022

This series of posts will follow the same structure to make it easier in the future to reference them.

Application Title

Victor's Cloud Notes. A static site deployed to AWS

Application Description

The first application I built in the cloud is this blog. It is a static website generated with Nuxt 3 and Content v2, with a little bit of TailwindCSS for styling. The application itself is pretty simple, but it will serve as a record of what I'm learning and as the first application I deploy to AWS.

The static content will live in a S3 bucket. The distribution will be the responsibility of a CloudFront distribution. There is also some interactions with Amazon Route 53 for the CDNs, and even some AWS Lambdas.

Architecture Diagram

StaticWebsiteDiagram.png

Services Used

Here is a little overview of the services used to build this application. Keep in mind that the definitions are my own. I wouldn't take them as a serious study material. They are just the way I currently understand the services and what does they do.

Amazon S3

A storage service. This is the oldest service provided by Amazon. It allow you to store your data on the cloud. The service includes a function to open your content as a static web site. This provides you with a cheap option if you want to host your site. However unless there is something specific you need AWS for, there are free options out there that may be a better option for most of your projects.

Amazon CloudFront

This is Amazon's content delivery network (CDN) service. It will enable you to have a cached version of your site closer to your users.

Amazon Route 53

Amazon's DNS service. This service will help you to route your users to the correct website or application. The service offers you the possibility of register new domains or you can use your existing domains.

Amazon Certificate Manager (ACM)

A service to provision, store, manage and update your SSL/TSL certificates.

Amazon Lambda Functions

A serverless compute offering that allows you to run your code without provisioning or managing any server.

Building Steps

Now this is where thinks are interesting. How do you host a static web site in AWS? and why do you needed Lambda?

Assuming you already have your application. The following summarize the steps you typically need to follow in order to publish it through AWS:

  1. Create a new S3 bucket.
  2. Upload your site files to your new bucket
  3. Set up the correct permissions in your bucket
  4. Create a new distribution in CloudFront
  5. Configure your routes with Amazon Route 53

The order if I were to do it again, is as follow:

1. Configure Route 53 as your CDN for your domain

I already owned this domain (I bough it from a different provider) so I only needed to delegate to Route 53 the DNS service.

1.1 Create a hosted zone in Route 53

  1. Login into your AWS account and go to Route 53
  2. Select Hosted zones in the lateral menu
  3. Click the Create hosted zone button in the main area
  4. Fill the form with your Domain name, and select Public hosted zone as the type. Optionally input a description and the tags you want.
  5. Click the Create hosted zone button

Now you need to use the values in the NS record to update the DNS servers in the provider you have your domain. The steps are dependent on your specific register so I'm not going to provide any. Once you have done it, you need to wait until they are reflected. It can take some minutes or a couple of hours.

1.2 Create a SSL/TLS certificate for our domain

  1. Login into your AWS account and go to Certificate Manager
  2. Select Request certificate in the lateral menu
  3. Choose the Request a public certificate option
  4. Click the Next button at the bottom of the form
  5. Type your domain name in Fully qualified domain name. You can add multiple names if you want for your subdomains, or use an '*' as a wildcard to protect multiple subdomains. For example mysite.com and *.mysite.com will protect all your subdomains for mysite.com
  6. Choose DNS validation option in the Select validation method section
  7. Click the Request button at the end of the form
  8. You will get redirected to the List certificates page. A new notification will apear at the top of the page.
  9. Click the View certificate button in the top notification.
  10. Once in the certificate page, click the Create records in Route 53 button in the Domains region
  11. In the new page, check that everything is ok and click the Create records button

Now you just need to wait until the certificate validation process is finished.

2. Create an S3 bucket to host your site

While you are waiting for the DNS to replicate your changes you can start creating an S3 bucket and uploading your website.

2.1 Create the S3 Bucket

  1. Login into your AWS account and go to S3
  2. Select the Buckets option in the lateral menu
  3. Click the Create bucket button in the main area
  4. Fill the Bucket name and AWS Region in the form. For the rest of the options, you can keep the default values.
  5. Click the Create bucket button at the bottom of the page

With the bucket created, it's time to upload your files.

2.2 Upload your site's files

  1. Login into your AWS account and go to S3
  2. Select the Buckets option in the lateral menu
  3. Click your bucket's name in the table in the main area
  4. Click the Upload button
  5. Drag and drop your files and folders or click the Add files or Add folder buttons to select the files/folders you want to upload
  6. Review and validate that the files are correct in the table
  7. Click the Upload button once you are satisfied
  8. In the status view, click the Close button to return to the bucket's view.

Optional. Check your site is working correctly

Enable website hosting

  1. Login into your AWS account and go to S3
  2. Select the Buckets option in the lateral menu
  3. Click your bucket's name in the table in the main area
  4. Click Properties in the tab options
  5. Scroll down until you reach the Static website hosting section and click its Edit edit button
  6. Select Enable in the form
  7. On the new options, select Host a static website
  8. Type the name of your Index document. The rest of the options can be left in blank
  9. Click Save changes

Before you can view your site you need to grant public access to it.

Grant public access

  1. Login into your AWS account and go to S3
  2. Select the Buckets option in the lateral menu
  3. Click your bucket's name in the table in the main area
  4. Click Permissions in the tab options
  5. In the Block public access section click Edit button
  6. Uncheck Block all public access
  7. Click Save changes
  8. Confirm the change typing confirm in the dialog's textbox and clicking the Confirm button
  9. In the Bucket policy section click the Edit button
  10. Copy-paste the following policy or type a new policy that grants read access to anonymous users. Make sure to replace with your bucket's name. And keep the /* part at the end of the resource name
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicRead",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion"
            ],
            "Resource": [
                "arn:aws:s3:::<your-S3-bucket-name>/*"
            ]
        }
    ]
}
  1. Click Save changes

Now go back to the Properties tab, and you can see in the Static website hosting section the URL where you can consult your new site.

If you don't want to keep this access open you need to reverse these steps.

3. Create a CloudFront distribution for your site

At this point, I have all the files my site uses in the cloud. But I still need to make it available and accessible for my users. The service I will use is Amazon CloudFront, as it will deliver content with low latency and high speed.

3.1 Create a CloudFront distribution

  1. Login into your AWS account and go to CloudFront
  2. Select the Distributions option in the lateral menu
  3. Click the Create distribution button in the main area
  4. In the Origin section, choose the S3 bucket created in step 2.1 for the Origin domain
  5. Choose Yes use OAI option for the new S3 bucket access question that will apear
  6. Click Create new OAI button and click Create in the dialog that apear.
  7. Choose the Yes, update the bucket policy option in the Bucket policy question
  8. For the Default cache behavior section, you can change the Viewer protocol policy if you want to only support HTTPS or to redirect HTTP to HTTPS. Same for the Allowed HTTP methods. Select whatever you need here.
  9. Finally, go to the last section, Settings. Select the regions you want to have edge locations in the Price class.
  10. Click the Add item button in the Alternate domain name (CNAME) question and write the domain you will use for your site. You should add all the alias you will use, for example: mysite.com and www.mysite.com. Add a new item for each domain alias.
  11. Choose the certificate we created in step 1.2 in the Custom SSL certificate question.
  12. Type the root object of your website in the Default root object. Usually, it will be "index.html"
  13. Click the Create distribution button at the end of the form

At this point, your distribution is created. You need to wait until it is deployed. That process can take a couple of minutes. An important note is that every time you made a change to the distribution, it will need to re deploy and take several minutes again. Patience is needed when working with CloudFront as your changes will not always be immediately reflected.

Optional. Create a lambda function to change the origin request behavior

In some cases you will need to create a lambda function to change the request that CloudFront makes to your S3 bucket for files. Especially when you have a folder structure for your site. For example if you try to access mysite.com/some/path CloudFront will ask S3 for /some/path file: a file named path in the some directory. What we normally want in those cases is that CloudFront asks S3 for the file /some/path/index.html.

In that situation we need to modify the Origin request that CloudFront will do to S3. For that the option we have is create a lambda function that will be invoked on the Request origin event and will modify the petition.

Create a Lambda to transform the requests

  1. Login into your AWS account and go to Lambda
  2. Select the Functions option in the lateral menu
  3. Click the Create function button in the main area
  4. Select Author from scratch option in the form
  5. Input the Function name you want to use for your lambda function
  6. Select the Runtime you want.
  7. Click the Create function button at the end of the form
  8. In the code editor, input a new function to make the modification to the request. You can use the code below if you selected node as the runtime environment
exports.handler = async (event, context, callback) => {  // Extract the request from the CloudFront event that is sent to Lambda@Edge  var request = event.Records[0].cf.request;  // Extract the URI from the request  var olduri = request.uri;  // Match any '/' that occurs at the end of a URI. Replace it with a default index  var newuri = olduri.replace(/\/$/, "/index.html");  // Match any path not ending with a file extension. Add a default index  if (!newuri.match(/\/.+\..+$/)) {    newuri = newuri + "/index.html";  }  // Replace the received URI with the URI that includes the index page  request.uri = newuri;  // Return to CloudFront  return callback(null, request);};
  1. Click the Deploy button to save your code as a Lambda function

Configure the lambda to be able to use in CloudFront

  1. Login into your AWS account and go to Lambda
  2. Select the Functions option in the lateral menu
  3. Click in your function name in the main area
  4. Go to the Versions tab and click the Publish a new version button. Click Publish when the dialog appears.
  5. If you are not redirected to the version page, click the last published version in the Versions tab to go to the version page
  6. Click in the Permissions option in the lateral menu
  7. Click in the Role name in the Execution role section. You will be redirected to IAM service
  8. In the page of the role, go to the Trust relationships tab
  9. Click the Edit trust policy
  10. Add "edgelambda.amazonaws.com" to the list of services, or copy the code below
{  "Version": "2012-10-17",  "Statement": [    {      "Effect": "Allow",      "Principal": {        "Service": ["edgelambda.amazonaws.com", "lambda.amazonaws.com"]      },      "Action": "sts:AssumeRole"    }  ]}
  1. Click Update policy to save the changes

Trigger the Lambda

  1. Login into your AWS account and go to Lambda
  2. Select the Functions option in the lateral menu
  3. Click in your function name in the main area
  4. Go to the Versions tab and click the version number you published
  5. In the Function overview section click the Add trigger button
  6. Select CloudFront in the select control.
  7. In the Distribution question, select the id of your CloudFront distribution
  8. Select Origin request in the CloudFront event question
  9. Check the box Confirm deploy to Lambda@Edge
  10. Click the Add button at the end

4. Serve your site using Route 53

The last thing we need to do, is redirect trafic from our domain to our CloudFront distribution

  1. Login into your AWS account and go to Route 53
  2. Select the Hosted zones option in the lateral menu
  3. Click in your domain name in the table in the main area
  4. In the new page, click the Create record button
  5. In the Record name type the subdomain of your site or keep it empty if will use the root domain. For example, type www if you want to access your distribution going to www.mysite.com
  6. In Record type select A
  7. Switch on the Alias toggle button
  8. Select Alias to CloudFront distribution in the Route traffic to select
  9. In the search box, select the name of your distribution.
  10. If you want to enable IPv6, click the Add another record button and repeat from step 5 to step 8, but select AAAA as the Record type
  11. Repeat steps 5 - 10 for each subdomain you want to use your distribution for.

If you completed all the steps in this guide, you now should be able to access your site from the domain configured

Learnings

This was an interesting project that took me more time than I estimated. I tried to do it without really knowing how everything worked in the AWS console and without real understanding of all the services. Some parts were really easy and intuitive, but others not so much. There was also the issue with the index file in the subfolder that I couldn't understand at the beginning and where I spend a lot of time figuring out what and why it was happening.

Creating the S3 bucket and uploading my files using the console was pretty easy. The user interface is really intuitive. Once I tried to enable it as a static website hosting things started to be less straightforward.

First of all, it is not enough to enable the hosting option. You need to turn off public access blocking in a different section. So far, not that hard. But you need to create the correct policy to enable public access, and that is not clear just by looking at the UI. You really need to understand how the S3 service works.

Next, there is CloudFront. The term Distribution was not clear for me. It didn't tell me much about what it was for. Knowing what the service was used for, and being it the main CTA in the service landing page I assumed that it was the right option.

After that initial struggle, the rest was easy. Selecting S3 as the origin, creating the correct policy, filling the domain information, deciding the protocols to support and the behavior, selecting the supported regions.

Something that was not clear is that you can use an S3 bucket as an origin in two ways. As the S3 API or as a website. The default is using the S3 API that introduce new challenges that are not clearly explained.

After a couple of days I found I needed to change the behavior of the origin request using a Lambda function. I don't really know all the options the event has, so I was not fully able to solve it for myself. Fortunately there are a couple of examples on how to modify the request origin request and I was able to create a function that works for my use case.

Another problem I run into with the lambda function is that you need to enable a special set of roles and permissions for it to work with CloudFront. I was confused by the terms roles as in AWS those have a different function than in other software systems I'm most used to. In AWS a role does not grant permissions for users, but for services.

Next steps

The next step is automate the deployment of new blog posts. With the current status, the only way to add a new entry is to create a new post in my local environment (the Nuxt application), and run the build command again. Once I have the new files, delete the previous ones in S3 and upload the new ones.

Instead of doing all of that, I want to set an automated pipeline so that every time I commit with a new post to my git repository all the steps I described are executed immediately and I have my blog site updated.

© 2022