<?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: Priyanka Vuddemari</title>
    <description>The latest articles on DEV Community by Priyanka Vuddemari (@priyanka_vuddemari).</description>
    <link>https://dev.to/priyanka_vuddemari</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%2F4124910%2F28484aec-948e-4d6f-9a62-b70aebd2dc56.jpg</url>
      <title>DEV Community: Priyanka Vuddemari</title>
      <link>https://dev.to/priyanka_vuddemari</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/priyanka_vuddemari"/>
    <language>en</language>
    <item>
      <title>Cloud Resume Challenge Week 4— Testing the Serverless Backend with Playwright and Python</title>
      <dc:creator>Priyanka Vuddemari</dc:creator>
      <pubDate>Mon, 05 Oct 2026 07:02:21 +0000</pubDate>
      <link>https://dev.to/priyanka_vuddemari/cloud-resume-challenge-testing-the-serverless-backend-with-playwright-and-python-4gi</link>
      <guid>https://dev.to/priyanka_vuddemari/cloud-resume-challenge-testing-the-serverless-backend-with-playwright-and-python-4gi</guid>
      <description>&lt;p&gt;Chunk 4 is complete — with my background in DevOps and Test automation, this chunk felt most natural.&lt;/p&gt;

&lt;p&gt;The goal is straightforward: write automated tests to verify that the serverless backend works correctly. In this post I'll walk through the tests I wrote, why I chose Playwright over Cypress, and how I integrated everything into the CI/CD pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Playwright over Cypress?
&lt;/h2&gt;

&lt;p&gt;The challenge gives you two options — Cypress (JavaScript) or Playwright (Python). I chose Playwright for one simple reason: Python.&lt;/p&gt;

&lt;p&gt;The Lambda function is already written in Python, and writing tests in the same language keeps the project consistent. Playwright's Python API is also clean, well-documented, and fits naturally with pytest  which is already a standard in the Python testing world.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to Test?
&lt;/h2&gt;

&lt;p&gt;The book recommends two types of tests:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unit tests&lt;/strong&gt; — test Lambda function logic in isolation, mocking DynamoDB calls. Simple but limited since you are mostly testing mocked behavior rather than real infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smoke tests / End-to-end tests&lt;/strong&gt; which run after deployment against the real live API URL. These verify the full stack is working — Lambda, API Gateway, DynamoDB, and permissions ,all in one test run.&lt;/p&gt;

&lt;p&gt;I went with smoke tests and end-to-end browser tests. They are far more valuable and generate much better interview conversation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up Playwright
&lt;/h2&gt;

&lt;p&gt;Install Playwright and its pytest plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pytest-playwright requests
playwright &lt;span class="nb"&gt;install &lt;/span&gt;chromium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also updated the dev container so Playwright is installed automatically on every Codespace rebuild:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"postCreateCommand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pip install pytest-playwright &amp;amp;&amp;amp; playwright install chromium"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Tests
&lt;/h2&gt;

&lt;p&gt;I created a &lt;code&gt;tests/&lt;/code&gt; folder with a single test file &lt;code&gt;test_resume.py&lt;/code&gt;.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="n"&gt;API_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://YOUR_API_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;RESUME_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://YOUR-CLOUDFRONT-URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# ── API Smoke Tests ──
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_api_returns_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;API should return a 200 status code&lt;/span&gt;&lt;span class="sh"&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;requests&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="n"&gt;API_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&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;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_api_returns_views_key&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;API response should contain a views key&lt;/span&gt;&lt;span class="sh"&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;requests&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="n"&gt;API_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;data&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;views&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;data&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_api_views_is_integer&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Views count should be an integer&lt;/span&gt;&lt;span class="sh"&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;requests&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="n"&gt;API_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;data&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;views&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;int&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;test_api_views_increments&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Views count should increment on each call&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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="n"&gt;API_URL&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;views&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;second&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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="n"&gt;API_URL&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;views&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;second&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_api_views_greater_than_zero&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Views count should be greater than zero&lt;/span&gt;&lt;span class="sh"&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;requests&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="n"&gt;API_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;data&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;views&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="c1"&gt;# ── End to End Browser Tests ──
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_resume_page_loads&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Resume page should load successfully&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&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;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RESUME_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&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;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
        &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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;test_resume_has_title&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Resume page should have a title&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RESUME_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
        &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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;test_visitor_counter_displays&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Visitor counter should be visible on the page&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RESUME_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for_selector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#views-count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inner_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#views-count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What Each Test Covers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;API Smoke Tests:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API returns HTTP 200 — basic health check&lt;/li&gt;
