How to build a free static resume site with AWS S3, Cloudfront, and Route 53
Tags: aws, devops, git, s3
Published: 2020-05-14 20:59:11

<!-- wp:paragraph -->
<p><a href="https://forrestbrazeal.com/">Forrest Brazeal</a>, an AWS Serverless Hero who I follow on Twitter, recently tweeted about a #CloudResumeChallenge to build a resume site using AWS. In return, Forrest would use his network to help everyone who completed the challenge land a job. An awesome deal! I already have IT experience (which disqualifies me) but I thought it would be fun to build the site anyway and write up a post about how I did it.</p>
<!-- /wp:paragraph -->

<!-- wp:core-embed/twitter {"url":"https://twitter.com/forrestbrazeal/status/1254822417203113986?s=20","type":"rich","providerNameSlug":"twitter","align":"center"} -->
<figure class="wp-block-embed-twitter aligncenter wp-block-embed is-type-rich is-provider-twitter"><div class="wp-block-embed__wrapper"><a href="https://twitter.com/forrestbrazeal/status/1254822417203113986?s=20">He tweeted about it here</a>
</div></figure>
<!-- /wp:core-embed/twitter -->

<!-- wp:ub/table-of-contents-block {"blockID":"8ef8b552-6bfd-4503-a04c-cdaebb35ea2b","title":"Table of Contents","allowedHeaders":[true,true,true,true,true,true],"links":"[{\u0022content\u0022:\u0022Setting up the site content\u0022,\u0022level\u0022:4,\u0022anchor\u0022:\u00220-setting-up-the-site-content\u0022},{\u0022content\u0022:\u0022Create an S3 Static Site\u0022,\u0022level\u0022:4,\u0022anchor\u0022:\u00221-create-an-s3-static-site\u0022},{\u0022content\u0022:\u0022Purchase a domain \u0022,\u0022level\u0022:4,\u0022anchor\u0022:\u00222-purchase-a-domain-\u0022},{\u0022content\u0022:\u0022Create an SSL certificate in ACM\u0022,\u0022level\u0022:4,\u0022anchor\u0022:\u00223-create-an-ssl-certificate-in-acm\u0022},{\u0022content\u0022:\u0022Create a CloudFront Distribution\u0022,\u0022level\u0022:4,\u0022anchor\u0022:\u00224-create-a-cloudfront-distribution\u0022},{\u0022content\u0022:\u0022Deploy a DynamoDB table with CloudFormation\u0022,\u0022level\u0022:4,\u0022anchor\u0022:\u00225-deploy-a-dynamodb-table-with-cloudformation\u0022},{\u0022content\u0022:\u0022Deploy a serverless API with Lambda and API Gateway\u0022,\u0022level\u0022:4,\u0022anchor\u0022:\u00226-deploy-a-serverless-api-with-lambda-and-api-gateway\u0022},{\u0022content\u0022:\u0022Add the visitor counter to the site\u0022,\u0022level\u0022:4,\u0022anchor\u0022:\u00227-add-the-visitor-counter-to-the-site\u0022},{\u0022content\u0022:\u0022Create a pipeline for automated deployments\u0022,\u0022level\u0022:4,\u0022anchor\u0022:\u00228-create-a-pipeline-for-automated-deployments\u0022}]","gaps":[1,1,1,1,1,1,1,1,1]} /-->

<!-- wp:paragraph -->
<p>The challenge set some rules for the website:</p>
<!-- /wp:paragraph -->

<!-- wp:list -->
<ul><li>Resume must be written in HTML and styled with CSS</li><li>Deployed as an Amazon S3 static website</li><li>Should be accessible via HTTPS</li><li>Have a custom domain name</li><li>Include a Javascript visitor counter</li><li>Visitor count stored on DynamoDB</li><li>The site must communicate with DynamoDB via Lambda and API Gateway</li><li>Lambda and API Gateway should be deployed with AWS SAM CLI</li><li>Website automatically updates on push to GitHub</li></ul>
<!-- /wp:list -->

