{"id":156,"date":"2026-05-29T02:09:33","date_gmt":"2026-05-29T06:09:33","guid":{"rendered":"https:\/\/securebydefault.io\/blog\/?p=156"},"modified":"2026-07-13T12:48:33","modified_gmt":"2026-07-13T16:48:33","slug":"cloud-resume-challenge-aws-portfolio-visitor-counter","status":"publish","type":"post","link":"https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/","title":{"rendered":"How I Built a Cloud Portfolio Website With a Visitor Counter Using AWS &#8211; From Start to Finish"},"content":{"rendered":"\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"536\" src=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_BlogPost5_Featured-1024x536.png\" alt=\"\" class=\"wp-image-157\" srcset=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_BlogPost5_Featured-1024x536.png 1024w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_BlogPost5_Featured-300x157.png 300w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_BlogPost5_Featured-768x402.png 768w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_BlogPost5_Featured.png 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">There&#8217;s a point in most cloud learning journeys where tutorials stop being enough. You can follow along, click the buttons, watch things appear, and still not feel like you actually built something. The Cloud Resume Challenge exists specifically for that moment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The idea is simple: build a real portfolio website, host it on AWS, add a live visitor counter backed by a serverless API, wire the whole thing to automated CI\/CD, and document it. No step-by-step tutorial to follow. No hand-holding. Just you, AWS, and a blank text editor.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I built mine. It&#8217;s live at <a href=\"https:\/\/ron-mercier101.com\/\" type=\"link\" id=\"https:\/\/ron-mercier101.com\/\">ron-mercier101.com<\/a>. This is the complete walkthrough on what I built, why I made the decisions I made, what broke, and what I&#8217;d do differently.<\/p>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\"><strong>What I Was Building<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The project splits into two distinct pieces, which is actually a good representation of how real cloud architecture works: a frontend that serves your content, and a backend that handles dynamic functionality.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The frontend is a static portfolio website with HTML, CSS, and JavaScript, served globally via AWS. The backend is a serverless API that tracks how many people have visited, backed by a database that persists the count. When you load the site, the browser calls the API, the API increments the counter in DynamoDB, and the updated number shows up in the footer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two separate GitHub repositories. Two separate deployment pipelines. One project that demonstrates you understand how modern cloud infrastructure actually fits together.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s what I used on each side:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Frontend: S3 (file storage and static hosting) \u00b7 CloudFront (CDN + HTTPS) \u00b7 Route 53 (custom domain DNS) \u00b7 ACM (SSL\/TLS certificate) \u00b7 GitHub Actions (automated deployment)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Backend: AWS Lambda (Python 3.12, serverless function) \u00b7 API Gateway HTTP API (the public endpoint) \u00b7 DynamoDB (PAY_PER_REQUEST, the database) \u00b7 AWS SAM (Infrastructure as Code) \u00b7 GitHub Actions with OIDC (automated deployment without stored credentials)<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"536\" src=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_BlogPost5_Architecture-1024x536.png\" alt=\"\" class=\"wp-image-158\" srcset=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_BlogPost5_Architecture-1024x536.png 1024w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_BlogPost5_Architecture-300x157.png 300w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_BlogPost5_Architecture-768x402.png 768w, https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_BlogPost5_Architecture.png 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\"><strong>Part 1: The Frontend &#8211; Hosting a Static Site on S3<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The first decision is one that trips people up: S3 is not a web server. It&#8217;s object storage that happens to have a static website hosting feature. That distinction matters because it affects how you configure access, how you connect it to CloudFront, and what security model you use.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I started by creating an S3 bucket. The name doesn&#8217;t matter much, but I named it to match my domain (<a href=\"https:\/\/ron-mercier101.com\/\" type=\"link\" id=\"https:\/\/ron-mercier101.com\/\">ron-mercier101.com<\/a>) for clarity. The region matters slightly more, so I chose us-east-1 because it&#8217;s where ACM certificates must live when used with CloudFront (more on that shortly) and because the latency difference for a personal portfolio is negligible.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Under Object Ownership, I enabled ACLs during initial setup. ACLs (Access Control Lists) let you control permissions at the individual object level rather than the bucket level. For a simple static site where everything is meant to be public, this is the most direct approach. Without making the objects public (either via ACLs or a bucket policy), you&#8217;ll get a 403 Forbidden error when you try to load the site, which is a confusing first experience if you&#8217;re not expecting it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Static website hosting is a checkbox in the bucket Properties tab. Enable it, set index.html as the index document, and you&#8217;ll get an HTTP URL that looks like your-bucket.s3-website-us-east-1.amazonaws.com. That URL works, but it has two problems: it&#8217;s HTTP only (no HTTPS), and it&#8217;s not your domain. Both get solved in the next step.<\/p>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\"><strong>Part 2: CloudFront &#8211; HTTPS, CDN, and the Custom Domain Bridge<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">CloudFront is AWS&#8217;s content delivery network. It sits in front of your S3 bucket and does several valuable things at once: it distributes your content across AWS edge locations globally (so someone in Tokyo gets files from a nearby edge location rather than routing all the way back to us-east-1), it enforces HTTPS, and it&#8217;s where your custom domain connects.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><br>Creating a CloudFront distribution requires specifying your S3 bucket as the origin. There&#8217;s a subtlety here: if you point it at the S3 static website hosting endpoint (the .s3-website- URL), it handles things differently than if you point it at the S3 REST API endpoint (the .s3.amazonaws.com URL). I pointed at the REST API endpoint and enabled Origin Access Control (OAC) so that only CloudFront can read from the bucket; the bucket itself remains private to the public internet. This is the more secure approach, and AWS now recommends it over the older public bucket + CloudFront combination.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><br>The HTTPS piece requires an ACM certificate. There&#8217;s a gotcha: ACM certificates for CloudFront must be issued in us-east-1 regardless of where your bucket or distribution lives. Request a public certificate, add the CNAME validation records ACM provides to Route 53, wait for them to show as Issued (usually a few minutes), and attach the certificate to your CloudFront distribution. Set the viewer protocol policy to Redirect HTTP to HTTPS so nobody accidentally lands on an unencrypted version.<\/p>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\"><strong>Part 3: Route 53 &#8211; Making It Your Domain<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Route 53 is AWS&#8217;s DNS service. The goal here is simple: when someone types ron-mercier101.com, Route 53 directs them to your CloudFront distribution instead of sending them directly to S3.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><br>You create a hosted zone for your domain, add an A record (alias) pointing to your CloudFront distribution, and optionally add an AAAA record for IPv6. If your domain is registered elsewhere (mine was), you update the domain registrar&#8217;s nameservers to point to Route 53&#8217;s four nameservers. DNS propagation can take anywhere from a few minutes to a few hours, and the site will start working once it finishes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><br>After this step, the domain loads your portfolio over HTTPS with your own domain. The S3 URL still works, but nobody needs to know it exists.<\/p>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\"><strong>Part 4: GitHub Actions &#8211; Never Upload Files Manually Again<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is where the project starts feeling like a real workflow rather than a tutorial. The goal: when I push a change to the main branch, my website updates automatically without me touching the AWS console.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The GitHub Actions workflow lives in .github\/workflows\/ in the repository. It runs on push to main, assumes an IAM role with permission to write to the S3 bucket, syncs the files (aws s3 sync), and optionally invalidates the CloudFront cache so the change is visible immediately rather than waiting for the cached version to expire.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The authentication piece is worth understanding. The naive approach is to create an IAM user, generate an access key and secret, and store them as GitHub Secrets. This works but it&#8217;s not great, because those keys are long-lived credentials that don&#8217;t expire, and if they get exposed, someone has persistent AWS access until you manually rotate them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The better approach (and the one I used) is OIDC (OpenID Connect). Instead of storing credentials, you create an IAM OIDC identity provider in AWS that trusts GitHub&#8217;s token service, create an IAM role with a trust policy scoped to your specific repository, and the GitHub Actions workflow assumes that role temporarily using a short-lived token. No credentials stored anywhere. The token expires when the workflow ends.<\/p>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\"><strong>Part 5: The Backend &#8211; Where It Gets Interesting<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The visitor counter is where the project stops being a &#8216;host a static file&#8217; tutorial and starts being actual cloud engineering. The requirement: every time someone loads the portfolio, a counter increments, and the current count displays in the footer. This requires persistent storage, server-side logic, and a public API, none of which you can do with a static S3 site alone.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><br>I chose to build this serverlessly. No EC2 instance, no always-on server paying for compute time that isn&#8217;t being used. Instead, API Gateway receives the request, triggers a Lambda function, which reads and increments a value in DynamoDB, and returns the updated count as JSON.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><br>The entire backend: API Gateway, Lambda, DynamoDB, IAM roles, and CORS configuration, is defined in a single file called template.yaml using AWS SAM (Serverless Application Model). SAM is a CloudFormation transform that makes serverless IaC significantly less verbose. What would take 60+ lines of raw CloudFormation takes 15 lines in SAM. It compiles down to CloudFormation under the hood, so there&#8217;s no lock-in.<\/p>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\"><strong>Part 6: DynamoDB &#8211; Storing the Count<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">DynamoDB is AWS&#8217;s fully managed NoSQL database. For a visitor counter, the data model is about as simple as it gets: a single table with a single item containing a single attribute, which is the count. The key is an ID field (String type, value &#8216;visitors&#8217;), and the count is a Number attribute that gets atomically incremented on each request.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><br>The billing choice matters more than it might seem. DynamoDB offers two modes: provisioned (you pre-allocate read\/write capacity units) and on-demand (PAY_PER_REQUEST). For a portfolio site, provisioned capacity would mean paying for reserved throughput that sits mostly idle. PAY_PER_REQUEST charges per request and scales automatically, which costs nothing when nobody is visiting. For a project with unpredictable, low-volume traffic, it&#8217;s clearly the right choice.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><br>The table is named visitorCount, and the SAM template defines it with a DeletionPolicy of Retain, which means CloudFormation won&#8217;t delete the table when you tear down the stack. This is deliberate, so you don&#8217;t accidentally wipe your visitor count data when you run sam delete.<\/p>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\"><strong>Part 7: Lambda &#8211; The Logic Layer<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Lambda function is a Python 3.12 handler that does one thing: increment the visitor count in DynamoDB and return it. The handler reads the DynamoDB table name from an environment variable (TABLE_NAME, injected by SAM via !Ref VisitorTable), calls update_item with an atomic ADD operation, and returns the updated count as a JSON response with appropriate CORS headers.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Using an atomic ADD rather than a read-modify-write is important. If two people visit the page at the same millisecond and both read a count of 41, then both write 42, you&#8217;ve lost a count. DynamoDB&#8217;s atomic counter operations handle concurrent updates correctly by design.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Lambda function has an IAM role attached via SAM&#8217;s DynamoDBCrudPolicy, scoped specifically to the visitorCount table. The role is set for Least privilege, so the function can only read and write that one table, nothing else in the account.<\/p>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\"><strong>Part 8: API Gateway &#8211; The Public Endpoint<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">API Gateway is what gives the Lambda function a public URL the browser can call. I used HTTP API (not REST API) for two reasons: it&#8217;s cheaper (roughly 70% less per request) and it&#8217;s lower latency. REST API&#8217;s premium features (caching, request validation, usage plans, API keys) are overkill for a simple counter endpoint.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The route is GET \/count, deployed to a stage called Prod. CORS is configured to only accept requests from your domain. The visitor counter won&#8217;t work from any other origin, which is the intended behavior. The preflight response is cached for 3600 seconds (MaxAge: 3600) so browsers don&#8217;t send a CORS preflight on every single request.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After deployment, SAM prints the API endpoint in the stack outputs. That URL goes into the JavaScript in index.html, which calls it on page load and animates the count up from zero.<\/p>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\"><strong>Part 9: Testing &#8211; Before It Hits Production<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The backend repository has a tests\/ directory with pytest unit tests for the Lambda handler. The tests mock the DynamoDB client so no real AWS resources are needed to run them. This matters for the CI\/CD pipeline: tests run before the build and deploy steps, so a broken handler never makes it to production.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The test-build-deploy sequence in GitHub Actions looks like this: pytest passes, sam build compiles the Lambda package, sam deploy pushes the CloudFormation stack update to AWS. If tests fail, the pipeline stops and nothing deploys. If the build fails, same result. Only a successful test and build proceeds to deployment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After any deploy, I run a smoke test: call the actual API endpoint and verify it returns valid JSON with a count field. This confirms the full stack is working, Lambda executed, DynamoDB updated, and API Gateway routed correctly. Unit tests verify the handler logic in isolation; the smoke test verifies the infrastructure actually deployed correctly. Both matter.<\/p>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\"><strong>What I&#8217;d Do Differently<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A few things I&#8217;d change if I were starting fresh:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use OAC from the beginning. I initially set up the S3 bucket with public ACLs, which is the simpler approach and what the original Cloud Resume Challenge README described. The more secure model (private bucket with CloudFront OAC) is worth setting up correctly from day one, even if it takes slightly longer. Public buckets have a way of generating anxiety.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Rename master to main immediately. The backend repository&#8217;s default branch is still called master. It&#8217;s a minor thing but it&#8217;s inconsistent with the frontend repo and with modern convention. Worth fixing early before the discrepancy becomes a habit.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Remove .aws-sam from version control. The .aws-sam\/ directory contains build artifacts that don&#8217;t belong in git, so it&#8217;s the equivalent of committing node_modules. It inflates the repository size and adds noise to the commit history. Should have been in .gitignore from the start.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Set up a CloudFront cache invalidation step in the GitHub Actions workflow. Right now, pushing a change deploys to S3 but the old version stays cached in CloudFront until the cache expires. Adding a cache invalidation step (aws cloudfront create-invalidation &#8211;distribution-id $ID &#8211;paths &#8216;\/*&#8217;) to the workflow means changes are visible immediately after deploy.<\/p>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\"><strong>The Full Stack, Summarized<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The browser request arrives at Route 53, which points to CloudFront. CloudFront checks its cache and, if it has the file, serves it from the nearest edge location. If not, fetches from S3 (via OAC, the bucket stays private). The page loads. JavaScript calls the API Gateway endpoint. API Gateway invokes the Lambda function. Lambda does an atomic increment on the DynamoDB table and returns the updated count. JavaScript animates the number in the footer. The whole round-trip from page load to the counter display takes under 200 milliseconds on a warm Lambda.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><br>On the deployment side: I push to main on either repo. GitHub Actions authenticates to AWS via OIDC, so no stored credentials. Frontend workflow syncs files to S3. Backend workflow runs tests, builds the Lambda package with SAM, and deploys the CloudFormation stack. If everything passes, the update is live.<\/p>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\"><strong>What This Project Actually Teaches<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The Cloud Resume Challenge is sometimes described as a beginner project, which undersells it. Getting S3 + CloudFront + Route 53 + ACM all wired together correctly, with the certificate in the right region, OAC configured properly, CORS locked to your domain, and OIDC set up instead of stored credentials, requires actually understanding what each service does and how they interact. The number of ways to get it subtly wrong is substantial.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The backend: SAM, Lambda, DynamoDB, API Gateway, OIDC auth, automated testing, CI\/CD, covers most of the core serverless development loop. If you can build and deploy this, you can build and deploy most simple serverless APIs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The site is live at ron-mercier101.com. The backend repository is at <a href=\"https:\/\/github.com\/RonMercier\/cloud-resume-backend\" type=\"link\" id=\"https:\/\/github.com\/RonMercier\/cloud-resume-backend\">github.com\/RonMercier\/cloud-resume-backend<\/a> and the frontend at <a href=\"https:\/\/github.com\/RonMercier\/AWS-S3-Static-Website\" type=\"link\" id=\"https:\/\/github.com\/RonMercier\/AWS-S3-Static-Website\">github.com\/RonMercier\/AWS-S3-Static-Website<\/a>. Both have full READMEs with setup steps if you want to build something similar.<\/p>\n\n\n\n<p class=\"has-medium-font-size wp-block-paragraph\"><strong>Key Takeaways<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>S3 static website hosting + CloudFront + Route 53 + ACM is the standard AWS stack for a fast, globally distributed, HTTPS-only static site.<\/li>\n\n\n\n<li>Use Origin Access Control (OAC) to keep your S3 bucket private. CloudFront fetches from it, nobody else can.<\/li>\n\n\n\n<li>ACM certificates for CloudFront must be created in us-east-1 regardless of where everything else lives.<\/li>\n\n\n\n<li>OIDC authentication in GitHub Actions eliminates the need for stored IAM access keys; short-lived tokens assumed per workflow run.<\/li>\n\n\n\n<li>AWS SAM compresses serverless IaC significantly; Lambda + API Gateway + DynamoDB in 15 lines instead of 60+.<\/li>\n\n\n\n<li>PAY_PER_REQUEST DynamoDB billing is the right choice for unpredictable, low-volume traffic. Zero cost at zero traffic.<\/li>\n\n\n\n<li>HTTP API is cheaper and faster than REST API for simple endpoints with no need for caching, usage plans, or request validation.<\/li>\n\n\n\n<li>Atomic DynamoDB counter operations (ADD) handle concurrent updates correctly; read-modify-write does not.<\/li>\n\n\n\n<li>Test before you deploy. Always.<\/li>\n<\/ul>\n\n\n\n<div style=\"margin:48px 0 20px;padding:36px 32px;background:#050C18;border:1px solid #1A3A5C;border-top:3px solid #00D4FF;border-radius:8px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;text-align:center;\">\n\n  <div style=\"font-family:'Courier New',monospace;font-size:11px;letter-spacing:0.18em;text-transform:uppercase;color:#00D4FF;margin-bottom:14px;\">\n    \/\/ Before you go\n  <\/div>\n\n  <h3 style=\"margin:0 0 14px;font-size:24px;font-weight:800;color:#EEF5FF;line-height:1.25;\">\n    Get the security checklist most<br>businesses skip.\n  <\/h3>\n\n  <p style=\"margin:0 auto 24px;max-width:440px;font-size:15px;line-height:1.65;color:#8BB8D8;\">\n    A free 25-point audit covering the exact gaps attackers hit first \u2014\n    engineer-built, no jargon. Plus one practical security breakdown\n    every Tuesday. No fluff, no fear-mongering.\n  <\/p>\n\n  <a href=\"https:\/\/newsletter.securebydefault.io\" target=\"_blank\" rel=\"noopener\"\n     style=\"display:inline-block;background:#00D4FF;color:#050C18;text-decoration:none;\n     font-weight:700;font-size:15px;padding:15px 36px;border-radius:4px;letter-spacing:0.02em;\">\n    Get the Free Checklist &rarr;\n  <\/a>\n\n  <p style=\"margin:18px 0 0;font-family:'Courier New',monospace;font-size:11px;color:#4A7A9B;letter-spacing:0.04em;\">\n    Free on signup &nbsp;\u00b7&nbsp; Unsubscribe anytime &nbsp;\u00b7&nbsp; ~1 email per week\n  <\/p>\n\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>A complete walkthrough of how I built and deployed a portfolio website on AWS S3 static hosting, CloudFront CDN, Route 53 custom domain, a serverless visitor counter with Lambda and DynamoDB, and automated CI\/CD with GitHub Actions. No console clicking required.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[6,5],"tags":[57,58,52,65,54,56,60,55,62,53,61,63],"class_list":["post-156","post","type-post","status-publish","format-standard","hentry","category-cloud-security","category-server-security","tag-api","tag-api-gateway","tag-aws","tag-cloud-resume-challenge","tag-cloudfront","tag-dynamodb","tag-github-actions","tag-lambda","tag-oidc","tag-s3","tag-sam","tag-serverless"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.0 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>How I Built a Cloud Portfolio Website With a Visitor Counter Using AWS - From Start to Finish - SecureByDefault<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"How I Built a Cloud Portfolio Website With a Visitor Counter Using AWS - From Start to Finish - SecureByDefault\" \/>\n<meta property=\"og:description\" content=\"A complete walkthrough of how I built and deployed a portfolio website on AWS S3 static hosting, CloudFront CDN, Route 53 custom domain, a serverless visitor counter with Lambda and DynamoDB, and automated CI\/CD with GitHub Actions. No console clicking required.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/\" \/>\n<meta property=\"og:site_name\" content=\"SecureByDefault\" \/>\n<meta property=\"article:published_time\" content=\"2026-05-29T06:09:33+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-07-13T16:48:33+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_BlogPost5_Featured.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"628\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Ron Mercier\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Ron Mercier\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"13 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/cloud-resume-challenge-aws-portfolio-visitor-counter\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/cloud-resume-challenge-aws-portfolio-visitor-counter\\\/\"},\"author\":{\"name\":\"Ron Mercier\",\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/#\\\/schema\\\/person\\\/2ee989263a69e3324bce0cbed28ec0e8\"},\"headline\":\"How I Built a Cloud Portfolio Website With a Visitor Counter Using AWS &#8211; From Start to Finish\",\"datePublished\":\"2026-05-29T06:09:33+00:00\",\"dateModified\":\"2026-07-13T16:48:33+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/cloud-resume-challenge-aws-portfolio-visitor-counter\\\/\"},\"wordCount\":2746,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/#\\\/schema\\\/person\\\/2ee989263a69e3324bce0cbed28ec0e8\"},\"image\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/cloud-resume-challenge-aws-portfolio-visitor-counter\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/SecureByDefault_BlogPost5_Featured-1024x536.png\",\"keywords\":[\"API\",\"API Gateway\",\"AWS\",\"Cloud Resume Challenge\",\"CloudFront\",\"DynamoDB\",\"GitHub Actions\",\"Lambda\",\"OIDC\",\"S3\",\"SAM\",\"serverless\"],\"articleSection\":[\"Cloud Security\",\"Server Security\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/securebydefault.io\\\/blog\\\/cloud-resume-challenge-aws-portfolio-visitor-counter\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/cloud-resume-challenge-aws-portfolio-visitor-counter\\\/\",\"url\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/cloud-resume-challenge-aws-portfolio-visitor-counter\\\/\",\"name\":\"How I Built a Cloud Portfolio Website With a Visitor Counter Using AWS - From Start to Finish - SecureByDefault\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/cloud-resume-challenge-aws-portfolio-visitor-counter\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/cloud-resume-challenge-aws-portfolio-visitor-counter\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/SecureByDefault_BlogPost5_Featured-1024x536.png\",\"datePublished\":\"2026-05-29T06:09:33+00:00\",\"dateModified\":\"2026-07-13T16:48:33+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/cloud-resume-challenge-aws-portfolio-visitor-counter\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/securebydefault.io\\\/blog\\\/cloud-resume-challenge-aws-portfolio-visitor-counter\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/cloud-resume-challenge-aws-portfolio-visitor-counter\\\/#primaryimage\",\"url\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/SecureByDefault_BlogPost5_Featured.png\",\"contentUrl\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/SecureByDefault_BlogPost5_Featured.png\",\"width\":1200,\"height\":628},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/cloud-resume-challenge-aws-portfolio-visitor-counter\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"How I Built a Cloud Portfolio Website With a Visitor Counter Using AWS &#8211; From Start to Finish\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/\",\"name\":\"SecureByDefault\",\"description\":\"Cloud Security &amp; Cybersecurity for IT Professionals\",\"publisher\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/#\\\/schema\\\/person\\\/2ee989263a69e3324bce0cbed28ec0e8\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/#\\\/schema\\\/person\\\/2ee989263a69e3324bce0cbed28ec0e8\",\"name\":\"Ron Mercier\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/SecureByDefault_Log.png\",\"url\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/SecureByDefault_Log.png\",\"contentUrl\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/SecureByDefault_Log.png\",\"width\":512,\"height\":512,\"caption\":\"Ron Mercier\"},\"logo\":{\"@id\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/SecureByDefault_Log.png\"},\"sameAs\":[\"https:\\\/\\\/securebydefault.io\\\/blog\",\"https:\\\/\\\/www.linkedin.com\\\/in\\\/ron-mercier\\\/\",\"https:\\\/\\\/www.youtube.com\\\/channel\\\/UCDyWOTMI23S8Y3zwPoX3UkQ\"],\"url\":\"https:\\\/\\\/securebydefault.io\\\/blog\\\/author\\\/sbd_admin\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"How I Built a Cloud Portfolio Website With a Visitor Counter Using AWS - From Start to Finish - SecureByDefault","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/","og_locale":"en_US","og_type":"article","og_title":"How I Built a Cloud Portfolio Website With a Visitor Counter Using AWS - From Start to Finish - SecureByDefault","og_description":"A complete walkthrough of how I built and deployed a portfolio website on AWS S3 static hosting, CloudFront CDN, Route 53 custom domain, a serverless visitor counter with Lambda and DynamoDB, and automated CI\/CD with GitHub Actions. No console clicking required.","og_url":"https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/","og_site_name":"SecureByDefault","article_published_time":"2026-05-29T06:09:33+00:00","article_modified_time":"2026-07-13T16:48:33+00:00","og_image":[{"width":1200,"height":628,"url":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_BlogPost5_Featured.png","type":"image\/png"}],"author":"Ron Mercier","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Ron Mercier","Est. reading time":"13 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/#article","isPartOf":{"@id":"https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/"},"author":{"name":"Ron Mercier","@id":"https:\/\/securebydefault.io\/blog\/#\/schema\/person\/2ee989263a69e3324bce0cbed28ec0e8"},"headline":"How I Built a Cloud Portfolio Website With a Visitor Counter Using AWS &#8211; From Start to Finish","datePublished":"2026-05-29T06:09:33+00:00","dateModified":"2026-07-13T16:48:33+00:00","mainEntityOfPage":{"@id":"https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/"},"wordCount":2746,"commentCount":0,"publisher":{"@id":"https:\/\/securebydefault.io\/blog\/#\/schema\/person\/2ee989263a69e3324bce0cbed28ec0e8"},"image":{"@id":"https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/#primaryimage"},"thumbnailUrl":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_BlogPost5_Featured-1024x536.png","keywords":["API","API Gateway","AWS","Cloud Resume Challenge","CloudFront","DynamoDB","GitHub Actions","Lambda","OIDC","S3","SAM","serverless"],"articleSection":["Cloud Security","Server Security"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/","url":"https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/","name":"How I Built a Cloud Portfolio Website With a Visitor Counter Using AWS - From Start to Finish - SecureByDefault","isPartOf":{"@id":"https:\/\/securebydefault.io\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/#primaryimage"},"image":{"@id":"https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/#primaryimage"},"thumbnailUrl":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_BlogPost5_Featured-1024x536.png","datePublished":"2026-05-29T06:09:33+00:00","dateModified":"2026-07-13T16:48:33+00:00","breadcrumb":{"@id":"https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/#primaryimage","url":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_BlogPost5_Featured.png","contentUrl":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_BlogPost5_Featured.png","width":1200,"height":628},{"@type":"BreadcrumbList","@id":"https:\/\/securebydefault.io\/blog\/cloud-resume-challenge-aws-portfolio-visitor-counter\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/securebydefault.io\/blog\/"},{"@type":"ListItem","position":2,"name":"How I Built a Cloud Portfolio Website With a Visitor Counter Using AWS &#8211; From Start to Finish"}]},{"@type":"WebSite","@id":"https:\/\/securebydefault.io\/blog\/#website","url":"https:\/\/securebydefault.io\/blog\/","name":"SecureByDefault","description":"Cloud Security &amp; Cybersecurity for IT Professionals","publisher":{"@id":"https:\/\/securebydefault.io\/blog\/#\/schema\/person\/2ee989263a69e3324bce0cbed28ec0e8"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/securebydefault.io\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/securebydefault.io\/blog\/#\/schema\/person\/2ee989263a69e3324bce0cbed28ec0e8","name":"Ron Mercier","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_Log.png","url":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_Log.png","contentUrl":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_Log.png","width":512,"height":512,"caption":"Ron Mercier"},"logo":{"@id":"https:\/\/securebydefault.io\/blog\/wp-content\/uploads\/2026\/05\/SecureByDefault_Log.png"},"sameAs":["https:\/\/securebydefault.io\/blog","https:\/\/www.linkedin.com\/in\/ron-mercier\/","https:\/\/www.youtube.com\/channel\/UCDyWOTMI23S8Y3zwPoX3UkQ"],"url":"https:\/\/securebydefault.io\/blog\/author\/sbd_admin\/"}]}},"_links":{"self":[{"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/posts\/156","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/comments?post=156"}],"version-history":[{"count":5,"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/posts\/156\/revisions"}],"predecessor-version":[{"id":222,"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/posts\/156\/revisions\/222"}],"wp:attachment":[{"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/media?parent=156"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/categories?post=156"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/securebydefault.io\/blog\/wp-json\/wp\/v2\/tags?post=156"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}