&lt;li&gt;Response contains a &lt;code&gt;views&lt;/code&gt; key — correct response structure&lt;/li&gt;
&lt;li&gt;Views count is an integer — correct data type&lt;/li&gt;
&lt;li&gt;Views count increments on each call — counter logic works&lt;/li&gt;
&lt;li&gt;Views count is greater than zero — database has been initialised&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;End to End Browser Tests:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Resume page loads successfully — CloudFront + S3 serving correctly&lt;/li&gt;
&lt;li&gt;Resume page has a title — HTML is rendering properly&lt;/li&gt;
&lt;li&gt;Visitor counter displays — JavaScript fetch and DOM update working&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Running the Tests
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest tests/ &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All 8 tests passed on the first run locally.&lt;/p&gt;




&lt;h2&gt;
  
  
  Adding Tests to the CI/CD Pipeline
&lt;/h2&gt;

&lt;p&gt;The real value of automated tests is running them automatically. I added two steps to the GitHub Actions workflow:&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="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;Install Python test dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;pip install pytest-playwright requests&lt;/span&gt;
          &lt;span class="s"&gt;playwright install chromium&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;pytest tests/ -v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every push to main triggers the full pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Push to GitHub
      ↓
Sync files to S3
      ↓
Invalidate CloudFront cache
      ↓
Terraform apply
      ↓
Run Playwright tests ← new!
      ↓
All green ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smoke tests beat unit tests for serverless&lt;/strong&gt; — testing against the real live API verifies Lambda, API Gateway, DynamoDB, and IAM permissions all at once&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Playwright + pytest is a natural fit&lt;/strong&gt; — clean API, familiar tooling, easy to extend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests in CI/CD is non-negotiable&lt;/strong&gt; — running tests locally is good, running them automatically on every push is better&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The visitor counter test is the most valuable&lt;/strong&gt; — it validates the entire chain from browser to database in a single assertion&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Chunk 4 wraps up the core challenge. Next up is the final post — a complete retrospective of the entire Cloud Resume Challenge journey.&lt;/p&gt;




&lt;p&gt;*My Cloud Resume Challenge series &lt;a href="https://dev.to/priyanka_vuddemari/series"&gt;here&lt;/a&gt; *&lt;/p&gt;

</description>
      <category>aws</category>
      <category>playwright</category>
      <category>testing</category>
      <category>python</category>
    </item>
    <item>
      <title>Cloud Resume Challenge Week 3— Infrastructure as Code with Terraform and CI/CD with GitHub Actions</title>
      <dc:creator>Priyanka Vuddemari</dc:creator>
      <pubDate>Sun, 27 Sep 2026 13:56:34 +0000</pubDate>
      <link>https://dev.to/priyanka_vuddemari/cloud-resume-challenge-week-3-infrastructure-as-code-with-terraform-and-cicd-with-github-actions-3gh0</link>
      <guid>https://dev.to/priyanka_vuddemari/cloud-resume-challenge-week-3-infrastructure-as-code-with-terraform-and-cicd-with-github-actions-3gh0</guid>
      <description>&lt;p&gt;Chunk 3 is complete — and this one pushed me the furthest outside my comfort zone. Infrastructure as Code, remote state management, and a fully automated CI/CD pipeline are all now in place.&lt;/p&gt;

&lt;p&gt;In this post I'm documenting exactly what I built, the decisions I made, and the bugs I hit along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Chunk 3 about?
&lt;/h2&gt;

&lt;p&gt;The goal is to stop clicking through the AWS Console and start defining infrastructure as code — and then automate deployments so every push to GitHub triggers a full deploy automatically.&lt;/p&gt;

&lt;p&gt;The tools involved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Terraform&lt;/strong&gt; — define all AWS resources as code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions&lt;/strong&gt; — automate the deployment pipeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 remote backend&lt;/strong&gt; — store Terraform state so CI/CD and local machines share the same view of infrastructure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev Container&lt;/strong&gt; — reproducible development environment in GitHub Codespaces&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why Terraform over AWS SAM?
&lt;/h2&gt;

&lt;p&gt;The challenge recommends AWS SAM for IaC, but I chose Terraform instead. Here is why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform is cloud agnostic — the same skills work on AWS, Azure, and GCP&lt;/li&gt;
&lt;li&gt;It is by far the most in-demand IaC tool in the industry right now&lt;/li&gt;
&lt;li&gt;HCL (HashiCorp Configuration Language) is clean and readable&lt;/li&gt;
&lt;li&gt;The Terraform ecosystem and community are excellent&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Setting Up the Dev Container
&lt;/h2&gt;

&lt;p&gt;Since I am working in GitHub Codespaces, the first thing I did was create a dev container so every time the environment starts, all tools are pre-installed automatically.&lt;/p&gt;