<!-- wp:heading {"level":4} -->
<h4 id="0-setting-up-the-site-content">Setting up the site content</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>All right, let's start with the content. Since I'm not in this for the networking, I didn't think it made much sense to develop another personal site for this challenge. I did it on <a href="https://en.wikipedia.org/wiki/Alan_Turing">Alan Turing</a> instead. Why? Because he's cool, and none of us would be looking at this without him. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Anyway, create an empty Git repo.</p>
<!-- /wp:paragraph -->

<!-- wp:code {"className":"language-bash"} -->
<pre class="wp-block-code language-bash"><code>git clone git@github.com:seanziegler/AlanTuringResume.git</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>I hate HTML and CSS so I stole a nice little resume template from this <a href="https://css-tricks.com/one-page-resume-site/">CSS-Tricks article</a>.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Originally, it was based on Cthulu which.... was a little strange, but okay. After some Wikipedia research and a little front-end stumbling, I had a clean one-page resume site about Alan Turing developed. </p>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":793,"width":508,"height":486,"sizeSlug":"large"} -->
<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img src="https://seanjziegler.com/wp-content/uploads/2020/05/AlanTuringResumeSite-1024x983.png" alt="" class="wp-image-793" width="508" height="486"/><figcaption>Well, this makes my resume look weak.</figcaption></figure></div>
<!-- /wp:image -->

<!-- wp:heading {"level":4} -->
<h4 id="1-create-an-s3-static-site">Create an S3 Static Site</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Okay, on to the more interesting part, the back-end. The first requirement of the site is it should be a static site hosted by AWS S3. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>First thing we need is an S3 bucket. You'll need to configure the AWS CLI if you haven't done so already.</p>
<!-- /wp:paragraph -->

<!-- wp:code {"className":"language-bash"} -->
<pre class="wp-block-code language-bash"><code>aws s3 mb s3://turingresume</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Copy all the files for your website into the bucket. The root page of your website (often index.html) should be at the root level of the S3 bucket.</p>
<!-- /wp:paragraph -->

<!-- wp:code {"className":"language-bash"} -->
<pre class="wp-block-code language-bash"><code>aws s3 cp &lt;project_dir> s3://turingresume --recursive</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Last, tell S3 that this should be a static website, not a normal bucket.</p>
<!-- /wp:paragraph -->

<!-- wp:code {"className":"language-bash"} -->
<pre class="wp-block-code language-bash"><code>aws s3 website s3://turingresume --index index.html</code></pre>
<!-- /wp:code -->

<!-- wp:html -->
<p>Check to see if your website is available. You can access it at <code>http://"bucketname".s3-website-"bucketregion".amazonaws.com/</code></p>
<!-- /wp:html -->

