<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Trinity Klein</title>
    <description>The latest articles on Forem by Trinity Klein (@tlklein).</description>
    <link>https://forem.com/tlklein</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3215228%2F3134815c-0dc6-4542-b541-efcaa88ddd48.png</url>
      <title>Forem: Trinity Klein</title>
      <link>https://forem.com/tlklein</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/tlklein"/>
    <language>en</language>
    <item>
      <title>Cloud Resume Challenge - Chunk 5 - The Final Write-Up</title>
      <dc:creator>Trinity Klein</dc:creator>
      <pubDate>Tue, 11 Nov 2025 17:11:47 +0000</pubDate>
      <link>https://forem.com/tlklein/cloud-resume-challenge-chunk-5-the-final-write-up-4n06</link>
      <guid>https://forem.com/tlklein/cloud-resume-challenge-chunk-5-the-final-write-up-4n06</guid>
      <description>&lt;p&gt;After weeks of effort, late-night debugging, and countless commits, my &lt;strong&gt;Cloud Resume Challenge&lt;/strong&gt; is finally complete.&lt;/p&gt;

&lt;p&gt;This project wasn’t just about hosting a résumé on AWS. It was about learning &lt;strong&gt;real-world cloud engineering practices&lt;/strong&gt;, from IAM fundamentals to Infrastructure as Code, CI/CD pipelines, and supply chain security.&lt;/p&gt;

&lt;p&gt;In this post, I’ll tie everything together, walk through the full journey, and share what I learned.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧩 Breaking It Down - The 5 Chunks
&lt;/h2&gt;

&lt;p&gt;Throughout the challenge, I split the work into &lt;strong&gt;five chunks&lt;/strong&gt;. Here’s a recap:&lt;/p&gt;

&lt;h3&gt;
  
  
  🔹 &lt;strong&gt;Chunk 0 - Access, Credentials, and Certification Prep&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Got &lt;strong&gt;AWS Certified Cloud Practitioner&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Created and secured an &lt;strong&gt;AWS Organization&lt;/strong&gt; (1 org, 2 OUs, MFA everywhere).&lt;/li&gt;
&lt;li&gt;Configured &lt;strong&gt;billing alerts&lt;/strong&gt; (\$0.01 threshold 🚨).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;a href="https://dev.to/tlklein/trinity-kleins-resume-and-portfolio-website-chunk-0-access-credentials-and-certification-prep-278a-temp-slug-4455053?preview=22dc752b359e4820188dec4395ac88a69d383a79d8d93411278c1ee0d181457fcd182ac8eff2bfff57b5632bf47f0a90bf27ccf0a275f902acbd1e67"&gt;Read Chunk 0 Blog&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 &lt;strong&gt;Chunk 1 - Building the Front-End&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Created my résumé with &lt;strong&gt;Hugo&lt;/strong&gt; (console theme for a Linux-terminal feel).&lt;/li&gt;
&lt;li&gt;Styled with &lt;strong&gt;HTML/CSS/JS&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Deployed to &lt;strong&gt;S3&lt;/strong&gt;, secured behind &lt;strong&gt;CloudFront&lt;/strong&gt; (no public bucket access).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;a href="https://dev.to/tlklein/trinity-kleins-resume-website-chunk-1-building-the-front-end-3cke-temp-slug-3131993?preview=b99be7490f68576d10525b8ef6cba506f7a8ef8f00860c8d9ed2be30ccaf24c3716995561e6ad56e18f9cd3bd0647feb39a96b75f900c51678ff68b4"&gt;Read Chunk 1 Blog&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 &lt;strong&gt;Chunk 2 - Building the API&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Added a &lt;strong&gt;visitor counter&lt;/strong&gt; using &lt;strong&gt;DynamoDB + Lambda + API Gateway&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Improved to track &lt;strong&gt;total + unique visits&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Enabled &lt;strong&gt;S3 bucket versioning + lifecycle policies&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;a href="https://dev.to/tlklein/trinity-kleins-resume-website-chunk-2-building-the-api-1mol-temp-slug-9727809?preview=6dc285524eb1627f199cd394906c3e309ec95c819ebdd4d658249efa932483137195c34f397c8027379944f6f93c15063d68bf19d7ef2dac649cf32c"&gt;Read Chunk 2 Blog&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 &lt;strong&gt;Chunk 3 - Front-End &amp;amp; Back-End Integration&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Built a &lt;strong&gt;GitHub Actions pipeline&lt;/strong&gt; with OIDC (no long-term creds).&lt;/li&gt;
&lt;li&gt;Automated: build → deploy → invalidate CloudFront cache → run Playwright smoke tests.&lt;/li&gt;
&lt;li&gt;Pipeline permissions scoped to a single IAM role.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;👉 &lt;a href="https://dev.to/tlklein/trinity-kleins-resume-website-chunk-3-building-the-front-endback-end-integration-836-temp-slug-5343347?preview=e9c876765ae87045b44f6b64619036c37e0673936f3ed1fed6c93ff9326129beadc5e982ebf51ddd720d395787d01bb10721eea8d938ee1d1b833f24"&gt;Read Chunk 3 Blog&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 &lt;strong&gt;Chunk 4 — Building the Automation and CI&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Migrated infrastructure into &lt;strong&gt;Terraform&lt;/strong&gt; for IaC.&lt;/li&gt;
&lt;li&gt;Hardened &lt;strong&gt;supply chain security&lt;/strong&gt; (pinned Actions, checksum verification, minimal IAM).&lt;/li&gt;
&lt;li&gt;Documented with &lt;strong&gt;architecture diagrams&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  👉 &lt;a href="https://dev.to/tlklein/trinity-kleins-resume-website-chunk-4-building-the-automation-and-ci-5hnh-temp-slug-9132662?preview=4593fb03a3c30834b7824215528008f0f9aa94d133ebdef3a93a8a77d5b41448100bea1029537bc4f87e1ca0647c9fbe40ec5abb6227e919fb06c731"&gt;Read Chunk 4 Blog&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Perfect — here’s a section you can insert &lt;strong&gt;after Chunk 4&lt;/strong&gt; (or right before your “Final Architecture” section). It introduces and explains the &lt;strong&gt;Supply Chain Security&lt;/strong&gt; challenge in the same polished tone and structure as your other chunks, keeping the technical depth and clarity consistent with your blog’s progression.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 &lt;strong&gt;Securing the Software Supply Chain&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;The software supply chain is everything that connects your laptop to production, the code you write, the dependencies you install, and the pipelines that deploy it all.&lt;br&gt;
As recent high-profile incidents have shown, &lt;strong&gt;a single compromised dependency or misconfigured pipeline&lt;/strong&gt; can cascade into a massive breach. This chunk was all about building trust and verifiability into that chain.&lt;/p&gt;


&lt;h2&gt;
  
  
  🧠 The Goal
&lt;/h2&gt;

&lt;p&gt;The objective was to extend my Cloud Resume project to &lt;strong&gt;add integrity checks, signed commits, automated scanning, and dependency validation&lt;/strong&gt;, in short, to treat my résumé like production-grade software.&lt;/p&gt;

&lt;p&gt;Here’s what I implemented:&lt;/p&gt;


&lt;h3&gt;
  
  
  🖋️ &lt;strong&gt;Signed Commits and Verified Merges&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Every commit to my front-end and back-end repositories is now &lt;strong&gt;cryptographically signed&lt;/strong&gt; using a GPG key.&lt;br&gt;