&lt;p&gt;I created &lt;code&gt;.devcontainer/devcontainer.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cloud Resume Challenge"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcr.microsoft.com/devcontainers/base:ubuntu"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"features"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ghcr.io/devcontainers/features/aws-cli:1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ghcr.io/devcontainers/features/terraform:1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ghcr.io/devcontainers/features/python:1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3.12"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ghcr.io/devcontainers/features/github-cli:1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This installs AWS CLI, Terraform, Python, and GitHub CLI automatically on every Codespace rebuild. No more manual setup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Terraform Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform/
├── main.tf        ← all AWS resources
├── variables.tf   ← input variables
├── outputs.tf     ← output values
└── providers.tf   ← AWS provider + backend config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The resources defined in Terraform:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S3 bucket with static website hosting and public access policy&lt;/li&gt;
&lt;li&gt;DynamoDB table for the visitor counter&lt;/li&gt;
&lt;li&gt;IAM role and policies for Lambda&lt;/li&gt;
&lt;li&gt;Lambda function&lt;/li&gt;
&lt;li&gt;API Gateway HTTP API with GET /count route&lt;/li&gt;
&lt;li&gt;Lambda permission for API Gateway&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Remote Backend — The Most Important Step
&lt;/h2&gt;

&lt;p&gt;This is the step most beginners skip and then wonder why CI/CD fails.&lt;/p&gt;

&lt;p&gt;By default Terraform stores its state file locally on your machine. When GitHub Actions runs Terraform it has no idea what already exists in AWS — so it tries to create everything from scratch and hits conflicts.&lt;/p&gt;

&lt;p&gt;The fix is a &lt;strong&gt;remote backend&lt;/strong&gt; — store the state file in S3 so both your local machine and GitHub Actions share the same state.&lt;/p&gt;

&lt;p&gt;I added this to &lt;code&gt;providers.tf&lt;/code&gt;:&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;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&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;"priyanka-terraform-state"&lt;/span&gt;
  &lt;span class="nx"&gt;key&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"cloud-resume/terraform.tfstate"&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ap-south-2"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform init &lt;span class="nt"&gt;-migrate-state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This moved the local state file to S3. From that point on both local and CI/CD shared the same state.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Key lesson: Always set up remote state before setting up CI/CD. If you do it the other way around you will hit resource conflict errors on every pipeline run.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Bugs I Hit Along the Way
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Bug 1 — Resource conflicts on first apply&lt;/strong&gt;&lt;br&gt;
Since I had already created S3, DynamoDB, Lambda, and API Gateway manually earlier in the challenge, Terraform tried to create them again and hit 409 conflict errors.&lt;/p&gt;

&lt;p&gt;Fix: import existing resources into Terraform state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform import aws_s3_bucket.resume priyanka-cloud-resume-challenge
terraform import aws_lambda_function.counter cloud-resume-counter
terraform import aws_dynamodb_table.counter cloud-resume-counter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Bug 2 — DynamoDB table item import not supported&lt;/strong&gt;&lt;br&gt;
Terraform does not support importing &lt;code&gt;aws_dynamodb_table_item&lt;/code&gt; resources. The item already existed from manual setup so Terraform kept trying to create it and failing.&lt;/p&gt;

&lt;p&gt;Fix: removed the &lt;code&gt;aws_dynamodb_table_item&lt;/code&gt; resource from &lt;code&gt;main.tf&lt;/code&gt; entirely. The item already exists and Lambda manages the views count dynamically — Terraform does not need to manage it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 3 — GitHub Actions had no Terraform state&lt;/strong&gt;&lt;br&gt;
The pipeline kept hitting resource conflict errors because the state file was local and not shared with GitHub Actions.&lt;/p&gt;

&lt;p&gt;Fix: set up the S3 remote backend as described above. Once state was in S3, both local and CI/CD shared the same view of infrastructure and conflicts stopped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 4 — String concatenation error in outputs.tf&lt;/strong&gt;&lt;br&gt;
Used &lt;code&gt;+&lt;/code&gt; operator to concatenate strings in Terraform — which is a number operator, not a string operator.&lt;/p&gt;

&lt;p&gt;Fix: use string interpolation instead:&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;value&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${trimsuffix(aws_apigatewayv2_stage.default.invoke_url, "&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="s2"&gt;")}/count"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The GitHub Actions Pipeline
&lt;/h2&gt;

&lt;p&gt;The pipeline runs automatically on every push to main and also supports manual triggers via &lt;code&gt;workflow_dispatch&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&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="s"&gt;main&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&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@v4&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;Configure AWS credentials&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;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;aws-access-key-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_ACCESS_KEY_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;aws-secret-access-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_SECRET_ACCESS_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ap-south-2&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;Sync files to S3&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;aws s3 sync . s3://priyanka-cloud-resume-challenge \&lt;/span&gt;
            &lt;span class="s"&gt;--exclude "*" \&lt;/span&gt;
            &lt;span class="s"&gt;--include "index.html" \&lt;/span&gt;
            &lt;span class="s"&gt;--include "styles.css" \&lt;/span&gt;
            &lt;span class="s"&gt;--delete&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;Invalidate CloudFront cache&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;aws cloudfront create-invalidation \&lt;/span&gt;
            &lt;span class="s"&gt;--distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \&lt;/span&gt;
            &lt;span class="s"&gt;--paths "/*"&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;Setup Terraform&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;hashicorp/setup-terraform@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;Terraform Init&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./terraform&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;terraform init&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;Terraform Apply&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./terraform&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;terraform apply -auto-approve&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;Smoke test&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;response=$(curl -s https:YOUR_URL)&lt;/span&gt;
          &lt;span class="s"&gt;if echo "$response" | grep -q "views"; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "Smoke test passed!"&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;echo "Smoke test failed!"&lt;/span&gt;
            &lt;span class="s"&gt;exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What the Pipeline Does
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Push to GitHub
      ↓