<!-- wp:paragraph -->
<p>If you can see your website, you've successfully deployed the static site using S3. Let's work on making it easier to reach your site since that S3 website URL isn't exactly memorable.  </p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":4} -->
<h4 id="2-purchase-a-domain-">Purchase a domain </h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>We're off to Route 53 where you can purchase a domain from inside the AWS console. Unfortunately, AlanTuring.com wasn't available, so I went with the much friendlier heyitsalan.com. I realized later that it looks a lot like "hey its Satan" which is unfortunate, but I already paid for the domain and I'm not made of money.</p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":802,"sizeSlug":"large"} -->
<figure class="wp-block-image size-large"><img src="https://i1.wp.com/seanjziegler.com/wp-content/uploads/2020/05/Buying-Domain-Name-Route53.png?fit=1024%2C169&amp;ssl=1" alt="" class="wp-image-802"/><figcaption>All right, it's not the best domain, but it works.</figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>Ok, that's all we can do in Route53 for now. </p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":4} -->
<h4 id="3-create-an-ssl-certificate-in-acm">Create an SSL certificate in ACM</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Since we will serve the site over HTTPS, we will need an SSL certificate. Fortunately, the AWS Certificate Manager makes it simple to generate a certificate. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Make sure you request a certificate for *.yourdomain.com so it covers all sub-domains (especially the www and non-www versions of your URL).</p>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":803,"width":580,"height":399,"sizeSlug":"large"} -->
<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img src="https://i1.wp.com/seanjziegler.com/wp-content/uploads/2020/05/Creating-SSL-Cert-ACM.png?fit=1024%2C705&amp;ssl=1" alt="" class="wp-image-803" width="580" height="399"/></figure></div>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>You'll need to add a TXT record to the DNS records of your domain to prove that you own it. </p>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":805,"width":385,"height":219,"sizeSlug":"full"} -->
<div class="wp-block-image"><figure class="aligncenter size-full is-resized"><img src="https://seanjziegler.com/wp-content/uploads/2020/05/Select-DNS-validation-Route53.png" alt="" class="wp-image-805" width="385" height="219"/><figcaption>DNS Validation proves you own the domain covered by the SSL certificate.</figcaption></figure></div>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>If you bought your domain on Route53 like I did, you can just click the "Add DNS Records to Route53" button. If you're on a different host, you'll have to look at their documentation.</p>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":806,"width":512,"height":410,"sizeSlug":"large"} -->
<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img src="https://seanjziegler.com/wp-content/uploads/2020/05/Create-DNS-Records-Route53-1024x819.png" alt="" class="wp-image-806" width="512" height="410"/></figure></div>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>Once AWS Certificate Manager sees the DNS record and knows you own the domain, it will issue your SSL certificate.</p>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":807,"width":512,"height":74,"sizeSlug":"large"} -->
<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img src="https://i2.wp.com/seanjziegler.com/wp-content/uploads/2020/05/ACM-Issued-SSL-Cert.png?fit=1024%2C147&amp;ssl=1" alt="" class="wp-image-807" width="512" height="74"/></figure></div>
<!-- /wp:image -->

<!-- wp:heading {"level":4} -->
<h4 id="4-create-a-cloudfront-distribution">Create a CloudFront Distribution</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>We're ready to tie everything together and get this domain name tied up. All we need to do is create a HTTPS CloudFront Distribution and with our S3 website as its origin. After that, we'll point the domain name to the CloudFront distribution to finish. Start by creating a CloudFront distribution. </p>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":812,"width":512,"height":486,"sizeSlug":"large"} -->
<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img src="https://i1.wp.com/seanjziegler.com/wp-content/uploads/2020/05/Create-CloudFront-Distribution-1.png?fit=1024%2C972&amp;ssl=1" alt="" class="wp-image-812" width="512" height="486"/></figure></div>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>You'll also need to add your domain as an alternate CNAME in the CloudFront distribution and import the SSL certificate you generated earlier. </p>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":814,"width":512,"height":415,"sizeSlug":"large"} -->
<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img src="https://i2.wp.com/seanjziegler.com/wp-content/uploads/2020/05/CloudFront-SSL-Setup-1.png?fit=1024%2C829&amp;ssl=1" alt="" class="wp-image-814" width="512" height="415"/><figcaption>Don't forget to add * in front of your domain so all sub-domains redirect here.</figcaption></figure></div>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>Fire up Route53 and head to the DNS records for the domain you registered. We will add CNAME alias record, which points to the domain name for the CloudFront distribution created earlier. </p>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":837,"width":446,"height":214,"sizeSlug":"large"} -->
<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img src="https://seanjziegler.com/wp-content/uploads/2020/05/Route53-CNAME-for-CloudFront.png" alt="" class="wp-image-837" width="446" height="214"/></figure></div>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>Now, you can test your domain and see if it works (although you might need to wait a few minutes for the site to propagate through the CloudFront edge locations).</p>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":816,"width":296,"height":66,"sizeSlug":"large"} -->
<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img src="https://seanjziegler.com/wp-content/uploads/2020/05/SSL-on-domain.png" alt="" class="wp-image-816" width="296" height="66"/><figcaption>Another requirement down.</figcaption></figure></div>
<!-- /wp:image -->

<!-- wp:heading {"level":4} -->
<h4 id="5-deploy-a-dynamodb-table-with-cloudformation">Deploy a DynamoDB table with CloudFormation</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>Since our problem statement calls for a DynamoDB table to hold the visitor count, I will use CloudFormation. CloudFormation is an excellent product for deploying AWS resources and makes life so much easier when you need to update or manage your resources later.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>This is a super simple DynamoDB table. In all honestly, I'm not convinced this is a good use of DynamoDB, but the problem statement calls for it, so who am I to argue?</p>
<!-- /wp:paragraph -->