You’ll see the &lt;strong&gt;“Verified”&lt;/strong&gt; badge on each commit in GitHub, proof that code changes are truly mine.&lt;/p&gt;

&lt;p&gt;To take it a step further, I enforced &lt;strong&gt;branch protection rules&lt;/strong&gt;:&lt;br&gt;
✅ Only signed commits can be merged.&lt;br&gt;
✅ Status checks must pass before merging.&lt;/p&gt;

&lt;p&gt;This ensures no unsigned or unreviewed changes slip through.&lt;/p&gt;


&lt;h3&gt;
  
  
  🔍 &lt;strong&gt;Automated Code Scanning with CodeQL&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Next, I enabled &lt;strong&gt;GitHub’s CodeQL analysis&lt;/strong&gt;, a static analysis engine that detects security vulnerabilities in code patterns.&lt;/p&gt;

&lt;p&gt;My workflow now runs CodeQL scans automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On every &lt;strong&gt;pull request merge&lt;/strong&gt; to &lt;code&gt;main&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;On a &lt;strong&gt;monthly schedule&lt;/strong&gt; to catch dependency decay&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any “High” or “Critical” findings cause the build to fail automatically, protecting the integrity of my main branch.&lt;/p&gt;


&lt;h3&gt;
  
  
  🧾 &lt;strong&gt;Generating an SBOM (Software Bill of Materials)&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;An &lt;strong&gt;SBOM&lt;/strong&gt; lists every dependency your project includes, like an ingredient list for your software.&lt;/p&gt;

&lt;p&gt;During each CI/CD pipeline run, I used &lt;strong&gt;Syft&lt;/strong&gt; to generate an SBOM for the Lambda back-end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;syft &lt;span class="nb"&gt;dir&lt;/span&gt;:. &lt;span class="nt"&gt;-o&lt;/span&gt; json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sbom.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures I know &lt;em&gt;exactly&lt;/em&gt; what’s deployed, every library, its version, and its source.&lt;/p&gt;




&lt;h3&gt;
  
  
  🕵🏻 &lt;strong&gt;Automated Vulnerability Checks&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;To validate my SBOM, I layered in &lt;strong&gt;defense-in-depth vulnerability scans&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Grype&lt;/strong&gt; — scanned the SBOM for known CVEs across multiple databases.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OSV API&lt;/strong&gt; — queried Google’s Open Source Vulnerability database for any dependency issues.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s a snippet from my GitHub Actions step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Running OSV API vulnerability scan..."&lt;/span&gt;
jq &lt;span class="s1"&gt;'if .artifacts then .artifacts |= map(select(.name != null and .version != null and .version != "UNKNOWN")) else . end'&lt;/span&gt; sbom.json &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sbom-clean.json
&lt;span class="nb"&gt;cat &lt;/span&gt;sbom-clean.json | jq &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'.artifacts[] | {name: .name, version: .version}'&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; pkg&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nv"&gt;NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pkg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.name'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pkg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.version'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;RESPONSE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.osv.dev/v1/query"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;package&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: {&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$NAME&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}, &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;version&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$VERSION&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESPONSE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'.vulns'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'.'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"⚠️ Vulnerability found in &lt;/span&gt;&lt;span class="nv"&gt;$NAME&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="nv"&gt;$VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESPONSE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.vulns[] | "• \(.id): \(.summary)"'&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="k"&gt;fi
done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The job fails if any compromised dependency is detected — preventing insecure code from being deployed.&lt;/p&gt;




&lt;h3&gt;
  
  
  🧾 &lt;strong&gt;Artifact Signing&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Since my API runs on AWS Lambda (not containers), I integrated &lt;strong&gt;AWS Lambda Code Signing&lt;/strong&gt; directly into my Terraform definition.&lt;br&gt;
If I had been using containers, I’d have used &lt;strong&gt;Cosign&lt;/strong&gt; with &lt;strong&gt;AWS KMS&lt;/strong&gt; to sign my images, verifying that my deployment artifacts were unaltered from build to deploy.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧩 What This Achieved
&lt;/h2&gt;

&lt;p&gt;By layering these protections, my pipeline now provides:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Provenance&lt;/strong&gt; — cryptographic verification from commit to deploy&lt;br&gt;
✅ &lt;strong&gt;Integrity&lt;/strong&gt; — dependencies scanned and tracked via SBOM&lt;br&gt;
✅ &lt;strong&gt;Security Automation&lt;/strong&gt; — no manual checks needed&lt;br&gt;
✅ &lt;strong&gt;Trustworthiness&lt;/strong&gt; — everything verifiable, auditable, and repeatable&lt;/p&gt;

&lt;p&gt;This chunk transformed my project from “secure enough” to &lt;strong&gt;professionally hardened&lt;/strong&gt;, aligned with real DevSecOps practices.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ Final Architecture
&lt;/h2&gt;

&lt;p&gt;Here’s the &lt;strong&gt;big picture&lt;/strong&gt; of the project:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftmwa8rt8cgm8iy6ts78n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftmwa8rt8cgm8iy6ts78n.png" alt=" " width="800" height="589"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the &lt;strong&gt;front-end&lt;/strong&gt; of the project:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fys9cbz4ldo1qner3v9qd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fys9cbz4ldo1qner3v9qd.png" alt=" " width="752" height="624"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the &lt;strong&gt;back-end&lt;/strong&gt; of the project:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0o1n6ncnfgcad0g2oeou.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0o1n6ncnfgcad0g2oeou.png" alt=" " width="602" height="842"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is the &lt;strong&gt;S3 Lifecycle management&lt;/strong&gt; of the project:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgqqyjgo8ob6p37l4ehqj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgqqyjgo8ob6p37l4ehqj.png" alt=" " width="652" height="203"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🌐 The Final Product
&lt;/h2&gt;

&lt;p&gt;✨ Live Resume Website: &lt;a href="https://www.trinityklein.dev/" rel="noopener noreferrer"&gt;https://www.trinityklein.dev/&lt;/a&gt;&lt;br&gt;
📂 GitHub Repository: &lt;a href="https://github.com/tlklein/portfolio-website" rel="noopener noreferrer"&gt;https://github.com/tlklein/portfolio-website&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both the code and live deployment are currently &lt;strong&gt;publicly available&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;--&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 Key Lessons Learned
&lt;/h2&gt;

&lt;p&gt;This challenge taught me so much more than AWS commands and YAML syntax.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security First&lt;/strong&gt; → MFA, least privilege, no long-term creds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation Wins&lt;/strong&gt; → GitHub Actions reduced manual deployment risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure as Code&lt;/strong&gt; → Terraform makes my project reproducible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resilience Through Testing&lt;/strong&gt; → Playwright ensured code stability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation Matters&lt;/strong&gt; → Architecture diagrams simplified communication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was more than a résumé site, it became a &lt;strong&gt;mini production system&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 Helpful Resources
&lt;/h2&gt;

&lt;p&gt;If you’re inspired to start your own Cloud Resume Challenge, here are some key references:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cloudresumechallenge.dev" rel="noopener noreferrer"&gt;The Cloud Resume Challenge Official Site&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloudresumechallenge.dev/docs/extensions/terraform-getting-started/" rel="noopener noreferrer"&gt;Terraform Extension&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloudresumechallenge.dev/docs/extensions/supply-chain/" rel="noopener noreferrer"&gt;Supply Chain Security Extension&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/free" rel="noopener noreferrer"&gt;AWS Free Tier&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🙌 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Completing the Cloud Resume Challenge wasn’t easy. There were plenty of moments where I thought, &lt;em&gt;“Why isn’t this working?!”&lt;/em&gt; But those moments became the most valuable, because they forced me to dig deeper, learn faster, and think like a real cloud engineer.&lt;/p&gt;