Checkout code
      ↓
Configure AWS credentials
      ↓
Sync index.html + styles.css → S3
      ↓
Invalidate CloudFront cache
      ↓
Terraform init + apply
      ↓
Smoke test → verify API returns views count
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Remote state is not optional for CI/CD&lt;/strong&gt; — without it every pipeline run hits resource conflicts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Import existing resources before applying&lt;/strong&gt; — if you created resources manually first, import them before letting Terraform manage them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform string interpolation&lt;/strong&gt; — use &lt;code&gt;${}&lt;/code&gt; not &lt;code&gt;+&lt;/code&gt; for string concatenation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev containers save time&lt;/strong&gt; — define your environment once, never set it up again&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;workflow_dispatch is underrated&lt;/strong&gt; — being able to trigger the pipeline manually without a dummy commit is very useful&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Next up is writing tests for the Lambda function and then tackling the challenge mods — AI Engineer Mod and DevOps multi-stage pipeline.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Part of my Cloud Resume Challenge series.&lt;/em&gt; &lt;a href="https://dev.to/priyanka_vuddemari/series"&gt;Read here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>githubactions</category>
      <category>devops</category>
    </item>
    <item>
      <title>Cloud Resume Challenge Week 2 — Building the Serverless Backend with Lambda, DynamoDB &amp; API Gateway</title>
      <dc:creator>Priyanka Vuddemari</dc:creator>
      <pubDate>Thu, 24 Sep 2026 03:16:32 +0000</pubDate>
      <link>https://dev.to/priyanka_vuddemari/cloud-resume-challenge-building-the-serverless-backend-with-lambda-dynamodb-api-gateway-1d88</link>
      <guid>https://dev.to/priyanka_vuddemari/cloud-resume-challenge-building-the-serverless-backend-with-lambda-dynamodb-api-gateway-1d88</guid>
      <description>&lt;p&gt;Chunk 2 is complete — and this was by far the most challenging and rewarding part of the challenge so far.&lt;/p&gt;

&lt;p&gt;In this post I'm documenting exactly how I built the serverless visitor counter backend : Lambda, DynamoDB, API Gateway , and the bugs I encountered along the way. The debugging section is the most valuable part, so I've kept it honest and detailed.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is Chunk 2 about?
&lt;/h2&gt;

&lt;p&gt;The goal is to build a &lt;strong&gt;serverless API&lt;/strong&gt; that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stores a visitor count in a database&lt;/li&gt;
&lt;li&gt;Increments it every time someone visits the resume&lt;/li&gt;
&lt;li&gt;Returns the count to the website to display&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AWS services involved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB&lt;/strong&gt; — NoSQL database to store the view count&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt; — Python function to read and update the count&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway&lt;/strong&gt; — exposes Lambda via a public URL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript&lt;/strong&gt; — calls the API from the resume page&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1 — Creating the DynamoDB Table
&lt;/h2&gt;

&lt;p&gt;DynamoDB is AWS's managed NoSQL database. For this project it stores one simple record as the visitor count.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to DynamoDB → &lt;strong&gt;Create table&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Table name: &lt;code&gt;cloud-resume-counter&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Partition key: &lt;code&gt;id&lt;/code&gt; → type &lt;strong&gt;String&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Leave everything else as default → &lt;strong&gt;Create table&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once created, add the initial item:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt; = &lt;code&gt;1&lt;/code&gt; (String)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;views&lt;/code&gt; = &lt;code&gt;0&lt;/code&gt; (Number)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Important:&lt;/strong&gt; Make sure &lt;code&gt;id&lt;/code&gt; is set as &lt;strong&gt;String&lt;/strong&gt; type, not Number. This caused issues later. DynamoDB was creating duplicate items because of a type mismatch between String &lt;code&gt;"1"&lt;/code&gt; and Number &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 2 — Creating the Lambda Function
&lt;/h2&gt;

