<?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[Code to Cloud]]></title><description><![CDATA[Code to Cloud]]></description><link>https://bhargav19.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Code to Cloud</title><link>https://bhargav19.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 08 Sep 2026 04:01:05 GMT</lastBuildDate><atom:link href="https://bhargav19.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How I Built a Self-Hosted AI Agent to Scan GitHub PRs for OWASP Vulnerabilities]]></title><description><![CDATA[The Problem I Was Trying to Solve
When developers push code changes, someone should review that code for security issues. Things like SQL injection, hardcoded passwords, or broken authentication logic]]></description><link>https://bhargav19.hashnode.dev/how-i-built-a-self-hosted-ai-agent-to-scan-github-prs-for-owasp-vulnerabilities</link><guid isPermaLink="true">https://bhargav19.hashnode.dev/how-i-built-a-self-hosted-ai-agent-to-scan-github-prs-for-owasp-vulnerabilities</guid><category><![CDATA[DevSecOps]]></category><category><![CDATA[GitHub]]></category><category><![CDATA[github-actions]]></category><category><![CDATA[owasp]]></category><category><![CDATA[llm]]></category><category><![CDATA[NVIDIA]]></category><category><![CDATA[open source]]></category><category><![CDATA[automation]]></category><category><![CDATA[Security]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[venkatabhargav229]]></dc:creator><pubDate>Wed, 15 Jul 2026 15:25:19 GMT</pubDate><content:encoded><![CDATA[<hr />
<h2><strong>The Problem I Was Trying to Solve</strong></h2>
<p>When developers push code changes, someone should review that code for security issues. Things like SQL injection, hardcoded passwords, or broken authentication logic. These are documented under something called the <strong>OWASP Top 10</strong> — the most common and dangerous security risks in web applications.</p>
<p>In real companies, security engineers do this manually Or they pay thousands of dollars a month for tools that do it automatically.</p>
<p>I couldn't afford those tools. So I built something instead.</p>
<hr />
<h2><strong>What I Built</strong></h2>
<p>A self-hosted AI agent that watches every Pull Request, reads the code changes, and asks an AI model — "does this look dangerous?"</p>
<p>The result shows up directly on the PR as a comment. If the risk score is high enough, it also fires a Slack alert.</p>
<p>Here is the full flow:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a465af63a5c14d50daac196/04254778-efe5-45fe-a75c-36410471ef3f.png" alt="" style="display:block;margin:0 auto" />

<blockquote>
<p>Developer opens a Pull Request → GitHub Actions triggers the workflow → build_and_test job runs first → ai_security_gate job starts → Fetch changed file list from git → Filter out irrelevant files → Grab each file's diff → Wrap in XML tags → Send to NVIDIA NIM API → Parse the JSON response → Post comment on PR → risk_score ≥ 6 → Send Slack alert</p>
</blockquote>
<p>The Python scripts live on the VM at <code>/opt/security-agent/</code> — not inside the repo. And the <code>ai_security_gate</code> job only runs on pull requests, not on every push to main.</p>
<p>Both of those decisions were intentional. I'll explain why.</p>
<hr />
<h2><strong>The Two Jobs in the Workflow</strong></h2>
<p>The GitHub Actions workflow has two jobs that run in sequence.</p>
<pre><code class="language-yaml">jobs:
  build_and_test:
    runs-on: self-hosted
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Install dependencies
        run: npm install
      - name: Run tests
        run: npm test

  ai_security_gate:
    runs-on: self-hosted
    needs: build_and_test
    if: github.event_name == 'pull_request'
