Dev.to · 8 min read

I Built a Serverless Resume Site on AWS. Here's Everything That Broke Along the Way

I Built a Serverless Resume Site on AWS. Here's Everything That Broke Along the Way

After a decade in insurance operations, I decided to make my pivot into cloud engineering official by doing the Cloud Resume Challenge! I wanted to build it according to real-world best practices, so in addition to the steps the challenge provides, I incorporated a private S3 bucket secured with CloudFront Origin Access Control, a dedicated least-privilege IAM user scoped to only the permissions my CI/CD pipeline needed, and mocked unit tests with moto so nothing touches real AWS resources during testing. The site is live at derekjackson.click. It's a static resume served over HTTPS through CloudFront, backed by a Lambda-powered visitor counter, fully codified in Terraform, and deployed automatically via GitHub Actions. Here's how I built it and, more importantly, everything that went wrong along the way. The Architecture Frontend: S3 stores the static site, fully private with Block Public Access on. CloudFront sits in front of S3, using Origin Access Control (OAC) so CloudFront can read the private bucket. AWS Certificate Manager issues the HTTPS certificate (requested in us-east-1, a hard CloudFront requirement regardless of where the rest of the infrastructure is deployed.) Route 53 hosts the domain and holds the alias record pointing it at CloudFront. Backend: DynamoDB has a single item, on-demand table holds the visitor count. Lambda (Python) atomically increments that count on every request, avoiding race conditions from concurrent visitors. API Gateway (HTTP API) exposes the Lambda over a public GET /visitor-count endpoint the frontend calls when the page is loaded. Operations: Code is written, stored and edited locally. 2.pytest and moto test the Lambda function against a fully mocked DynamoDB before anything touches AWS. 3.After successful testing, git push commits the code to main initiating the pipeline. 4.GitHub Actions deploys frontend changes to S3 and backend changes to Lambda on every push to main. 5.Terraform codifies and imports all 13 live resources. Hiccup #1: My Domain Registration Just... Failed Before I could even get to work, my Route 53 domain registration failed with a very unhelpful error: "We can't finish registering your domain." Turns out the cause was that upgrading my account from the free tier to a paid plan had triggered a fraud alert on AWS's side, which blocked the registration. I opened a support case and it sat unassigned for two more days with no response. Instead of waiting around indefinitely, I signed up for a free trial of the Business Support+ plan specifically to get a faster response time, and kept building everything that didn't depend on DNS (Terraform, tests, and GitHub) in the meantime. Once Support resolved the fraud flag on their end and the domain came through clean, i was ready for my next step! Lesson: Do not let a third party's blocked dependency stop your progress. Real projects often depend on outside factors that you don't control. Focus on what you can control and hopefully, by the time you finish, the impediment will have been resolved. Hiccup #2: My SSL Certificate Died While I Was Waiting The good times continued to roll as while I was waiting on the domain to resolve, I requested an ACM certificate for DNS validation. As you may know, DNS validation requires a Route 53 hosted zone, which didn't exist yet because the domain wasn't registered. Chicken meet egg. By the time the domain finally came through, ACM had given up and marked my original certificate request as FAILED. Of course I didn't realize this until Terraform tried to import it and AWS rejected the import outright "no object exists with the given id." I verified the failure directly with the AWS CLI (aws acm list-certificates) rather than trusting the Terraform error alone, confirmed it was genuinely dead, and requested a fresh certificate once the hosted zone existed. Clean validation, no drama the second time. Lesson: Always verify errors at the source. Also a resource can fail silently while you wait on something else. Whenever you are ready to take your next step its never a bad idea to ensure any dependent resources are in their correct state. Hiccup #3: Terraform and the Console infrastructure did not align I wanted to build the infrastructure in the console first, for practice and so that I could visualize each service and their setup before importing everything into Terraform. As I'm sure you can imagine, there were a few small discrepancies between what Terraform assumed and what AWS actually had: My Lambda's IAM role lived under /service-role/, but my Terraform block didn't specify a path, so Terraform used the default IAM path / and wanted to destroy and recreate the role that was set up in the console. This was caught in the terraform plan output before being applied. A transcription error (irntxzj vs. irnntxzj) caused an import to fail with 'non-existent remote object'. This was resolved by querying AWS directly via the CLI for the correct role. -Terraform initiated to hashicorp/aws v5.0, but I was using the Python 3.14 Lambda runtime which required upgrading to v6, since v5.0 did not recognize it as a valid runtime value. That upgrade caused terraform plan to want to recreate my already-imported ACM certificate, even though terraform state show confirmed the state entry was fully intact. I learned that this is a known issue with major provider version jumps and was able to fix it with a terraform state rm and re-import. Lesson: Always run terraform plan before terraform apply, and review its output closely, especially any line showing a destroy or replace. Treat a mismatch between the plan and what is actually deployed as a signal to fix the .tf file, not a signal to let Terraform "correct" a resource that already works. Hiccup #4: My Text Editor Corrupted My Code This one is a total rookie mistake. Initially, I saved my script.js file as an Apple Pages document instead of using TextEdit and its Plain Text mode. Rich text formats embed hidden formatting metadata, so the "code" that got uploaded was unreadable. Thankfully, it was an easy fix. I just changed the format to "Make Plain Text". Lesson: Always confirm the format before saving anything code-related, and to run cat filename in Terminal after every edit to verify things were saved as intended. Hiccup #5: My Local Python Environment Fought Back I ran pip install to install my test dependencies. Homebrew labels its system Python as externally managed, so pip refused to install packages into it directly and threw an externally-managed-environment error. To resolve this, I created an isolated Python virtual environment (venv) to get past it. My first local test run inside the venv then failed with botocore.exceptions.NoRegionError. When Lambda runs a function, AWS automatically sets the region for boto3 to use. My local test ran outside of Lambda, so that automatic region setting did not exist, and boto3 raised the error. To resolve this, I added an explicit region_name="us-east-1" to the boto3 client in the Lambda function itself, then synced that same change into the deployed Lambda code so the local and live environments matched. Lesson: The venv let my test run without an error, but that didn't mean my code was actually correct. My code assumed AWS would supply a region, which Lambda does automatically in production but my local environment did not. The venv fixed a separate problem: pip being blocked, and never touched the missing region. Only adding region_name="us-east-1" fixed that. I learned that a test passing only confirms nothing crashed. It does not confirm that the code's assumptions about its environment, like where the region comes from, are actually correct. Hiccup #6: My Own GitHub Push Got Rejected The push that added my GitHub Actions workflow file got rejected with this error: "refusing to allow a Personal Access Token to create or update workflow .github/workflows/deploy.yml without workflow scope." As a security measure, GitHub requires a token to have workflow scope before it can add or change a workflow file. To fix it, I generated a new token with the workflow scope added. I then cleared the old cached token from macOS Keychain, so Git would use the new token instead of the old one. The push went through after that. To ensure the CI/CD pipeline was secure, I made sure it did not use my personal AWS credentials, which have broad permissions across my account. Instead, I created a dedicated github-actions-deploy IAM user with a custom policy scoped to three actions: S3 operations on one named bucket, lambda:UpdateFunctionCode on one named function, and loudfront:CreateInvalidation on one named distribution. The policy grants nothing beyond those three actions, so if there was ever a leak, an attacker could only affect this one project, not my entire AWS account. What's Next This is the first of three projects I'm building to round out my AWS portfolio: Cloud Resume Challenge (this one) — serverless fundamentals An automated insurance claims document processor using S3, Lambda, Textract, and DynamoDB — connecting my actual insurance operations background to AWS AI/data engineering A high-availability, multi-tier web app on a custom VPC with an ALB, Auto Scaling, and Multi-AZ RDS — proving out traditional enterprise networking fundamentals If you're doing the Cloud Resume Challenge yourself, or you've hit any of these same walls, I'd love to hear about it in the comments. Proof of work: Live demo: derekjackson.click Repository: github.com/DIJSAA/cloud-resume-challenge Connect with me: linkedin.com/in/derekijackson89

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News