&lt;p&gt;This challenge proved that building cloud-native projects requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Technical skills&lt;/strong&gt; 🛠️&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security awareness&lt;/strong&gt; 🔐&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistence&lt;/strong&gt; 💪&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And now, I have not only a working cloud résumé, but also a solid foundation for my cloud career.&lt;/p&gt;




&lt;h2&gt;
  
  
  🫰🏻 Let’s Connect
&lt;/h2&gt;

&lt;p&gt;If you’re following this challenge, or just passing by, I’d love to connect!&lt;br&gt;&lt;br&gt;
I’m always happy to help if you need guidance, want to swap ideas, or just chat about tech. 🚀  &lt;/p&gt;

&lt;p&gt;I’m also open to new opportunities, so if you have any inquiries or collaborations in mind, let me know!  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/trinity-klein/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐙 &lt;a href="https://github.com/tlklein" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;✍️ &lt;a href="https://dev.to/tlklein"&gt;Dev.to Blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;✉️ &lt;a href="mailto:tlklein05@gmail.com"&gt;Email Me&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>portfolio</category>
      <category>aws</category>
      <category>cloud</category>
      <category>career</category>
    </item>
    <item>
      <title>Cloud Resume Challenge - Chunk 4 - Building the Automation and CI</title>
      <dc:creator>Trinity Klein</dc:creator>
      <pubDate>Sun, 09 Nov 2025 20:17:22 +0000</pubDate>
      <link>https://forem.com/tlklein/cloud-resume-challenge-chunk-4-building-the-automation-and-ci-3fbo</link>
      <guid>https://forem.com/tlklein/cloud-resume-challenge-chunk-4-building-the-automation-and-ci-3fbo</guid>
      <description>&lt;p&gt;By now, my Cloud Resume journey had already given me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ A strong AWS foundation (Chunk 0)&lt;/li&gt;
&lt;li&gt;✅ A static front-end (Chunk 1)&lt;/li&gt;
&lt;li&gt;✅ A serverless API visitor counter (Chunk 2)&lt;/li&gt;
&lt;li&gt;✅ Automated deployments + smoke testing (Chunk 3)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But there was still &lt;strong&gt;one major gap&lt;/strong&gt;: my infrastructure wasn’t yet described in code, and my software supply chain needed extra hardening.&lt;/p&gt;

&lt;p&gt;This chunk was all about &lt;strong&gt;professional-grade DevOps practices&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;Terraform&lt;/strong&gt; to manage AWS resources as code.&lt;/li&gt;
&lt;li&gt;Secure the &lt;strong&gt;supply chain&lt;/strong&gt; for my build + deployment pipeline.&lt;/li&gt;
&lt;li&gt;Document the system with &lt;strong&gt;architecture diagrams&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Share the &lt;strong&gt;final product + GitHub repo&lt;/strong&gt; for transparency.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🌍 Terraforming the Cloud Resume
&lt;/h2&gt;

&lt;p&gt;ClickOps (manually clicking through the AWS console) works when learning, but in production it’s a recipe for drift and risk.&lt;br&gt;
Terraform solved that.&lt;/p&gt;

&lt;p&gt;I converted every major AWS component into &lt;strong&gt;modular Terraform code&lt;/strong&gt;, covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3&lt;/strong&gt; — hosts static assets with public access disabled, versioning enabled, and &lt;code&gt;force_destroy = false&lt;/code&gt; to prevent accidental wipes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront&lt;/strong&gt; — handles global delivery via an &lt;strong&gt;Origin Access Control (OAC)&lt;/strong&gt;, with &lt;strong&gt;ACM-managed TLS certificates&lt;/strong&gt; and &lt;strong&gt;custom error responses&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB&lt;/strong&gt; — stores visit counts and Terraform’s remote state locking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda + API Gateway&lt;/strong&gt; — backend logic for the visitor counter API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM roles &amp;amp; policies&lt;/strong&gt; — fine-grained permissions following the &lt;strong&gt;least-privilege&lt;/strong&gt; principle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route53 &amp;amp; ACM&lt;/strong&gt; — DNS management and SSL provisioning for the custom domain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example Terraform snippet for the S3 bucket module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"resume_site"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-cloud-resume-site"&lt;/span&gt;
  &lt;span class="nx"&gt;acl&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"private"&lt;/span&gt;

  &lt;span class="nx"&gt;versioning&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;lifecycle_rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"expire-old-versions"&lt;/span&gt;
    &lt;span class="nx"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="nx"&gt;noncurrent_version_expiration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Terraform, my infrastructure is now:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repeatable&lt;/strong&gt; → the entire stack can be recreated in minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditable&lt;/strong&gt; → every change tracked via Git history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secure&lt;/strong&gt; → hardening built directly into the configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modular&lt;/strong&gt; → easily reused for staging or new environments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🧱 &lt;strong&gt;Remote state&lt;/strong&gt; is stored in S3 and locked in DynamoDB to avoid concurrent changes during CI/CD runs, the same pattern used in enterprise environments.&lt;/p&gt;

&lt;p&gt;📖 Reference: &lt;a href="https://cloudresumechallenge.dev/docs/extensions/terraform-getting-started/" rel="noopener noreferrer"&gt;Terraform Extension — Cloud Resume Challenge&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🔐 Securing the Software Supply Chain
&lt;/h2&gt;

&lt;p&gt;A DevOps pipeline is only as secure as its weakest link, so I focused on &lt;strong&gt;supply chain hardening&lt;/strong&gt; this round.&lt;/p&gt;

&lt;p&gt;Here’s how I reinforced mine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Removed long-lived AWS credentials&lt;/strong&gt; → replaced with OIDC for GitHub Actions (introduced in Chunk 3).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pinned GitHub Actions versions&lt;/strong&gt; → no &lt;code&gt;@latest&lt;/code&gt; tags, reducing risk of malicious updates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checksum verification for dependencies&lt;/strong&gt; → ensuring integrity of Hugo themes, Node modules, and Terraform providers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM permissions scoped tightly&lt;/strong&gt; → CI/CD roles can deploy &lt;em&gt;only&lt;/em&gt; the resources they own.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lifecycle + versioning policies&lt;/strong&gt; → applied to S3 and artifacts for rollback and forensic traceability.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every deployment now runs with &lt;strong&gt;temporary, scoped credentials&lt;/strong&gt; and logs all actions through AWS CloudTrail.&lt;/p&gt;

&lt;p&gt;📖 Reference: &lt;a href="https://cloudresumechallenge.dev/docs/extensions/supply-chain/" rel="noopener noreferrer"&gt;Securing Your Software Supply Chain — Cloud Resume Challenge&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ Architecture Diagrams
&lt;/h2&gt;

&lt;p&gt;Sometimes a visual story says more than 1,000 lines of Terraform.&lt;br&gt;
These diagrams document how the system works, end to end.&lt;/p&gt;