</code></pre>
<p><code>build_and_test</code> runs first. If tests fail, the security scan never even starts. No point scanning broken code.</p>
<p>ai_security_gate only fires on pull requests. Push directly to main? No scan. Open a PR? Scan runs automatically.</p>
<p>The needs: build_and_test line enforces that order. Clean and simple.</p>
<hr />
<h2><strong>Why the Scripts Live on the VM</strong></h2>
<p>Most GitHub Actions tutorials put everything inside the repo. I did it differently — the Python scripts live at <code>/opt/security-agent/</code> on the VM itself.</p>
<p>The workflow just calls them:</p>
<pre><code class="language-yaml">- name: Run Gatekeeper Script
  env:
    PR_NUMBER: ${{ github.event.pull_request.number }}
    GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  run: |
    python /opt/security-agent/scan_diff.py
</code></pre>
<p>Why? A few reasons.</p>
<p>The scripts contain configuration and logic I don't want exposed in the repo. The <code>.env</code> file with API keys lives at <code>/opt/security-agent/.env</code> — completely separate from the codebase. Anyone who clones the repo gets the workflow file, not the scanner internals.</p>
<p>I can update the scanner without touching the repo at all. SSH into the VM, edit the script, done. Next PR scan picks up the changes automatically.</p>
<hr />
<h2><strong>How It Finds the Changed Files</strong></h2>
<p>The scanner starts by asking git — what actually changed in this PR?</p>
<pre><code class="language-python">def get_pr_diff():
    diff_output = subprocess.check_output(
        ["git", "diff", "origin/main...HEAD", "--name-only"],
        text=True,
        timeout=30
    )
    files = [f for f in diff_output.strip().split('\n') if f]
    return files
</code></pre>
<p><code>origin/main...HEAD</code> means "everything that changed between main and the tip of this PR branch." The <code>--name-only</code> flag gives back just the file names, not the actual diff yet.</p>
<p>Then it filters out anything that doesn't need scanning:</p>
<pre><code class="language-python">EXCLUDED_EXTENSIONS = {
    '.md', '.txt', '.json', '.lock', '.yaml', '.yml',
    '.png', '.jpg', '.jpeg', '.svg', '.pdf', '.env', '.pem'
}

MAX_FILE_SIZE_BYTES = 50 * 1024  # 50KB limit
</code></pre>
<p>Notice <code>.env</code> and <code>.pem</code> are in that exclusion list. Those files should never contain diff-worthy changes in a PR anyway. If they do, that's a different problem.</p>
<p>The 50KB limit per file is a practical guard. Massive auto-generated files would eat your token budget fast. Skip them.</p>
<h2><strong>The XML Trick</strong></h2>
<p>After filtering, each file's actual diff gets fetched and wrapped in XML tags:</p>
<pre><code class="language-python">sanitized_payload.append(
    f"&lt;file name=\"{file_path}\"&gt;\n{file_diff}\n&lt;/file&gt;"
)
</code></pre>
<p>So what the AI model actually receives looks like this:</p>
<pre><code class="language-xml">&lt;file name="src/auth/login.py"&gt;
diff --git a/src/auth/login.py b/src/auth/login.py
-    query = "SELECT * FROM users WHERE email = %s"
+    query = f"SELECT * FROM users WHERE email = '{email}'"
&lt;/file&gt;
</code></pre>
<p>Why XML and not just plain text? The tags create clear boundaries. The model knows everything inside <code>&lt;file&gt;</code> tags is code to analyze, not instructions to follow. It makes the output significantly more consistent.</p>
<p>One thing I want to be upfront about — XML tags do not prevent prompt injection. If someone puts something malicious in a code comment inside their PR, the model will still see it. The XML helps with structure and clarity, not security. The real protection is validating the output on your side and never using the AI score as an automatic merge block.</p>
<hr />
<h2><strong>Talking to the AI Model</strong></h2>
<p>The scanner uses the OpenAI SDK pointed at NVIDIA's API endpoint instead of OpenAI's:</p>
<pre><code class="language-python">client = OpenAI(
    base_url="https://integrate.api.nvidia.com/v1",
    api_key=nvidia_key,
    timeout=90.0
)
</code></pre>
<p>Same SDK, different model, different URL. That's it.</p>
<p>The system prompt tells the model exactly what it is and exactly how to respond:</p>
<pre><code class="language-python">system_instruction = (
    "You are an automated corporate security gateway. Analyze the following code diffs wrapped in XML elements. "
    "Flag OWASP findings, logical vulnerabilities, or accidental credential exposures. "
    "You must output your findings strictly as valid raw JSON matching this scheme:\n"
    "{\n  \"risk_score\": &lt;0-10 integer&gt;,\n  \"findings\": [\"Description of issue 1\", \"Description of issue 2\"]\n}"
)
</code></pre>
<p>And the API call:</p>
<pre><code class="language-python">completion = client.chat.completions.create(
    model="nvidia/nemotron-3-ultra-550b-a55b",
    messages=[
        {"role": "system", "content": system_instruction},
        {"role": "user", "content": f"Review these PR diff changes:\n\n{diff_data}"}
    ],
    temperature=0.2,
    top_p=0.95,
    max_tokens=4000,
    extra_body={"chat_template_kwargs": {"thinking": False}},
    stream=False
)
</code></pre>
<p>Three settings that matter here:</p>
<p><code>temperature: 0.2</code> — Keeps the model focused and deterministic. High temperature means creative output. Creative output means broken JSON.</p>
<p><code>thinking: False</code> — NVIDIA's models have a chain-of-thought reasoning mode. Great for complex reasoning tasks. Terrible when you need clean structured JSON output. Disable it.</p>
<p><code>timeout: 90.0</code> — The API can be slow under load. Without a timeout your script hangs forever and the PR just sits there.</p>
<hr />
<h2><strong>Parsing the Response</strong></h2>
<p>The model is supposed to return clean JSON every time. It mostly does. But "mostly" isn't good enough when your pipeline depends on it.</p>
<pre><code class="language-python">response_content = completion.choices[0].message.content.strip()