&lt;p&gt;Lambda is a serverless compute service that runs your code without managing any servers.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to Lambda → &lt;strong&gt;Create function&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Function name: &lt;code&gt;cloud-resume-counter&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Runtime: &lt;strong&gt;Python 3.14&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Leave everything else as default → &lt;strong&gt;Create function&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the Python code:&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="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;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&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;resource&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;region_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ap-south-2&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&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="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cloud-resume-counter&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;lambda_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="c1"&gt;# Get current count
&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;table&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;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;id&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="n"&gt;item&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="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;Item&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;views&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&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;views&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="c1"&gt;# Increment count
&lt;/span&gt;    &lt;span class="n"&gt;views&lt;/span&gt; &lt;span class="o"&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;views&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="c1"&gt;# Update the count in DynamoDB
&lt;/span&gt;    &lt;span class="n"&gt;table&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;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;id&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="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 #v = :val&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ExpressionAttributeNames&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;#v&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;views&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;:val&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;views&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Return the new count
&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;Access-Control-Allow-Methods&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;GET, POST, OPTIONS&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;Access-Control-Allow-Headers&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;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;views&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;views&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;Click &lt;strong&gt;Deploy&lt;/strong&gt; to save the function.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 — IAM Permissions
&lt;/h2&gt;

&lt;p&gt;By default Lambda has no access to DynamoDB. Permissions must be granted explicitly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Configuration&lt;/strong&gt; tab → &lt;strong&gt;Permissions&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click the execution role name&lt;/li&gt;
&lt;li&gt;In IAM → &lt;strong&gt;Add permissions&lt;/strong&gt; → &lt;strong&gt;Attach policies&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Search for &lt;code&gt;AmazonDynamoDBFullAccess&lt;/code&gt; → attach it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Skipping this step results in an &lt;code&gt;AccessDeniedException&lt;/code&gt; when the function runs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bugs I Encountered Along the Way
&lt;/h2&gt;

&lt;p&gt;This section documents the real issues I ran into because the debugging process is where the actual learning happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 1 — AccessDeniedException&lt;/strong&gt;&lt;br&gt;
Lambda could not access DynamoDB at all.&lt;br&gt;
Fix: attach &lt;code&gt;AmazonDynamoDBFullAccess&lt;/code&gt; policy to the Lambda execution role.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 2 — Region Mismatch&lt;/strong&gt;&lt;br&gt;
Lambda was created in &lt;code&gt;eu-north-1&lt;/code&gt; and DynamoDB in &lt;code&gt;ap-south-2&lt;/code&gt; by mistake. Lambda was looking for the table in the wrong region and returning empty responses.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Key lesson:&lt;/strong&gt; Lambda and DynamoDB &lt;strong&gt;must be in the same AWS region&lt;/strong&gt;. Always verify the region selector in the top right corner of the AWS Console before creating resources.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Bug 3 — DynamoDB Type Mismatch&lt;/strong&gt;&lt;br&gt;
The table ended up with two items: one with &lt;code&gt;id&lt;/code&gt; as String &lt;code&gt;"1"&lt;/code&gt; and another with &lt;code&gt;id&lt;/code&gt; as Number &lt;code&gt;1&lt;/code&gt;. Lambda was creating a new item instead of updating the existing one.&lt;br&gt;
Fix: delete the Number &lt;code&gt;1&lt;/code&gt; item, keep only the String &lt;code&gt;"1"&lt;/code&gt; item.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 4 — KeyError: 'Item'&lt;/strong&gt;&lt;br&gt;
This error persisted because Lambda was connecting to the wrong region and could not find the item at all. Aligning Lambda and DynamoDB to the same region resolved it.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 4 — Setting Up API Gateway
&lt;/h2&gt;

&lt;p&gt;API Gateway provides a public HTTPS URL in front of the Lambda function so the browser can call it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to API Gateway → &lt;strong&gt;Create API&lt;/strong&gt; → &lt;strong&gt;HTTP API&lt;/strong&gt; → &lt;strong&gt;Build&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Add integration → &lt;strong&gt;Lambda&lt;/strong&gt; → select &lt;code&gt;cloud-resume-counter&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;API name: &lt;code&gt;cloud-resume-api&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Route: &lt;strong&gt;GET&lt;/strong&gt; &lt;code&gt;/count&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Stage: &lt;code&gt;$default&lt;/code&gt; with auto-deploy enabled&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The invoke URL will look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://abc123.execute-api.ap-south-2.amazonaws.com/count
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test it in the browser — the response should be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"views"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5 — Connecting the Resume Website
&lt;/h2&gt;

&lt;p&gt;The final step — adding JavaScript to &lt;code&gt;index.html&lt;/code&gt; to fetch and display the count.&lt;/p&gt;

&lt;p&gt;Add this where the counter should appear:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"visitor-counter"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  This resume has been viewed
  &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"views-count"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt; times
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this script before &lt;code&gt;&amp;lt;/body&amp;gt;&lt;/code&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="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="s1"&gt;https://YOUR-API-GATEWAY-URL/count&lt;/span&gt;&lt;span class="dl"&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;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&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="s1"&gt;views-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;textContent&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;views&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;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Error fetching views:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&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="s1"&gt;views-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;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-&lt;/span&gt;&lt;span class="dl"&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;Upload the updated &lt;code&gt;index.html&lt;/code&gt; to S3 and the resume now displays a live visitor counter.&lt;/p&gt;




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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser → CloudFront → S3 (HTML + CSS)
    ↓
