<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Alfiya | Cloud & DevOps Developer Blog]]></title><description><![CDATA[Technical deep dives, AWS architecture breakdowns, and hands-on DevOps projects by a cloud engineering student.]]></description><link>https://alfiyajaved.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 04 Nov 2026 17:58:19 GMT</lastBuildDate><atom:link href="https://alfiyajaved.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Day 25: AWS Cloud Resume Challenge]]></title><description><![CDATA[Every AWS resource in Terraform. A CI/CD pipeline ran clean, tests gated every deploy, and zero credentials were stored. By most measures, this project was…]]></description><link>https://alfiyajaved.hashnode.dev/day-25-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-25-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Fri, 04 Sep 2026 04:18:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/221f2187-b77d-4b8e-a092-881558246b04.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>The Project Was Complete, Except It Couldn't Tell Me When It Broke</h3>
<p>Every AWS resource in Terraform. A CI/CD pipeline ran clean, tests gated every deploy, and zero credentials were stored. By most measures, this project was done.</p>
<p>Except it had a real, honest gap: if my Lambda started failing, or DynamoDB got throttled, or my AWS bill quietly climbed past what I expected — I would have no way of knowing. Not until a visitor told me something was broken, or a bill arrived. That's not really "done." So today I added monitoring.</p>
<h3>The Cost Worry, Resolved</h3>
<p>My first instinct was to worry about cost — my account is past its 12-month free tier window, and this whole project has been built around staying near $0. Before adding anything, I checked properly instead of assuming: CloudWatch's alarm free tier — 10 alarms, default AWS metrics — isn't part of the 12-month window at all. It's classified as "Always Free," permanently, regardless of account age. Same for SNS — 1,000 free email notifications a month, forever. Good thing to actually verify instead of skipping monitoring out of a cost fear that turned out not to apply here.</p>
<h3>Two Permission Gaps I Expected, Just Not Yet</h3>
<p>Tried to apply the new alarms and SNS topics, and got hit with an authorisation error immediately — my <code>terraform-project</code> IAM user wasn't allowed to touch either service. Made sense once I thought about it: this user's permissions were scoped months ago to exactly the services the project touched at the time. SNS and CloudWatch were never on that list, because nothing in this project had ever needed them before today. Attached the two missing policies and moved on.</p>
<h3>The Setting That Had Been Renamed</h3>
<p>The billing alarm needed one more thing — enabling billing alerts in my account settings, so AWS would actually start publishing cost data to CloudWatch in the first place. Went looking for the setting everyone's guides call "Receive Billing Alerts" and couldn't find it anywhere. Turns out it's been renamed—it now lives under a section called "Alert preferences," with the checkbox labelled "Receive CloudWatch Billing Alerts." Small thing, but a good reminder that documentation and tutorials go stale, and the real console is the only source of truth.</p>
<h3>What's Actually Watching Now</h3>
<p>Three alarms, wired to two SNS topics — one for application issues in my main region, one specifically in us-east-1, since billing metrics only exist there globally, no matter where the rest of the infrastructure lives. Lambda errors, DynamoDB throttling, and a billing threshold, all routed to email, all confirmed and working.</p>
<h3>Where This Actually Leaves Me</h3>
<p>The project is genuinely complete now, in a way it wasn't yesterday. Not just "the resources exist," but "I'll actually know if something stops working." That's the difference between a demo and something built the way real production systems are supposed to work.</p>
<p>#AWSCloudResumeChallengeCompleted #AWS #Terraform #CloudWatch #SNS #GitHubActions #BuildInPublic</p>
]]></content:encoded></item><item><title><![CDATA[Day 24: AWS Cloud Resume Challenge]]></title><description><![CDATA[Yesterday I designed the pipeline. Today was about actually getting it to run — which turned out to be its own separate challenge, full of small, precise…]]></description><link>https://alfiyajaved.hashnode.dev/day-24-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-24-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Mon, 31 Aug 2026 15:04:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/dd97a408-1cfe-4fd1-991d-f59c315f0c4d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Getting the Pipeline From Designed to Actually Running</h3>
<p>Yesterday I designed the pipeline. Today was about actually getting it to run — which turned out to be its own separate challenge, full of small, precise details that had nothing to do with the architecture itself.</p>
<h3>Wrong Folder, First Try</h3>
<p>GitHub Actions only looks for workflow files in one exact, non-negotiable path: <code>.github/workflows/</code>, plural. I put mine directly in <code>.github/</code> instead. No error, no warning — GitHub just silently never recognised them as workflows at all. Moving them to the right path fixed it, but not before I also managed to create a stray file literally named <code>workflows</code> with no extension, which then blocked me from creating the real folder with that same name. Small mistake, layered on a small mistake.</p>
<h3>YAML Doesn't Forgive Indentation</h3>
<p>Hit a syntax error next — one line of my <code>deploy</code> job had drifted to the wrong indentation level, which made YAML read it as a brand new top-level key instead of a second job nested under <code>jobs:</code>. A single space, in the wrong place, and the whole file becomes invalid.</p>
<h3>A Security Boundary I Didn't Know Existed</h3>
<p>Pushing the fix got rejected outright: my GitHub token didn't have permission to touch workflow files at all. Turns out this is deliberate — files inside <code>.github/workflows/</code> can execute real code automatically, so GitHub requires a separate, explicit scope before any token can create or modify them. My token was created weeks ago, before I ever needed that permission. Added the scope, fixed.</p>
<h3>One Typo, Three Layers Deep</h3>
<p>Then a credentials error — <code>Could not assume role with OIDC: Request ARN is invalid</code>. Took a moment to find: <code>github-actons-deploy-role</code> instead of <code>github-actions-deploy-role</code>, missing an "i", sitting inside a YAML file I'd been editing directly through GitHub's web interface across several rounds of fixes already. Fixed the spelling, and both my test job and my deploy job finally ran clean, end to end, for the first time.</p>
<h3>The Warning That Wasn't Actually About My Code</h3>
<p>One more thing surfaced right after — a Node.js version deprecation warning on three of the actions I'm using. Nothing I wrote caused it; it's a platform-wide runtime migration GitHub is rolling out. The real fix was bumping the major version of each action to the release their maintainers had already updated for the new runtime. I was tempted to stay a version behind out of general caution, but thought it through properly — a <em>patch</em> version bump is low-risk by definition, but staying on an old <em>major</em> version here meant staying on something scheduled for removal entirely. Pinned to the current major versions instead, understanding the actual reasoning rather than just picking a number.</p>
<h3>Where This Leaves Me</h3>
<p>Test gate working. Deploy following only if tests pass. Authentication with zero stored credentials. No warnings, no silent failures. The pipeline isn't just designed anymore — it's real, and I understand every piece of why it broke on the way there, not just that it eventually worked.</p>
<p><strong>Up next:</strong> deciding on a monitoring approach so I actually know if something breaks in production, instead of finding out from a user.</p>
<p>#AWSCloudResume #CI/CDPipeline #GitHubAction #Workflows #Tests #Deployment #AWS #OICD #Day24BuildInPublic</p>
]]></content:encoded></item><item><title><![CDATA[Day 23: AWS Cloud Resume Challenge]]></title><description><![CDATA[Started the CI/CD phase today — two GitHub Actions workflows, one for my frontend, one for backend. Here's how I actually thought through each decision…]]></description><link>https://alfiyajaved.hashnode.dev/day-23-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-23-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Sat, 29 Aug 2026 16:37:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/514655de-0259-496d-b119-42bab46a2386.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Started the CI/CD phase today — two GitHub Actions workflows, one for my frontend, one for backend. Here's how I actually thought through each decision.</p>
<h3>Keeping Terraform and Deployment Separate</h3>
<p>The first thing I had to be clear on: my Lambda function's <em>infrastructure</em> — its role, its permissions, its existence — already lives fully in Terraform. That was the whole point of the last several weeks. CI/CD isn't supposed to redefine any of that. Its only job is updating the <em>code</em> inside a function that already exists, using a direct call like <code>aws lambda update-function-code</code>. Nothing here touches Terraform's job, and Terraform never touches this pipeline's job. Two tools, two clearly separate responsibilities.</p>
<h3>Tests Have Actually to Gate the Deployment</h3>
<p>I didn't want a pipeline that pushes whatever's in the folder. The whole point of calling this "CI" — Continuous <em>Integration</em> — is that it verifies the code is actually correct before anything ships. So my backend pipeline runs my real test suite first, in its own separate job, and the deploy job only runs if that job succeeds. If a test fails, nothing gets pushed to my live function. That's not a nice-to-have; that's the actual reason this is worth automating at all.</p>
<h3>Why OIDC, Not Stored Keys</h3>
<p>I've been thinking about credential exposure since early in this project — the same reason I didn't want long-lived AWS keys sitting around on my own laptop indefinitely. Storing a raw AWS access key as a GitHub secret works, but it's a permanent, static credential sitting somewhere, and if it ever leaked, it would keep working until someone manually noticed and rotated it.</p>
<p>OIDC solves this differently. Instead of a stored secret, GitHub Actions proves its identity to AWS fresh, every single run, using a short-lived signed token — and I get to define exactly which repo and which branch are allowed to use it. No permanent key exists anywhere to leak in the first place. I set this up as new Terraform code — an IAM identity provider trusting GitHub specifically, and a role that only my exact repo, on my exact branch, can ever assume. Same shape of thinking as the source-ARN conditions I added to my S3 policy and my Lambda permission earlier in this project — scope trust to exactly what should have it, nothing broader.</p>
<h3>Verifying Real Values Instead of Assuming Defaults</h3>
<p>By now this is just a habit from this whole project: check the real region, the real runtime version, the real folder names, rather than trusting a default or an assumption. My actual resources run in<code>us-east-2</code>, not <code>us-east-1</code>. My real Lambda runtime is Python 3.14. My real folders are capitalised — <code>Frontend</code> and <code>Backend</code> — and GitHub's runners are case-sensitive, so getting that wrong would mean a workflow that silently never triggers at all, no error, just permanent silence. Every one of these got checked against the real thing before going into the workflow file.</p>
<h3>The One Risk That Was Specific to My Own Project</h3>
<p>My deploy step syncs my <code>Frontend</code> folder to S3 using <code>--delete</code>, which removes anything in the bucket that isn't in that local folder. I'd uploaded my resume PDF straight to S3 a while back, outside of git entirely. If I hadn't caught that, the very first real run of this pipeline would have quietly deleted it. Not a generic risk — a risk that only existed because of a specific choice I'd made earlier in this exact project, which is exactly why checking your own setup, not just the pipeline logic, matters.</p>
<h3>Where This Leaves Me</h3>
<p>Frontend workflow is done. Backend workflow — with the test gate, the OIDC auth, and the direct Lambda code update — is next.</p>
<p><strong>Up next:</strong> finishing the backend workflow, then watching this pipeline actually run for the first time.</p>
<p>#AWSCloudResume #CI/CD #GithubAction #OICD #Day23BuildInPublic</p>
]]></content:encoded></item><item><title><![CDATA[Day 22: AWS Cloud Resume Challenge]]></title><description><![CDATA[This one closes out something that started back on Day 14 — turning every piece of infrastructure I'd clicked together by hand into real, working Terraform…]]></description><link>https://alfiyajaved.hashnode.dev/day-22-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-22-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Mon, 24 Aug 2026 15:45:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/863254ff-4cd2-4c0a-a77b-d87629f14ed5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>The Whole Project Is Actually in Terraform Now</h3>
<p>This one closes out something that started back on Day 14 — turning every piece of infrastructure I'd clicked together by hand into real, working Terraform code. It took longer than I expected, and today the last two open items finally got closed.</p>
<h3>The Two Things I Deliberately Left Open</h3>
<p>For weeks, two things sat unfinished on purpose, rather than rushed through: my S3 bucket's public access settings were still unlocked, even though CloudFront and OAC had been the only real path in for a while. And my Lambda's IAM permissions still had full access to every DynamoDB table in my account, not just the one it actually uses.</p>
<p>Both got closed today. The DynamoDB policy is now scoped to exactly one action — <code>UpdateItem</code> — after actually reading my own Lambda code and confirming that's the <em>only</em> DynamoDB call it makes. Not a guess, not "probably fine" — checked against the real file. And the S3 bucket's public access block flipped from fully open to fully restricted, all four settings. Reloaded the site immediately after both changes. Still worked, exactly as expected, since nothing was ever actually relying on that open access to begin with.</p>
<h3>One More Thing I Found Along the Way — Two Systems, Same Job</h3>
<p>While reviewing my Lambda code, I noticed it had its own hardcoded CORS header — <code>Access-Control-Allow-Origin: '*'</code> — sitting right next to an API Gateway CORS configuration I'd already scoped down to my real domain a while back. Two different systems both claiming authority over the same header, with two different values. Removed it from Lambda entirely, since API Gateway's HTTP API type is supposed to handle this on its own. Tested it live, checked the browser console specifically for CORS errors — clean.</p>
<h3>And My Tests Got Honest Too</h3>
<p>Went back through my test file and found my most important assertion — actually checking the response body contained the right data — was commented out this whole time. The test only ever confirmed the function didn't crash, never that it returned the correct number. Fixed that, removed a mock for a DynamoDB call my function never actually makes, and added a check that verifies the exact parameters sent to DynamoDB, not just that something happened.</p>
<h3>What "Done" Actually Means Now</h3>
<p>Every single resource this project uses — DynamoDB, S3, IAM, Lambda, API Gateway, Route 53, ACM, CloudFront — is genuinely under Terraform, imported carefully, matching what's actually live. Not "mostly," not "except for two things." Actually all of it.</p>
<p>Looking back at the whole stretch to get here: the S3/CloudFront saga that ate two full days, the IAM policy attachment confusion, more typos than I want to count, a WAF I didn't know I had until Terraform told me. None of it was fast. All of it was real.</p>
<p><strong>Up next:</strong> CI/CD. Starting with the safest, smallest piece first — getting my tests to run automatically on every push, before touching anything that actually deploys to AWS.</p>
<p>#AWSCloudResumeChallenge #Terraform #IaaC #AWS #DynamoDB #ACM #Route53 #CloudFront #Lambda #S3 #APIGateway</p>
]]></content:encoded></item><item><title><![CDATA[Day 21: AWS Cloud Resume Challenge]]></title><description><![CDATA[Today was supposed to be the last resource in this whole Terraform project — the CloudFront distribution itself. It was, but not before one wrong command…]]></description><link>https://alfiyajaved.hashnode.dev/day-21-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-21-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Sat, 22 Aug 2026 14:04:36 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/05b5fa85-722a-49b2-9d06-d0bc782b94a7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>The Firewall I Didn't Know I Had</h3>
<p>Today was supposed to be the last resource in this whole Terraform project — the CloudFront distribution itself. It was, but not before one wrong command and one real surprise sitting inside my own infrastructure.</p>
<h3>The Wrong ID</h3>
<p>First attempt at importing failed with almost no useful information:</p>
<pre><code class="language-plaintext">Error: couldn't find resource
</code></pre>
<p>No detail, no request ID, nothing like the errors I'd gotten used to reading. Took a moment to realise why: I had used my Origin Access Control's ID by mistake, not my distribution's ID. Two completely different resource types, and the value I passed in was a real, valid-looking ID — just for the wrong thing entirely. That's likely why the error was so bare; the API wasn't confused about permissions or formatting, it just genuinely had nothing to return for a distribution that didn't exist under that ID.</p>
<p>Fixed it with the correct distribution ID, and the import went through cleanly.</p>
<h3>The Plan That Actually Mattered</h3>
<p><code>terraform plan</code> came back with three differences. Two were things I expected by now — a missing tag, and <code>is_ipv6_enabled</code> quietly defaulting to <code>false</code> in my code when the real distribution actually had it turned on. Small, but a real setting, not just cosmetic.</p>
<p>The third one stopped me.</p>
<pre><code class="language-plaintext">- web_acl_id = "arn:aws:wafv2:us-east-1:...:global/webacl/CreatedByCloudFront-0db22b05/..." -&gt; null
</code></pre>
<p>My distribution has an actual AWS WAF — a Web Application Firewall — attached to it. Filtering traffic at the edge, before it ever reaches my S3 bucket. I didn't know this existed until Terraform's plan surfaced it. The name suggests CloudFront created it automatically at some point, probably from a console prompt I clicked through without fully realising what it did.</p>
<p>If I had applied that plan without reading it closely, I would have silently detached my own firewall. Not because I decided to — because my code simply didn't know it was there, and Terraform assumes anything undeclared should be removed.</p>
<p>This is the same shape of thing my iptables setup does in my EC2 project — filtering unwanted traffic before it reaches the real server — except this one's automatic, running at AWS's edge, and I'd apparently had it running this whole time without knowing.</p>
<h3>The Fix</h3>
<p>Added all three missing pieces back into my code — the tag <code>is_ipv6_enabled = true</code>and the WAF's ARN — and ran the plan again.</p>
<pre><code class="language-plaintext">No changes. Your infrastructure matches the configuration.
</code></pre>
<h3>Where This Leaves Me</h3>
<p>Every resource type in this project — DynamoDB, S3, IAM, Lambda, API Gateway, Route 53, ACM, and now CloudFront — is genuinely under Terraform, matching what's actually live.</p>
<p>I almost called this "the whole project done" today. It isn't, quite. Two things are still deliberately left open: the S3 bucket's public access settings are still unlocked, since I wanted OAC fully proven working first, and my Lambda's DynamoDB permissions are still broader than they need to be. Both get fixed tomorrow, on purpose, not rushed tonight.</p>
<p><strong>Up next:</strong> locking those two down, then moving into the next real phase of this project — a CI/CD pipeline, since Terraform was never meant to redeploy my code every time I fix a bug.</p>
<p>#AWSCloudResumeChallenge #CloudFront #Terraform #Day21BuildInPublic</p>
]]></content:encoded></item><item><title><![CDATA[Day 20: AWS Cloud Resume Challenge]]></title><description><![CDATA[Two sessions' worth of work today — Route 53 from last night, which I was unable to post until too late, but better to have it, and the start of ACM and…]]></description><link>https://alfiyajaved.hashnode.dev/day-20-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-20-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Fri, 21 Aug 2026 16:08:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/31395444-1787-4ddb-bc08-92b5d14aa622.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Route 53, a Second Provider, and My First Multi-Region Resource</h3>
<p>Two sessions' worth of work today — Route 53 from last night, which I was unable to post until too late, but better to have it, and the start of ACM and CloudFront today. Smaller wins than the S3 saga, but each one taught me something I hadn't touched before.</p>
<h3>Route 53 — The Easy One I Needed</h3>
<p>I imported my hosted zone first. Clean, no surprises — a hosted zone barely has any settings to begin with.</p>
<p><code>terraform plan</code> Did flag one thing: <code>force_destroy = false</code>. At first I wasn't sure if this described something real happening in AWS. It doesn't. This setting only exists on Terraform's side — it decides whether Terraform is <em>allowed</em> to delete this zone later if it still has real DNS records inside it. Set to <code>false</code>Terraform refuses to destroy it by accident. I applied it, and nothing about my actual live DNS changed — it was pure safety bookkeeping, not an infrastructure change.</p>
<p>Then the A record points my domain at CloudFront. One detail worth remembering: the <code>alias.zone_id</code> value for a CloudFront target isn't something you look up per-distribution — it's a fixed constant (<code>Z2FDTNDATAQYW2</code>) that's the same for every CloudFront distribution on every AWS account. I double-checked my actual distribution's domain name against the CLI output before trusting it, rather than assuming an old value was still correct.</p>
<h3>Today — My First Resource That Needed a Second Provider</h3>
<p>ACM certificates for CloudFront have to live in<code>us-east-1</code>, no matter what region everything else runs in. This meant writing my first <em>second</em> provider block:</p>
<pre><code class="language-hcl">provider "aws" {
  alias   = "us_east_1"
  region  = "us-east-1"
  profile = "terraform-project"
}
</code></pre>
<p>What I had to understand first: this doesn't touch or replace my original provider block at all. My existing block has no which makes it the default — every resource I've written so far uses it automatically without ever naming it. This new block only applies to a resource that explicitly writes <code>provider = aws.us_east_1</code> inside itself, which is exactly the one line I added to my ACM certificate resource. Everything else in the project keeps working exactly as before.</p>
<p>I also skipped <code>terraform init</code> by accident after adding this and got worried — but it still worked fine. Turns out that matters less than I thought here, because this wasn't a <em>new</em> provider, just the same AWS plugin I already had downloaded, configured a second way. Different from the <code>archive</code> provider mistake earlier in this project, where Terraform genuinely needed to fetch something it had never seen before.</p>
<h3>The Resource I Expected to Write, But Didn't Need To</h3>
<p>I assumed <code>aws_acm_certificate_validation</code> would be next. It isn't needed here at all—and understanding why was the real lesson. That resource doesn't represent a real object in AWS with its own ID. It exists purely to make Terraform <em>wait</em> for a brand-new certificate's DNS validation to finish before anything else uses it. My certificate is already validated — my HTTPS has been working this whole time — so there's nothing to wait for. I can just reference the certificate directly wherever it's needed later.</p>
<h3>Last Piece Today — The OAC From The S3 Saga</h3>
<p>Imported the Origin Access Control I created manually weeks ago during the S3 debugging saga. Clean import, one small diff — a missing <code>description</code> field — which I added and applied.</p>
<h3>Where This Leaves Me And Up Next</h3>
<p>Route 53's zone and record, done. The ACM certificate, imported and correctly routed through a second provider. The OAC, done. The only piece left in this group is the big one — the CloudFront distribution itself, the most complex single resource block in this whole project. That gets its own full session, not a tired late-night tail end.</p>
<p>#AWSCloudResumeChallenge #AWS #Terraform #CloudFront #Route53 #Day20BuildInPublic</p>
]]></content:encoded></item><item><title><![CDATA[Day 19: AWS Cloud Resume Challenge]]></title><description><![CDATA[Today I finished writing the last piece of my backend in Terraform — API Gateway. Four small resources, all coded, all imported cleanly. Then I ran…]]></description><link>https://alfiyajaved.hashnode.dev/day-19-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-19-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Thu, 13 Aug 2026 02:39:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/a4ca3a19-bbd8-4d26-9874-b425304e80cc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Today I finished writing the last piece of my backend in Terraform — API Gateway. Four small resources, all coded, all imported cleanly. Then I ran <code>terraform plan</code> to double-check everything, and the output stopped me cold.</p>
<h3>What The Plan Said</h3>
<p>The very first line read: <code>aws_apigatewayv2_api.visitor_api will be created</code>. Not updated. Created — as in, a brand new one.</p>
<p>Right under it, four more resources — my integration, my route, my stage, and my Lambda permission — all said <code>must be replaced</code>. Destroy, then recreate, all four.</p>
<p>If I had run <code>apply</code> without reading closely, this would have built a second, empty API Gateway, and destroyed the real one my actual live website depends on. My visitor counter would have broken.</p>
<h3>What Actually Went Wrong</h3>
<p>I had imported the integration, the route, the stage, and the permission — but never the API resource itself. Terraform genuinely had no idea it existed.</p>
<p>Here's the part that actually explains the whole cascade: every one of those four resources references the API's ID inside my code. But since Terraform's records didn't have that API in them, it treated the ID as unknown — not as the real value it actually was. And since an API ID can't just be swapped on an existing resource, Terraform's only option was to destroy and recreate everything attached to it.</p>
<p>One missing import caused five different lines to look broken.</p>
<h3>The Fix</h3>
<p>One command:</p>
<pre><code class="language-bash">terraform import aws_apigatewayv2_api.visitor_api opgaqw2eqqh
</code></pre>
<p>Ran <code>plan</code> again right after, and every single forced replacement disappeared at once — not because I fixed five things, but because I fixed the one real cause underneath all of them.</p>
<h3>What Was Left After That</h3>
<p>Two real, intentional changes remained. First, my CORS settings updated from wide open (<code>*</code>) to my actual domain only — something I'd deliberately meant to fix. Second, my integration's URI format changed to match the format Terraform expects for this kind of Lambda connection — a cosmetic difference from how the console originally set it up, not a functional break.</p>
<p>I applied both, reloaded my site immediately, and the visitor counter still worked exactly as it should.</p>
<h3>The Actual Lesson</h3>
<p>This is the second time in this project that a plan looking scary turned out to have one small, findable root cause underneath it — not five separate problems, one problem wearing five different symptoms. Reading <em>why</em> something says "destroy" instead of just reacting to the word "destroy" is turning into a real skill, not just a rule I follow because I was told to.</p>
<p><strong>Up next:</strong> Actually going back and understanding—properly— every resource I've written, from the IAM policy attachments through today's API Gateway work. I got through it, but I wasn't sure why. Fixing that before moving on to CloudFront and ACM.</p>
<p>#AWSCloudResumeChallenge #APIGateway #LambdaFunction #Day19BuildInPublic #Terraform</p>
]]></content:encoded></item><item><title><![CDATA[Day 18: AWS Cloud Resume Challenge]]></title><description><![CDATA[My last post ended with a plan: lock down public access on my S3 bucket, then move on to CloudFront and ACM. That's not quite what happened. Eight days…]]></description><link>https://alfiyajaved.hashnode.dev/day-18-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-18-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Fri, 07 Aug 2026 18:41:56 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/ffaee901-a8f5-451e-b076-c66dc60d5e7d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>My last post ended with a plan: lock down public access on my S3 bucket, then move on to CloudFront and ACM. That's not quite what happened. Eight days passed, and instead of a straight line to CloudFront, I went sideways into IAM roles and my Lambda function first. Here's everything that actually happened in between.</p>
<h3>Making My Code Less Hardcoded</h3>
<p>Before touching anything new, I cleaned up something small. My bucket policy had my AWS account ID and CloudFront distribution ID typed directly into the code. Not really a secret — account IDs and distribution IDs aren't sensitive information — but hardcoded values like that make code less reusable. If I ever rebuild this under a different AWS account, every hardcoded number would need to be found and changed by hand.</p>
<p>So I replaced the account ID with something that asks AWS directly:</p>
<pre><code class="language-hcl">data "aws_caller_identity" "current" {}
</code></pre>
<p>And moved the distribution ID into a variable instead of a fixed string. Small change, but it made the code more correct, not just neater.</p>
<h3>Bringing My Lambda's IAM Role Into Terraform</h3>
<p>Next was the IAM role my Lambda function actually runs as. I imported it, ran <code>terraform plan</code>, and got a scary-looking result: <strong>destroy and recreate</strong>.</p>
<p>The cause was one small attribute — <code>path</code>. IAM roles created automatically through the Lambda console get nested under <code>/service-role/</code> by default. My code assumed the default path instead, and <code>path</code> is one of those IAM settings that can't just be updated — Terraform's only option is to delete the old one and make a new one. That would have broken my live function's permissions if I'd let it run.</p>
<p>The fix was adding the real path to my code:</p>
<pre><code class="language-hcl">resource "aws_iam_role" "lambda_exec" {
  name = "visit_counter_function-role-doxa8sz4"
  path = "/service-role/"
  ...
}
</code></pre>
<p>After that, the plan came back clean — an update, not a destroy.</p>
<h3>Expecting One Thing, Finding Another</h3>
<p>I planned to write an inline IAM policy next — the kind you define directly inside your own code. But when I went looking for it to import, there wasn't one. My role wasn't using an inline policy at all. It was using two separate <strong>managed policy attachments</strong> instead — a completely different thing in Terraform, needing a different resource (<code>aws_iam_role_policy_attachment</code>, not <code>aws_iam_role_policy</code>).</p>
<p>Looking at what was actually attached, I found something worth knowing: one of the two policies was <code>AmazonDynamoDBFullAccess</code> — meaning my Lambda function technically has permission to touch <em>every</em> DynamoDB table in my account, not just my one visitor-counter table. Far more access than it actually needs. I'm not fixing that today — I imported it as-is, to match reality first — but it's now a known, real thing to tighten later, the same way the S3 bucket policy started too open before I scoped it down properly.</p>
<h3>My Lambda Function — Three Mistakes in One Sitting</h3>
<p>Writing the actual <code>aws_lambda_function</code> resource is where most of the real trouble happened.</p>
<p><strong>First</strong>, I added a new provider (<code>archive</code>, for zipping my code automatically) but hadn't told Terraform about it yet. One clean fix: <code>terraform init -upgrade</code>. I made a manual backup of my lock file first, out of caution — then realised I didn't need to, since that file's already tracked in git from an earlier fix. Git already was the backup.</p>
<p><strong>Second</strong>, and this one actually scared me — I mixed up my output filename in the code that zips my Lambda function. I meant to save the zip as<code>lambda_function.zip</code>, but typed <code>.py</code> instead. Terraform took my actual Python file, zipped it, and saved the result on top of my original file, with the same name. My real code was gone, replaced by a zipped file wearing a <code>.py</code> extension.</p>
<p><code>git restore</code> brought it back in one command — because I'd already committed that file before this happened. That's the exact payoff of a habit I built early in this project and almost didn't think twice about.</p>
<p><strong>Third</strong>, <code>terraform plan</code> refused to recognise my function's actual runtime — Python 3.14 — even though that's genuinely what's running on AWS right now. Took a while to understand why: AWS added Python 3.14 support to Lambda back in November, but the Terraform tool I'm using to manage AWS didn't get support for that same version until a later update of its own. My provider was still pinned to an older version, set weeks ago, back when 3.14 didn't even exist as an option yet. Updating the version constraint and running <code>init -upgrade</code> again fixed it — the same command as mistake one, solving a completely different problem this time.</p>
<h3>Where This Actually Leaves Me</h3>
<p>Not where I predicted eight days ago, but further along in a different, useful direction. DynamoDB, S3 (including its policy), the IAM role and its permissions, and now the Lambda function itself are all real, working Terraform code — imported carefully, matching what's actually live, nothing guessed at.</p>
<p><strong>Actually, up next this time:</strong> API Gateway, wiring it to this Lambda function, and confirming the whole visitor counter still works end to end. CloudFront and ACM are still coming — just later than I said last time.</p>
<p>#AWSCloudResumeChallenge #Debugging #Terraform #AWS #Lambda #IAM #Day18BuildInPublic</p>
]]></content:encoded></item><item><title><![CDATA[Day 17: AWS Cloud Resume Challenge]]></title><description><![CDATA[This one took longer than I thought — two days, not the quick "import and move on" I expected after Day 16. I want to write about it honestly, even though…]]></description><link>https://alfiyajaved.hashnode.dev/day-17-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-17-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Thu, 30 Jul 2026 06:23:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/540e3c70-e42d-45ee-ac36-b4f6e28caae1.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>The Two-Day Detour I Didn't Plan For</h3>
<p>This one took longer than I thought — two days, not the quick "import and move on" I expected after Day 16. I want to write about it honestly, even though it wasn't a clean, quick fix.</p>
<h3>What Started It</h3>
<p>On Day 16, I found a problem in my S3 bucket policy. It said <code>Principal: "*"</code> — this means anyone on the internet could read my files. I had written this rule way back on Day 11, just to fix a 403 error. I wrote a better, safer rule this time — one that only lets CloudFront access my bucket, and only <em>my</em> specific CloudFront distribution.</p>
<p>I saved this new rule. My website immediately broke and showed a 403 error.</p>
<h3>The Problems I Had To Find, One By One</h3>
<p>It turns out my CloudFront setup was never actually using something called <strong>Origin Access Control (OAC)</strong> — even though I thought it was, this whole time. My origin was set to "Public." This means CloudFront was just quietly using my bucket's public policy to get in. When I removed that public policy and added the safer one, I accidentally removed the <em>only</em> thing that was letting CloudFront in, because CloudFront was never set up the right way to begin with.</p>
<p>To fix it, I had to do these things, one at a time:</p>
<ol>
<li><p>Change my origin from the S3 <strong>website link</strong> to the S3 <strong>REST API link</strong>. OAC only works with the second one, not the website one I set up back on Day 11.</p>
</li>
<li><p>Actually create an OAC — it didn't exist before, even though I assumed it did.</p>
</li>
<li><p>Check that my bucket's real region matched what I typed into the new origin. In my case, it did match, but it was still worth checking, since a wrong region would also break things.</p>
</li>
<li><p>Fix my <strong>Default Root Object</strong> setting. This tells CloudFront "when someone visits my homepage with no file name, show them <code>index.html</code>." Without this set correctly, CloudFront didn't know which file to ask for.</p>
</li>
</ol>
<p>That last one took the longest to figure out, and here's why: when this setting is wrong, S3 doesn't show a normal "file not found" error. Instead, it shows the same "Access Denied" error as a permissions problem. So a missing-file problem looked exactly like a permissions problem, the whole time, and I couldn't tell which one it actually was.</p>
<h3>What Finally Fixed It</h3>
<p>Fixing the Default Root Object setting was the last piece. Everything else — OAC, the origin type, the bucket policy, the region — was already correct by that point. I just couldn't tell, because every different problem showed me the same error message.</p>
<h3>What I Actually Learned</h3>
<p>Two days is longer than I wanted this to take. But looking back, each of those four things — origin type, OAC, region, root object — was a real, separate reason my site could have been broken. I checked each one calmly, one at a time, instead of guessing randomly or undoing everything out of panic. That's a different skill than just writing a correct policy the first time.</p>
<p>Real systems usually don't break because of one obvious thing. They break because two or three small things are wrong at the same time, and the error message doesn't tell you which one is the real cause.</p>
<p>My site is working again. My bucket policy finally does what I thought it was already doing since Day 11. And now I have real Terraform code that matches what's actually running — not just a guess.</p>
<p><strong>Next up:</strong> going back to Terraform — locking down public access on my bucket completely now that OAC is set up properly, and then finally moving on to CloudFront and ACM.</p>
<p>#AWSCloudResumeChallenge #Day17BuildInPublic #BucketPolicy #CloudFront #OAC #403ForbiddenError #AccessDeniedError #RealWorldProblemDebugging</p>
]]></content:encoded></item><item><title><![CDATA[Day 16: AWS Cloud Resume Challenge]]></title><description><![CDATA[Picking up from yesterday's teaser — today was S3's turn to get imported into Terraform. It went smoother than DynamoDB in some ways, and revealed…]]></description><link>https://alfiyajaved.hashnode.dev/day-16-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-16-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Mon, 27 Jul 2026 17:43:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/910b4e92-47d2-449a-9e59-852077bb810e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>I Imported My S3 Bucket... and Found My Front Door Wide Open</h3>
<p>Picking up from yesterday's teaser — today was S3's turn to get imported into Terraform. It went smoother than DynamoDB in some ways, and revealed something much bigger in others.</p>
<h3>The Quick One First</h3>
<p>Same category of mistake as my <code>my_table</code> vs <code>visit_counter</code> mix-up from Day 14 — except this time it wasn't my naming, it was AWS being inconsistent with itself. I wrote an output referencing <code>aws_s3_bucket.resume_site.name</code>, and Terraform threw:</p>
<pre><code class="language-plaintext">Error: Unsupported attribute
This object has no argument, nested block, or exported attribute named "name".
</code></pre>
<p>Turns out <code>aws_s3_bucket</code> doesn't expose a <code>.name</code> attribute at all — it's <code>.bucket</code>, matching the argument I originally set. Different AWS resource types just don't agree on what to call the same concept. Quick fix, moving on.</p>
<h3>The Import Went Clean — Then I Looked Closer</h3>
<pre><code class="language-plaintext">terraform import aws_s3_bucket.resumesite alfiyajaved.in
</code></pre>
<p>Ran it, got a success message, ran <code>terraform plan</code> right after — <strong>"No changes."</strong> Felt like another win, same as DynamoDB.</p>
<p>But this time I stopped myself before moving on. Unlike DynamoDB, which is one single resource block covering everything, S3 in modern Terraform is split — the bucket itself, public access settings, versioning, and the bucket policy are all <em>separate</em> resource blocks. My <code>main.tf</code> only had the bare bucket declared. "No changes" didn't mean my whole S3 setup was under control — it meant the one narrow thing I'd written matched. Everything else was just invisible to Terraform, not confirmed safe.</p>
<p>So before writing more code, I went and actually read my bucket's real settings in the console. And that's where today got interesting.</p>
<h3>The Bucket Policy I'd Completely Forgotten About</h3>
<pre><code class="language-json">{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:....",
            "Resource": "arn:aws:s3:...."
        }
    ]
}
</code></pre>
<p><code>"Principal": "*"</code> means <em>anyone on the internet, no authentication, no questions asked,</em> can read any object in this bucket. I recognised this policy immediately — it's the exact fix from my Day 11 post, when I hit a 403 error right after enabling static website hosting. Opening it up to everyone solved the problem <em>at that moment</em>.</p>
<p>The problem is that it was before CloudFront and Origin Access Control existed in my setup. Once I added CloudFront specifically to keep this bucket private and route everything through a proper CDN, I never went back and tightened this policy. It just sat there, quietly making the whole CloudFront/OAC setup partially pointless — my bucket name is literally my domain, so anyone could hit it directly, bypass caching, bypass HTTPS enforcement, and rack up direct S3 requests that CloudFront was supposed to be absorbing.</p>
<p>Nothing sensitive lives in this bucket — it's a static portfolio — but it defeated the actual architecture I built.</p>
<h3>The Real Fix</h3>
<p>Instead of <code>"Principal": "*"</code>, the correct policy scopes access to the CloudFront <strong>service</strong> itself, plus a <code>Condition</code> that ties it to <em>my specific distribution</em> — not just any CloudFront distribution from any AWS account:</p>
<pre><code class="language-json">{
    "Effect": "Allow",
    "Principal": { "Service": "cloudfront.amazonaws.com" },
    "Action": "s3:...",
    "Resource": "arn:aws:s3:....",
    "Condition": {
        "StringEquals": {
            "AWS:SourceArn": "arn:aws:cloudfront::MY_ACCOUNT_ID:distribution/MY_DISTRIBUTION_ID"
        }
    }
}
</code></pre>
<p>That <code>Condition</code> block matters more than it looks — without it, in theory <em>any</em> CloudFront distribution, even someone else's, could be pointed at my bucket and read from it. Locking it to my exact distribution's ARN closes that gap.</p>
<p><strong>Up next:</strong> writing this corrected policy as a <code>aws_s3_bucket_policy</code> resource in Terraform, watching the plan diff show the exact change from open to locked-down, and applying it to my actual live site — carefully, with a browser reload check after.</p>
<p>#AWSCloudResumeChallenge #S3Bucket #Debugging #Terraform #BuildInPublic #AWS #S3BucketPolicy</p>
]]></content:encoded></item><item><title><![CDATA[Day 15: AWS Cloud Resume Challenge]]></title><description><![CDATA[After yesterday's win with the DynamoDB import, I could've jumped straight into the S3 bucket next. Instead, I spent today thinking about something less…]]></description><link>https://alfiyajaved.hashnode.dev/day-15-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-15-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Sat, 18 Jul 2026 18:03:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/15049782-feb0-4c74-bcd0-12110c51861b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Locking the Front Door Before Building More Rooms</h3>
<p>After yesterday's win with the DynamoDB import, I could've jumped straight into the S3 bucket next. Instead, I spent today thinking about something less exciting but more important: <strong>who's actually allowed to touch my infrastructure, and what happens if those credentials leak.</strong></p>
<h3>Why I Didn't Just Keep Using Root</h3>
<p>I mentioned in my last post that I created a separate IAM user instead of running Terraform as root. Today I actually sat down and thought through <em>why</em> that matters, not just that it's "best practice."</p>
<p>Root has unrestricted access to everything in my AWS account — billing, every service, every region, no exceptions. If those credentials ever leaked — a misconfigured <code>.gitignore</code>, a careless commit, anything — whoever got them would own my entire account. A scoped-down IAM user limits the blast radius. Even in the worst case, a leaked key only exposes what that specific user was allowed to do.</p>
<p>For now, I attached broad-but-scoped AWS-managed policies (S3, DynamoDB, Lambda, CloudFront, Route 53, ACM, and IAM, since Terraform needs to manage my Lambda's execution role). It's not perfect least-privilege — a fully locked-down setup would scope IAM permissions down to exact actions on exact resource ARNs — but it's a real, meaningful step up from "everything, always," and it's honest about being a stepping stone rather than the final answer.</p>
<h3>Understanding What Actually Costs Money</h3>
<p>Somewhere in all this, I also went down a small rabbit hole on DynamoDB billing modes, since I wanted to actually understand the <code>PAY_PER_REQUEST</code> setting I'd used rather than just copying it because a tutorial said so.</p>
<p>Here's what I landed on: with <strong>On-Demand</strong> (<code>PAY_PER_REQUEST</code>), you're charged per actual read/write request, with a decent free-tier allowance before anything gets billed — great for a low-traffic personal project like mine. With <strong>Provisioned</strong> billing, you commit to a fixed read/write capacity upfront and pay for it continuously whether you use it or not, and if your traffic goes over that committed capacity without auto-scaling turned on, requests just get throttled instead of silently overcharging you.</p>
<p>For a visitor counter that gets maybe a few dozen hits a day, On-Demand is clearly the right call. But the bigger habit I picked up from this: I went into AWS Budgets and set up a cost alert — a simple email trigger if my forecasted bill goes above a small threshold. Cheap insurance against any future mistake, Terraform-related or not.</p>
<h3>What's Next</h3>
<p>With the IAM setup feeling more solid and a better grip on what actually drives cost, I'm heading into the <strong>S3 bucket</strong> next — same import process as DynamoDB, but with its own quirks waiting, since modern Terraform splits things like public access blocking and website configuration into separate resource blocks instead of one big one. I already know from experience that I'll probably hit at least one dumb mistake along the way. Looking forward to writing about it.</p>
<p>#AWSCloudResumeChallenge #AWS #DynamoDB #IAM #Day15BuildInPublic</p>
]]></content:encoded></item><item><title><![CDATA[Day 14: AWS Cloud Resume Challenge]]></title><description><![CDATA[It's been a while since my last post here. Life happened — I got pulled into prepping an internship application for a few weeks, and my "post daily" streak…]]></description><link>https://alfiyajaved.hashnode.dev/day-14-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-14-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Thu, 16 Jul 2026 14:50:16 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/913ee5d0-1857-4666-8327-4ed75c59fb5c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>Breaking Ground on Terraform (and Tripping Over My Own Code Immediately)</h3>
<p>It's been a while since my last post here. Life happened — I got pulled into prepping an internship application for a few weeks, and my "post daily" streak took a hit. But I'm back, and today I finally started the part of the Cloud Resume Challenge I'd been putting off: <strong>Terraform</strong>.</p>
<p>Until now, every single resource in my project — S3, CloudFront, Lambda, DynamoDB, API Gateway — was clicked together manually in the AWS console. It works, but it's not really how real infrastructure gets managed. If I lost my laptop tomorrow, or wanted to recreate this whole setup for a second project, I'd have to click through every single step again from memory. That's the exact problem Terraform solves — you write your infrastructure as code, and it becomes repeatable, version-controlled, and reviewable, instead of living only inside your click history.</p>
<h3>First Rule I Learned: Don't Touch It With Root</h3>
<p>Before writing a single line of Terraform, I stopped myself on something. I'd built this entire project using my AWS root user — the account owner, with unrestricted access to everything. That's fine for clicking around and learning, but handing that same root-level power to a tool that's about to start creating and destroying real infrastructure on my behalf felt like a bad idea.</p>
<p>So the very first thing I did wasn't Terraform code at all — it was creating a dedicated IAM user just for this project, with only the permissions it actually needs (S3, DynamoDB, Lambda, CloudFront, Route 53, ACM, and IAM access so Terraform can manage its own Lambda execution role). No console password, no login capability — just programmatic access keys, since this user only ever needs to talk to AWS through the CLI and Terraform, never through a browser.</p>
<h3>The "Ohio" Bug</h3>
<p>Here's a mistake that's a little embarrassing to admit, but it's exactly the kind of thing I want this blog to be honest about.</p>
<p>When I set up my new AWS CLI profile for this IAM user, it asked for a region. I typed <strong>"Ohio"</strong> Not <code>us-east-</code>2 — the actual region code — just the word "Ohio," because that's the human-readable name AWS shows you in the console.</p>
<p>Running a simple test command afterwards gave me this:</p>
<pre><code class="language-plaintext">Could not connect to the endpoint URL: "https://sts.ohio.amazonaws.com/"
</code></pre>
<p>Turns out AWS doesn't know what "Ohio" means as a region identifier — it only understands region <em>codes</em>. The console shows you friendly names, but under the hood, everything is <code>us-east-1</code>, <code>us-east-2</code>, <code>ap-south-1</code>, and so on. A two-minute fix once I understood it, but a good reminder that the AWS console hides a layer of naming that CLI tools don't translate for you automatically.</p>
<h3>Writing My First Resource Block</h3>
<p>With the profile finally working, I wrote my first real Terraform resource — my DynamoDB table, since it doesn't depend on anything else I hadn't built yet:</p>
<pre><code class="language-hcl">resource "aws_dynamodb_table" "my_table" {
  name         = "visit_counter"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "id"

  attribute {
    name = "id"
    type = "S"
  }
}
</code></pre>
<p>This is where I actually understood something I'd been fuzzy on — the difference between a <strong>partition key</strong> (<code>hash_key</code>) and a <strong>range key</strong> (<strong>sort key</strong>). My table only ever stores one item — a single running counter — so it only needs a partition key. A range key only matters when multiple items share the same partition key and need sorting or grouping, which isn't my situation here at all.</p>
<h3>"1 to Add" — And a Small Moment of Panic</h3>
<p>I ran:</p>
<pre><code class="language-plaintext">terraform init
terraform plan
</code></pre>
<p>And got: <strong>"Plan: 1 to add."</strong></p>
<p>My immediate thought was — wait, is this about to create a <em>second</em>, duplicate DynamoDB table next to my real one? Since my actual table already exists, created manually months ago, this felt like the wrong outcome.</p>
<p>Turns out, this is completely expected. Terraform's state file — the thing that tracks what it "knows" it manages — starts empty. As far as Terraform was concerned, my table didn't exist yet, because <em>it</em> never created it. The fix isn't <code>apply</code> — it's:</p>
<pre><code class="language-plaintext">terraform import aws_dynamodb_table.my_table visit_counter
</code></pre>
<p>This tells Terraform "hey, this table already exists in AWS — go adopt it into your state instead of creating a new one." Running <code>plan</code> again afterwards showed a much smaller, safer diff instead of a full creation.</p>
<h3>The Naming Mistake That Actually Taught Me Something</h3>
<p>Right after the import, I hit an error trying to reference my table in <code>outputs.tf</code>:</p>
<pre><code class="language-plaintext">Error: Reference to undeclared resource
A managed resource "aws_dynamodb_table" "visit_counter" has not been declared
</code></pre>
<p>This confused me at first, because my table's actual <em>name</em> in AWS <strong>is</strong> <code>visit_counter</code> — so why was Terraform saying it didn't exist?</p>
<p>Here's the thing that finally clicked: three separate names are floating around in Terraform, and I'd been treating them as one. There's the <strong>resource type</strong> (<code>aws_dynamodb_table</code>), there's my own <strong>internal label</strong> for referring to it in code (I'd called mine <code>my_table</code>), and there's the <strong>actual AWS-facing name</strong> (<code>"visit_counter"</code>). My outputs file was trying to reference <code>aws_dynamodb_table.visit_counter</code> — using the <em>AWS name</em> where Terraform expected my <em>internal label</em>. Once I pointed it at <code>aws_dynamodb_table.my_table</code> instead, it resolved immediately.</p>
<p>Small mistake, but it's the kind of thing that only really makes sense once you've been burned by it once.</p>
<h3>One Tag Terraform Didn't Know About</h3>
<p>After fixing the naming issue, <code>terraform plan</code> showed one more thing — a tag update:</p>
<pre><code class="language-plaintext">~ tags = {
    - "Project" = "StaticWebsiteHosting" -&gt; null
  }
</code></pre>
<p>My real table already had a <code>Project</code> tag attached from way back when I first built it — but my new Terraform code never declared it. Terraform's rule is simple: whatever's in AWS but <em>not</em> in your code gets flagged for removal, since your code is now treated as the source of truth. I added the tag back into my resource block so nothing would get silently stripped away, and ran <code>plan</code> one more time.</p>
<h3>The Win</h3>
<pre><code class="language-plaintext">No changes. Your infrastructure matches the configuration.
</code></pre>
<p>That line felt genuinely satisfying. My real, already-running DynamoDB table is now fully described in code, safely imported, with nothing duplicated and nothing lost.</p>
<p><strong>Up next:</strong> Doing the same thing for my S3 bucket — except this time, I already know what to watch out for.</p>
<p>#AWSCloudResumeChallenge #CloudComputing #DevOps #Debugging #Terraform #AWSDynamoDB #Day14BuildInPublic #InfrastructureAsCode</p>
]]></content:encoded></item><item><title><![CDATA[Day 13: AWS Cloud Resume Challenge]]></title><description><![CDATA[If you read my last post, you might remember my exact words in the "Up Next" section: "I’m going to focus on the Design phase—using CSS to make this…]]></description><link>https://alfiyajaved.hashnode.dev/day-13-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-13-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Mon, 04 May 2026 17:15:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/5c1e4666-57de-4dbe-9915-30454f7c255e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you read my last post, you might remember my exact words in the "Up Next" section: "I’m going to focus on the Design phase—using CSS to make this counter look like a professional, integrated part of my resume."</p>
<p>But when I sat down at my laptop today, my gut told me to hit the brakes.</p>
<p>Before I made the frontend look pretty or rushed into automating my code with a CI/CD pipeline, I had a realisation. If I just blindly update my code and push it to AWS, a single typo could take down my live Lambda function or corrupt my database. I was building a machine without a safety net.</p>
<p>So, I completely changed my mind. I threw my previous "Up Next" out the window, pivoted back to the backend, and decided to conquer local testing. Here is how I brought my Lambda function out of the cloud and tested it on my Ubuntu machine.</p>
<h3><strong>The Virtual Environment Battle</strong></h3>
<p>Since I had originally written my code directly in the AWS Lambda console ("ClickOps"), my first step was downloading it into a local VS Code folder. I quickly realised my laptop was not AWS.</p>
<p>When I tried to run a simple import boto3 to connect to my database, my Ubuntu machine threw a ModuleNotFoundError. I learned that Linux is incredibly protective of its global Python packages. I couldn't just install the AWS SDK; I had to build a soundproof room for it.</p>
<p>I spent the first chunk of my session installing Python3-venv and activating a virtual environment. It felt like an annoying extra step, but seeing that (venv) pop up in my terminal made me realise my laptop was finally configured like a real developer's workstation.</p>
<h3><strong>The Danger of the Real Internet</strong></h3>
<p>Once my environment was set up, I ran my test... and got blasted with a massive wall of red error text.</p>
<p>At the very bottom of the traceback, I found the culprit: botocore.exceptions.NoRegionError. Even worse, when I tried to fix it, I accidentally typed us-east-3 (which doesn't exist) and crashed it again!</p>
<p>But this led to a massive "Aha!" moment. Why was my test trying to connect to the internet at all?</p>
<p>I realised that if I ran my local code as it was, it would connect to my real DynamoDB table in AWS and artificially inflate my website's visitor count every single time I ran a test. I needed to cut the cord.</p>
<h3><strong>Faking the Cloud (Mocking Boto3)</strong></h3>
<p>To protect my live database, I had to learn how to "Mock" AWS.</p>
<p>Using Python's unittest and MagicMock libraries, I wrote a script to intercept Boto3 before my code even loaded. I fed my laptop dummy AWS credentials and a fake region, and then injected a fake database response directly into the system's brain.</p>
<p>Here is the exact code that safely disconnected my tests from the real world:</p>
<p>Python import os import sys import unittest from unittest.mock import MagicMock</p>
<p>os.environ['AWS_DEFAULT_REGION'] = 'us-east-1' os.environ['AWS_ACCESS_KEY_ID'] = 'testing' os.environ['AWS_SECRET_ACCESS_KEY'] = 'testing'</p>
<p>mock_boto3 = MagicMock() sys.modules['boto3'] = mock_boto3</p>
<p>import lambda_function</p>
<h3><strong>The Green Light</strong></h3>
<p>After hours of battling tracebacks, learning how system paths work, and fixing my us-east-3 typo, I ran my final command: python3 -m unittest test_lambda.py</p>
<p>My terminal returned a single. and OK.</p>
<p>It might not look like much, but that green success message means my code is finally testable, safe, and ready for automation. I am so glad I trusted my gut and pivoted to testing today.</p>
<h3><strong>Up Next:</strong></h3>
<p>Now that my safety net is firmly in place, I can finally jump into Git, push this to GitHub, and build that CI/CD pipeline. (And yes, I promise I will actually style the frontend soon!)</p>
<p>#AWSCloudResumeChallenge #CloudComputing #DevOps #Debugging #Boto3 #UnitTest #Day13BuildInPublic</p>
]]></content:encoded></item><item><title><![CDATA[Day 12: AWS Cloud Resume Challenge]]></title><description><![CDATA[Today was the day it all came together. I had the database, the logic, and the API ready—I just needed to write the code that would actually "place the…]]></description><link>https://alfiyajaved.hashnode.dev/day-12-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-12-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Sat, 25 Apr 2026 18:42:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/b0d4e92b-c626-4137-9dfc-c9bdc9e12543.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Today was the day it all came together. I had the database, the logic, and the API ready—I just needed to write the code that would actually "place the order" from my website.</p>
<h3><strong>The Frontend Fetch</strong></h3>
<p>I spent today working in JavaScript, using the fetch() API. It’s a relatively simple block of code, but it does a lot of heavy lifting. It reaches out to my API Gateway URL, waits for the response, and then tells my HTML: "Hey, take this number and put it in that specific spot on the page."</p>
<h3><strong>The Deployment "Gotcha"</strong></h3>
<p>After I updated my code and uploaded it to S3, I refreshed my site and... nothing changed.</p>
<p>This was a great lesson in CDN Caching. Because my site sits behind CloudFront, the old version of my JavaScript was still saved in the "edge" locations. I had to run a CloudFront Invalidation (/*) to force AWS to dump the old cache and grab my brand-new code from S3.</p>
<h3><strong>Proof of Life</strong></h3>
<p>Once the invalidation finished, I refreshed the page and saw it: "You are visitor number: 1".</p>
<p>It’s just a small number on a screen, but knowing that number travelled from my browser, through a secure API, triggered a Python script, updated a NoSQL database, and came all the way back is an incredible feeling.</p>
<h3><strong>Up Next</strong></h3>
<p>The backend is bulletproof, but the UI is currently just plain black text. Now that the logic is working, I’m going to focus on the "Design" phase—using CSS to make this counter look like a professional, integrated part of my resume rather than an afterthought.</p>
<p>#CloudResumeChallenge #FullStack #AWS #JavaScript #S3 #CloudFront #Serverless #Day12BuildInPublic</p>
]]></content:encoded></item><item><title><![CDATA[Day 11: AWS Cloud Resume Challenge ]]></title><description><![CDATA[For the past two days, I built my database (DynamoDB) and wrote the backend logic (AWS Lambda). Today, I needed to build the bridge that allows my website…]]></description><link>https://alfiyajaved.hashnode.dev/day-11-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-11-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Fri, 24 Apr 2026 18:31:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/fa57ad60-2719-479f-95aa-2fd6bdf1a6a9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For the past two days, I built my database (DynamoDB) and wrote the backend logic (AWS Lambda). Today, I needed to build the bridge that allows my website to actually talk to that logic. Enter <strong>API Gateway</strong>.</p>
<p>If Lambda is the "Chef" cooking up the data, API Gateway is the "Waiter" taking the order from the website and handing it to the kitchen.</p>
<p>I decided to go with an <strong>HTTP API</strong> instead of a traditional REST API for this project. <strong>Why?</strong> AWS introduced HTTP APIs as a newer, leaner alternative. They are specifically designed for serverless architectures, they are significantly faster, and they are up to 71% cheaper than REST APIs. Since I just need a simple way to trigger a Lambda function, HTTP API was the perfect, cost-effective tool for the job.</p>
<p>Because API Gateway can be notoriously confusing to navigate, I kept my setup process as streamlined as possible:</p>
<ul>
<li><p><strong>Created the API:</strong> Spun up a standard HTTP API with auto-deploy enabled.</p>
</li>
<li><p><strong>Set the Route:</strong> Created a simple <code>GET</code> route at <code>/count</code>.</p>
</li>
<li><p><strong>The Handoff:</strong> Attached my Lambda function as the integration target so the API knows exactly what to trigger.</p>
</li>
<li><p><strong>The Security Guard (CORS):</strong> Configured Cross-Origin Resource Sharing (CORS) to allow <code>GET</code> requests. Without this crucial step, web browsers will automatically block the data for security reasons!</p>
</li>
</ul>
<p>After saving the configuration, I took the live Invoke URL, pasted it into my browser, added <code>/count</code> to the end, and hit enter. Seeing my JSON visitor count pop up on a live webpage was incredibly satisfying!</p>
<h3><strong>Up Next:</strong></h3>
<p>Everything is fully functional. Tomorrow, I just need to plug this API URL into my frontend JavaScript, and the full-stack loop will be complete!</p>
<p>#AWS #APIGateway #Serverless #CloudComputing #DevOps #AWSCloudResumeChallenge #Day11BuildInPublic</p>
]]></content:encoded></item><item><title><![CDATA[Day 10: AWS Cloud Resume Challenge ]]></title><description><![CDATA[Yesterday, I set up my DynamoDB table to store my website's visitor count. Today, the goal was to write the backend logic that actually updates that…]]></description><link>https://alfiyajaved.hashnode.dev/day-10-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-10-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Thu, 23 Apr 2026 18:45:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/58c60708-ba56-46bd-8abf-6c75d5c2d786.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Yesterday, I set up my DynamoDB table to store my website's visitor count. Today, the goal was to write the backend logic that actually updates that number. I used AWS Lambda and wrote the script in Python using the Boto3 library.</p>
<p>Connecting a serverless function to a database sounds straightforward, but I ran into a couple of great real-world debugging scenarios along the way.</p>
<h3><strong>Security:</strong></h3>
<p>By default, an AWS Lambda function does not have permission to interact with other AWS services. Before running any code, I had to configure an IAM Execution Role and attach the AmazonDynamoDBFullAccess policy. This acts as an ID badge, legally giving my code permission to read and write to my new table.</p>
<h3><strong>The Reserved Keyword Trap:</strong></h3>
<p>When writing the Boto3 logic to add +1 to my visitor count, my first test threw an error. My table uses the column name count. As it turns out, <strong>COUNT</strong> is a reserved keyword in DynamoDB's internal system.</p>
<p>Instead of renaming my database setup, I learned a great workaround: using <strong>Expression Attribute Names</strong>. By setting a placeholder like #c in my update expression and defining it as count later in the code, I was able to safely bypass the reserved word restriction.</p>
<h3><strong>Debugging the Stack Trace</strong></h3>
<p>My second test failed with a massive wall of error text. Instead of panicking, I read the first line of the stack trace: Unknown parameter in input: <strong>"ExpressionAttributeName"</strong>.</p>
<p>It was a classic syntax typo. Boto3 is incredibly strict, and I had missed the "<strong>s</strong>" at the end of <strong>ExpressionAttributeNames</strong>. One single letter was breaking the entire function!</p>
<h3><strong>The Result</strong></h3>
<p>After fixing the typo, I hit "Test" and finally saw that glorious green <strong>Execution</strong> <strong>result: succeeded</strong> box. I navigated over to my DynamoDB console, explored my table items, and saw my visitor count officially sitting at 1.</p>
<h3><strong>Up Next</strong></h3>
<p>My backend logic is fully working! Tomorrow, the goal is to set up API Gateway to act as the bridge between my frontend JavaScript and this new Lambda function.</p>
<p>#AWS #Lambda #Boto3 #Debugging #CloudResumeChallenge #DevOps #CloudComputing #Day10BuildInPublic</p>
]]></content:encoded></item><item><title><![CDATA[Day 9: AWS Cloud Resume Challenge]]></title><description><![CDATA[Okay, confession time: making numbers dynamically appear on the frontend using JavaScript was actually pretty fun. But today, I had to officially cross the…]]></description><link>https://alfiyajaved.hashnode.dev/day-9-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-9-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Wed, 22 Apr 2026 17:54:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/a45e0fd0-be8a-4886-a59c-3d4e99ce1540.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3><strong>Stepping into the Backend</strong> ☁️</h3>
<p>Okay, confession time: making numbers dynamically appear on the frontend using JavaScript was actually pretty fun. But today, I had to officially cross the bridge into the backend.</p>
<p>Instead of rushing straight into writing code, I spent today carefully clicking through the AWS Console to set up my infrastructure and make sure my foundation was solid.</p>
<h3><strong>Building the Database DynamoDB</strong></h3>
<p>My main task today was creating a place to actually store my visitor count. I navigated to DynamoDB and spun up a new table named visitor_count.</p>
<p><strong>Setting it up was simpler than I thought:</strong></p>
<p><strong>Partition Key:</strong> I set this to id (String). DynamoDB requires a unique identifier, so this was mandatory.</p>
<p><strong>Sort Key:</strong> I left this completely blank. Since I am literally just storing one simple number, adding a sort key would just overcomplicate the architecture!</p>
<h3><strong>Best Practices:</strong></h3>
<p>Resource Tagging Before logging off, I made sure to add a simple tag to my table: `Project: CloudResumeChallenge`.</p>
<p>When you are learning cloud computing, billing can be a bit intimidating. Adding Cost Allocation Tags is a great, simple habit to track exactly where your resources are, so you can keep an eye on things without any surprises.</p>
<h3><strong>Up Next:</strong></h3>
<p>My database is safely provisioned and properly tagged! Tomorrow, I will tackle the actual brain of the operation: writing an AWS Lambda function using Python to open this table, read the count, and add +1.</p>
<p>#AWS #DynamoDB #CloudComputing #DevOps #AWSCloudResumeChallenege #BuildInPublic</p>
]]></content:encoded></item><item><title><![CDATA[Day 8: AWS Cloud Resume Challenge]]></title><description><![CDATA[After resting up over the weekend, I kicked off my Monday morning by diving straight into the deep end of JavaScript. My goal for today's session was…]]></description><link>https://alfiyajaved.hashnode.dev/day-8-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-8-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Mon, 13 Apr 2026 01:57:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/8230bbef-bbfb-4ed3-b6dd-09fe83e7c584.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>After resting up over the weekend, I kicked off my Monday morning by diving straight into the deep end of JavaScript. My goal for today's session was two-fold: make my HTML dynamic using the DOM, and teach my code how to fetch live data from the internet using an API.</p>
<p>It was time to learn about APIs and JSON!</p>
<p><strong>The Goal: Fetching Live Data</strong></p>
<p>Since my AWS database isn't built yet, I used a free, public "dummy API" to test my code. I needed to write a script that could talk to this API and update my website's visitor counter with the fetched data.</p>
<p><strong>What I Learned:</strong></p>
<ul>
<li><p><strong>APIs:</strong> The "waiter" that takes my request to the server and brings the data back. I used the <code>fetch()</code> function and <code>async/await</code> to make this happen.</p>
</li>
<li><p><strong>JSON:</strong> The universal text format that the data comes back in. My JavaScript translates this JSON so it can read the numbers inside.</p>
</li>
</ul>
<p><strong>The Roadblock: The Stubborn Zero</strong></p>
<p>I wrote the code, linked the script, and refreshed my browser. But the counter stubbornly stayed at <code>0</code>. It didn't work. Instead of panicking, I opened up the console, started debugging, and realised I had made a simple typo in my code!</p>
<p><strong>The Big Win:</strong></p>
<p>I corrected the typo, hit save, and refreshed the page. Instantly, the <code>0</code> vanished, and live data fetched straight from the dummy API populated my screen. It is an incredibly satisfying feeling to debug your own code and watch it finally execute perfectly.</p>
<p><strong>Up Next:</strong></p>
<p>My front-end code is officially ready to receive data! Tomorrow, the real fun begins: stepping back into the AWS console to build the actual backend (DynamoDB, Lambda, and API Gateway) to replace this dummy API.</p>
<p>#javaScript #API #JSON #AWSCloudResumeChallenge #Debugging #DevOps #BuildInPblic</p>
]]></content:encoded></item><item><title><![CDATA[Day 7: AWS Cloud Resume Challenge]]></title><description><![CDATA[After solidifying my JavaScript fundamentals, I realised something important: I don't need to finish a 40-hour JS tutorial before I start building. It was…]]></description><link>https://alfiyajaved.hashnode.dev/day-7-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-7-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Fri, 10 Apr 2026 17:07:16 GMT</pubDate><content:encoded><![CDATA[<p>After solidifying my JavaScript fundamentals, I realised something important: I don't need to finish a 40-hour JS tutorial before I start building. It was time to pivot to "Just-In-Time" learning and start writing the actual code for my resume!</p>
<p>Today, I officially created the JavaScript file for my project and connected it to my HTML.</p>
<p><strong>The Setup:</strong></p>
<p>Escaping Tutorial Hell Instead of blindly following generic lessons, I researched exactly what I needed to make a dummy visitor counter work locally.</p>
<p>First, I created a brand new script.js file.</p>
<p>Then, I learned how to properly link it by adding an external <code>&lt;script&gt;</code> tag right before the closing <code>&lt;body&gt;</code> tag of my <code>index.html</code> file.</p>
<h3>The Action: Functions and the DOM</h3>
<p>With the files connected, it was time to make them talk to each other. I didn't want to overcomplicate things, so my goal was simple: make JavaScript find the number 0 on my webpage and change it to 10.</p>
<p>Here is exactly how I did it:</p>
<ul>
<li><p><strong>The Target:</strong> I added a <code>&lt;span&gt;</code> tag <code>id="visitor-count"</code> around the number 0 in my HTML.</p>
</li>
<li><p><strong>The Function:</strong> In my JS file, I wrapped my code inside a <code>function</code> so I could control exactly when it runs.</p>
</li>
<li><p><strong>The DOM:</strong> I used <code>document.getElementById()</code> to reach into the HTML, grab that specific span element, and dynamically change the inner text to my test number.</p>
</li>
</ul>
<h3>The Big Win</h3>
<p>Refreshing my browser and watching that 0 instantly change to a 10 completely via my own custom JavaScript logic was an incredible feeling! It proves that my files are communicating perfectly.</p>
<h3>Up Next:</h3>
<p>Right now, the counter is just a static, fake number. The next massive step is learning about the <strong>Fetch API</strong> and <strong>JSON</strong> so I can eventually make this local JavaScript talk to a real AWS database!</p>
<p>#<code>javaScript</code> #<code>CloudResumeChallenge</code> #<code>WebDevelopment</code> #<code>DevOps</code> #<code>DOM</code></p>
]]></content:encoded></item><item><title><![CDATA[Day 6: AWS Cloud Resume Challenge]]></title><description><![CDATA[The power is back on, the local grid is stable, and I am officially back to writing code! Before I jump into connecting my front-end HTML to an AWS…]]></description><link>https://alfiyajaved.hashnode.dev/day-6-aws-cloud-resume-challenge</link><guid isPermaLink="true">https://alfiyajaved.hashnode.dev/day-6-aws-cloud-resume-challenge</guid><dc:creator><![CDATA[Alfiya Javed]]></dc:creator><pubDate>Thu, 09 Apr 2026 18:04:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/69ce97e50ff860b6de08a0e7/faccb0f2-a454-455a-962f-b2eabfc6f644.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The power is back on, the local grid is stable, and I am officially back to writing code!</p>
<p>Before I jump into connecting my front-end HTML to an AWS database for the visitor counter, I knew I needed to strengthen my JavaScript fundamentals. Yesterday was about basic math and the console; today was about understanding how JavaScript actually stores and manipulates data under the hood.</p>
<h3><strong>Upgrading My JavaScript Arsenal 🛠️</strong></h3>
<p>Here is my technical review of what I mastered today:</p>
<p><strong>Variables, Memory, and Modern Syntax:</strong></p>
<p>I learned that variables hold reusable data in a program and associate it with a specific name, storing it directly in memory. But how you declare them matters:</p>
<p><strong>var:</strong> The old-school, pre-ES6 way to declare variables (which I'm avoiding in favour of modern syntax).</p>
<p><strong>let:</strong> The preferred way to declare a variable when I know the value will change or be reassigned later (perfect for a visitor count that goes up!).</p>
<p><strong>const:</strong> The go-to keyword for declaring a variable with a constant value that should never change.</p>
<p><strong>undefined:</strong> The primitive data type that JavaScript assigns to a variable if I declare it but forget to initialise it with a value.</p>
<p><strong>Math &amp; Strings Mathematical Assignment Operators:</strong></p>
<p>Instead of writing out long equations, these operators make it incredibly easy to calculate a new value and immediately assign it back to the same variable.</p>
<p><strong>String Concatenation:</strong> Using the + operator to stitch strings together, including string values held inside variables.</p>
<p><strong>Template Literals (ES6):</strong> This is an absolute game-changer! Instead of using clumsy + signs, ES6 allows you to use backticks ` and ${} to smoothly interpolate variables directly inside a string. It makes the code so much cleaner to read.</p>
<p><strong>The typeof Keyword:</strong></p>
<p>When data starts flying back and forth between my code and the AWS backend, I need to know exactly what kind of data I'm handling. The typeof keyword is the perfect tool for this—it returns the data type of a value (like telling me if something is a string or a number).</p>
<h3><strong>Up Next:</strong></h3>
<p>With variables, data types, and template literals locked in, I finally have the foundational vocabulary I need. Tomorrow, my goal is to learn how to write <strong>Functions</strong>, interact with the <strong>DOM</strong> to update my HTML, and start looking at the <strong>Fetch API</strong> so my code can actually talk to the cloud.</p>
<p>#javaScript #AWSCloudComputing #WebDevelopment #DevOps #Day6BuildInPublic</p>
]]></content:encoded></item></channel></rss>