&lt;h3&gt;
  
  
  🔹 High-Level Overview
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faohut9fda4ob8mkl4402.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faohut9fda4ob8mkl4402.png" alt=" " width="800" height="589"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: Hugo → S3 → CloudFront&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: API Gateway → Lambda → DynamoDB&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD&lt;/strong&gt;: GitHub Actions (OIDC) → Terraform → AWS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring&lt;/strong&gt;: Billing alerts, versioning, and logs baked in&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔹 Front-end
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F95i3o3y0nbmo1uaiyvdk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F95i3o3y0nbmo1uaiyvdk.png" alt=" " width="752" height="624"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Static files stored in S3 with encryption + access via CloudFront OAC&lt;/li&gt;
&lt;li&gt;Lifecycle rules manage old versions&lt;/li&gt;
&lt;li&gt;Cache invalidation triggered automatically on deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔹 Back-end
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhx464hgi9o3icursxycl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhx464hgi9o3icursxycl.png" alt=" " width="602" height="842"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lambda handles visitor tracking logic&lt;/li&gt;
&lt;li&gt;DynamoDB stores unique + total hits&lt;/li&gt;
&lt;li&gt;API Gateway exposes secure endpoints with error fallback&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔹 S3 Lifecycle Management
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4p6rp8j2exegwjux84o7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4p6rp8j2exegwjux84o7.png" alt=" " width="652" height="203"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Older noncurrent objects automatically transition to cheaper storage classes&lt;/li&gt;
&lt;li&gt;Retention and cleanup policies guard against bloat and accidental deletion&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🧱 AWS Account Structure &amp;amp; Hardening
&lt;/h2&gt;

&lt;p&gt;Beyond just code, I also aligned my AWS account structure with &lt;strong&gt;production-grade practices&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS Organization&lt;/strong&gt; with &lt;code&gt;production&lt;/code&gt; and &lt;code&gt;test&lt;/code&gt; OUs.&lt;/li&gt;
&lt;li&gt;Separate accounts for isolated workloads.&lt;/li&gt;
&lt;li&gt;MFA enforced across all users.&lt;/li&gt;
&lt;li&gt;Root account locked and only used for billing.&lt;/li&gt;
&lt;li&gt;Budget alerts trigger at $0.01 to catch any unintended spend early.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This structure ensures that even as the project scales, &lt;strong&gt;security, cost control, and governance remain intact&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌐 View the Final Product
&lt;/h2&gt;

&lt;p&gt;✨ &lt;strong&gt;Live Resume Website:&lt;/strong&gt; &lt;a href="https://www.trinityklein.dev/" rel="noopener noreferrer"&gt;https://www.trinityklein.dev/&lt;/a&gt;&lt;br&gt;
📂 &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/tlklein/portfolio-website/" rel="noopener noreferrer"&gt;https://github.com/tlklein/portfolio-website/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both the code and infrastructure are &lt;strong&gt;public, versioned, and reproducible&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Lessons Learned in Chunk 4
&lt;/h2&gt;

&lt;p&gt;By completing this chunk, I achieved:&lt;/p&gt;

&lt;p&gt;✅ Infrastructure as Code with Terraform&lt;br&gt;
✅ Stronger software supply chain security&lt;br&gt;
✅ Visual documentation via architecture diagrams&lt;br&gt;
✅ Multi-account AWS setup for safety + scalability&lt;br&gt;
✅ A fully public and auditable portfolio project&lt;/p&gt;

&lt;p&gt;At this stage, the Cloud Resume Challenge evolved from a portfolio exercise into a &lt;strong&gt;production-ready personal platform&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 Helpful Resources
&lt;/h2&gt;

&lt;p&gt;Resources that helped along the way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cloudresumechallenge.dev/docs/extensions/terraform-getting-started/" rel="noopener noreferrer"&gt;Terraform Extension — Cloud Resume Challenge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloudresumechallenge.dev/docs/extensions/supply-chain/" rel="noopener noreferrer"&gt;Securing Your Software Supply Chain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs" rel="noopener noreferrer"&gt;Terraform AWS Provider Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://owasp.org/www-community/attacks/Software_Supply_Chain_Attacks" rel="noopener noreferrer"&gt;OWASP Supply Chain Security Guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  💬 Let’s Connect
&lt;/h2&gt;

&lt;p&gt;That wraps up &lt;strong&gt;Chunk 4&lt;/strong&gt; of the Cloud Resume Challenge! 🎉&lt;/p&gt;

&lt;p&gt;Which IaC tool do you prefer for your cloud builds, &lt;strong&gt;Terraform, CDK, or CloudFormation&lt;/strong&gt;?&lt;br&gt;
Drop your thoughts below, I’d love to compare notes with other builders. 👇&lt;/p&gt;




&lt;p&gt;Would you like me to turn this version into a &lt;strong&gt;LinkedIn article format&lt;/strong&gt; next (optimized for publishing directly on LinkedIn with preview sections, emojis, and image layout cues)? It would pair perfectly with your series posts.&lt;/p&gt;

&lt;h2&gt;
  
  
  🫰🏻 Let’s Connect
&lt;/h2&gt;

&lt;p&gt;If you’re following this challenge, or just passing by, I’d love to connect!&lt;br&gt;&lt;br&gt;
I’m always happy to help if you need guidance, want to swap ideas, or just chat about tech. 🚀  &lt;/p&gt;

&lt;p&gt;I’m also open to new opportunities, so if you have any inquiries or collaborations in mind, let me know!  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/trinity-klein/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐙 &lt;a href="https://github.com/tlklein" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;✍️ &lt;a href="https://dev.to/tlklein"&gt;Dev.to Blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;✉️ &lt;a href="mailto:tlklein05@gmail.com"&gt;Email Me&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>cicd</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Cloud Resume Challenge - Chunk 3 - Front-End &amp; Back-End Integration</title>
      <dc:creator>Trinity Klein</dc:creator>
      <pubDate>Fri, 26 Sep 2025 14:00:00 +0000</pubDate>
      <link>https://forem.com/tlklein/cloud-resume-challenge-chunk-3-front-end-back-end-integration-11e5</link>
      <guid>https://forem.com/tlklein/cloud-resume-challenge-chunk-3-front-end-back-end-integration-11e5</guid>
      <description>&lt;p&gt;By this stage of the &lt;strong&gt;Cloud Resume Challenge&lt;/strong&gt;, you’ve got a working backend (API Gateway + Lambda + DynamoDB) and a static website hosted on S3 + CloudFront. The next step is where the pieces start to come together: connecting the frontend to the backend, automating deployments, and making sure your project doesn’t break every time you push new code.&lt;/p&gt;

&lt;p&gt;In this blog, I’ll walk through exactly how I tackled &lt;strong&gt;Chunk 3&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connecting the API to the website with JavaScript&lt;/li&gt;
&lt;li&gt;Writing smoke tests to validate the system end-to-end&lt;/li&gt;
&lt;li&gt;Automating deployments with GitHub Actions&lt;/li&gt;
&lt;li&gt;Securing everything with OIDC roles&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. Connecting the API to the Website
&lt;/h2&gt;

&lt;p&gt;The goal here is to &lt;strong&gt;display the live visitor count&lt;/strong&gt; from DynamoDB on the website.&lt;/p&gt;

&lt;h3&gt;
  
  
  Writing the JavaScript Call
&lt;/h3&gt;

&lt;p&gt;At its core, this is just a &lt;code&gt;fetch()&lt;/code&gt; request to your API Gateway endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateVisitorCount&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://your-api-gateway-id.execute-api.region.amazonaws.com/prod/visitors&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visitor-count&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;updateVisitorCount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it—just a couple of lines!&lt;/p&gt;

