
我在AWS上建造了一个没有服务器的再现站点. 这里的一切 打破沿途
I Built a Serverless Resume Site on AWS. Here's Everything That Broke Along the Way
经过十年的保险业务,我决定通过做"云再生挑战"来使我的支柱成为云工程官员! 我想按照现实世界的最佳做法来建造它,因此除了挑战所提供的步骤外,我还加入了一个用CloudFront Origin Access Control安全的私人S3水桶,一个专用的最小特权的IAM用户范围只为我的CI/CD管线所需的权限,并且用Moto来模拟单位测试,在测试过程中没有任何东西触及真正的AWS资源. 网站在德里克杰克森现场,点击 它是一个通过CloudFront在HTTPS上空服务的静态回放,由Lambda动力的访客计数器支持,完全被编入Terraform,并通过GitHub Actions自动部署. 我是这样建造的,更重要的是,所有的一切...
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 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, commits the code to initiating the pipeline. 4.GitHub Actions deploys frontend changes to S3 and backend changes to Lambda on every push to . 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 () 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 , 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 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 to want to recreate my already-imported ACM certificate, even though 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 and re-import. Lesson: Always run before , 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 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 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 in Terminal after every edit to verify things were saved as intended. Hiccup #5: My Local Python Environment Fought Back I ran to install my test dependencies. Homebrew labels its system Python as externally managed, so refused to install packages into it directly and threw an 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 . 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 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 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 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