if response_content.startswith("```json"):
    response_content = response_content.split("```json")[1].split("```")[0].strip()
elif response_content.startswith("```"):
    response_content = response_content.split("```")[1].split("```")[0].strip()

results = json.loads(response_content)
</code></pre>
<p>Even with <code>temperature: 0.2</code> and explicit instructions to return raw JSON, the model sometimes wraps the output in markdown code fences. This strips them out before parsing.</p>
<p>If <code>json.loads</code> still fails after that, the whole thing crashes with an exception and <code>sys.exit(1)</code>. The pipeline fails visibly. That's intentional — a silent failure that marks a vulnerable PR as clean would be worse than a noisy crash.</p>
<hr />
<h2><strong>What Gets Posted Back</strong></h2>
<p>Once the findings are parsed, they go back to the PR as a comment:</p>
<pre><code class="language-python">comment_body = (
    f"### 🛡️ AI Security Gatekeeper Audit\n\n"
    f"**Risk Score:** `{risk_score}/10`\n\n"
    f"#### Findings:\n{bullet_findings if bullet_findings else '- No major vulnerability signatures detected.'}"
)
</code></pre>
<p>And if the risk score hits 6 or above, Slack gets a message:</p>
<pre><code class="language-python">if risk_score &gt;= 6:
    send_slack_alert(risk_score, findings, safe_pr, safe_repo)