&lt;h3&gt;
  
  
  Solving CORS Issues
&lt;/h3&gt;

&lt;p&gt;The biggest headache is &lt;strong&gt;CORS (Cross-Origin Resource Sharing)&lt;/strong&gt;. By default, browsers will block calls between your static website domain and your API Gateway domain.&lt;/p&gt;

&lt;p&gt;To fix this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In API Gateway, enable &lt;strong&gt;CORS&lt;/strong&gt; for your routes.&lt;/li&gt;
&lt;li&gt;Allow the domain of your CloudFront distribution (not just &lt;code&gt;*&lt;/code&gt;) for better security.&lt;/li&gt;
&lt;li&gt;Test again in the browser console to confirm the API call succeeds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once working, your static site is now dynamic, pulling in live data from AWS.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Writing Smoke Tests with Playwright
&lt;/h2&gt;

&lt;p&gt;Manual testing is fine when you’re experimenting, but production-ready cloud apps need &lt;strong&gt;automation&lt;/strong&gt;. That’s where &lt;strong&gt;Playwright&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;Playwright allows you to run &lt;strong&gt;end-to-end smoke tests&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Visitor counter API updates correctly&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://your-api-gateway-id.execute-api.region.amazonaws.com/prod/visitors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeGreaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test confirms:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The API is reachable.&lt;/li&gt;
&lt;li&gt;It returns valid data.&lt;/li&gt;
&lt;li&gt;The visitor count increments as expected.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Other smoke tests I added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API rejects malformed requests.&lt;/li&gt;
&lt;li&gt;Visitor count initializes correctly (no undefined/null values).&lt;/li&gt;
&lt;li&gt;Database updates persist between calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These tests validate not just the Lambda code, but the &lt;strong&gt;entire deployed stack&lt;/strong&gt;: API Gateway, Lambda, IAM roles, and DynamoDB.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Automating with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Now that the site talks to the backend and tests exist, the next step is to &lt;strong&gt;automate deployments&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here’s what my &lt;strong&gt;GitHub Actions pipeline&lt;/strong&gt; does on every push to &lt;code&gt;main&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deploy Static Website&lt;/strong&gt; – Syncs the Hugo build to the S3 bucket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invalidate CloudFront Cache&lt;/strong&gt; – Ensures new changes show instantly worldwide.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy Lambda + Infrastructure&lt;/strong&gt; – Uses IaC (e.g., Terraform or SAM) for backend updates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run Smoke Tests&lt;/strong&gt; – Executes Playwright tests against the live API endpoint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example &lt;code&gt;deploy.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Cloud Resume Challenge&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Infrastructure&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./deploy_infra.sh&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Playwright Tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This workflow means &lt;strong&gt;no manual deployments&lt;/strong&gt;. Every code push goes live automatically—with tests ensuring nothing breaks.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Securing with OIDC Roles
&lt;/h2&gt;

&lt;p&gt;One of the most important lessons from this challenge: &lt;strong&gt;never use long-lived AWS credentials in CI/CD pipelines&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Instead, I set up &lt;strong&gt;OIDC (OpenID Connect) roles&lt;/strong&gt; between GitHub Actions and AWS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub is configured as an OIDC identity provider in AWS IAM.&lt;/li&gt;
&lt;li&gt;A dedicated IAM role allows GitHub workflows to assume permissions temporarily.&lt;/li&gt;
&lt;li&gt;No AWS keys stored in the repo—security risks eliminated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is how professional teams handle authentication for CI/CD. It’s secure, scalable, and reduces human error.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Chunk 3 transformed my project from “a static site with a backend” into a &lt;strong&gt;production-ready cloud application&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The frontend and backend communicate securely.&lt;/li&gt;
&lt;li&gt;Automated smoke tests validate every deployment.&lt;/li&gt;
&lt;li&gt;CI/CD pipelines push changes with confidence.&lt;/li&gt;
&lt;li&gt;OIDC roles keep the workflow secure.
These are what connecting services, automating deployments, and securing infrastructure that make it a complete application.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Helpful Resources
&lt;/h2&gt;

&lt;p&gt;Here’s what helped me in Chunk 3:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cloudresumechallenge.dev/" rel="noopener noreferrer"&gt;Cloud Resume Challenge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://playwright.dev/docs/intro" rel="noopener noreferrer"&gt;Playwright Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html" rel="noopener noreferrer"&gt;CloudFront Invalidation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/aws-actions/configure-aws-credentials" rel="noopener noreferrer"&gt;GitHub Actions for AWS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html" rel="noopener noreferrer"&gt;AWS OIDC with GitHub Actions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Let’s Connect
&lt;/h2&gt;

&lt;p&gt;If you’re following this challenge, or just passing by, I’d love to connect! I’m always happy to help if you need guidance, want to swap ideas, or just chat about tech.  &lt;/p&gt;

&lt;p&gt;I’m also open to new opportunities, so if you have any inquiries or collaborations in mind, let me know!  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/trinity-klein/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐙 &lt;a href="https://github.com/tlklein" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;✍️ &lt;a href="https://dev.to/tlklein"&gt;Dev.to Blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;✉️ &lt;a href="//mailto:tlklein05@gmail.com"&gt;Email Me&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>serverless</category>
      <category>aws</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Cloud Resume Challenge - Chunk 2 - Building the API</title>
      <dc:creator>Trinity Klein</dc:creator>
      <pubDate>Fri, 19 Sep 2025 14:00:00 +0000</pubDate>
      <link>https://forem.com/tlklein/cloud-resume-challenge-chunk-2-building-the-api-4dgn</link>
      <guid>https://forem.com/tlklein/cloud-resume-challenge-chunk-2-building-the-api-4dgn</guid>
      <description>&lt;p&gt;After getting my &lt;strong&gt;front-end live on S3 + CloudFront&lt;/strong&gt; in &lt;a href="https://dev.to/tlklein/trinity-kleins-resume-website-chunk-1-building-the-front-end-3cke-temp-slug-3131993?preview=b99be7490f68576d10525b8ef6cba506f7a8ef8f00860c8d9ed2be30ccaf24c3716995561e6ad56e18f9cd3bd0647feb39a96b75f900c51678ff68b4"&gt;Chunk 1&lt;/a&gt;, it was time to give it some brains. 🧠&lt;/p&gt;

&lt;p&gt;The goal for this stage:&lt;br&gt;
👉 Add a &lt;strong&gt;visitor counter&lt;/strong&gt; (hit counter → visitor counter) to my portfolio website.&lt;/p&gt;

&lt;p&gt;This wasn’t just about displaying a number, it was about learning how to stitch together &lt;strong&gt;AWS Lambda, API Gateway, DynamoDB, and IAM&lt;/strong&gt; into a working serverless backend.&lt;/p&gt;


&lt;h2&gt;
  
  
  🗄️ Designing the Visitor Counter
&lt;/h2&gt;

&lt;p&gt;The stack I chose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB&lt;/strong&gt; → store visitor data (IPs + visit counts).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt; → serverless compute that updates/query the table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway&lt;/strong&gt; → REST API to expose the Lambda function securely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM Roles&lt;/strong&gt; → restrict who/what can read/write from DynamoDB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s the flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser → API Gateway → Lambda → DynamoDB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On page load, the &lt;strong&gt;API Gateway&lt;/strong&gt; calls the Lambda function, which fetches &amp;amp; updates the DynamoDB table. The number is then displayed in my site’s footer.&lt;/p&gt;

