<?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>DEV Community: cristianxcueva</title>
    <description>The latest articles on DEV Community by cristianxcueva (@cristianxcueva).</description>
    <link>https://dev.to/cristianxcueva</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4008465%2F24161561-eda0-470f-90f4-76cd7530f187.png</url>
      <title>DEV Community: cristianxcueva</title>
      <link>https://dev.to/cristianxcueva</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cristianxcueva"/>
    <language>en</language>
    <item>
      <title>Cloud Resume Challenge</title>
      <dc:creator>cristianxcueva</dc:creator>
      <pubDate>Mon, 29 Jun 2026 15:53:22 +0000</pubDate>
      <link>https://dev.to/cristianxcueva/cloud-resume-challenge-69c</link>
      <guid>https://dev.to/cristianxcueva/cloud-resume-challenge-69c</guid>
      <description>&lt;h1&gt;
  
  
  Building a Serverless Resume on AWS
&lt;/h1&gt;

&lt;p&gt;I rebuilt my resume as a live AWS application instead of a PDF. It's a static site backed by a real serverless pipeline: a visitor counter that reads and writes to a database, behind an API, deployed through infrastructure as code, with its own CI/CD pipeline pushing updates automatically. I did this through the Cloud Resume Challenge, and this post walks through how it's built and what I actually learned doing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;The site itself is static: HTML and CSS, no server rendering anything on the fly. It's hosted in an S3 bucket, sitting behind CloudFront, with Route 53 pointing my custom domain at the CloudFront distribution. Nobody hits S3 directly. Every request goes through CloudFront first, which means consistent load times no matter where in the world someone's loading the page from, and it keeps the actual storage layer shielded from direct traffic.&lt;/p&gt;

&lt;p&gt;That's the whole story for the page itself. The visitor counter is a separate thing entirely, and it only starts once the page has already finished loading.&lt;/p&gt;

&lt;p&gt;Once the page renders, JavaScript running in the browser fires off a fetch to API Gateway. API Gateway invokes a Lambda function through a resource policy attached directly to it, that's a different kind of permission than the role Lambda itself uses, one controls who can call Lambda, the other controls what Lambda is allowed to do once it's running. The Lambda function uses its own execution role to read and write a single item in a DynamoDB table: the current visitor count. It increments that number, writes it back, and the result travels back through Lambda, through API Gateway, and into the page, where the count updates on screen.&lt;/p&gt;

&lt;p&gt;Every piece of this, the S3 bucket, CloudFront, Route 53, the IAM roles, the Lambda function, the DynamoDB table, API Gateway, was provisioned through Terraform. None of it was clicked together in the AWS console. And once it was built, GitHub Actions took over deployment entirely: push code, the pipeline runs tests, and if they pass, it deploys automatically.&lt;/p&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fylnp364cu7wi7won509m.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fylnp364cu7wi7won509m.png" alt=" " width="800" height="474"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Terraform changed how I think about building anything in AWS. Every resource in this project, down to the IAM policies, lives in code I can read, version, and redeploy from scratch. I learned the actual shape of writing Terraform for a wide range of services, and I ran into real ordering problems along the way, &lt;code&gt;depends_on&lt;/code&gt; exists because Terraform doesn't always know that one resource needs to fully finish before another can safely apply, and I hit that failure firsthand before I understood why it mattered.&lt;/p&gt;

&lt;p&gt;I also learned to actually reason about service choices instead of defaulting to the familiar option. DynamoDB made sense here over something like Aurora Serverless because this is a single counter, not relational data, and DynamoDB's schemaless model and on-demand pricing fit that far better than spinning up anything resembling a traditional database.&lt;/p&gt;

&lt;p&gt;Identity and permissions turned out to be deeper than I expected. Lambda doesn't need an instance profile the way EC2 does, it just takes an execution role directly. And that role only covers what Lambda can &lt;em&gt;do&lt;/em&gt;, it has nothing to do with who's allowed to &lt;em&gt;invoke&lt;/em&gt; Lambda in the first place, that's a separate, resource based permission entirely. I also learned, the hard way, that my own AWS credentials had no business being inside a GitHub Actions pipeline, even though they technically would have worked. An automated system running unsupervised deserves its own scoped identity, not my personal admin access, so I built dedicated IAM users for each pipeline instead.&lt;/p&gt;

&lt;p&gt;Most of the real learning happened in the failures. One implicit AWS region setting worked fine on my machine, because my local AWS CLI was quietly filling in a value I never actually specified in the code, and failed instantly the moment that same code ran on a fresh GitHub Actions runner with no such fallback available. Another time, an automation user's IAM policy didn't grant it permission to read information about &lt;em&gt;itself&lt;/em&gt;, so the pipeline failed trying to manage a resource it owned. That exact pattern, permissions falling behind as the infrastructure grew, showed up three separate times across this project, and recognizing it as one repeating shape instead of three unrelated bugs was its own kind of lesson.&lt;/p&gt;

&lt;p&gt;This project also started as a single repository, built milestone by milestone, before getting split into separate frontend and backend repos once CI/CD needed two independent pipelines. Restructuring an already working project, rather than getting it right from the start, turned out to be its own real skill.&lt;/p&gt;

</description>
      <category>cloudskills</category>
    </item>
  </channel>
</rss>