</code></pre>
<p>The Slack message uses color coding. Findings get truncated at 1000 characters so the Slack notification stays readable.</p>
<hr />
<h2><strong>The Cost Breakdown</strong></h2>
<table>
<thead>
<tr>
<th><strong>Resources</strong></th>
<th><strong>Monthly Cost</strong></th>
</tr>
</thead>
<tbody><tr>
<td>Self-hosted VM (2 vCPU, 4GB RAM)</td>
<td>~$12–20</td>
</tr>
<tr>
<td>NVIDIA NIM API calls</td>
<td>~$3–15</td>
</tr>
<tr>
<td>GitHub self-hosted runner</td>
<td>Free</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td><strong>~$15–35</strong></td>
</tr>
</tbody></table>
<p>GitHub Advanced Security costs \(49 per seat per month. For a 10-person team that's \)490. Enterprise SAST tools start at $1,000+.</p>
<p>This setup costs less than a Netflix subscription.</p>
<hr />
<h2><strong>What It Actually Catches</strong></h2>
<p>It's good at finding things that are visible in a diff:</p>
<ul>
<li><p>SQL injection when someone switches from parameterized queries to f-strings</p>
</li>
<li><p>Hardcoded API keys or passwords accidentally committed</p>
</li>
<li><p>Path traversal — user input going directly into file paths</p>
</li>
<li><p>XSS sinks — unescaped user input going into HTML</p>
</li>
</ul>
<p>It struggles with things that need full codebase context:</p>
<ul>
<li><p>Business logic bugs that only make sense when you understand the whole application</p>
</li>
<li><p>Vulnerabilities where the dangerous sink is in a completely different file</p>
</li>
<li><p>Race conditions</p>
</li>
</ul>
<p>This is a first-pass filter. It surfaces the obvious stuff fast. It does not replace a real security review</p>
<hr />
<h2><strong>Lessons From Building This</strong></h2>
<p><strong>Consistent JSON output is harder than it sounds.</strong> Low temperature, explicit schema in the prompt, disabling chain-of-thought, and stripping markdown fences after — you need all of it. Any one piece alone isn't enough.</p>
<p><strong>Keeping scripts off the repo was the right call.</strong> Separation between the workflow definition and the scanner logic feels clean. The repo stays simple. The scanner stays flexible.</p>
<p><strong>A failing pipeline beats a silent pass.</strong> When JSON parsing fails, the script exits with code 1. The whole job fails red. That's uncomfortable but it's honest. A crashed scan is visible. A silently-passed vulnerable PR is not.</p>
<hr />
<h2><strong>What's Next</strong></h2>
<p>A few things I want to add when I get time:</p>
<ul>
<li><p>Move the risk threshold from hardcoded <code>6</code> to an environment variable so it's configurable without touching code</p>
</li>
<li><p>Suggested fix in the PR comment — not auto-applied, just shown so the developer knows what to change</p>
</li>
<li><p>Better handling of massive PRs instead of just hitting the token limit</p>
</li>
</ul>
<hr />
<h2><strong>Try It Yourself</strong></h2>
<p>Everything is open source. The repo has the workflow file and the structure. You bring your own VM, your own NVIDIA NIM API key, and drop the scripts in <code>/opt/security-agent/</code>.</p>
<p><strong>Repo:</strong> <a href="https://github.com/munnavuyyuru/github-actions-ai-gatekeeper">github.com/munnavuyyuru/github-actions-ai-gatekeeper</a></p>
<p>If something is wrong or could be done better, I genuinely want to know. Drop it in the comments.</p>
<hr />
<p><em>Built by</em></p>
<p><code>Venkata Bhargav Vuyyuru</code></p>
<hr />
]]></content:encoded></item><item><title><![CDATA[From Docker to Kubernetes: My Journey Deploying 2-Tier and 3-Tier Applications on kind and Amazon EKS]]></title><description><![CDATA[I thought deploying Docker containers to Kubernetes would be straightforward.
After all, my applications were already running perfectly with Docker.
I assumed Kubernetes would simply orchestrate the c]]></description><link>https://bhargav19.hashnode.dev/from-docker-to-kubernetes-my-journey-deploying-2-tier-and-3-tier-applications-on-kind-and-amazon-eks</link><guid isPermaLink="true">https://bhargav19.hashnode.dev/from-docker-to-kubernetes-my-journey-deploying-2-tier-and-3-tier-applications-on-kind-and-amazon-eks</guid><category><![CDATA[Docker]]></category><category><![CDATA[AWS]]></category><category><![CDATA[EKS]]></category><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[venkatabhargav229]]></dc:creator><pubDate>Thu, 02 Jul 2026 14:13:40 GMT</pubDate><content:encoded><![CDATA[<hr />
<p>I thought deploying Docker containers to Kubernetes would be straightforward.</p>
<p>After all, my applications were already running perfectly with Docker.</p>
<p>I assumed Kubernetes would simply orchestrate the containers I had already built.</p>
<p>Instead, I ran into <strong>ImagePullBackOff</strong> errors, broken networking, <strong>ConfigMap issues</strong>, and a MySQL database that refused to start because of a missing <strong>CSI driver</strong>.</p>
<p>This article isn't a Kubernetes tutorial. It's the story of how I built, broke, debugged, and finally deployed two applications—from a local kind cluster to Amazon EKS—and the lessons I learned along the way.</p>
<p><strong>Note:</strong> All the source code, Dockerfiles, Kubernetes manifests, architecture diagrams, and deployment guides are available in my GitHub repository:<br /><a href="https://github.com/munnavuyyuru/k8s-deployments"><strong>https://github.com/munnavuyyuru/k8s-deployments</strong></a></p>
<hr />
<h2>What You'll Learn</h2>
<p>By the end of this article, you'll understand:</p>
<ul>
<li><p>How I containerized multiple applications using Docker</p>
</li>
<li><p>Why I used both <strong>Docker Hub</strong> and <strong>Amazon ECR</strong></p>
</li>
<li><p>How I deployed a <strong>2-tier application</strong> on a local <strong>kind</strong> cluster</p>
</li>
<li><p>How I deployed a <strong>3-tier application</strong> on <strong>Amazon EKS</strong></p>
</li>
<li><p>How I debugged real Kubernetes issues like <strong>localhost networking</strong>, <strong>ConfigMap scoping</strong>, and <strong>PersistentVolume provisioning failures</strong></p>
</li>
<li><p>The lessons I learned that every beginner should know before working with Kubernetes</p>
</li>
</ul>
<hr />
<h2>Project Overview</h2>
<p>To understand Kubernetes properly, I decided to build two different projects instead of deploying a simple "Hello World" application.</p>
<p>The first project was intentionally simple so I could learn the fundamentals of Kubernetes networking and deployments.</p>
<p>The second project introduced persistent storage, namespaces, secrets, ingress, and cloud infrastructure, making it much closer to a real production application.</p>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Project 1</th>
<th>Project 2</th>
</tr>
</thead>
<tbody><tr>
<td>Architecture</td>
<td>2-Tier</td>
<td>3-Tier</td>
</tr>
<tr>
<td>Frontend</td>
<td>Nginx</td>
<td>Flask</td>
</tr>
<tr>
<td>Backend</td>
<td>Node.js</td>
<td>Flask API</td>
</tr>
<tr>
<td>Database</td>
<td>None</td>
<td>MySQL</td>
</tr>
<tr>
<td>Registry</td>
<td>Docker Hub</td>
<td>Amazon ECR</td>
</tr>
<tr>
<td>Kubernetes</td>
<td>kind</td>
<td>Amazon EKS</td>
</tr>
</tbody></table>
<h3>2-Tier Architecture Diagram</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a465af63a5c14d50daac196/dbe3ed78-93bd-40b5-bd45-0f64fc57f5d4.png" alt="2-Tier Architecture" style="display:block;margin:0 auto" />

