Hosting a Static Website in AWS
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
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:
- Create a new S3 bucket.
- Upload your site files to your new bucket
- Set up the correct permissions in your bucket
- Create a new distribution in CloudFront
- 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
- Login into your AWS account and go to Route 53
- Select
Hosted zones
in the lateral menu - Click the
Create hosted zone
button in the main area - Fill the form with your
Domain name
, and selectPublic hosted zone
as the type. Optionally input a description and the tags you want. - 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
- Login into your AWS account and go to Certificate Manager
- Select
Request certificate
in the lateral menu - Choose the
Request a public certificate
option - Click the
Next
button at the bottom of the form - 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 examplemysite.com
and*.mysite.com
will protect all your subdomains formysite.com
- Choose
DNS validation
option in theSelect validation method
section - Click the
Request
button at the end of the form - You will get redirected to the
List certificates
page. A new notification will apear at the top of the page. - Click the
View certificate
button in the top notification. - Once in the certificate page, click the
Create records in Route 53
button in theDomains
region - 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
- Login into your AWS account and go to S3
- Select the
Buckets
option in the lateral menu - Click the
Create bucket
button in the main area - Fill the
Bucket name
andAWS Region
in the form. For the rest of the options, you can keep the default values. - 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
- Login into your AWS account and go to S3
- Select the
Buckets
option in the lateral menu - Click your bucket's name in the table in the main area
- Click the
Upload
button - Drag and drop your files and folders or click the
Add files
orAdd folder
buttons to select the files/folders you want to upload - Review and validate that the files are correct in the table
- Click the
Upload
button once you are satisfied - 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
- Login into your AWS account and go to S3
- Select the
Buckets
option in the lateral menu - Click your bucket's name in the table in the main area
- Click
Properties
in the tab options - Scroll down until you reach the
Static website hosting
section and click itsEdit
edit button - Select
Enable
in the form - On the new options, select
Host a static website
- Type the name of your
Index document
. The rest of the options can be left in blank - Click
Save changes
Before you can view your site you need to grant public access to it.
Grant public access
- Login into your AWS account and go to S3
- Select the
Buckets
option in the lateral menu - Click your bucket's name in the table in the main area
- Click
Permissions
in the tab options - In the
Block public access
section clickEdit
button - Uncheck
Block all public access
- Click
Save changes
- Confirm the change typing
confirm
in the dialog's textbox and clicking theConfirm
button - In the
Bucket policy
section click theEdit
button - 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>/*"
]
}
]
}
- 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
- Login into your AWS account and go to CloudFront
- Select the
Distributions
option in the lateral menu - Click the
Create distribution
button in the main area - In the
Origin
section, choose theS3 bucket
created in step 2.1 for theOrigin domain
- Choose
Yes use OAI
option for the newS3 bucket access
question that will apear - Click
Create new OAI
button and clickCreate
in the dialog that apear. - Choose the
Yes, update the bucket policy
option in theBucket policy
question - For the
Default cache behavior
section, you can change theViewer protocol policy
if you want to only supportHTTPS
or to redirectHTTP
toHTTPS
. Same for theAllowed HTTP methods
. Select whatever you need here. - Finally, go to the last section,
Settings
. Select the regions you want to have edge locations in thePrice class
. - Click the
Add item
button in theAlternate 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
andwww.mysite.com
. Add a new item for each domain alias. - Choose the certificate we created in step 1.2 in the
Custom SSL certificate
question. - Type the root object of your website in the
Default root object
. Usually, it will be"index.html"
- 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
- Login into your AWS account and go to Lambda
- Select the
Functions
option in the lateral menu - Click the
Create function
button in the main area - Select
Author from scratch
option in the form - Input the
Function name
you want to use for your lambda function - Select the
Runtime
you want. - Click the
Create function
button at the end of the form - 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);};
- Click the
Deploy
button to save your code as a Lambda function
Configure the lambda to be able to use in CloudFront
- Login into your AWS account and go to Lambda
- Select the
Functions
option in the lateral menu - Click in your function name in the main area
- Go to the
Versions
tab and click thePublish a new version
button. ClickPublish
when the dialog appears. - If you are not redirected to the version page, click the last published version in the
Versions
tab to go to the version page - Click in the
Permissions
option in the lateral menu - Click in the Role name in the
Execution role
section. You will be redirected to IAM service - In the page of the role, go to the
Trust relationships
tab - Click the
Edit trust policy
- 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" } ]}
- Click
Update policy
to save the changes
Trigger the Lambda
- Login into your AWS account and go to Lambda
- Select the
Functions
option in the lateral menu - Click in your function name in the main area
- Go to the
Versions
tab and click the version number you published - In the
Function overview
section click theAdd trigger
button - Select
CloudFront
in the select control. - In the
Distribution
question, select the id of your CloudFront distribution - Select
Origin request
in theCloudFront event
question - Check the box
Confirm deploy to Lambda@Edge
- 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
- Login into your AWS account and go to Route 53
- Select the
Hosted zones
option in the lateral menu - Click in your domain name in the table in the main area
- In the new page, click the
Create record
button - In the
Record name
type the subdomain of your site or keep it empty if will use the root domain. For example, typewww
if you want to access your distribution going towww.mysite.com
- In
Record type
selectA
- Switch on the
Alias
toggle button - Select
Alias to CloudFront distribution
in theRoute traffic to
select - In the search box, select the name of your distribution.
- If you want to enable IPv6, click the
Add another record
button and repeat from step 5 to step 8, but selectAAAA
as theRecord type
- 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.