&lt;p&gt;If the data can’t be fetched, the site gracefully falls back to showing &lt;strong&gt;"Loading..."&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔁 From Hit Counter → Visitor Counter
&lt;/h2&gt;

&lt;p&gt;Originally, this was just a simple &lt;strong&gt;hit counter&lt;/strong&gt;, every page refresh added +1. But I refactored it into a proper &lt;strong&gt;visitor counter&lt;/strong&gt; with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Total visits&lt;/strong&gt; (every page load).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unique visitors&lt;/strong&gt; (per IP, per 24-hour window).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;new DynamoDB table&lt;/strong&gt; to store hashed IP addresses.&lt;/li&gt;
&lt;li&gt;A smarter &lt;strong&gt;Lambda function&lt;/strong&gt; (see below).&lt;/li&gt;
&lt;li&gt;Test data (dummy items) to validate in production.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  📝 The Lambda Function (v2)
&lt;/h2&gt;

&lt;p&gt;Here’s the core Lambda function I deployed (Python 3.9):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Version 2 of lambda function
# Stores IP addresses in a one-way hash
# Only counts unique visits once per 24 hours
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;

&lt;span class="n"&gt;dynamodb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dynamodb&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;VisitorCounter&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;UNIQUE_VISITOR_WINDOW_HOURS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;  &lt;span class="c1"&gt;# uniqueness window
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Incoming event:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# Extract and hash IP
&lt;/span&gt;    &lt;span class="n"&gt;ip_address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_ip_address&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ip_address&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;error_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unable to determine IP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;hashed_ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hash_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip_address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;now_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ip_address&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hashed_ip&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;is_new_visitor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Item&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;last_visit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Item&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;last_visit&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;last_visit_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_visit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last_visit_time&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UNIQUE_VISITOR_WINDOW_HOURS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;is_new_visitor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
                &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ip_address&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hashed_ip&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
                    &lt;span class="n"&gt;UpdateExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET visit_count = visit_count + :inc, last_visit = :now&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:inc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:now&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now_str&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;is_new_visitor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
            &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip_address&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hashed_ip&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;visit_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;first_visit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now_str&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_visit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now_str&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;scan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;AttributesToGet&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;visit_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;total_visits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;visit_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;unique_visitors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unique_visitors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;unique_visitors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_visits&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;total_visits&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_new_visitor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;is_new_visitor&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;error_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Internal server error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_ip_address&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;ip_sources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requestContext&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sourceIp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requestContext&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;identity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sourceIp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-forwarded-for&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-real-ip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-Real-IP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cf-connecting-ip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CF-Connecting-IP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ip_sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hash_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;error_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔒 &lt;strong&gt;Privacy-first&lt;/strong&gt; → IP addresses are &lt;strong&gt;SHA-256 hashed&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;🕒 &lt;strong&gt;Uniqueness window&lt;/strong&gt; → Only 1 count per IP in 24 hours.&lt;/li&gt;
&lt;li&gt;📊 &lt;strong&gt;Metrics returned&lt;/strong&gt; → total visits, unique visitors, is_new_visitor.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🛡️ IAM Role Setup
&lt;/h2&gt;

&lt;p&gt;To keep things secure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Lambda function only has &lt;code&gt;dynamodb:GetItem&lt;/code&gt;, &lt;code&gt;PutItem&lt;/code&gt;, &lt;code&gt;UpdateItem&lt;/code&gt;, and &lt;code&gt;Scan&lt;/code&gt; permissions for the specific table.&lt;/li&gt;
&lt;li&gt;API Gateway was given permission to invoke the Lambda.&lt;/li&gt;
&lt;li&gt;No overly broad permissions, &lt;strong&gt;least privilege only&lt;/strong&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# IAM Policy Example
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["dynamodb:UpdateItem", "dynamodb:GetItem"],
      "Resource": "arn:aws:dynamodb:us-east-1:123456789012:table/VisitorCounter"
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  📦 S3 Bucket Improvements
&lt;/h2&gt;

&lt;p&gt;While working on the API, I also hardened my S3 setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Versioning enabled&lt;/strong&gt; → recover files in case of accidental overwrite.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Lifecycle policies&lt;/strong&gt; → move old versions to cheaper storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gave me resilience + cost control.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔄 REST API vs Real-Time
&lt;/h2&gt;

&lt;p&gt;I briefly considered building a &lt;strong&gt;real-time visitor counter&lt;/strong&gt; using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API Gateway (WebSockets)&lt;/li&gt;
&lt;li&gt;DynamoDB Streams&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But here’s the truth:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⚠️ It would be &lt;strong&gt;more expensive&lt;/strong&gt; at scale.&lt;/li&gt;
&lt;li&gt;⚠️ It would be &lt;strong&gt;much more complex&lt;/strong&gt; to set up and optimize.&lt;/li&gt;
&lt;li&gt;✅ And… the user experience would be basically the same.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I stuck with a &lt;strong&gt;REST API&lt;/strong&gt;, simple, reliable, and cost-effective.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Lessons Learned in Chunk 2
&lt;/h2&gt;

&lt;p&gt;By the end of this chunk, I achieved:&lt;br&gt;
✅ Built an &lt;strong&gt;API Gateway + Lambda + DynamoDB&lt;/strong&gt; visitor counter&lt;br&gt;
✅ Upgraded from &lt;strong&gt;hit counter → unique visitor counter&lt;/strong&gt;&lt;br&gt;
✅ Secured roles with IAM least privilege&lt;br&gt;
✅ Added resilience with &lt;strong&gt;bucket versioning + lifecycle policies&lt;/strong&gt;&lt;br&gt;
✅ Made intentional design decisions (REST &amp;gt; WebSocket for this use case)&lt;/p&gt;

&lt;p&gt;This was the first time my project felt &lt;strong&gt;full-stack serverless&lt;/strong&gt;, front-end + backend working together. 💡&lt;/p&gt;




&lt;h2&gt;
  
  
  🌟 What’s Next?
&lt;/h2&gt;

&lt;p&gt;Next up: &lt;strong&gt;Chunk 3&lt;/strong&gt;, adding a &lt;strong&gt;CI/CD pipeline&lt;/strong&gt; so that deployments are automated, tested, and production-ready.&lt;/p&gt;

&lt;p&gt;That’s when the project will really level up. ⚡&lt;/p&gt;

&lt;p&gt;Drop a comment with how you approached your visitor counter, did you try REST, WebSockets, or something else? I would love to know how you approached it.&lt;/p&gt;




&lt;h2&gt;
  
  
  📚 Helpful Resources
&lt;/h2&gt;

&lt;p&gt;Here’s what helped me in Chunk 2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cloudresumechallenge.dev/" rel="noopener noreferrer"&gt;Cloud Resume Challenge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/best-practices.html" rel="noopener noreferrer"&gt;AWS DynamoDB Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html" rel="noopener noreferrer"&gt;IAM Policies Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-rest-api.html" rel="noopener noreferrer"&gt;AWS API Gateway REST API Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/ObjectLifecycleConfiguration.html" rel="noopener noreferrer"&gt;S3 Versioning + Lifecycle Management&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🫰🏻 Let’s Connect
&lt;/h2&gt;

&lt;p&gt;If you’re following this challenge, or just passing by, I’d love to connect!&lt;br&gt;&lt;br&gt;
I’m always happy to help if you need guidance, want to swap ideas, or just chat about tech. 🚀  &lt;/p&gt;