<p><code>Frontend → Nginx Reverse Proxy → Backend</code></p>
<h3>3-Tier Architecture Diagram</h3>
<img src="https://cdn.hashnode.com/uploads/covers/6a465af63a5c14d50daac196/592b3e18-cfef-442f-99ea-ff26eb29f4dd.png" alt="3-Tier Architecture" style="display:block;margin:0 auto" />

<p><code>Ingress → Frontend → API → MySQL</code></p>
<hr />
<h2>Stage 1: Containerizing the Applications with Docker</h2>
<p>Before writing a single Kubernetes manifest, I wanted to make sure every application worked perfectly inside Docker.</p>
<p>This became one of the most valuable habits I developed during the project.</p>
<p>If an application doesn't work correctly inside a Docker container, Kubernetes won't magically fix it.</p>
<p>For the <strong>2-tier application</strong>, I created:</p>
<ul>
<li><p>A Docker image for the Node.js backend</p>
</li>
<li><p>A separate Docker image for the Nginx frontend</p>
</li>
<li><p>A multi-stage Docker build for the frontend to reduce the final image size</p>
</li>
</ul>
<p>For the <strong>3-tier application</strong>, I created three separate Docker images:</p>
<ul>
<li><p>Flask Frontend</p>
</li>
<li><p>Flask API</p>
</li>
<li><p>MySQL Database</p>
</li>
</ul>
<p>Since MySQL stores application data, I deployed it later using persistent storage rather than treating it like a stateless container.</p>
<p>Before touching Kubernetes, I tested every image locally using <code>docker build</code> and <code>docker run</code>. This helped me isolate Docker-related problems before introducing Kubernetes into the workflow.</p>
<hr />
<h2>Stage 2: Publishing Images to a Container Registry</h2>
<p>Once the Docker images were working locally, the next step was making them available for Kubernetes.</p>
<p>For my first project, <strong>Docker Hub</strong> was sufficient.</p>
<p>Since the cluster was relatively simple, pushing images to Docker Hub and configuring <code>imagePullSecrets</code> allowed Kubernetes to authenticate and pull private images successfully.</p>
<p>For the second project running on <strong>Amazon EKS</strong>, I wanted a more production-oriented workflow.</p>
<p>Instead of Docker Hub, I used <strong>Amazon Elastic Container Registry (ECR)</strong>.</p>
<p>Using ECR simplified image management inside AWS and integrated naturally with the EKS cluster.</p>
<hr />
<h2>Stage 3: Deploying the 2-Tier Application on kind</h2>
<p>With the images available, it was finally time to deploy my first Kubernetes application.</p>
<p>I chose <strong>kind (Kubernetes in Docker)</strong> because it provides a lightweight local Kubernetes cluster without requiring cloud resources.</p>
<p>The deployment included:</p>
<ul>
<li><p>A dedicated namespace</p>
</li>
<li><p>Deployments for both frontend and backend</p>
</li>
<li><p>Services for internal and external communication</p>
</li>
<li><p>A ConfigMap containing the Nginx reverse proxy configuration</p>
</li>
</ul>
<p>One of the most important design decisions was exposing only the frontend.</p>
<p>The backend remained private using a <strong>ClusterIP Service</strong>, while the frontend was exposed externally through <strong>MetalLB</strong>, which provided LoadBalancer functionality inside the kind cluster.</p>
<p>At this point, I finally had a fully working application running on Kubernetes.</p>
<p><code>Applying Kubernetes manifests.</code></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a465af63a5c14d50daac196/ad54c5e2-8f25-482f-8771-60d2936f8470.png" alt="" style="display:block;margin:0 auto" />