<!-- wp:code {"className":"language-yaml"} -->
<pre class="wp-block-code language-yaml"><code>AWSTemplateFormatVersion: "2010-09-09"
Resources: 
  TuringResumeCounterDynamodb: 
    Type: AWS::DynamoDB::Table
    Properties: 
      AttributeDefinitions: 
        - 
          AttributeName: "Site"
          AttributeType: "N"
      KeySchema: 
        - 
          AttributeName: "Site"
          KeyType: "HASH"
      ProvisionedThroughput: 
        ReadCapacityUnits: "1"
        WriteCapacityUnits: "1"
      TableName: "turingresumecounter"</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Deploying this is a single AWS CLI command (I love CloudFormation!).</p>
<!-- /wp:paragraph -->

<!-- wp:code {"className":"language-bash"} -->
<pre class="wp-block-code language-bash"><code>aws cloudformation deploy --template-file dynamodb.yml --stack-name turingresumecounter</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>I will create a single attribute to hold the visitor count.</p>
<!-- /wp:paragraph -->

<!-- wp:code {"className":"language-bash"} -->
<pre class="wp-block-code language-bash"><code>aws dynamodb put-item --table-name turingresumecounter --item '{"Site":{"N":"0"}, "Visits": {"N": "0"}}'</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Great, now I can read and write from this attribute to keep track of the visitor count. I don't think this is a good solution if you are running in a production setup, so I might re-factor this later. </p>
<!-- /wp:paragraph -->

<!-- wp:heading {"level":4} -->
<h4 id="6-deploy-a-serverless-api-with-lambda-and-api-gateway">Deploy a serverless API with Lambda and API Gateway</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>The site still needs some interface to communicate with DynamoDB. I'm going to implement this with Lambda and API Gateway since they are the de facto standard for serverless APIs on AWS. I'm a big fan of API Gateway because it makes it a breeze to set up rate limits, throttling, and other usage plan metrics for your API.</p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>CloudFormation is great, but it can be a bear to use when deploying serverless resources. AWS makes a companion product called the <a href="https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-command-reference.html">SAM CLI</a>. The SAM CLI designed to make deploying serverless assets easier and is perfect for our use case. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The first step is to create another YAML file that describes the deployment. </p>
<!-- /wp:paragraph -->

<!-- wp:code {"className":"language-yaml"} -->
<pre class="wp-block-code language-yaml"><code>AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Resources:
  Counter:
    Type: AWS::Serverless::Function
    Properties:
      Handler: function.handler
      Runtime: python3.8
      Policies: AmazonDynamoDBFullAccess
      Events:
        HttpGet:
          Type: Api
          Properties:
            Path: '/counter'
            Method: get</code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>The <code>Handler: function.handler</code> line above tells SAM that we will provide the lambda function in a file named <code>function.py</code> with a function named <code>handler</code>. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>The <code>handler</code> function uses the AWS SDK for Python called <a href="https://boto3.amazonaws.com/v1/documentation/api/latest/index.html">boto3</a>.</p>
<!-- /wp:paragraph -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>UPDATE: After talking to Forrest, we decided it would be best if future participants had to design these parts on their own. Good luck :)</p></blockquote>
<!-- /wp:quote -->

<!-- wp:paragraph -->
<p>Great, now when we send a GET request to the API Gateway URL we will see the visitor count. Now, let's work that into the site itself.</p>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":840,"width":336,"height":177,"sizeSlug":"large"} -->
<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img src="https://seanjziegler.com/wp-content/uploads/2020/05/HTTP-Endpoint-Response.png" alt="" class="wp-image-840" width="336" height="177"/><figcaption>Wow, someone call Google, this API is HOT!</figcaption></figure></div>
<!-- /wp:image -->

<!-- wp:heading {"level":4} -->
<h4 id="7-add-the-visitor-counter-to-the-site">Add the visitor counter to the site</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>The site needs an element to display the visitor counter. I'm going to create some HTML to display the visitor count and add it to the bottom of the page.  </p>
<!-- /wp:paragraph -->