&lt;p&gt;I’m also open to new opportunities, so if you have any inquiries or collaborations in mind, let me know!  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💼 &lt;a href="https://www.linkedin.com/in/trinity-klein/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐙 &lt;a href="https://github.com/tlklein" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;✍️ &lt;a href="https://dev.to/tlklein"&gt;Dev.to Blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;✉️ &lt;a href="//mailto:tlklein05@gmail.com"&gt;Email Me&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>devchallenge</category>
      <category>serverless</category>
      <category>aws</category>
    </item>
    <item>
      <title>Cloud Resume Challenge - Chunk 1 - Building the Front-End</title>
      <dc:creator>Trinity Klein</dc:creator>
      <pubDate>Fri, 12 Sep 2025 14:00:00 +0000</pubDate>
      <link>https://forem.com/tlklein/cloud-resume-challenge-chunk-1-building-the-front-end-49hi</link>
      <guid>https://forem.com/tlklein/cloud-resume-challenge-chunk-1-building-the-front-end-49hi</guid>
      <description>&lt;h1&gt;
  
  
  Cloud Resume Challenge – Chunk 1: Building and Hosting the Front-End
&lt;/h1&gt;

&lt;p&gt;The second step of the &lt;strong&gt;Cloud Resume Challenge&lt;/strong&gt; is all about creating and hosting the front-end of your resume website. While this section may feel simple compared to the upcoming back-end work, it’s critical because it sets the stage for later integration. You’ll build a static site, host it securely on &lt;strong&gt;AWS S3&lt;/strong&gt;, and distribute it globally with &lt;strong&gt;CloudFront&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This guide explains exactly how I approached Chunk 1, the decisions I made, and the AWS best practices I followed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Why the Front-End Matters
&lt;/h2&gt;

&lt;p&gt;It’s tempting to think of HTML and CSS as the easy part of the challenge. In reality, this stage is more about &lt;strong&gt;completeness and professionalism&lt;/strong&gt; than design. A static website:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Demonstrates end-to-end deployment skills.&lt;/li&gt;
&lt;li&gt;Gives you something tangible to showcase in an interview.&lt;/li&gt;
&lt;li&gt;Prepares you for the bigger challenge of &lt;strong&gt;front-end + back-end integration&lt;/strong&gt;, which is where cloud engineering skills really shine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The purpose is not to become a front-end expert, but to build a solid, secure platform that can connect with AWS services like DynamoDB, Lambda, and API Gateway later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Choosing a Framework
&lt;/h2&gt;

&lt;p&gt;Instead of writing a single massive HTML file, I used the &lt;strong&gt;Hugo Console theme&lt;/strong&gt;. Hugo is a &lt;strong&gt;static site generator&lt;/strong&gt;, which makes your website more maintainable and scalable over time. Benefits of Hugo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extremely fast build times (~55ms load time on my site).&lt;/li&gt;
&lt;li&gt;Modular templates for pages like &lt;strong&gt;Resume&lt;/strong&gt;, &lt;strong&gt;Projects&lt;/strong&gt;, &lt;strong&gt;Blog&lt;/strong&gt;, and &lt;strong&gt;Contact&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Easy customization of text, images, and layouts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you prefer JavaScript frameworks, you could use &lt;strong&gt;Next.js, React, Vue, or Svelte&lt;/strong&gt;, but for the purposes of this challenge, Hugo (or Eleventy) is more than enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Hosting the Static Site on AWS S3
&lt;/h2&gt;