<p><code>Verifying Deployments, Pods, Services, and ConfigMaps.</code></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a465af63a5c14d50daac196/38378571-dc56-40d7-9204-3d9946207bd9.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>Stage 4: Deploying the 3-Tier Application on Amazon EKS</h2>
<p>After successfully deploying the local application, I wanted to experience Kubernetes in a real cloud environment.</p>
<p>I created an Amazon EKS cluster and deployed a complete three-tier architecture.</p>
<p>Compared to the first project, this deployment introduced several additional Kubernetes concepts.</p>
<p>The application consisted of:</p>
<ul>
<li><p>Separate namespaces for frontend, backend, and database</p>
</li>
<li><p>ConfigMaps for application configuration</p>
</li>
<li><p>Secrets for database credentials</p>
</li>
<li><p>Deployments for the stateless frontend and API</p>
</li>
<li><p>A StatefulSet for MySQL</p>
</li>
<li><p>Persistent storage backed by Amazon EBS</p>
</li>
<li><p>An Ingress resource for external access</p>
</li>
</ul>
<p>Unlike the previous deployment, the database required persistent storage because losing data whenever a pod restarted would not be acceptable.</p>
<p>Using a StatefulSet ensured that MySQL maintained a stable identity while PersistentVolumes preserved the database across pod restarts.</p>
<p><code>Separate namespaces for better resource organization</code></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a465af63a5c14d50daac196/61470e99-b603-4a4f-9f72-241cdea70292.png" alt="" style="display:block;margin:0 auto" />