<!-- wp:code {"className":"language-html"} -->
<pre class="wp-block-code language-html"><code>&lt;div>
    &lt;p style = "text-align: center;">
    Visits: &lt;span id="visits">0&lt;/span>
    &lt;/p>
&lt;/div></code></pre>
<!-- /wp:code -->

<!-- wp:paragraph -->
<p>Yes, I know inline CSS is bad. Sue me. Let's add some Javascript magic to the footer. This function sends a GET request to the API gateway endpoint every time the page is refreshed. Technically, this is a counter of page views not visitors, but I digress. </p>
<!-- /wp:paragraph -->

<!-- wp:quote -->
<blockquote class="wp-block-quote"><p>UPDATE: After talking to Forrest, we decided it would be best if future participants had to design these parts on their own. Good luck :)</p></blockquote>
<!-- /wp:quote -->

<!-- wp:paragraph -->
<p>Cool, let's check the page.</p>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":841,"width":512,"height":190,"sizeSlug":"large"} -->
<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img src="https://i0.wp.com/seanjziegler.com/wp-content/uploads/2020/05/Visit-Counter.png?fit=1024%2C380&amp;ssl=1" alt="" class="wp-image-841" width="512" height="190"/><figcaption>Looking good.</figcaption></figure></div>
<!-- /wp:image -->

<!-- wp:heading {"level":4} -->
<h4 id="8-create-a-pipeline-for-automated-deployments">Create a pipeline for automated deployments</h4>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>We are nearing the finish line! All that's left is creating a pipeline in CodePipeline to deploy our code to the S3 bucket when we push to Git. I already wrote about <a href="https://seanjziegler.com/deploying-code-from-github-to-aws-ec2-with-codepipeline/">how to deploy from GitHub to EC2</a>, but since we're going to S3 this time, let's do a quick overview. </p>
<!-- /wp:paragraph -->

<!-- wp:paragraph -->
<p>Setup a pipeline.</p>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":844,"width":512,"height":407,"sizeSlug":"large"} -->
<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img src="https://i0.wp.com/seanjziegler.com/wp-content/uploads/2020/05/Pipeline-Settings.png?fit=1024%2C813&amp;ssl=1" alt="" class="wp-image-844" width="512" height="407"/><figcaption>I usually allow AWS to create the service role - much easier that way.</figcaption></figure></div>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>Make the Git repo your source.</p>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":845,"width":512,"height":472,"sizeSlug":"large"} -->
<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img src="https://i1.wp.com/seanjziegler.com/wp-content/uploads/2020/05/Pipeline-Source-Stage.png?fit=1024%2C944&amp;ssl=1" alt="" class="wp-image-845" width="512" height="472"/></figure></div>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>Setup a deployment stage which pushes the files to your S3 bucket. </p>
<!-- /wp:paragraph -->

<!-- wp:image {"align":"center","id":846,"width":434,"height":512,"sizeSlug":"large"} -->
<div class="wp-block-image"><figure class="aligncenter size-large is-resized"><img src="https://i2.wp.com/seanjziegler.com/wp-content/uploads/2020/05/Successful-Deployment-.png?fit=868%2C1024&amp;ssl=1" alt="" class="wp-image-846" width="434" height="512"/><figcaption>Finally done.</figcaption></figure></div>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>That's all there is to do! You've got a fully functional static S3 that updates on Git push and is served over HTTPs, all for essentially free. </p>
<!-- /wp:paragraph -->

<!-- wp:image {"id":847,"sizeSlug":"large"} -->
<figure class="wp-block-image size-large"><img src="https://i2.wp.com/seanjziegler.com/wp-content/uploads/2020/05/Final-Product.png?fit=1024%2C444&amp;ssl=1" alt="" class="wp-image-847"/><figcaption>Final product doesn't look too bad for a back-end engineer!</figcaption></figure>
<!-- /wp:image -->

<!-- wp:paragraph -->
<p>If you enjoyed this content, follow me on Twitter to see more stuff like this!</p>
<!-- /wp:paragraph -->