JavaScript fetch()
    ↓
API Gateway → Lambda (Python) → DynamoDB
              increments count    stores views
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS regions matter&lt;/strong&gt; — services must be in the same region to communicate properly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IAM permissions are explicit&lt;/strong&gt; — no service can access another without being granted permission&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB types are strict&lt;/strong&gt; — String &lt;code&gt;"1"&lt;/code&gt; and Number &lt;code&gt;1&lt;/code&gt; are treated as completely different partition keys&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch logs are essential&lt;/strong&gt; — when Lambda fails, logs show exactly what went wrong&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serverless is cost effective&lt;/strong&gt; — the entire backend runs at essentially $0 on AWS free tier&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Next up is the CI/CD pipeline using GitHub Actions automating deployments so changes push to S3 automatically on every commit.&lt;/p&gt;




</description>
      <category>aws</category>
      <category>lambda</category>
      <category>dynamodb</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Cloud Resume Challenge Week 1 — What I Built and How I Did It</title>
      <dc:creator>Priyanka Vuddemari</dc:creator>
      <pubDate>Tue, 15 Sep 2026 18:40:39 +0000</pubDate>
      <link>https://dev.to/priyanka_vuddemari/cloud-resume-challenge-week-1-what-i-built-and-how-i-did-it-1dc</link>
      <guid>https://dev.to/priyanka_vuddemari/cloud-resume-challenge-week-1-what-i-built-and-how-i-did-it-1dc</guid>
      <description>&lt;p&gt;Week 1 is done and my resume is live on the internet! 🎉&lt;/p&gt;

&lt;p&gt;In my &lt;a href="https://dev.to/priyanka_vuddemari/im-taking-the-cloud-resume-challenge-heres-what-it-is-2394"&gt;last post&lt;/a&gt; I introduced the Cloud Resume Challenge. This time I'm documenting exactly what I did, step by step i.e. the decisions I made, the gotchas I hit, and what I learned along the way.&lt;/p&gt;

&lt;p&gt;If you're following along or planning to do the challenge yourself, hopefully this saves you some time.&lt;/p&gt;




&lt;h2&gt;
  
  
  What was the goal for Week 1?
&lt;/h2&gt;

&lt;p&gt;Build a resume as a static website and get it properly live on AWS. Not just thrown on a free hosting platform properly, with a CDN, HTTPS, and cloud infrastructure. Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ HTML &amp;amp; CSS resume page&lt;/li&gt;
&lt;li&gt;✅ Code stored in GitHub&lt;/li&gt;
&lt;li&gt;✅ Hosted on AWS S3&lt;/li&gt;
&lt;li&gt;✅ CloudFront distribution in front of it&lt;/li&gt;
&lt;li&gt;✅ HTTPS via SSL certificate&lt;/li&gt;
&lt;li&gt;✅ Resume live and accessible on the internet&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1 — Building the Resume in HTML &amp;amp; CSS
&lt;/h2&gt;

&lt;p&gt;I built my resume as a plain HTML page with a separate CSS file. The challenge is about cloud skills, not front-end wizardry, so I kept it clean and professional without overcomplicating it.&lt;/p&gt;

&lt;p&gt;The files ended up being:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;index.html&lt;/code&gt; — the resume content&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;styles.css&lt;/code&gt; — the styling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once done I pushed both files to a &lt;strong&gt;GitHub repository&lt;/strong&gt; which will be home base for the entire project going forward.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 *&lt;em&gt;Tip: *&lt;/em&gt; Name your main file &lt;code&gt;index.html&lt;/code&gt; from the start S3 static website hosting looks for this file by default when someone hits your URL.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 2 — Creating the S3 Bucket
&lt;/h2&gt;

&lt;p&gt;I went to AWS S3 and created a &lt;strong&gt;General Purpose bucket&lt;/strong&gt; in the &lt;code&gt;ap-south-2&lt;/code&gt; (Hyderabad) region and named it &lt;code&gt;priyanka-cloud-resume-challenge&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A few things to note when creating the bucket:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Leave all default settings for now&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block all public access&lt;/strong&gt; will be ON by default — that's fine, we'll change it shortly&lt;/li&gt;
&lt;li&gt;Don't worry about any other settings at this stage&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 3 — Enabling Static Website Hosting
&lt;/h2&gt;

&lt;p&gt;This is what tells S3 to serve your files as a website rather than just storing them.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click into your bucket → &lt;strong&gt;Properties&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Scroll down to &lt;strong&gt;Static website hosting&lt;/strong&gt; → click &lt;strong&gt;Edit&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Index document&lt;/strong&gt; to &lt;code&gt;index.html&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Save changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At this point S3 gives you a &lt;strong&gt;bucket website endpoint URL&lt;/strong&gt; — something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://priyanka-cloud-resume-challenge.s3-website.ap-south-2.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hold onto this — you'll use it to test in a moment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4 — Uploading the Files
&lt;/h2&gt;