<p><code>MySQL running successfully inside a StatefulSet</code></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a465af63a5c14d50daac196/821eae65-d1f1-4aa2-8e38-9b9e4a37577a.png" alt="" style="display:block;margin:0 auto" />

<p><code>Backend API pods running successfully</code></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a465af63a5c14d50daac196/dc2d93de-5c67-43ba-a3fb-a06fce671518.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h2>The Biggest Challenges I Faced</h2>
<p>This was the most educational part of the project.</p>
<p>Every issue forced me to understand Kubernetes more deeply instead of simply following documentation.</p>
<h3>Challenge 1: The "localhost" Networking Trap</h3>
<h3>Problem</h3>
<p>The frontend loaded successfully, but every API request failed.</p>
<h3>Root Cause</h3>
<p>My frontend JavaScript was calling:</p>
<p><a href="http://localhost:8001"><code>http://localhost:8001</code></a></p>
<p>Inside the browser, <strong>localhost refers to the user's own machine</strong>, not the Kubernetes backend running inside the cluster.</p>
<h3>Investigation</h3>
<p>I checked:</p>
<ul>
<li><p>Browser Network Tab</p>
</li>
<li><p>Kubernetes Services</p>
</li>
<li><p>Pod Logs</p>
</li>
</ul>
<p>Everything looked healthy except the frontend was pointing to the wrong location.</p>
<h3>Solution</h3>
<p>Instead of calling localhost directly, I changed the API endpoint to a relative path:</p>
<p><code>/api</code></p>
<p>Then I configured <strong>Nginx as a reverse proxy</strong> to forward every <code>/api/*</code> request to the backend Kubernetes Service.</p>
<p>This completely solved the networking issue while keeping the backend hidden from the public.</p>
<hr />
<h3>Challenge 2: Amazon EBS CSI Driver Failure</h3>
<h3>Problem</h3>
<p>The MySQL pod remained stuck in the <strong>Pending</strong> state.</p>
<h3>Investigation</h3>
<p>Running:</p>
<ul>
<li><p><code>kubectl get pvc</code></p>
</li>
<li><p><code>kubectl describe pvc</code></p>
</li>
<li><p><code>kubectl get events</code></p>
</li>
</ul>
<p>revealed that Kubernetes couldn't provision a PersistentVolume.</p>
<h3>Root Cause</h3>
<p>The <strong>Amazon EBS CSI Driver</strong> had failed to install because the required IAM permissions were missing.</p>
<h3>Solutions :</h3>
<ul>
<li><p>Removed the broken add-on</p>
</li>
<li><p>Installed the EKS Pod Identity Agent</p>
</li>
<li><p>Reinstalled the EBS CSI Driver</p>
</li>
<li><p>Associated the correct IAM role</p>
</li>
</ul>
<p>Once the driver became healthy, Kubernetes automatically provisioned the PersistentVolume, and the MySQL pod started successfully.</p>
<p><code>CSI Driver Fix Screenshot</code></p>
<img src="https://cdn.hashnode.com/uploads/covers/6a465af63a5c14d50daac196/649cb1ea-ad43-499e-bb9e-1e656062b870.png" alt="" style="display:block;margin:0 auto" />

<hr />
<h3>Challenge 3: ConfigMap Not Found</h3>
<h3>Problem</h3>
<p>The backend API pod failed with: <code>CreateContainerConfigError</code></p>
<h3>Root Cause</h3>
<p>The pod expected a ConfigMap named <code>mysql-config</code>.</p>
<p>However, the ConfigMap existed inside the <strong>database namespace</strong>, while the backend application was running inside the <strong>backend namespace</strong>.</p>
<p>ConfigMaps are <strong>namespace-scoped</strong>, so Kubernetes couldn't access it.</p>
<h3>Solution</h3>
<p>I created another ConfigMap inside the backend namespace.</p>
<p>After applying the new ConfigMap, the application started immediately.</p>
<p>This taught me one of Kubernetes' most important rules:</p>
<p><strong>Pods can only directly access ConfigMaps and Secrets inside their own namespace.</strong></p>
<hr />
<h3>Lessons I Learned</h3>
<p>Completing these two projects changed how I think about Kubernetes.</p>
<p>Some of my biggest takeaways were:</p>
<ul>
<li><p><strong>Docker success doesn't guarantee Kubernetes success</strong></p>
</li>
<li><p><strong>Networking is usually harder than writing YAML</strong></p>
</li>
<li><p><code>localhost</code> <strong>means something different depending on where your code is running</strong></p>
</li>
<li><p><strong>Namespaces provide isolation but require careful resource organization</strong></p>
</li>
<li><p><strong>Stateful applications need persistent storage and should usually run as StatefulSets</strong></p>
</li>
<li><p><strong>Debugging becomes much easier when you use</strong> <code>kubectl describe</code><strong>,</strong> <code>kubectl logs</code><strong>, and</strong> <code>kubectl get events</code> <strong>before changing configuration files</strong></p>
</li>
<li><p><strong>Understanding why something failed is far more valuable than simply making it work</strong></p>
</li>
</ul>
<hr />
<h3>If You Want to Try This Yourself</h3>
<p>Everything used in this article is available in my GitHub repository.</p>
<p>It includes:</p>
<ul>
<li><p>Dockerfiles</p>
</li>
<li><p>Kubernetes manifests</p>
</li>
<li><p>Architecture diagrams</p>
</li>
<li><p>Deployment documentation</p>
</li>
<li><p>Troubleshooting notes</p>
</li>
</ul>
<p>To reproduce the projects, you'll need:</p>
<ul>
<li><p>Docker</p>
</li>
<li><p>kubectl</p>
</li>
<li><p>kind (for the local deployment)</p>
</li>
<li><p>An AWS account (for the EKS deployment)</p>
</li>
</ul>
<p>I recommend starting with the local kind deployment before moving to Amazon EKS.</p>
<p>Understanding the fundamentals locally makes debugging cloud deployments much easier.</p>
<hr />
<h1>Conclusion</h1>
<p>When I started this project, I thought Kubernetes was mostly about writing YAML files.</p>
<p>After completing both deployments, I realized Kubernetes is really about understanding how <strong>containers, networking, storage, security, and cloud infrastructure</strong> work together.</p>
<p>The most valuable lessons didn't come from successful deployments.</p>
<p>They came from the failures.</p>
<p>Every ImagePullBackOff, every networking issue, every storage problem, and every debugging session taught me something new about how Kubernetes actually works.</p>
<p>If you're beginning your own Kubernetes journey, don't be afraid to break things.</p>
<p>Sometimes the fastest way to learn is by fixing your own mistakes.</p>
<p>Thank you for reading!</p>
<p>If you've faced similar Kubernetes challenges—or have suggestions for improving this setup—I’d love to hear from you in the comments.</p>
<p>Happy learning, and happy building! 🚀</p>
]]></content:encoded></item></channel></rss>