&lt;p&gt;Once the site files were generated, I hosted them on an &lt;strong&gt;Amazon S3 bucket&lt;/strong&gt;. The naive approach is to make the bucket public for static hosting—but that’s insecure. Instead, I followed the professional setup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create an S3 bucket&lt;/strong&gt; with the same name as your domain (e.g., &lt;code&gt;mydomain.com&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block all public access&lt;/strong&gt;. This prevents anyone from directly reaching your bucket objects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload the Hugo-generated files&lt;/strong&gt; to the S3 bucket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable static website hosting&lt;/strong&gt; to get an endpoint, though it won’t be public yet.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 4: Securing with CloudFront and OAC
&lt;/h2&gt;

&lt;p&gt;To deliver the site securely and globally, I set up &lt;strong&gt;CloudFront&lt;/strong&gt;. This provides caching, speed, and access control:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a CloudFront distribution&lt;/strong&gt; pointing to the S3 bucket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable Origin Access Control (OAC)&lt;/strong&gt; so only CloudFront can fetch objects from the bucket.&lt;/li&gt;
&lt;li&gt;Update the &lt;strong&gt;S3 bucket policy&lt;/strong&gt; to grant CloudFront permission.&lt;/li&gt;
&lt;li&gt;Configure &lt;strong&gt;cache behaviors&lt;/strong&gt; to improve performance while still allowing invalidation when updates are deployed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With this, visitors only interact with CloudFront, not the S3 bucket, which keeps the environment secure and professional.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Testing the Setup
&lt;/h2&gt;

&lt;p&gt;After configuration, I validated the site:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verified global accessibility&lt;/strong&gt; by testing load times from multiple locations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirmed cache invalidation&lt;/strong&gt; worked properly when I updated site content.&lt;/li&gt;
&lt;li&gt;Checked that &lt;strong&gt;no direct public access&lt;/strong&gt; to the S3 bucket was possible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result was a lightweight, professional-grade static site ready to integrate with AWS back-end services in future chunks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static sites are the foundation&lt;/strong&gt;: This step isn’t about flashy design; it’s about creating a maintainable, secure project you can build on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use frameworks like Hugo&lt;/strong&gt;: They save time and give you clean, fast-loading templates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow AWS security best practices&lt;/strong&gt;: Block public access to S3, use OAC, and route all traffic through CloudFront.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Think ahead&lt;/strong&gt;: This site will soon interact with APIs, databases, and automation. Setting it up securely now pays dividends later.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Helpful Resources
&lt;/h2&gt;

&lt;p&gt;Here are some resources that helped me through Chunk 1:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cloudresumechallenge.dev/" rel="noopener noreferrer"&gt;Cloud Resume Challenge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gohugo.io/installation/" rel="noopener noreferrer"&gt;Hugo Extended&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/mrmierzejewski/hugo-theme-console/" rel="noopener noreferrer"&gt;Hugo Console Theme&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteHosting.html" rel="noopener noreferrer"&gt;AWS Static Website Whitepapers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/dev/WebsiteHosting.html" rel="noopener noreferrer"&gt;Hosting a Static Website on S3 + CloudFront (AWS Docs)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/premiumsupport/knowledge-center/cloudfront-serve-static-website/" rel="noopener noreferrer"&gt;CloudFront + S3 Security Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Let’s Connect
&lt;/h2&gt;

&lt;p&gt;If you’re following this challenge, or just passing by, I’d love to connect! I’m always happy to help if you need guidance, want to swap ideas, or just chat about tech.&lt;/p&gt;

&lt;p&gt;I’m also open to new opportunities, so if you have any inquiries or collaborations in mind, let me know!  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.linkedin.com/in/trinity-klein/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/tlklein" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/tlklein"&gt;Dev.to Blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="//mailto:tlklein05@gmail.com"&gt;Email Me&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>career</category>
      <category>tutorial</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Cloud Resume Challenge - Chunk 0 - Access, Credentials, and Certification Prep</title>
      <dc:creator>Trinity Klein</dc:creator>
      <pubDate>Fri, 05 Sep 2025 14:00:00 +0000</pubDate>
      <link>https://forem.com/tlklein/cloud-resume-challenge-chunk-0-access-credentials-and-certification-prep-56db</link>
      <guid>https://forem.com/tlklein/cloud-resume-challenge-chunk-0-access-credentials-and-certification-prep-56db</guid>
      <description>&lt;p&gt;The &lt;strong&gt;Cloud Resume Challenge&lt;/strong&gt; starts long before you deploy your first Lambda function or wire up a DynamoDB table. The first step is creating a &lt;strong&gt;secure foundation&lt;/strong&gt;—because AWS is powerful, and mistakes can lead to credential leaks, security incidents, or runaway billing. This guide walks through how I set up my environment, managed credentials professionally, and earned my &lt;strong&gt;AWS Certified Cloud Practitioner&lt;/strong&gt; certification to validate my understanding of the cloud.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Setup and Safety (Before You Begin)
&lt;/h2&gt;

&lt;p&gt;Think of AWS like a power tool: misused, it can cause damage. Before provisioning anything, take these precautions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Enable MFA on the root account.&lt;/strong&gt; Never use the root user for daily tasks. Store its credentials in a password manager and only log in for account setup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create an IAM user with MFA.&lt;/strong&gt; Use this user to manage access, but avoid attaching long-term access keys directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adopt IAM roles.&lt;/strong&gt; Instead of static credentials, assume roles with temporary credentials for each session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set billing alerts.&lt;/strong&gt; Go to the AWS Billing Console and create alerts to notify you if costs exceed your expected usage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete unused resources.&lt;/strong&gt; The fastest way to eliminate unexpected charges is to remove resources once you’re done testing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These steps prevent attackers from hijacking your account and keep costs predictable while learning.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Access and Credentials
&lt;/h2&gt;

&lt;p&gt;You’ll need two ways to access AWS:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The AWS Console&lt;/strong&gt; – the web interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The CLI/SDKs&lt;/strong&gt; – where automation and code-driven access live.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are two approaches to configuring access:&lt;/p&gt;

&lt;h3&gt;
  
  
  The Original Way (if you must)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create an &lt;strong&gt;IAM user&lt;/strong&gt; with programmatic access.&lt;/li&gt;
&lt;li&gt;Store credentials in configuration files on your local machine.&lt;/li&gt;
&lt;li&gt;Optionally use tools like &lt;strong&gt;aws-vault&lt;/strong&gt; or &lt;strong&gt;awsume&lt;/strong&gt; to manage role assumptions securely.&lt;/li&gt;
&lt;li&gt;Always enable MFA and never hardcode credentials into code or repositories.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This method works, but static credentials are a liability.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Professional Way (recommended)
&lt;/h3&gt;

&lt;p&gt;This is how cloud teams operate in production, and how I structured my environment:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create an AWS Organization&lt;/strong&gt; in your root account.&lt;/li&gt;
&lt;li&gt;Add at least two &lt;strong&gt;Organizational Units (OUs)&lt;/strong&gt; – one for &lt;strong&gt;Production&lt;/strong&gt;, one for &lt;strong&gt;Development/Test&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Inside each OU, create accounts. Use &lt;code&gt;+&lt;/code&gt; email suffixes (e.g., &lt;code&gt;you+dev@gmail.com&lt;/code&gt;) to reuse your main email.&lt;/li&gt;
&lt;li&gt;Set up &lt;strong&gt;AWS IAM Identity Center (formerly SSO)&lt;/strong&gt;. This lets you sign in securely without relying on static keys.&lt;/li&gt;
&lt;li&gt;Configure the CLI with Identity Center for seamless login across accounts. Tools like &lt;code&gt;aws-sso-util&lt;/code&gt; make this easier.&lt;/li&gt;
&lt;li&gt;Always enforce &lt;strong&gt;MFA on SSO logins&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This setup takes longer, but once complete, you’ll have a professional-grade AWS environment with no need for long-lived credentials.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: AWS Certified Cloud Practitioner
&lt;/h2&gt;

&lt;p&gt;Before diving deep into infrastructure, I prepared for and passed the &lt;strong&gt;AWS Certified Cloud Practitioner&lt;/strong&gt; exam.&lt;/p&gt;

&lt;p&gt;This entry-level certification validated my knowledge of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Core AWS services&lt;/strong&gt;: S3, EC2, Lambda, DynamoDB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global infrastructure&lt;/strong&gt;: regions, availability zones, edge locations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security best practices&lt;/strong&gt;: IAM, MFA, shared responsibility model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing and pricing models&lt;/strong&gt;: free tier, pay-as-you-go, cost optimization strategies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The study process reinforced the importance of secure setup and gave me the vocabulary to navigate AWS documentation confidently.&lt;/p&gt;

&lt;p&gt;📎 View my verified badge here: &lt;a href="https://lnkd.in/gWzvGaxD" rel="noopener noreferrer"&gt;AWS Cloud Practitioner on Credly&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Guarding Against Costs
&lt;/h2&gt;

&lt;p&gt;Even within the free tier, costs can accumulate unexpectedly. Best practices I followed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Delete unused accounts/resources&lt;/strong&gt; after testing.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;billing alerts&lt;/strong&gt; with SNS notifications for thresholds.&lt;/li&gt;
&lt;li&gt;Regularly review the &lt;strong&gt;Billing Dashboard&lt;/strong&gt; to confirm free-tier compliance.&lt;/li&gt;
&lt;li&gt;If mistakes happen (e.g., leaving an expensive instance running), contact AWS Support—first-time errors are often forgiven.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Start with &lt;strong&gt;security-first principles&lt;/strong&gt;: MFA, least privilege, temporary credentials.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;AWS Organizations and IAM Identity Center&lt;/strong&gt; for scalable, professional credential management.&lt;/li&gt;
&lt;li&gt;Validate knowledge with the &lt;strong&gt;AWS Certified Cloud Practitioner&lt;/strong&gt; exam.&lt;/li&gt;
&lt;li&gt;Monitor and control &lt;strong&gt;billing&lt;/strong&gt; to avoid surprises.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this foundation, you’re ready to move into the hands-on build phases of the Cloud Resume Challenge: deploying static websites, APIs, and serverless functions with confidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Helpful Resources
&lt;/h2&gt;

&lt;p&gt;If you’re starting, here are some resources that helped me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/certification/certified-cloud-practitioner/" rel="noopener noreferrer"&gt;AWS Certified Cloud Practitioner Exam Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloudresumechallenge.dev/" rel="noopener noreferrer"&gt;The Cloud Resume Challenge Official Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/aws-builders/"&gt;Minimal AWS SSO setup for personal AWS development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/monitor_estimated_charges_with_cloudwatch.html" rel="noopener noreferrer"&gt;Setting up billing alerts with CloudWatch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/organizations/" rel="noopener noreferrer"&gt;AWS Organizations Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Let’s Connect
&lt;/h2&gt;

&lt;p&gt;If you’re following this challenge, or just passing by, I’d love to connect! I’m always happy to help if you need guidance, want to swap ideas, or just chat about tech.&lt;/p&gt;

&lt;p&gt;I’m also open to new opportunities, so if you have any inquiries or collaborations in mind, let me know!  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.linkedin.com/in/trinity-klein/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/tlklein" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/tlklein"&gt;Dev.to Blog&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="//mailto:tlklein05@gmail.com"&gt;Email Me&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>career</category>
      <category>beginners</category>
      <category>security</category>
    </item>
  </channel>
</rss>