&lt;p&gt;Still inside the bucket:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to the &lt;strong&gt;Objects&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Upload&lt;/strong&gt; → &lt;strong&gt;Add files&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select both &lt;code&gt;index.html&lt;/code&gt; and &lt;code&gt;styles.css&lt;/code&gt; together&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Upload&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both files go into the same bucket — simple and clean.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5 — Making the Bucket Public
&lt;/h2&gt;

&lt;p&gt;By default S3 blocks all public access. Since this is a public resume website, we need to open it up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First — turn off Block Public Access:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;Permissions&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Edit&lt;/strong&gt; on Block public access&lt;/li&gt;
&lt;li&gt;Uncheck "Block all public access"&lt;/li&gt;
&lt;li&gt;Save → type &lt;code&gt;confirm&lt;/code&gt; when prompted&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Then — add a Bucket Policy:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Still on Permissions tab → scroll to &lt;strong&gt;Bucket policy&lt;/strong&gt; → Edit&lt;/li&gt;
&lt;li&gt;Paste this policy (replace the bucket name with yours):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PublicReadGetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3:GetObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:s3:::priyanka-cloud-resume-challenge/*"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Save changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now visit your bucket website endpoint URL — your resume should load! 🎉&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6 — Setting Up CloudFront
&lt;/h2&gt;

&lt;p&gt;S3 hosting works but it has limitations — no HTTPS, slower for global visitors, and the URL is ugly. &lt;strong&gt;CloudFront&lt;/strong&gt; fixes all of this — it's AWS's CDN (Content Delivery Network) that sits in front of S3 and serves your content from edge locations worldwide.&lt;/p&gt;

&lt;p&gt;Here's how I set it up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;CloudFront&lt;/strong&gt; in AWS Console → &lt;strong&gt;Create Distribution&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;Origin&lt;/strong&gt;, select your S3 bucket&lt;/li&gt;
&lt;li&gt;When prompted — choose &lt;strong&gt;"Use website endpoint"&lt;/strong&gt; (important!)&lt;/li&gt;
&lt;li&gt;Skip WAF or leave default basic protection&lt;/li&gt;
&lt;li&gt;Leave all other settings as default&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create Distribution&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;CloudFront takes about &lt;strong&gt;5-10 minutes&lt;/strong&gt; to deploy globally. Once the status changes from &lt;em&gt;Deploying&lt;/em&gt; to &lt;em&gt;Enabled&lt;/em&gt;, you'll see your CloudFront domain name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://d1234abcd.cloudfront.net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Why "Use website endpoint"?&lt;/strong&gt; Because we enabled Static Website Hosting on S3, the website endpoint already knows to serve &lt;code&gt;index.html&lt;/code&gt; by default. Using the S3 bucket directly instead can cause routing issues.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 7 — Testing HTTPS
&lt;/h2&gt;

&lt;p&gt;This is the satisfying part. Open your CloudFront URL with &lt;code&gt;https://&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://d1234abcd.cloudfront.net
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Your resume loading perfectly&lt;/li&gt;
&lt;li&gt;✅ A 🔒 padlock in the browser address bar&lt;/li&gt;
&lt;li&gt;✅ SSL certificate provided automatically by CloudFront&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No extra SSL setup needed — CloudFront handles it automatically!&lt;/p&gt;

&lt;h2&gt;
  
  
  What about the custom domain?
&lt;/h2&gt;

&lt;p&gt;The challenge officially requires a custom domain name registered &lt;br&gt;
in Route 53. I'm skipping this for now and will add it at the end &lt;br&gt;
once everything else is built.&lt;/p&gt;

&lt;p&gt;A few reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All the real learning — Lambda, DynamoDB, CI/CD — happens without needing a domain&lt;/li&gt;
&lt;li&gt;I want to complete the full challenge end to end first&lt;/li&gt;
&lt;li&gt;I'll add the domain as the final step to officially wrap it up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd rather get the full architecture working first and add the &lt;br&gt;
domain at the end. I'll document it when I get there! 😊&lt;/p&gt;




&lt;h2&gt;
  
  
  What I actually learned this week
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3 is not just file storage&lt;/strong&gt; — it can host entire websites without a server&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bucket policies control access&lt;/strong&gt; — understanding the JSON structure matters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudFront vs direct S3&lt;/strong&gt; — always use CloudFront for production; direct S3 has no HTTPS and is slower&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Use website endpoint" matters&lt;/strong&gt; — small setting, big difference in behaviour&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cloud resume is a real project&lt;/strong&gt; — not a tutorial you follow blindly, you actually make decisions&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Architecture so far
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser → (HTTPS) → CloudFront (CDN + SSL + Cache) → S3 Bucket (index.html + styles.css)
             ↑ d1234abc.cloudfront.net        ↑ ap-south-2 (Hyderabad)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple but real cloud architecture. Week 2 adds Lambda, DynamoDB and API Gateway on top of this.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;Before moving to Week 2 I'm spending the rest of this week on some of the optional &lt;strong&gt;Mods&lt;/strong&gt; from the challenge book:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DevOps Mod&lt;/strong&gt; — setting up GitHub Actions CI/CD to auto-deploy on every push&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Engineer Mod&lt;/strong&gt; — adding an LLM resume summarizer using Amazon Bedrock&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each mod will get its own post — stay tuned! 🚀&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources that helped
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteHosting.html" rel="noopener noreferrer"&gt;AWS S3 Static Website Hosting Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/GettingStarted.html" rel="noopener noreferrer"&gt;AWS CloudFront Getting Started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloudresumechallenge.dev" rel="noopener noreferrer"&gt;The Cloud Resume Challenge Book&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Part of my Cloud Resume Challenge series. Read Post 1 here&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>I'm Taking the Cloud Resume Challenge — Here's What It Is</title>
      <dc:creator>Priyanka Vuddemari</dc:creator>
      <pubDate>Mon, 14 Sep 2026 17:08:07 +0000</pubDate>
      <link>https://dev.to/priyanka_vuddemari/im-taking-the-cloud-resume-challenge-heres-what-it-is-2394</link>
      <guid>https://dev.to/priyanka_vuddemari/im-taking-the-cloud-resume-challenge-heres-what-it-is-2394</guid>
      <description>&lt;p&gt;Hey everyone! 👋&lt;/p&gt;

&lt;p&gt;I recently decided to take on the the &lt;strong&gt;Cloud Resume Challenge&lt;/strong&gt; by Forrest Brazeal, and I decided to go for it. I'll be documenting my journey here week by week — the wins, the confusion, and everything in between.&lt;/p&gt;

&lt;p&gt;But first,  what even is this challenge?&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the Cloud Resume Challenge?
&lt;/h2&gt;

&lt;p&gt;The Cloud Resume Challenge is a hands-on project designed to help you build and demonstrate real cloud skills. Instead of just reading docs or watching tutorials, you actually &lt;em&gt;build something&lt;/em&gt; a resume website and use a whole stack of cloud services to deploy and run it properly.&lt;/p&gt;

&lt;p&gt;It was created by &lt;strong&gt;Forrest Brazeal&lt;/strong&gt;, a cloud architect and AWS Community Hero, as a way to bridge the gap between "I passed a certification" and "I can actually build things on AWS."&lt;/p&gt;

&lt;h2&gt;
  
  
  What does the full challenge involve?
&lt;/h2&gt;

&lt;p&gt;Here's the big picture of what you end up building by the end:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;☁️ A resume hosted as a &lt;strong&gt;static website on AWS S3&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🌍 Served globally via &lt;strong&gt;CloudFront&lt;/strong&gt; (AWS's CDN)&lt;/li&gt;
&lt;li&gt;🔒 HTTPS enabled with an &lt;strong&gt;SSL certificate&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🌐 Accessible via a &lt;strong&gt;custom domain&lt;/strong&gt; registered in Route 53&lt;/li&gt;
&lt;li&gt;👀 A &lt;strong&gt;visitor counter&lt;/strong&gt; that tracks how many people view your resume&lt;/li&gt;
&lt;li&gt;⚡ The counter powered by &lt;strong&gt;AWS Lambda&lt;/strong&gt; (serverless functions)&lt;/li&gt;
&lt;li&gt;🗄️ Visit count stored in &lt;strong&gt;DynamoDB&lt;/strong&gt; (a NoSQL database)&lt;/li&gt;
&lt;li&gt;🔗 Everything connected through an &lt;strong&gt;API Gateway&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🧪 Tests written for your backend code&lt;/li&gt;
&lt;li&gt;🏗️ Infrastructure defined as &lt;strong&gt;code using AWS SAM&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;🔄 A &lt;strong&gt;CI/CD pipeline&lt;/strong&gt; so changes deploy automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yeah — it's a lot. But that's the point. You touch almost every layer of a real cloud application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why am I doing this?
&lt;/h2&gt;

&lt;p&gt;I was &lt;strong&gt;Test Automation Specialist&lt;/strong&gt; and working as SRE for some time now and looking to level up my cloud skills. I work with CI/CD pipelines and infrastructure daily, so understanding AWS deeply just makes sense for where I want to go.&lt;/p&gt;

&lt;p&gt;Also, honestly building something real always beats studying in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Week 1 about:
&lt;/h2&gt;

&lt;p&gt;Week 1 is all about the frontend — building the actual resume HTML page, pushing it to GitHub, hosting it on S3, setting up CloudFront, SSL, and a custom domain.&lt;/p&gt;

&lt;p&gt;I'll be posting a full breakdown of what I built and what I learned very soon.&lt;/p&gt;

&lt;p&gt;If you're thinking about taking on the challenge too do it don't think about it much  &lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is Post 1 of my Cloud Resume Challenge series. Follow along for weekly updates!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>devops</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
