<?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[Tried & Tested Software Solutions]]></title><description><![CDATA[I'm Farzam, a Software Engineer specializing in backend development. My mission: Collaborate, share proven tricks, and help you avoid the pricey surprises I've ]]></description><link>https://triedandtestedbuilds.com</link><generator>RSS for Node</generator><lastBuildDate>Thu, 16 Apr 2026 05:37:13 GMT</lastBuildDate><atom:link href="https://triedandtestedbuilds.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Setting Up Your Self-Hosted AI Stack - Part 3: From Receipts to Structured Data with Vision Language Models and n8n]]></title><description><![CDATA[It's Been a While
Sorry for being away for so long. I've been head-down strengthening my software engineering foundation—consuming rather than producing. After a while, I started feeling this pull to balance things out by building again.
I just finis...]]></description><link>https://triedandtestedbuilds.com/self-hosted-ai-stack-part-3</link><guid isPermaLink="true">https://triedandtestedbuilds.com/self-hosted-ai-stack-part-3</guid><category><![CDATA[Receipt OCR]]></category><category><![CDATA[n8n]]></category><category><![CDATA[OCR ]]></category><category><![CDATA[VLM]]></category><category><![CDATA[image processing]]></category><category><![CDATA[self-hosted-ai]]></category><category><![CDATA[AI]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[automation]]></category><category><![CDATA[AI-automation]]></category><category><![CDATA[Document Processing]]></category><category><![CDATA[Workflow Automation]]></category><category><![CDATA[Vision Language Models]]></category><category><![CDATA[image to text]]></category><category><![CDATA[Image to Text Converter]]></category><dc:creator><![CDATA[Farzam Mohammadi]]></dc:creator><pubDate>Tue, 03 Feb 2026 04:23:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770444297544/b25bce2a-7059-4332-b0f7-1e048614cb7b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-its-been-a-while">It's Been a While</h1>
<p>Sorry for being away for so long. I've been head-down strengthening my software engineering foundation—consuming rather than producing. After a while, I started feeling this pull to balance things out by building again.</p>
<p>I just finished <a target="_blank" href="https://csapp.cs.cmu.edu/">Computer Systems: A Programmer's Perspective (CS:APP)</a>, and I can't recommend it enough. If you've ever felt gaps in your foundational knowledge, this book fills them. It's one of the best technical books I've read. Now I'm working through <a target="_blank" href="https://www.oreilly.com/library/view/ai-engineering/9781098166298/">AI Engineering by Chip Huyen</a>, and it felt like the right time to pick up where we left off.</p>
<h1 id="heading-why-read-this">Why Read This</h1>
<p>My background is software engineering, not machine learning. Over the past year I've shifted into what Chip calls "AI engineering": building applications on top of foundation models. I want to share what I've learned with those who haven't yet gotten the chance to experience building in this space.</p>
<p>Life is demanding and the field keeps moving. Not everyone has time to keep up—reading papers, watching tutorials, building side projects. That's exactly why I'm building this series: real projects, real source code, walked through step by step. A way to stay current with what's actually possible.</p>
<hr />
<h1 id="heading-what-were-building">What We're Building</h1>
<p>A 10GB model. A 67-line prompt. 7 commands to set up. 2 config values.</p>
<p>That's what turns a crumpled receipt photo into structured JSON—line items, taxes, discounts, multiple currencies. All running locally on your laptop.</p>
<p><strong>From this:</strong></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770090971768/9da7bd52-2fa8-4564-9d34-03b10c806b66.png" alt class="image--center mx-auto" /></p>
<p><strong>To this:</strong></p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"currency"</span>: <span class="hljs-string">"USD"</span>,
  <span class="hljs-attr">"receipt_type"</span>: <span class="hljs-string">"service"</span>,
  <span class="hljs-attr">"tax_format"</span>: <span class="hljs-string">"added"</span>,
  <span class="hljs-attr">"receipt_subtotal"</span>: <span class="hljs-number">145.00</span>,
  <span class="hljs-attr">"receipt_total_tax_amount"</span>: <span class="hljs-number">9.06</span>,
  <span class="hljs-attr">"receipt_total_tax_percentage"</span>: <span class="hljs-number">6.25</span>,
  <span class="hljs-attr">"receipt_total"</span>: <span class="hljs-number">154.06</span>,
  <span class="hljs-attr">"items"</span>: [
    {
      <span class="hljs-attr">"item_name"</span>: <span class="hljs-string">"Front and rear brake cables"</span>,
      <span class="hljs-attr">"item_quantity"</span>: <span class="hljs-number">1</span>,
      <span class="hljs-attr">"item_unit_price"</span>: <span class="hljs-number">100.00</span>,
      <span class="hljs-attr">"item_total_price"</span>: <span class="hljs-number">100.00</span>,
      <span class="hljs-attr">"confidence_score"</span>: <span class="hljs-number">0.95</span>
    },
    {
      <span class="hljs-attr">"item_name"</span>: <span class="hljs-string">"New set of pedal arms"</span>,
      <span class="hljs-attr">"item_quantity"</span>: <span class="hljs-number">2</span>,
      <span class="hljs-attr">"item_unit_price"</span>: <span class="hljs-number">15.00</span>,
      <span class="hljs-attr">"item_total_price"</span>: <span class="hljs-number">30.00</span>,
      <span class="hljs-attr">"confidence_score"</span>: <span class="hljs-number">0.95</span>
    },
    {
      <span class="hljs-attr">"item_name"</span>: <span class="hljs-string">"Labor 3hrs"</span>,
      <span class="hljs-attr">"item_quantity"</span>: <span class="hljs-number">3</span>,
      <span class="hljs-attr">"item_unit_price"</span>: <span class="hljs-number">5.00</span>,
      <span class="hljs-attr">"item_total_price"</span>: <span class="hljs-number">15.00</span>,
      <span class="hljs-attr">"confidence_score"</span>: <span class="hljs-number">0.95</span>
    }
  ]
}
</code></pre>
<blockquote>
<p><strong>Note:</strong> The model correctly handles quantity math (2 × $15 = $30), extracts tax separately (6.25% = $9.06), and assigns confidence scores to each extraction.</p>
</blockquote>
<p>Let me show you how.</p>
<blockquote>
<p><strong>Did You Know: How VLMs "See" Images</strong></p>
<p>Vision Language Models don't process images like humans. They divide images into patches (typically 14×14 or 16×16 pixels), encode them through a vision transformer, then project into the language model's embedding space. Each patch becomes a "token" the model reasons about—similar to how words become tokens in text. A single receipt image might become hundreds or even thousands of visual tokens depending on resolution.</p>
</blockquote>
<hr />
<h1 id="heading-before-we-start">Before We Start</h1>
<p>You'll need:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Tool</td><td>Purpose</td><td>Install</td></tr>
</thead>
<tbody>
<tr>
<td><strong>Docker &amp; Docker Compose</strong></td><td>Container orchestration</td><td><a target="_blank" href="https://docs.docker.com/get-docker/">docs.docker.com/get-docker</a></td></tr>
<tr>
<td><strong>Ollama</strong></td><td>Local LLM runtime</td><td><a target="_blank" href="https://ollama.com/download">ollama.com/download</a></td></tr>
<tr>
<td><strong>Python 3.8+</strong></td><td>Setup script</td><td>Usually pre-installed on macOS/Linux</td></tr>
</tbody>
</table>
</div><p><strong>Hardware:</strong> 16GB RAM minimum, 24GB+ recommended for smooth VLM inference.</p>
<blockquote>
<p><strong>No GPU or limited RAM?</strong> You can use Ollama's cloud models instead of running locally. See the <a target="_blank" href="https://triedandtestedbuilds.com/setting-up-your-self-hosted-ai-stack-part-3-ai-powered-invoice-and-receipt-processing-with-n8n-workflows-and-vision-language-models?showSharer=true#heading-cloud-alternative-when-local-isnt-an-option">Cloud Alternative</a> section.</p>
</blockquote>
<p><strong>Model:</strong> We'll use <code>qwen3-vl:8b-thinking-q8_0</code> (~10GB). Pull it now:</p>
<pre><code class="lang-bash">ollama pull qwen3-vl:8b-thinking-q8_0
</code></pre>
<p>Everything else runs in Docker.</p>
<hr />
<h1 id="heading-why-vision-language-models">Why Vision Language Models</h1>
<p>Traditional OCR reads text without understanding context. Vision Language Models understand what they're looking at.</p>
<p><strong>When processing a restaurant receipt:</strong></p>
<ul>
<li><p>OCR sees: "Total: $47.83"</p>
</li>
<li><p>VLM understands: This is the final amount, separate from subtotal and tax</p>
</li>
</ul>
<p><strong>When processing an invoice:</strong></p>
<ul>
<li><p>OCR sees: "Net 30"</p>
</li>
<li><p>VLM understands: This is payment terms, not an amount</p>
</li>
</ul>
<p>VLMs read documents like humans do—understanding layout, context, and meaning.</p>
<hr />
<h1 id="heading-the-three-pillars">The Three Pillars</h1>
<h2 id="heading-1-vision-language-model-qwen3-vl">1. Vision Language Model (qwen3-vl)</h2>
<p><strong>The "eyes"</strong> that read and understand receipts and invoices. It processes images, understands document layouts (headers, line items, totals), and outputs structured JSON data.</p>
<h2 id="heading-2-n8n-workflow-engine">2. n8n Workflow Engine</h2>
<p><strong>The "brain"</strong> that orchestrates the entire process. It handles file uploads and validation, manages job queuing and processing, and coordinates data extraction and export through visual workflows.</p>
<p><strong>Also our backend.</strong> n8n's webhooks serve as our API layer—no separate backend needed. It handles job creation, result fetching, and CSV exports directly.</p>
<h2 id="heading-3-web-upload-interface">3. Web Upload Interface</h2>
<p><strong>The "front door"</strong> for easy document submission. Drag-and-drop interface with progress tracking and status updates, plus the ability to download processed results.</p>
<blockquote>
<p><strong>Did You Know: Why Visual Workflow Tools Matter</strong></p>
<p>n8n workflows are JSON under the hood. Each node has inputs, outputs, and configuration. The visual builder prevents syntax errors and makes debugging visible—you can see exactly where data flows and where it breaks.</p>
<p><strong>Pro tip I learned the hard way:</strong> Build workflows in the UI first, export them, then modify the JSON if needed. Trying to write n8n workflow JSON from scratch (even with AI assistance) often leads to syntax issues that take forever to debug.</p>
</blockquote>
<hr />
<h1 id="heading-system-architecture">System Architecture</h1>
<h2 id="heading-the-five-services">The Five Services</h2>
<p>Our Docker Compose stack orchestrates five services:</p>
<pre><code class="lang-mermaid">graph TB
    subgraph Docker Network
        WEB[Nginx&lt;br/&gt;Web Interface&lt;br/&gt;Port 8080]
        n8n[n8n&lt;br/&gt;Workflow Engine&lt;br/&gt;Port 5678]
        PG[(PostgreSQL&lt;br/&gt;Database)]
        MIG[DB Migrator&lt;br/&gt;Schema Setup]
        PGA[pgAdmin&lt;br/&gt;Optional Admin UI]
    end

    USER((User)) --&gt; WEB
    WEB --&gt; n8n
    n8n --&gt; PG
    n8n --&gt; OLLAMA
    MIG --&gt; PG
    PGA --&gt; PG

    OLLAMA[Ollama&lt;br/&gt;VLM on Host&lt;br/&gt;Port 11434]
</code></pre>
<ul>
<li><p><strong>postgres</strong>: PostgreSQL 15 Alpine for job tracking and extracted data</p>
</li>
<li><p><strong>db-migrator</strong>: Runs once on startup to apply SQL migrations</p>
</li>
<li><p><strong>n8n</strong>: Workflow engine (v2.4.8) with 6 workflows orchestrating the pipeline</p>
</li>
<li><p><strong>web</strong>: Nginx Alpine serving the upload interface</p>
</li>
<li><p><strong>pgadmin</strong>: Optional database admin panel</p>
</li>
</ul>
<h2 id="heading-data-flow">Data Flow</h2>
<pre><code class="lang-mermaid">sequenceDiagram
    participant U as User
    participant W as Web Interface
    participant N as n8n Webhooks
    participant Q as Queue Monitor
    participant V as VLM Processor
    participant O as Ollama (Host)
    participant D as PostgreSQL

    U-&gt;&gt;W: Upload receipt image
    W-&gt;&gt;N: POST /webhook/upload-receipt
    N-&gt;&gt;D: Create receipt record (status: pending)
    N-&gt;&gt;D: Create processing job
    N--&gt;&gt;W: Return job ID

    loop Every 30 seconds
        Q-&gt;&gt;D: Poll for pending jobs
        Q-&gt;&gt;V: Trigger VLM Processor
    end

    V-&gt;&gt;D: Fetch receipt file path
    V-&gt;&gt;O: Send image + prompt
    O--&gt;&gt;V: Return extracted JSON
    V-&gt;&gt;D: Update receipt with items
    V-&gt;&gt;D: Mark job completed

    U-&gt;&gt;W: View processed receipts
    W-&gt;&gt;N: GET /webhook/get-receipts
    N-&gt;&gt;D: Query completed receipts
    N--&gt;&gt;W: Return receipt list with items
</code></pre>
<hr />
<h1 id="heading-the-philosophy-setup-done-right">The Philosophy: Setup Done Right</h1>
<h2 id="heading-7-commands-2-config-values">7 Commands. 2 Config Values.</h2>
<p>I believe automation should respect your time. If you can't get from zero to working in under 10 minutes, something's wrong with the design.</p>
<p>Here's the complete setup:</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> https://github.com/FarzamMohammadi/self-hosted-ai-stack.git
<span class="hljs-built_in">cd</span> self-hosted-ai-stack/part-3-receipt-processing-with-vlm
cp .env.example .env
ollama pull qwen3-vl:8b-thinking-q8_0
<span class="hljs-comment"># Edit .env: set OLLAMA_VLM_MODEL</span>
docker-compose up -d
<span class="hljs-comment"># Open http://localhost:5678 → Settings → n8n API → Create API Key</span>
<span class="hljs-comment"># Add N8N_API_KEY=&lt;your-key&gt; to .env</span>
python scripts/setup-n8n.py
docker compose restart n8n
</code></pre>
<p>Done. The system is running:</p>
<ul>
<li><p><strong>Web Interface:</strong> http://localhost:8080</p>
</li>
<li><p><strong>n8n Workflows:</strong> http://localhost:5678</p>
</li>
<li><p><strong>pgAdmin:</strong> http://localhost:5050</p>
</li>
</ul>
<h2 id="heading-what-happens-behind-the-scenes">What Happens Behind the Scenes</h2>
<p>When you run <code>docker-compose up</code>, a carefully orchestrated sequence unfolds:</p>
<ol>
<li><p><strong>PostgreSQL starts</strong> and reports healthy (not just "running")</p>
</li>
<li><p><strong>Migrations run</strong>—in dependency order, wrapped in transactions</p>
</li>
<li><p><strong>n8n waits</strong> for migrations to <em>succeed</em>, not just complete</p>
</li>
<li><p><strong>Web server starts</strong> only after n8n is ready</p>
</li>
</ol>
<p>This isn't accidental. The compose file uses <code>service_healthy</code> and <code>service_completed_successfully</code> conditions—the strictest dependency types Docker offers. If migrations fail, n8n doesn't start. No silent failures. No corrupt state.</p>
<p>When you run <code>setup-n8n.py</code>, a second orchestration happens:</p>
<ol>
<li><p><strong>Validates</strong> your API key and database connection</p>
</li>
<li><p><strong>Creates</strong> PostgreSQL credentials inside n8n</p>
</li>
<li><p><strong>Uploads</strong> all 6 workflows from JSON files</p>
</li>
<li><p><strong>Activates</strong> each workflow</p>
</li>
</ol>
<p>The restart at the end? A workaround for a <a target="_blank" href="https://github.com/n8n-io/n8n/issues/21614">known n8n bug</a> where webhooks created via API don't register until the container restarts.</p>
<h2 id="heading-the-small-things-that-matter">The Small Things That Matter</h2>
<p><strong>Sensible defaults.</strong> Only 2 required config values. Database credentials, ports, timeouts—all have working defaults.</p>
<p><strong>Helpful errors.</strong> When something fails, the error tells you what to <em>do</em>, not just what went wrong:</p>
<pre><code class="lang-plaintext">Invalid N8N_API_KEY
Go to Settings → n8n API in n8n UI to create a valid API key
</code></pre>
<p><strong>Idempotent operations.</strong> Run the setup script twice? It skips what's already done. Migrations already applied? Tracked and skipped. Safe to re-run, safe to experiment.</p>
<p>I have a deep love for automation—there's something satisfying about simplifying processes until they just work. Every automated step is one less thing to remember, one less way to fail. Great developer experience isn't just for others; it's for future you at 2am, six months from now, debugging something you've completely forgotten.</p>
<hr />
<h1 id="heading-processing-a-receipt-together">Processing a Receipt Together</h1>
<p>You have the repo cloned. You have the stack running. Let's use it.</p>
<h2 id="heading-step-1-upload-a-receipt">Step 1: Upload a Receipt</h2>
<p><strong>Try it:</strong> Navigate to <strong>http://localhost:8080</strong> in your browser. You'll see the upload interface with a drag-and-drop zone.</p>
<p>Need a test receipt? There are 4 included in <code>test-receipts/</code>. Or use any receipt photo from your phone.</p>
<p>Drag your receipt onto the zone (or click to browse). You'll see a progress bar, then a success message showing the filename and "Pending" status. Two buttons appear: <strong>"Upload Another"</strong> and <strong>"View All Receipts"</strong>.</p>
<p><strong>What n8n does behind the scenes:</strong></p>
<ol>
<li><p>Validates file type (JPEG, PNG, WEBP only) and size (max 10MB)</p>
</li>
<li><p>Generates a UUID for the receipt</p>
</li>
<li><p>Creates a date-based path: <code>/app/uploads/receipts/2026/02/02/uuid-filename.jpg</code></p>
</li>
<li><p>Saves the file to disk (persisted to <code>volumes/uploads/</code> on host)</p>
</li>
<li><p>Creates a receipt record in PostgreSQL (status: <code>pending</code>)</p>
</li>
<li><p>Creates a processing job in the queue</p>
</li>
<li><p>Returns the job ID to the user</p>
</li>
</ol>
<p><strong>Key files:</strong></p>
<ul>
<li><p><code>web/index.html</code> - The upload page</p>
</li>
<li><p><code>web/js/upload.js</code> - Handles drag-drop, validation, and API calls</p>
</li>
<li><p><code>n8n/workflows/receipt-job-creation.json</code> - Workflow 1: Receipt Upload &amp; Job Creation</p>
</li>
</ul>
<blockquote>
<p><strong>Did You Know: Why UUIDs + Date Paths?</strong></p>
<p>UUIDs prevent collisions even in distributed systems—two users uploading <code>receipt.jpg</code> at the same millisecond get unique filenames. Date-based paths (<code>/2026/02/02/</code>) make archival and cleanup trivial: delete everything older than 90 days with a simple directory operation. File hashing provides basic duplicate detection—the system can flag if you're uploading the same receipt twice.</p>
</blockquote>
<h2 id="heading-step-2-what-gets-stored">Step 2: What Gets Stored</h2>
<p>Here's what lands in the database when you upload:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- receipts table (simplified)</span>
id:                UUID
filename:          "sample-receipt.jpg"
file_path:         "/app/uploads/receipts/2026/02/02/abc123-sample-receipt.jpg"
file_size:         245000
mime_type:         "image/jpeg"
file_hash:         "a1b2c3d4..."
processing_status: "pending"
items:             NULL
created_at:        NOW()
</code></pre>
<p><strong>Key file:</strong> <code>db/migrations/00001-receipts.sql</code></p>
<blockquote>
<p><strong>Did You Know: Why JSONB for AI Output?</strong></p>
<p>AI models return unpredictable structures. What if a receipt has 3 items? Or 30? What if some have discounts and others don't? JSONB stores flexible JSON with full indexing support (GIN indexes). You can query into it (<code>WHERE items-&gt;&gt;'item_name' LIKE '%coffee%'</code>), index specific fields, and evolve the schema without migrations.</p>
</blockquote>
<h2 id="heading-step-3-view-your-receipts">Step 3: View Your Receipts</h2>
<p><strong>Try it:</strong> Click <strong>"View All Receipts"</strong>, or navigate directly to <strong>http://localhost:8080/receipts.html</strong>.</p>
<p>You'll see a table with columns: thumbnail, filename, upload date, status, items count, and confidence score. The receipt you just uploaded shows <strong>"Pending"</strong> status—the AI hasn't processed it yet.</p>
<p><strong>Behind the scenes:</strong> The receipts page calls Workflow 4: Receipt List Provider (<code>receipt-list-provider.json</code>) to fetch receipts with filters and pagination.</p>
<p><strong>Key files:</strong></p>
<ul>
<li><p><code>web/receipts.html</code> - The management page</p>
</li>
<li><p><code>web/js/receipts.js</code> - List, filter, detail, export logic</p>
</li>
<li><p><code>n8n/workflows/receipt-list-provider.json</code> - Workflow 4: Receipt List Provider</p>
</li>
</ul>
<h2 id="heading-step-4-watch-the-processing">Step 4: Watch the Processing</h2>
<p><strong>Try it:</strong> Find the <strong>"Auto-refresh (10s)"</strong> toggle in the top right and enable it. Or click <strong>"Refresh"</strong> manually every few seconds.</p>
<p>Within 30-60 seconds, watch the status change: <strong>Pending</strong> → <strong>Processing</strong> → <strong>Completed</strong>. Once complete, the <strong>"Items"</strong> column shows how many line items were extracted, and a confidence score appears.</p>
<p><strong>What happens behind the scenes:</strong></p>
<p>Every 30 seconds, Workflow 2: Receipt VLM Processing Queue Monitor polls the database for pending jobs and triggers the VLM processor for each one.</p>
<p><strong>Why 30 seconds?</strong> It's a balance between latency and resource efficiency. For a small business processing a few receipts a day, this works well. For high volume, you can reduce it.</p>
<p><strong>Key file:</strong> <code>n8n/workflows/receipt-vlm-processing-queue-monitor.json</code> - Workflow 2: Receipt VLM Processing Queue Monitor</p>
<h2 id="heading-step-5-the-ai-magic-vlm-processing">Step 5: The AI Magic (VLM Processing)</h2>
<p>When Workflow 2 finds your pending job, it triggers Workflow 3: Receipt VLM Processing &amp; Item Extraction. This is where the AI reads your receipt.</p>
<p><strong>What Workflow 3 does:</strong></p>
<ol>
<li><p>Fetches the receipt file from disk</p>
</li>
<li><p>Encodes the image to base64</p>
</li>
<li><p>Sends the image + extraction prompt to Ollama</p>
</li>
<li><p>Parses the JSON response</p>
</li>
<li><p>Updates the receipt record with extracted items</p>
</li>
<li><p>Updates the job status (completed or failed)</p>
</li>
<li><p>If failed, retries up to 3 times</p>
</li>
</ol>
<blockquote>
<p><strong>Using cloud?</strong> Same workflow works—see <a target="_blank" href="https://triedandtestedbuilds.com/setting-up-your-self-hosted-ai-stack-part-3-ai-powered-invoice-and-receipt-processing-with-n8n-workflows-and-vision-language-models?showSharer=true#heading-cloud-alternative-when-local-isnt-an-option">Cloud Alternative</a>.</p>
</blockquote>
<p><strong>Key file:</strong> <code>n8n/workflows/receipt-vlm-processor.json</code> - Workflow 3: Receipt VLM Processing &amp; Item Extraction</p>
<h2 id="heading-step-6-the-extraction-prompt">Step 6: The Extraction Prompt</h2>
<p>Here's the prompt that tells the VLM how to extract receipt data (v9):</p>
<pre><code class="lang-markdown">Extract items AND totals from this receipt image as JSON.

Output format:
{
  "currency": "USD",
  "receipt<span class="hljs-emphasis">_type": "grocery|restaurant|retail|service|unknown",
  "tax_</span>format": "added|inclusive|none",
  "receipt<span class="hljs-emphasis">_subtotal": 0.00,
  "receipt_</span>total<span class="hljs-emphasis">_tax_</span>amount": 0.00,
  "receipt<span class="hljs-emphasis">_total_</span>tax<span class="hljs-emphasis">_percentage": 0.00,
  "receipt_</span>total": 0.00,
  "items": [
<span class="hljs-code">    {
      "item_name": "Product Name",
      "item_quantity": 1,
      "item_unit_price": 0.00,
      "item_base_price": 0.00,
      "item_discount_amount": null,
      "item_tax_price": null,
      "item_total_price": 0.00,
      "item_sequence": 1,
      "confidence_score": 0.95,
      ...
    }
  ]
}
</span>
Rules:

TAX HANDLING - Identify the tax format first:

<span class="hljs-bullet">1.</span> TAX ADDED (US/Canada style):
<span class="hljs-bullet">   -</span> Look for separate "Subtotal", "Tax/Sales Tax", and "Total" lines
<span class="hljs-bullet">   -</span> Total = Subtotal + Tax
<span class="hljs-bullet">   -</span> tax<span class="hljs-emphasis">_format = "added"

2. TAX INCLUSIVE (EU/Swiss style):
   - Look for "Total" with "Incl. X% MwSt/VAT/TVA: amount"
   - Tax is already part of the total, not added on top
   - tax_</span>format = "inclusive"
<span class="hljs-bullet">   -</span> receipt<span class="hljs-emphasis">_subtotal = receipt_</span>total - receipt<span class="hljs-emphasis">_total_</span>tax<span class="hljs-emphasis">_amount

3. NO TAX SHOWN:
   - tax_</span>format = "none", set tax fields to null

OTHER RULES:
<span class="hljs-bullet">-</span> Remove quantity prefixes (1x, 2x) from item<span class="hljs-emphasis">_name, put in item_</span>quantity
<span class="hljs-bullet">-</span> item<span class="hljs-emphasis">_total_</span>price = item<span class="hljs-emphasis">_base_</span>price - item<span class="hljs-emphasis">_discount_</span>amount (when discount exists)
<span class="hljs-bullet">-</span> Math: item<span class="hljs-emphasis">_quantity × item_</span>unit<span class="hljs-emphasis">_price = item_</span>base<span class="hljs-emphasis">_price
- Return JSON only</span>
</code></pre>
<p><strong>Key file:</strong> <code>prompts/ocr/v9-system-prompt.md</code></p>
<p>67 lines. Clear output schema. Essential rules only. This simplicity is intentional—and hard-won. More on that below.</p>
<blockquote>
<p><strong>Did You Know: The LLM Dictionary and Tokenization</strong></p>
<p>LLMs don't see characters—they see tokens. Each model has a "vocabulary" (dictionary) of ~32,000-100,000 tokens. Common words like "the" are single tokens, while rare words get split ("tokenization" might become "token" + "ization").</p>
<p>Why does this matter for prompts? The model processes tokens, not text. A well-structured JSON schema is unambiguous—the model knows exactly what format you expect. Generic placeholders like <code>0.00</code> or <code>"Product Name"</code> are familiar patterns from training data. This is why clear schemas work better than verbose explanations—they're precise, not just shorter.</p>
</blockquote>
<h2 id="heading-step-7-results-stored-in-database">Step 7: Results Stored in Database</h2>
<p>Once processing completes, here's what gets saved:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Updated receipt record</span>
processing_status: "completed"
items: [
  {
    "item_name": "Front and rear brake cables",
    "item_quantity": 1,
    "item_unit_price": 100.00,
    "item_total_price": 100.00,
    "confidence_score": 0.95
  },
  {
    "item_name": "New <span class="hljs-keyword">set</span> <span class="hljs-keyword">of</span> pedal arms<span class="hljs-string">",
    "</span>item_quantity<span class="hljs-string">": 2,
    "</span>item_unit_price<span class="hljs-string">": 15.00,
    "</span>item_total_price<span class="hljs-string">": 30.00,
    "</span>confidence_score<span class="hljs-string">": 0.95
  },
  ...
]
items_count: 3
total_confidence_score: 0.95
receipt_subtotal: 145.00
receipt_total_tax_amount: 9.06
receipt_total: 154.06</span>
</code></pre>
<p>The JSONB column holds all extracted items with their confidence scores, while computed fields (<code>items_count</code>, <code>total_confidence_score</code>) enable fast queries without re-parsing the JSON.</p>
<h2 id="heading-step-8-view-the-extracted-data">Step 8: View the Extracted Data</h2>
<p><strong>Try it:</strong> On the receipts page (<strong>http://localhost:8080/receipts.html</strong>), click any completed receipt row to open the detail modal.</p>
<p><strong>What you'll see:</strong></p>
<ul>
<li><p><strong>Left side:</strong> The original receipt image (scroll to zoom)</p>
</li>
<li><p><strong>Right side:</strong> Extracted metadata (filename, date, size, status, currency, receipt type) and a table of extracted items</p>
</li>
</ul>
<p>Each item shows: name, quantity, unit price, base price, discount (if any), tax, total price, and a confidence score.</p>
<p><strong>Behind the scenes:</strong> Workflow 5: Receipt Detail Provider (<code>receipt-detail-provider.json</code>) fetches the receipt record with the items array.</p>
<blockquote>
<p><strong>Did You Know: What Confidence Scores Actually Mean</strong></p>
<p>Confidence isn't probability of correctness. It's the model's internal certainty about its output—how "surprised" it would be if wrong. A 0.95 confidence means the model is very sure about its answer. But high confidence + wrong answer = hallucination territory. That's why we use confidence thresholds:</p>
<ul>
<li><p><strong>90-100%</strong>: Auto-approve for accounting</p>
</li>
<li><p><strong>70-89%</strong>: Quick manual review</p>
</li>
<li><p><strong>50-69%</strong>: Detailed review required</p>
</li>
<li><p><strong>Below 50%</strong>: Manual entry likely faster</p>
</li>
</ul>
</blockquote>
<h2 id="heading-step-9-export-your-data">Step 9: Export Your Data</h2>
<p><strong>Try it:</strong> Click the <strong>"Export CSV"</strong> button in the header to download all completed receipts.</p>
<p>The CSV includes: receipt ID, filename, upload date, currency, receipt type, subtotal, tax, total, and all extracted line items flattened into columns.</p>
<p><strong>What Workflows 4-6 do:</strong></p>
<ul>
<li><p><strong>Workflow 4: Receipt List Provider</strong> (<code>receipt-list-provider.json</code>): List receipts with filters and pagination</p>
</li>
<li><p><strong>Workflow 5: Receipt Detail Provider</strong> (<code>receipt-detail-provider.json</code>): Get receipt detail with items array</p>
</li>
<li><p><strong>Workflow 6: Receipt Export Generator</strong> (<code>receipt-export-generator.json</code>): Export completed receipts to CSV</p>
</li>
</ul>
<p><strong>Key files:</strong></p>
<ul>
<li><p><code>web/receipts.html</code> - The management page</p>
</li>
<li><p><code>web/js/receipts.js</code> - List, filter, detail, export logic</p>
</li>
<li><p><code>n8n/workflows/receipt-list-provider.json</code></p>
</li>
<li><p><code>n8n/workflows/receipt-detail-provider.json</code></p>
</li>
<li><p><code>n8n/workflows/receipt-export-generator.json</code></p>
</li>
</ul>
<hr />
<h1 id="heading-the-art-of-simplicity-a-prompt-optimization-journey">The Art of Simplicity: A Prompt Optimization Journey</h1>
<h2 id="heading-when-more-is-less">When More Is Less</h2>
<p>This is the lesson that humbled me most during this project.</p>
<p>The receipt processing system needed to handle diverse formats—different currencies (USD, CHF, GBP), quantity notations (2x, x3, QTY columns), discounts, and tax calculations. My original prompt was a 400+ line behemoth, complete with:</p>
<ul>
<li><p>Detailed parsing approaches for every scenario</p>
</li>
<li><p>Five examples with step-by-step breakdowns</p>
</li>
<li><p>Mathematical validation checklists</p>
</li>
<li><p>Multiple tax handling patterns</p>
</li>
<li><p>Exhaustive field documentation</p>
</li>
</ul>
<p>On paper, it looked thorough. Professional, even. I was proud of it.</p>
<p>In practice? Maybe 60-70% accuracy. Items would be missed, quantities misinterpreted, discounts incorrectly calculated. The model seemed to be... overthinking.</p>
<h2 id="heading-the-iterative-discovery">The Iterative Discovery</h2>
<p>I set up a systematic testing pipeline with four diverse receipt samples:</p>
<ol>
<li><p>A USD receipt with items and tax at bottom</p>
</li>
<li><p>A Swiss CHF receipt with quantity prefixes (2x Latte Macchiato) and European formatting</p>
</li>
<li><p>A US repair invoice with QTY columns and tabular data</p>
</li>
<li><p>A UK receipt with line-level discounts and manager overrides</p>
</li>
</ol>
<p>What followed was an exercise in humility. After multiple iterations—tweaking parameters, adjusting examples, refining instructions—I had a revelation: <strong>What if the problem isn't that I'm not explaining enough, but that I'm explaining too much?</strong></p>
<p>I stripped the prompt down. Out went the five detailed examples. Out went the philosophical explanations about tax patterns. Out went the validation checklists. What remained:</p>
<pre><code class="lang-plaintext">Extract items from this receipt image as JSON.

Output format:
{
  "currency": "USD",
  "has_total_tax_only": true,
  "items": [
    {
      "item_name": "Product Name",
      "item_quantity": 1,
      "item_unit_price": 5.00,
      "item_base_price": 5.00,
      "item_discount_amount": null,
      "item_total_price": 5.00,
      ...
    }
  ]
}

Rules:
- has_total_tax_only=true unless tax is shown on EACH item line separately
- Remove quantity prefixes (1x, 2x) from item_name, put in item_quantity
- item_total_price = item_base_price - item_discount_amount (when discount exists)
- Math: item_quantity × item_unit_price = item_base_price
- Return JSON only
</code></pre>
<p>About 30 lines instead of 400+.</p>
<h2 id="heading-the-results">The Results</h2>
<p>I ran stability tests—five times per receipt with varying random seeds:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Receipt</td><td>Runs</td><td>Result</td></tr>
</thead>
<tbody>
<tr>
<td>Simple USD receipt</td><td>5/5</td><td>All identical</td></tr>
<tr>
<td>Swiss CHF with quantities</td><td>5/5</td><td>All identical</td></tr>
<tr>
<td>US invoice with QTY column</td><td>5/5</td><td>All identical</td></tr>
<tr>
<td>UK receipt with discounts</td><td>5/5</td><td>All identical</td></tr>
</tbody>
</table>
</div><p><strong>20 out of 20 runs passed.</strong> Every quantity correct. Every discount captured. Every currency identified. The math checked out perfectly.</p>
<p>I stared at the results, equal parts vindicated and embarrassed. All those hours crafting the perfect 400-line prompt, and the model just needed me to get out of its way.</p>
<h2 id="heading-the-lesson">The Lesson</h2>
<p>The model already <em>knew</em> how to read receipts. It had seen millions of them during training. My elaborate prompt wasn't teaching it anything—it was <em>confusing</em> it.</p>
<p>When I bombarded it with examples, edge cases, and detailed instructions, the model tried to reconcile all that information with every image it saw. It started second-guessing obvious interpretations. It looked for complexities that weren't there.</p>
<p>The simple prompt worked because it:</p>
<ol>
<li><p><strong>Clearly defined the output format</strong> - Show, don't tell</p>
</li>
<li><p><strong>Stated only essential rules</strong> - Five rules instead of fifty paragraphs</p>
</li>
<li><p><strong>Trusted the model's inherent capabilities</strong> - It knows what a receipt looks like</p>
</li>
<li><p><strong>Avoided overthinking triggers</strong> - No examples that might not match the current image</p>
</li>
</ol>
<h2 id="heading-broader-implications">Broader Implications</h2>
<p>This principle extends beyond receipt processing:</p>
<ul>
<li><p><strong>Prompt engineering is often about subtraction, not addition</strong></p>
</li>
<li><p><strong>Clear output schemas beat extensive explanations</strong></p>
</li>
<li><p><strong>Trust the model's training—it's seen more examples than you can write</strong></p>
</li>
<li><p><strong>Test systematically with diverse inputs to validate changes</strong></p>
</li>
</ul>
<p>As Antoine de Saint-Exupéry wrote: <em>"Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away."</em></p>
<p>The journey from 400 lines to 30 was a reminder that in AI engineering, simplicity isn't just elegant—it's effective.</p>
<hr />
<h1 id="heading-testing-methodology-verify-before-you-deploy">Testing Methodology: Verify Before You Deploy</h1>
<h2 id="heading-the-trap-teaching-to-the-test">The Trap: Teaching to the Test</h2>
<p>After discovering the power of simplicity, I needed to extend the prompt to capture receipt-level totals (subtotal, tax, total)—not just individual items. Here's how I approached it.</p>
<p>My v7 prompt had a <code>has_total_tax_only</code> boolean flag but <strong>no fields to capture the actual tax amount</strong>. When processing a US invoice with:</p>
<ul>
<li><p>Subtotal: $145.00</p>
</li>
<li><p>Sales Tax 6.25%: $9.06</p>
</li>
<li><p>Total: $154.06</p>
</li>
</ul>
<p>The system would extract items correctly but lose the tax information entirely.</p>
<p>My first instinct was to add example values to the prompt:</p>
<pre><code class="lang-json"><span class="hljs-string">"receipt_subtotal"</span>: <span class="hljs-number">145.00</span>,
<span class="hljs-string">"receipt_total_tax_amount"</span>: <span class="hljs-number">9.06</span>,
<span class="hljs-string">"receipt_total_tax_percentage"</span>: <span class="hljs-number">6.25</span>,
<span class="hljs-string">"receipt_total"</span>: <span class="hljs-number">154.06</span>
</code></pre>
<p>This "worked"—but I was essentially giving the model the answers. When testing on the same receipt that matched my example values, of course it succeeded. Training data that matches test data tells you nothing about real-world performance.</p>
<p>I've made this mistake before. You'd think I'd learn.</p>
<h2 id="heading-the-fix-generic-placeholders">The Fix: Generic Placeholders</h2>
<p>I revised to use zeros as placeholders, forcing the model to read the receipt:</p>
<pre><code class="lang-json"><span class="hljs-string">"receipt_subtotal"</span>: <span class="hljs-number">0.00</span>,
<span class="hljs-string">"receipt_total_tax_amount"</span>: <span class="hljs-number">0.00</span>,
<span class="hljs-string">"receipt_total_tax_percentage"</span>: <span class="hljs-number">0.00</span>,
<span class="hljs-string">"receipt_total"</span>: <span class="hljs-number">0.00</span>
</code></pre>
<h2 id="heading-the-testing-protocol">The Testing Protocol</h2>
<p>Rather than running one test and calling it done, I ran <strong>10 tests with different random seeds</strong> on the same receipt:</p>
<pre><code class="lang-bash"><span class="hljs-keyword">for</span> seed <span class="hljs-keyword">in</span> 43 44 45 46 47 48 49 50 51 52; <span class="hljs-keyword">do</span>
  curl -s http://localhost:11434/api/generate \
    -d <span class="hljs-string">'{"model":"qwen3-vl:8b-thinking-q8_0", "seed":'</span><span class="hljs-variable">$seed</span><span class="hljs-string">', ...}'</span> \
    | jq <span class="hljs-string">'{tax:.receipt_total_tax_amount, pct:.receipt_total_tax_percentage}'</span>
<span class="hljs-keyword">done</span>
</code></pre>
<p><strong>US Invoice Results (Sample 3):</strong></p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Test</td><td>Subtotal</td><td>Tax</td><td>Tax %</td><td>Total</td><td>Items</td></tr>
</thead>
<tbody>
<tr>
<td>1</td><td>145.00</td><td>9.06</td><td>6.25</td><td>154.06</td><td>3</td></tr>
<tr>
<td>2</td><td>145.00</td><td>9.06</td><td>6.25</td><td>154.06</td><td>3</td></tr>
<tr>
<td>...</td><td>...</td><td>...</td><td>...</td><td>...</td><td>...</td></tr>
<tr>
<td>10</td><td>145.00</td><td>9.06</td><td>6.25</td><td>154.06</td><td>3</td></tr>
</tbody>
</table>
</div><p><strong>10/10 PASSED</strong> - Consistent extraction across all seeds.</p>
<h2 id="heading-testing-across-receipt-types">Testing Across Receipt Types</h2>
<p>One receipt type isn't enough. I tested against different formats:</p>
<p><strong>Swiss Receipt (European inclusive tax):</strong></p>
<ul>
<li><p>Format: "Incl. 7.6% MwSt 54.50 CHF: 3.85"</p>
</li>
<li><p>Result: 1/5 passed—the prompt struggled with tax-inclusive formats</p>
</li>
</ul>
<p>This revealed a gap: the v8 prompt handled US-style "tax added" receipts perfectly but needed refinement for European "tax included" formats. This led directly to the v9 prompt with explicit multi-region tax handling.</p>
<h2 id="heading-key-takeaways">Key Takeaways</h2>
<ol>
<li><p><strong>Never use real values as examples</strong> - Use generic placeholders (0.00, "Product Name") to test actual extraction ability</p>
</li>
<li><p><strong>Run multiple seeds</strong> - A single successful test means nothing. Run 5-10 tests with different random seeds to verify consistency</p>
</li>
<li><p><strong>Test diverse inputs</strong> - Different receipt formats (US tax-added, EU tax-inclusive, no-tax) expose edge cases</p>
</li>
<li><p><strong>Document failures</strong> - A 1/5 pass rate tells you exactly where to focus next</p>
</li>
<li><p><strong>Iterate systematically</strong> - Don't guess. Test, measure, refine, repeat</p>
</li>
</ol>
<p>This transforms prompt engineering from art to science. You stop hoping your prompt works and start knowing when and how it fails.</p>
<hr />
<h1 id="heading-when-things-go-wrong-built-in-safeguards">When Things Go Wrong: Built-in Safeguards</h1>
<p>Document processing hits edge cases. The system handles them:</p>
<p><strong>AI Response Failures:</strong></p>
<ul>
<li><p>Automatic retry with up to 3 attempts</p>
</li>
<li><p>Failed jobs marked for manual review</p>
</li>
<li><p>Error logging for troubleshooting</p>
</li>
</ul>
<p><strong>Image Quality Issues:</strong></p>
<ul>
<li><p>Pre-validation catches unsupported file types</p>
</li>
<li><p>Confidence scores below thresholds flag items for review</p>
</li>
<li><p>User gets clear feedback on what went wrong</p>
</li>
</ul>
<p><strong>Processing Timeouts:</strong></p>
<ul>
<li><p>60-second timeout prevents hung processes</p>
</li>
<li><p>Failed jobs remain in queue for retry</p>
</li>
<li><p>Queue monitor continues processing other jobs</p>
</li>
</ul>
<h2 id="heading-image-quality-tips">Image Quality Tips</h2>
<p>For best results:</p>
<ul>
<li><p><strong>Resolution</strong>: 300-600 DPI ideal, 150 DPI minimum</p>
</li>
<li><p><strong>Size</strong>: 1024px max width reduces processing time</p>
</li>
<li><p><strong>Orientation</strong>: Right-side up (rotation affects accuracy)</p>
</li>
<li><p><strong>Lighting</strong>: Good contrast between text and background</p>
</li>
<li><p><strong>Format</strong>: JPG or PNG preferred</p>
</li>
</ul>
<p>If you can't read it clearly, neither can the AI. Flatten creased receipts, use good lighting, fill the frame.</p>
<blockquote>
<p><strong>Did You Know: Image Preprocessing</strong></p>
<p>Poor-quality images can be improved before sending to the VLM. Techniques like contrast enhancement (CLAHE), deskewing, sharpening, and binarization can make faded or noisy receipts more readable. Tools like OpenCV or PIL can automate this. We didn't implement preprocessing here, but it's worth exploring for consistently low-quality scans.</p>
</blockquote>
<h2 id="heading-common-issues">Common Issues</h2>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Problem</td><td>Cause</td><td>Fix</td></tr>
</thead>
<tbody>
<tr>
<td>VLM processing hangs or fails</td><td>Ollama not running</td><td>Run <code>ollama serve</code> or check <code>curl http://localhost:11434/api/tags</code></td></tr>
<tr>
<td>Webhooks return 404</td><td>Workflows not imported</td><td>Run <code>python scripts/setup-n8n.py</code></td></tr>
<tr>
<td>Webhooks still 404 after import</td><td>Workflows not activated</td><td>Open n8n UI, activate each workflow, or restart: <code>docker compose restart n8n</code></td></tr>
</tbody>
</table>
</div><hr />
<h1 id="heading-what-youve-built">What You've Built</h1>
<p>This isn't just a receipt processor. It's a demonstration of what's possible when you combine:</p>
<ul>
<li><p><strong>Accessible AI</strong>—A 10GB model that runs on your laptop, not a cloud bill</p>
</li>
<li><p><strong>Thoughtful automation</strong>—7 commands to deploy, 2 values to configure</p>
</li>
<li><p><strong>Hard-won simplicity</strong>—A 67-line prompt that took 400+ lines to discover wasn't needed</p>
</li>
<li><p><strong>Engineering care</strong>—Health checks, validation chains, helpful errors</p>
</li>
</ul>
<p>The real achievement isn't the features. It's that it <em>works</em>—reliably, on modest hardware, without fighting you.</p>
<h2 id="heading-the-lessons">The Lessons</h2>
<p><strong>On prompts:</strong> Simplicity beats complexity. Trust the model's training.</p>
<p><strong>On setup:</strong> Great developer experience starts with the little details. Automate what can be automated. Simplify until it just works.</p>
<p><strong>On building:</strong> The best code is often the code you deleted.</p>
<hr />
<h1 id="heading-coming-up-in-part-4">Coming Up in Part 4</h1>
<p>We'll take n8n to the next level with <strong>agent orchestration</strong>—building workflows that don't just process data, but make decisions and use tools:</p>
<ul>
<li><p>AI agents that can call functions (calculators, web search, database queries)</p>
</li>
<li><p>Multi-step reasoning workflows</p>
</li>
<li><p>Agentic behavior patterns in n8n</p>
</li>
</ul>
<p><strong>The vision:</strong> Workflows that think, not just execute.</p>
<hr />
<h1 id="heading-resources">Resources</h1>
<ul>
<li><p><a target="_blank" href="https://docs.n8n.io/">n8n Documentation</a> - Workflow automation platform</p>
</li>
<li><p><a target="_blank" href="https://ollama.com/">Ollama</a> - Local LLM hosting</p>
</li>
<li><p><a target="_blank" href="https://ollama.com/library/qwen3-vl">qwen3-vl on Ollama</a> - Vision Language Model</p>
</li>
<li><p><a target="_blank" href="https://www.postgresql.org/docs/current/functions-json.html">PostgreSQL JSONB</a> - JSON operations</p>
</li>
<li><p><strong>GitHub Repository</strong> - <a target="_blank" href="https://github.com/FarzamMohammadi/self-hosted-ai-stack/tree/main/part-3-receipt-processing-with-vlm">Full source code</a></p>
</li>
</ul>
<hr />
<h1 id="heading-cloud-alternative-when-local-isnt-an-option">Cloud Alternative: When Local Isn't an Option</h1>
<p>If you have less than 16GB RAM or no dedicated GPU, use Ollama's cloud models instead. Same API, same workflow—just a different model name.</p>
<p><strong>Setup:</strong></p>
<pre><code class="lang-bash"><span class="hljs-comment"># Update to Ollama v0.12+</span>
ollama --version
<span class="hljs-comment"># Download latest from https://ollama.com/download if needed</span>

<span class="hljs-comment"># Sign in</span>
ollama signin

<span class="hljs-comment"># Update .env</span>
OLLAMA_VLM_MODEL=qwen3-vl:235b-cloud
</code></pre>
<p><strong>Available vision models:</strong> <code>qwen3-vl:235b-cloud</code> (recommended), <code>deepseek-v3.1:671b-cloud</code></p>
<p><strong>Cost:</strong> Ollama Turbo is $20/month. Ollama states they don't retain your data.</p>
<p><strong>Other providers:</strong> You can modify the n8n workflow to use OpenAI or other APIs by changing <code>VLM_API_URL</code>, adding auth headers, and adjusting the request format in <code>receipt-vlm-processor.json</code>.</p>
<hr />
<p><em>This is Part 3 of the "Complete Self-Hosted AI Infrastructure" series. We're building increasingly sophisticated AI capabilities, all running locally on your machine. Thanks for joining me on this journey.</em></p>
]]></content:encoded></item><item><title><![CDATA[Setting Up Your Self-Hosted AI Stack - Part 2: Document Processing and RAG with Apache Tika and Qdrant Vector Database]]></title><description><![CDATA[What we're building today
We're adding document processing capabilities to the foundation from Part 1. This extends your chat interface to work with your files and documents.
Two key additions make th]]></description><link>https://triedandtestedbuilds.com/self-hosted-ai-stack-part-2</link><guid isPermaLink="true">https://triedandtestedbuilds.com/self-hosted-ai-stack-part-2</guid><category><![CDATA[self-hosted-ai]]></category><category><![CDATA[self-hosted]]></category><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[LLM's ]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[OCR ]]></category><category><![CDATA[text extraction]]></category><category><![CDATA[apache tika]]></category><category><![CDATA[qdrant]]></category><category><![CDATA[ollama]]></category><category><![CDATA[open-webui]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[Document Processing]]></category><category><![CDATA[Intelligent Document Processing]]></category><dc:creator><![CDATA[Farzam Mohammadi]]></dc:creator><pubDate>Sun, 10 Aug 2025 21:47:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754859969268/55363c83-79aa-46eb-8385-b5a9f272c672.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>What we're building today</h1>
<p>We're adding document processing capabilities to the foundation from <a href="https://triedandtestedbuilds.com/self-hosted-ai-stack-part-1">Part 1</a>. This extends your chat interface to work with your files and documents.</p>
<p>Two key additions make this possible:</p>
<ul>
<li><p><strong>Document Processing</strong>: Extract text from documents like PDFs, Word docs, and text files using Apache Tika (with OCR capabilities for scanned documents)</p>
</li>
<li><p><strong>RAG (Retrieval-Augmented Generation)</strong>: Search and retrieve relevant information from your documents to answer questions</p>
</li>
</ul>
<p>This transforms your local AI from conversation-only to document-aware. Upload a file, ask questions about its contents, and get answers grounded in your actual documents rather than the model's training data.</p>
<h1>The bigger picture</h1>
<p>I'm building this self-hosted AI stack and documenting everything as I go. Part 2 is where real power emerges. We're adding the intelligence layer that transforms your local AI from a chat toy into a genuine knowledge assistant.</p>
<p>Here's the thing: even with today's resources, implementing proper document processing and RAG together is surprisingly complex. This guide captures the lessons learned getting both capabilities production ready.</p>
<p>What we've built so far:</p>
<ul>
<li><a href="https://triedandtestedbuilds.com/self-hosted-ai-stack-part-1"><strong>Part 1: Building the foundation with Open WebUI, Ollama, and Postgres</strong>: Your foundational chat interface with local LLM hosting</a></li>
</ul>
<p>What's still coming:</p>
<ul>
<li><p><a href="https://triedandtestedbuilds.com/self-hosted-ai-stack-part-3"><strong>Part 3: Visual Data Extraction</strong>: Web upload interface with N8N workflows and Vision Language Models for extracting structured data from images</a></p>
</li>
<li><p><strong>Part 4: Model Superpowers</strong>: Advanced WebUI configuration with tools and knowledge integration</p>
</li>
<li><p><strong>Part 5: Intelligent Automation</strong>: WebUI filters and N8N workflows for content processing</p>
</li>
</ul>
<h1>Prerequisites</h1>
<ul>
<li><p><a href="https://triedandtestedbuilds.com/self-hosted-ai-stack-part-1">Part 1 foundation stack running</a></p>
</li>
<li><p><strong>Ollama running</strong> (verify with <code>ollama serve</code> and if you see "address already in use", it's already running)</p>
</li>
<li><p>24GB+ RAM recommended (16GB minimum)</p>
</li>
<li><p>50GB+ storage for documents and vectors</p>
</li>
<li><p>Docker and Docker Compose</p>
</li>
</ul>
<blockquote>
<p><strong>Important</strong>: If Part 1 is running, stop it first with <code>cd ../part-1-building-the-foundation &amp;&amp; docker compose down</code> before starting Part 2.</p>
</blockquote>
<h1>Credits where credits are due</h1>
<p>Massive thanks to the creators and contributors of these open source projects that make this possible:</p>
<ul>
<li><p><a href="https://tika.apache.org/">Apache Tika</a> - Repository: <a href="https://github.com/apache/tika">apache/tika</a></p>
</li>
<li><p><a href="https://qdrant.tech/">Qdrant</a> - Repository: <a href="https://github.com/qdrant/qdrant">qdrant/qdrant</a></p>
</li>
<li><p><a href="https://ollama.com">Ollama</a> - Repository: <a href="https://github.com/ollama/ollama">ollama/ollama</a></p>
</li>
<li><p><a href="https://openwebui.com">Open WebUI</a> - Repository: <a href="https://github.com/open-webui/open-webui">open-webui/open-webui</a></p>
</li>
<li><p><a href="https://docker.com/">Docker</a> - Repositories: <a href="https://github.com/moby/moby">moby/moby</a> &amp; <a href="https://github.com/docker/compose">docker/compose</a></p>
</li>
</ul>
<h1>Quick start (skip explanations, just get it running)</h1>
<ol>
<li><p><strong>Download the embedding model</strong> (~275MB):</p>
<pre><code class="language-bash">ollama pull nomic-embed-text
</code></pre>
</li>
<li><p><strong>Clone the repository or pull latest</strong>:</p>
<pre><code class="language-bash">git clone https://github.com/FarzamMohammadi/self-hosted-ai-stack
# Or if you already have it: git pull origin main
</code></pre>
</li>
<li><p><strong>Navigate to part 2</strong>:</p>
<pre><code class="language-bash">cd part-2-rag-with-tika-and-qdrant
</code></pre>
</li>
<li><p><strong>Start the enhanced stack</strong>:</p>
<pre><code class="language-bash">docker compose up -d
</code></pre>
</li>
</ol>
<p>Done. Open <a href="http://localhost:3000">http://localhost:3000</a>, sign in, and you'll see document upload button (<code>+</code>) in the bottom left corner of the chat. Upload a PDF and start asking questions about it.</p>
<blockquote>
<p><strong>Important</strong>: Open WebUI handles files through two distinct pathways:</p>
<p><strong>Document Processing (what we built)</strong>: Extracts text from documents like PDFs, Word docs, and text files through Tika. Does NOT work with standalone image files (PNG, JPEG, etc.).</p>
<p><strong>Image Analysis (not covered here)</strong>: For standalone image files, you'll need to download and use a vision model.</p>
</blockquote>
<h1>Understanding what you built</h1>
<p>Let's break down what we just built and why each component matters. Understanding the architecture transforms you from someone following steps into someone who owns the system.</p>
<h2>What are document processing and RAG, and why you need both</h2>
<p>Your AI model was trained on data from months or years ago. It doesn't know about your company's latest reports, personal documents, or today's news. Ask about quarterly financials, and it apologizes for lacking access.</p>
<p><strong>Document Processing</strong> uses Apache Tika to extract machine-readable text directly from documents like PDFs, Word docs, and other formats by parsing their internal structure. For scanned documents or image-based content within PDFs, Tika automatically falls back to OCR using Tesseract when needed.</p>
<p><strong>RAG (Retrieval-Augmented Generation)</strong> solves the knowledge gap by giving your AI instant access to search through your documents for relevant information.</p>
<p>Together, they create a document intelligence system that processes documents and provides grounded answers.</p>
<p><strong>The transformation:</strong></p>
<table>
<thead>
<tr>
<th>Without Document Processing + RAG</th>
<th>With Document Processing + RAG</th>
</tr>
</thead>
<tbody><tr>
<td>"I can't access your documents."</td>
<td>"Based on your invoice PDF, the total amount due is $2,847."</td>
</tr>
<tr>
<td>"I don't have access to your company's financial data."</td>
<td>"Based on your Q3 report, revenue increased 23% compared to last quarter."</td>
</tr>
<tr>
<td>Limited to conversation only</td>
<td>Processes documents like PDFs, Word docs, text files</td>
</tr>
<tr>
<td>Operates with frozen training data</td>
<td>Searches YOUR content for relevant information</td>
</tr>
<tr>
<td>Generic responses, frequent "I don't have access"</td>
<td>Accurate answers with source attribution</td>
</tr>
</tbody></table>
<p><strong>Real examples</strong>:</p>
<p><em>Document processing:</em> "What's the total on this invoice?"</p>
<ul>
<li><p>Without document processing: "I can't read documents or extract text from files."</p>
</li>
<li><p>With document processing + RAG: "Based on your invoice PDF, the total amount due is $2,847, with a due date of March 15th."</p>
</li>
</ul>
<p><em>Document analysis:</em> "What were our biggest challenges last quarter?"</p>
<ul>
<li><p>Without RAG: "I don't have access to your quarterly reports."</p>
</li>
<li><p>With document processing + RAG: "Based on your Q3 report, the biggest challenges were supply chain delays (mentioned 8 times) and staffing shortages in the manufacturing division (pages 12-14)."</p>
</li>
</ul>
<p>This transforms your AI from a chat interface into a document intelligence system that reads, understands, and answers questions about your documents.</p>
<h2>How vector similarity works</h2>
<p>Traditional search looks for exact word matches. Search for "machine learning performance" and miss documents about "AI optimization" or "model efficiency" entirely.</p>
<p><strong>Embeddings solve this</strong>: mathematical representations that capture semantic meaning.</p>
<p>Think of embeddings as GPS coordinates for concepts:</p>
<ul>
<li><p>"Machine learning" → [0.2, 0.8, 0.1, ...] (768 dimensions)</p>
</li>
<li><p>"AI optimization" → [0.3, 0.7, 0.2, ...]</p>
</li>
<li><p>"Car repair" → [0.9, 0.1, 0.3, ...]</p>
</li>
</ul>
<p>Closer coordinates mean more similar meaning. When nomic-embed-text converts "reduce costs" into a 768-dimensional vector, it positions near related concepts:</p>
<ul>
<li><p>"Budget optimization" (distance: 0.23)</p>
</li>
<li><p>"Expense management" (distance: 0.31)</p>
</li>
<li><p>"Financial efficiency" (distance: 0.28)</p>
</li>
</ul>
<p>This mathematical precision ensures RAG retrieves conceptually relevant information beyond keyword matches.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754862170197/8177dfdc-999f-408e-8902-d467b45ae7c1.webp" alt="" style="display:block;margin:0 auto" />

<p><em>Vector search visualization showing how concepts cluster in high-dimensional space. Related concepts like "Kitten" and "Cat" appear close together in mathematical space, enabling semantic search that finds meaning beyond exact keyword matches.</em></p>
<blockquote>
<p>Source: <a href="https://odsc.medium.com/a-gentle-introduction-to-vector-search-3c0511bc6771">https://odsc.medium.com/a-gentle-introduction-to-vector-search-3c0511bc6771</a></p>
</blockquote>
<h2>Vector databases store and search embeddings</h2>
<p>Once you have embeddings, you need somewhere to store and search them efficiently. Regular databases can't handle similarity search.</p>
<p><strong>Traditional database (SQL):</strong></p>
<pre><code class="language-sql">SELECT * FROM documents WHERE title CONTAINS 'machine learning'
</code></pre>
<p>Finds: Documents with exact phrase "machine learning" in title</p>
<p><strong>Vector database:</strong></p>
<pre><code class="language-plaintext">SEARCH FOR documents SIMILAR TO embedding([0.2, 0.8, 0.1, ...])
</code></pre>
<p>Finds: Documents about AI, neural networks, model training, performance optimization, etc.</p>
<p>Vector databases use specialized algorithms to find similar vectors among millions quickly. Semantic search understands intent:</p>
<ul>
<li><p>Search: "reduce costs" → Finds: "budget optimization", "expense management", "financial efficiency"</p>
</li>
<li><p>Search: "team problems" → Finds: "communication challenges", "staff conflicts", "collaboration issues"</p>
</li>
</ul>
<h2>How the complete RAG flow works</h2>
<p><strong>Document Processing (Setup Phase):</strong> Upload document → <strong>Apache Tika server</strong> extracts machine-readable text from documents like PDFs, Word docs, and text files (using OCR via Tesseract for scanned content when needed) → Open WebUI splits text into 1000-character chunks → <strong>nomic-embed-text model</strong> (via Ollama) converts each chunk into 768-dimensional vectors → <strong>Qdrant vector database</strong> stores vectors with original text for fast similarity search</p>
<p><strong>Query Processing (When You Ask Questions):</strong> Question → <strong>nomic-embed-text model</strong> converts question to 768-dimensional vector → <strong>Qdrant vector database</strong> searches and finds 5 most similar document chunks → Open WebUI compiles relevant text excerpts → Enhanced prompt (question + retrieved excerpts) sent to <strong>Ollama LLM</strong> → AI generates grounded response with source attribution</p>
<p><strong>The complete document processing + RAG orchestration</strong>: Open WebUI handles this entire workflow behind the scenes:</p>
<ul>
<li><p><strong>Apache Tika</strong> extracts text from documents like PDFs, Word docs, and text files (with automatic OCR fallback for scanned content)</p>
</li>
<li><p><strong>nomic-embed-text</strong> converts extracted content into 768-dimensional vectors</p>
</li>
<li><p><strong>Qdrant</strong> stores and indexes vectors for fast similarity search</p>
</li>
<li><p><strong>Open WebUI</strong> orchestrates the complete pipeline from upload to intelligent response</p>
</li>
</ul>
<p>What typically requires complex document processing and RAG engineering becomes simple upload and query. Your documents transform into a searchable knowledge base within seconds.</p>
<h1>Setting up the document processing and RAG components</h1>
<p>Now that you understand the concepts, let's examine the technical implementation that makes everything work.</p>
<h2>System requirements</h2>
<table>
<thead>
<tr>
<th>Component</th>
<th>Memory</th>
<th>Capabilities</th>
<th>Storage Impact</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Apache Tika</strong></td>
<td>4GB</td>
<td>Text extraction from documents like PDFs, Word docs, and text files (with OCR for scanned content)</td>
<td>~2MB per processed document</td>
</tr>
<tr>
<td><strong>nomic-embed-text</strong></td>
<td>275MB</td>
<td>Document embedding generation</td>
<td>Creates 768-dimensional vectors</td>
</tr>
<tr>
<td><strong>Qdrant</strong></td>
<td>2GB</td>
<td>Vector storage and similarity search</td>
<td>~50KB per document chunk</td>
</tr>
<tr>
<td><strong>Overall system</strong></td>
<td>24GB+</td>
<td>Process 100+ documents concurrently</td>
<td>Scales to 10,000+ documents</td>
</tr>
<tr>
<td><strong>Storage growth</strong></td>
<td>50GB+</td>
<td>Raw documents + vector indexes</td>
<td>~5MB total per average business document</td>
</tr>
</tbody></table>
<p><em>Performance estimates are rough approximations based on system understanding and real-world usage patterns.</em></p>
<h1>Putting it all together</h1>
<p>Here's our enhanced <code>docker-compose.yml</code> that adds complete document processing and RAG capabilities to our foundation:</p>
<pre><code class="language-yaml">version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    container_name: postgres
    ports:
      - '5432:5432'
    environment:
      - POSTGRES_DB=openwebui
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=securepassword123
    volumes:
      - ./volumes/postgres/data:/var/lib/postgresql/data
    restart: unless-stopped
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres -d openwebui']
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - local-ai-network

  pgadmin:
    image: dpage/pgadmin4:latest
    container_name: pgadmin
    ports:
      - '5050:80'
    environment:
      - PGADMIN_DEFAULT_EMAIL=admin@local.ai
      - PGADMIN_DEFAULT_PASSWORD=admin123
      - PGADMIN_CONFIG_SERVER_MODE=False
      - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False
    volumes:
      - ./volumes/pgadmin:/var/lib/pgadmin
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - local-ai-network

  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: webui
    ports:
      - '3000:8080'
    volumes:
      - ./volumes/open-webui/data:/app/backend/data
    environment:
      # Ollama connection
      - OLLAMA_BASE_URL=http://host.docker.internal:11434

      # Database connection
      - DATABASE_URL=postgresql://postgres:securepassword123@postgres:5432/openwebui

      # Basic settings
      - WEBUI_SECRET_KEY=your-secret-key-here
      - WEBUI_AUTH=true
      - ENABLE_SIGNUP=true
      - DEFAULT_MODELS=qwen3:8b
      
      # Document Processing + RAG Configuration (NEW)
      - UPLOAD_DIR=/app/backend/data/uploads
      - ENABLE_PERSISTENT_CONFIG=false
      - CONTENT_EXTRACTION_ENGINE=tika
      - TIKA_SERVER_URL=http://tika:9998
      - VECTOR_DB=qdrant
      - QDRANT_URI=http://qdrant:6333/
      - QDRANT_COLLECTION_PREFIX=document_chunks
      - RAG_EMBEDDING_MODEL=nomic-embed-text
      - RAG_EMBEDDING_ENGINE=ollama
      - ENABLE_RAG_HYBRID_SEARCH=false
      - RAG_RELEVANCE_THRESHOLD=0.75
      - CHUNK_SIZE=1000
      - CHUNK_OVERLAP=100

    extra_hosts:
      - 'host.docker.internal:host-gateway'
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - local-ai-network

  qdrant:
    image: qdrant/qdrant:latest
    container_name: qdrant
    ports:
      - '6333:6333'
      - '6334:6334'
    environment:
      - QDRANT__SERVICE__HTTP_PORT=6333
      - QDRANT__SERVICE__GRPC_PORT=6334
    volumes:
      - ./volumes/qdrant/storage:/qdrant/storage
    restart: unless-stopped
    networks:
      - local-ai-network

  tika:
    image: apache/tika:latest-full
    container_name: tika-server
    ports:
      - '9998:9998'
    environment:
      - TIKA_CONFIG=/opt/tika/config/tika-config.xml
      - JAVA_OPTS=-Xmx2g -Xms512m -Dfile.encoding=UTF-8 -Djava.awt.headless=true
      - TIKA_OCR_LANGUAGE=eng
      - TIKA_PDF_OCR_STRATEGY=OCR_AND_TEXT_EXTRACTION
    volumes:
      - ../shared/tika/config:/opt/tika/config
    command:
      - --host=0.0.0.0
      - --port=9998
      - --config=/opt/tika/config/tika-config.xml
    restart: unless-stopped
    networks:
      - local-ai-network

networks:
  local-ai-network:
    driver: bridge
</code></pre>
<p><strong>What's new from Part 1:</strong></p>
<p>This Docker Compose builds on your existing Part 1 setup by adding two key containers:</p>
<ul>
<li><p><strong>Qdrant</strong>: Vector database for storing document embeddings</p>
</li>
<li><p><strong>Apache Tika</strong>: Text extraction engine for documents like PDFs, Word docs, and text files</p>
</li>
</ul>
<p><strong>Key configuration changes:</strong></p>
<ul>
<li><p><code>ENABLE_PERSISTENT_CONFIG=false</code>: Lets these new document processing + RAG settings override Part 1 configs</p>
</li>
<li><p><code>CONTENT_EXTRACTION_ENGINE=tika</code>: Routes document processing through the new Tika container</p>
</li>
<li><p><code>RAG_RELEVANCE_THRESHOLD=0.75</code>: Sets search quality threshold</p>
</li>
<li><p><code>QDRANT_COLLECTION_PREFIX=document_chunks</code>: Names your vector storage collections</p>
</li>
<li><p><code>TIKA_PDF_OCR_STRATEGY=OCR_AND_TEXT_EXTRACTION</code>: Extracts machine-readable text first, then uses OCR for scanned content within documents</p>
</li>
<li><p><code>CHUNK_SIZE=1000</code> &amp; <code>CHUNK_OVERLAP=100</code>: Document splitting settings for better search</p>
</li>
</ul>
<h2>Tika Configuration</h2>
<p><a href="https://github.com/FarzamMohammadi/self-hosted-ai-stack">The repository</a> includes a <a href="https://github.com/FarzamMohammadi/self-hosted-ai-stack/blob/main/shared/tika/config/tika-config.xml">pre-configured</a> <code>tika-config.xml</code> <a href="https://github.com/FarzamMohammadi/self-hosted-ai-stack/blob/main/shared/tika/config/tika-config.xml">file</a> mounted into the Tika container via the volume mapping <code>../shared/tika/config:/opt/tika/config</code> (shown on line 305 of the Docker Compose file). This configuration contains simple yet optimized settings for document processing, including PDF text extraction and OCR fallback capabilities, plus resource limits to prevent excessive usage during document processing.</p>
<h1>Testing your document processing + RAG setup</h1>
<p>Time to verify everything works as expected. Let's systematically test each component:</p>
<h2>1. Container health check</h2>
<pre><code class="language-bash">docker compose ps
</code></pre>
<p>All five containers should show as running:</p>
<ul>
<li><p><code>postgres</code> (healthy)</p>
</li>
<li><p><code>pgadmin</code></p>
</li>
<li><p><code>webui</code></p>
</li>
<li><p><code>qdrant</code></p>
</li>
<li><p><code>tika-server</code></p>
</li>
</ul>
<h2>2. Verify service endpoints</h2>
<p><strong>Qdrant Database:</strong></p>
<ul>
<li><p>Health check: Open <a href="http://localhost:6333/healthz">http://localhost:6333/healthz</a> - should show "healthz check successful"</p>
</li>
<li><p>Dashboard: Browse <a href="http://localhost:6333/dashboard">http://localhost:6333/dashboard</a> - explore your document collections and vector data</p>
</li>
</ul>
<p><strong>Tika server:</strong></p>
<ul>
<li><p>Quick version check: Open <a href="http://localhost:9998/version">http://localhost:9998/version</a> - shows Tika version info</p>
</li>
<li><p>All available routes: Browse <a href="http://localhost:9998/">http://localhost:9998/</a> - lists all Tika API endpoints</p>
</li>
</ul>
<p><strong>Ollama with both models:</strong></p>
<pre><code class="language-bash">ollama list
</code></pre>
<p>Should list both <code>qwen3:8b</code> and <code>nomic-embed-text</code>.</p>
<h2>3. Upload and test different document types</h2>
<ol>
<li><p>Navigate to <a href="http://localhost:3000">http://localhost:3000</a></p>
</li>
<li><p>Sign in with your existing account</p>
</li>
<li><p>Look for the document upload icon in the bottom left corner of the chat</p>
</li>
<li><p><strong>Upload documents</strong>: PDFs, Word docs, or text files</p>
</li>
<li><p>Wait for processing to complete</p>
</li>
<li><p>Ask specific questions about the document content</p>
</li>
</ol>
<p><strong>Test questions for document processing:</strong></p>
<ul>
<li><p>"What's the total amount on this invoice?"</p>
</li>
<li><p>"Extract the key information from this document"</p>
</li>
<li><p>"What are the main points in this document?"</p>
</li>
</ul>
<p><strong>Test questions for RAG analysis:</strong></p>
<ul>
<li><p>"What are the main topics covered in this document?"</p>
</li>
<li><p>"Summarize the key findings"</p>
</li>
<li><p>"What does the document say about [specific topic]?"</p>
</li>
</ul>
<h2>4. Verify vector storage</h2>
<p>Check that Qdrant is storing your document vectors: <a href="http://localhost:6333/dashboard#/collections">http://localhost:6333/dashboard#/collections</a></p>
<p>You should see collections created for your uploaded documents.</p>
<h2>5. View extracted text (optional)</h2>
<p>Want to see exactly what text was extracted from your uploaded documents? This helps verify text extraction accuracy (including OCR when used) and understand what content the AI uses.</p>
<ol>
<li><p>Visit <a href="http://localhost:5050">http://localhost:5050</a></p>
</li>
<li><p>Login with <code>admin@local.ai</code> / <code>admin123</code></p>
</li>
<li><p>If you haven't already connected to the database, add a server connection:</p>
<ol>
<li><p>Right click on <strong>Servers</strong> → <strong>Register</strong> → <strong>Server</strong></p>
</li>
<li><p><strong>General</strong> tab → <strong>Name</strong>: <code>local-ai</code></p>
</li>
<li><p><strong>Connection</strong> tab:</p>
<ul>
<li><p><strong>Host name/address</strong>: <code>postgres</code></p>
</li>
<li><p><strong>Port</strong>: <code>5432</code></p>
</li>
<li><p><strong>Maintenance database</strong>: <code>postgres</code></p>
</li>
<li><p><strong>Username</strong>: <code>postgres</code></p>
</li>
<li><p><strong>Password</strong>: <code>securepassword123</code></p>
</li>
</ul>
</li>
<li><p>Click <strong>Save</strong></p>
</li>
</ol>
</li>
<li><p>Navigate to <strong>local-ai</strong> → <strong>Databases</strong> → <strong>openwebui</strong> → <strong>Schemas</strong> → <strong>public</strong> → <strong>Tables</strong> → <strong>file</strong></p>
</li>
<li><p>Right-click on the <strong>file</strong> table and select <strong>View/Edit Data</strong> → <strong>All Rows</strong></p>
</li>
<li><p>Look at the <strong>data</strong> column to see the extracted text from your uploaded documents</p>
</li>
</ol>
<p><strong>What you'll see:</strong></p>
<ul>
<li><p>The <code>filename</code> column shows your original file names</p>
</li>
<li><p>The <code>data</code> column contains the full extracted text from each document</p>
</li>
<li><p>This is exactly what the AI uses to answer your questions about the documents</p>
</li>
</ul>
<p><strong>Troubleshooting tip:</strong> If the <code>data</code> column is empty or contains gibberish, the document may be corrupted or contain primarily images. This setup works best with standard document formats and doesn't process standalone image files.</p>
<h2>Troubleshooting common issues</h2>
<table>
<thead>
<tr>
<th>Issue</th>
<th>Quick Fix</th>
<th>Details</th>
</tr>
</thead>
<tbody><tr>
<td>Document upload fails</td>
<td>Check <code>docker compose logs tika-server</code></td>
<td>Sweet spot: 5 to 15MB files</td>
</tr>
<tr>
<td>No RAG responses</td>
<td>Verify <code>ollama list</code> includes both models</td>
<td>First upload takes 2 to 3 times longer</td>
</tr>
<tr>
<td>RAG responses seem inaccurate</td>
<td>Check chunk size and overlap settings</td>
<td>Adjust <code>CHUNK_SIZE=500</code> for technical docs</td>
</tr>
<tr>
<td>Tika memory errors</td>
<td>Increase to <code>-Xmx6g</code> or <code>-Xmx8g</code></td>
<td>4GB handles most documents</td>
</tr>
<tr>
<td>No text extracted</td>
<td>Standalone image file uploaded (PNG, JPEG, etc.)</td>
<td>Use vision models for standalone image files</td>
</tr>
<tr>
<td>System memory issues</td>
<td>Monitor with <code>docker stats</code></td>
<td>16GB minimum, 24GB comfortable</td>
</tr>
<tr>
<td>Vector search too slow</td>
<td>Check Qdrant storage space</td>
<td>Database performance degrades when disk is full</td>
</tr>
</tbody></table>
<h1>What's next</h1>
<p>You've successfully built a sophisticated document processing and RAG-enabled document intelligence system! Your self-hosted stack now includes:</p>
<p>✅ <strong>Ollama</strong> serving your LLM and embedding models</p>
<p>✅ <strong>Open WebUI</strong> with complete document intelligence</p>
<p>✅ <strong>PostgreSQL</strong> storing conversations and configurations</p>
<p>✅ <strong>Qdrant</strong> powering semantic document search and retrieval</p>
<p>✅ <strong>Apache Tika</strong> providing text extraction from documents like PDFs, Word docs, and text files (with OCR capabilities for scanned content)</p>
<h2>Coming up in Part 3</h2>
<p>We'll build a <strong>visual data extraction pipeline</strong> with:</p>
<ul>
<li><p><strong>Web upload interface</strong> for batch image processing</p>
</li>
<li><p><strong>N8N workflows</strong> orchestrating the extraction pipeline</p>
</li>
<li><p><strong>Vision Language Models (VLM)</strong> via Ollama for analyzing images</p>
</li>
<li><p><strong>PostgreSQL storage</strong> for tracking jobs and extracted structured data</p>
</li>
<li><p><strong>Automated job processing</strong> with retry logic and error handling</p>
</li>
</ul>
<p>This adds the ability to extract structured data from images at scale, perfect for processing receipts, invoices, forms, or any visual documents.</p>
<h2>Homework before Part 3</h2>
<p>Explore your new document processing and RAG capabilities:</p>
<ul>
<li><p><strong>Test document processing</strong>: Upload PDFs, Word docs, and text files</p>
</li>
<li><p><strong>Test RAG</strong>: Upload various documents and ask complex questions</p>
</li>
<li><p><strong>Multi-language</strong>: Review the <code>tika-config.xml</code> file in <code>self-hosted-ai-stack/shared/</code> directory to understand language settings, add new languages to it, then test documents in different languages (hands-on way to learn Tika configuration)</p>
</li>
<li><p><strong>Multi-format</strong>: Try Word docs, PowerPoint slides, Excel sheets (remember: this processes standard document formats, but not standalone image files)</p>
</li>
<li><p><strong>Advanced queries</strong>: Try complex multi-document queries across different formats</p>
</li>
<li><p><strong>Monitor processing</strong>: Browse vector collections in Qdrant's web interface at <a href="http://localhost:6333/dashboard">http://localhost:6333/dashboard</a></p>
</li>
</ul>
<h2>Helpful resources</h2>
<ul>
<li><p><a href="https://docs.openwebui.com/features/rag">Open WebUI RAG Documentation</a></p>
</li>
<li><p><a href="https://qdrant.tech/documentation/">Qdrant Documentation</a></p>
</li>
<li><p><a href="https://tika.apache.org/2.9.1/formats.html">Apache Tika Supported Formats</a></p>
</li>
<li><p><a href="https://ollama.com/search?q=embed">Ollama Embedding Models</a></p>
</li>
</ul>
<hr />
<p><em>This is part of my "Complete Self-Hosted AI Infrastructure" series. Follow along as we build increasingly sophisticated AI capabilities, all running self-hosted on your machine.</em></p>
]]></content:encoded></item><item><title><![CDATA[Setting Up Your Self-Hosted AI Stack - Part 1: Building the foundation with Open WebUI, Ollama, and Postgres]]></title><description><![CDATA[What we're building today
By the end of this post, you'll have a fully operational chat interface connected to the LLM of your choice, running completely locally. No external APIs, no data leaving you]]></description><link>https://triedandtestedbuilds.com/self-hosted-ai-stack-part-1</link><guid isPermaLink="true">https://triedandtestedbuilds.com/self-hosted-ai-stack-part-1</guid><category><![CDATA[self-hosted-ai]]></category><category><![CDATA[self-hosted]]></category><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[open-webui]]></category><category><![CDATA[ollama]]></category><category><![CDATA[PostgreSQL]]></category><dc:creator><![CDATA[Farzam Mohammadi]]></dc:creator><pubDate>Sun, 03 Aug 2025 18:14:42 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754243635149/0c7ab81e-b455-494c-a494-3b6ceafcebb6.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>What we're building today</h1>
<p>By the end of this post, you'll have a fully operational chat interface connected to the LLM of your choice, running completely locally. No external APIs, no data leaving your machine. Just pure local AI power.</p>
<p>We'll implement and orchestrate the foundational self-hosted LLM inference tools that form the backbone for everything we'll build in this series.</p>
<h1>The bigger picture</h1>
<p>I'm currently building a self-hosted AI stack from scratch, and I'm documenting everything as I go. I want to capture the knowledge while it's fresh and share the real-world lessons I've learned along the way.</p>
<p>Here's the thing: even with today's resources, piecing together a solid self-hosted AI stack is surprisingly challenging. The more complex your needs get, the scarcer the guidance becomes. This series is the comprehensive guide I wish I had when I started this journey. At least this way, people like you and me who are totally new to it can get up and start quickly, then evolve and expand from there.</p>
<p>What's coming up (outline may evolve as I discover better integration approaches while writing):</p>
<ul>
<li><p><a href="https://triedandtestedbuilds.com/self-hosted-ai-stack-part-2"><strong>Part 2</strong>: Document Processing and RAG with Apache Tika and Qdrant Vector Database</a></p>
</li>
<li><p><a href="https://triedandtestedbuilds.com/self-hosted-ai-stack-part-3"><strong>Part 3: Visual Data Extraction</strong>: Web upload interface with N8N workflows and Vision Language Models for extracting structured data from images</a></p>
</li>
<li><p><strong>Part 4: Model Superpowers</strong>: Advanced WebUI configuration with tools and knowledge integration</p>
</li>
<li><p><strong>Part 5: Intelligent Automation</strong>: WebUI filters and N8N workflows for content processing</p>
</li>
</ul>
<h1>Prerequisites</h1>
<ul>
<li><p>A machine with at least 16GB of RAM and 50GB of disk space</p>
</li>
<li><p>Docker installed</p>
</li>
<li><p>GPU recommended for better performance. Without a GPU, expect slower responses (10-30 seconds vs 1-3 seconds) as models run on CPU. Still totally usable, just requires patience. If performance is too slow, consider downloading a smaller model.</p>
</li>
</ul>
<h1>Credits where credits are due</h1>
<p>Massive thanks to the creators and contributors of these incredible open source projects that make all this possible:</p>
<ul>
<li><p><a href="https://ollama.com">Ollama</a> - Repository: <a href="https://github.com/ollama/ollama">ollama/ollama</a></p>
</li>
<li><p><a href="https://openwebui.com">Open WebUI</a> - Repository: <a href="https://github.com/open-webui/open-webui">open-webui/open-webui</a></p>
</li>
<li><p><a href="https://postgresql.org/">PostgreSQL</a> - Repository: <a href="https://github.com/postgres/postgres">postgres/postgres</a></p>
</li>
<li><p><a href="https://www.pgadmin.org/">pgadmin</a> - Repository: <a href="https://github.com/pgadmin-org/pgadmin4">pgadmin-org/pgadmin4</a></p>
</li>
<li><p><a href="https://docker.com/">Docker</a> - Repositories: <a href="https://github.com/moby/moby">moby/moby</a> &amp; <a href="https://github.com/docker/compose">docker/compose</a></p>
</li>
</ul>
<h1>Our foundation stack</h1>
<p>We're building with battle-tested open source tools that work beautifully together. This foundation stays simple and robust, perfect for expanding on in later parts.</p>
<p>Here's what we're working with:</p>
<ul>
<li><p><strong>Ollama</strong> - Self-hosted LLM hosting made simple</p>
</li>
<li><p><strong>Open WebUI</strong> - Beautiful and feature-rich chat UI for LLMs</p>
</li>
<li><p><strong>PostgreSQL</strong> - Reliable data storage for conversations and configs</p>
</li>
<li><p><strong>pgadmin</strong> - Web-based administration UI for PostgreSQL</p>
</li>
<li><p><strong>Docker &amp; Docker Compose</strong> - Container orchestration to tie it all together</p>
</li>
</ul>
<p>Why these specific tools? They're what I'm actually running in production, and I know their quirks inside and out. Feel free to swap any component for your preferred alternatives. The architecture stays the same.</p>
<h1>Quick start (skip explanations, just get it running)</h1>
<ol>
<li><p><strong>Install Ollama</strong>: Head to <a href="https://ollama.com/download">https://ollama.com/download</a></p>
<blockquote>
<p><strong>Mac users</strong>: Use the app instead of Docker. My experience with Docker on an M4 MacBook was rough. The containerized version couldn't access my GPU, making everything painfully slow.</p>
</blockquote>
</li>
<li><p><strong>Download a model</strong> (this pulls and runs it):</p>
<pre><code class="language-bash">ollama pull qwen3:8b
</code></pre>
<blockquote>
<p><strong>Why qwen3:8b?</strong> It hits the sweet spot between performance and compatibility. Since LLM usage demands significant computing power, this model is small enough to run on most machines without specialized hardware. Check out other options at <a href="https://ollama.com/search">https://ollama.com/search</a>.</p>
</blockquote>
</li>
<li><p><strong>Verify your model downloaded</strong>:</p>
<pre><code class="language-bash">ollama list
</code></pre>
<p>You should see <code>qwen3:8b</code> listed in the output:</p>
<pre><code class="language-plaintext">NAME                                   ID              SIZE      MODIFIED    
qwen3:8b                               bdbd181c33f2    5.3 GB    1 hour ago
</code></pre>
<blockquote>
<p><strong>Starting Ollama</strong>: If you ever need to get Ollama running, you can either start the app from your Applications folder or run <code>ollama serve</code> in your terminal. If you see "Error: listen tcp 127.0.0.1:11434: bind: address already in use", that means Ollama is already running.</p>
</blockquote>
</li>
<li><p><strong>Clone the repository</strong>:</p>
<pre><code class="language-bash">git clone https://github.com/FarzamMohammadi/self-hosted-ai-stack
</code></pre>
</li>
<li><p><strong>Navigate to the foundation</strong>:</p>
<pre><code class="language-bash">cd part-1-building-the-foundation
</code></pre>
</li>
<li><p><strong>Fire it up</strong> (make sure Docker is running):</p>
<pre><code class="language-bash">docker compose up -d
</code></pre>
</li>
</ol>
<p>That's it! Open <a href="http://localhost:3000">http://localhost:3000</a>, sign up, select qwen3:8b in the top-left corner, and start chatting with your self-hosted AI.</p>
<h1>Understanding what you built</h1>
<h2>Ollama</h2>
<p>Ollama makes self-hosted LLM hosting dead simple. It's built on top of the excellent <a href="https://github.com/ggerganov/llama.cpp">llama.cpp</a> project, trading some customization for incredible ease of use. Installation takes minutes, and Ollama automatically detects the optimal configuration for your hardware. No tweaking required. Just download and run.</p>
<h3>Installation options</h3>
<p>You can run Ollama via <a href="#docker-setup-alternative">Docker</a> or as a native app, but this guide uses the native app since that's what our docker-compose setup expects.</p>
<p><strong>Download the native app</strong> from <a href="https://ollama.com/download">https://ollama.com/download</a> and install it following the standard process for your operating system.</p>
<h3>Why I go with the native app</h3>
<p>My experience with Docker on an M4 MacBook was pretty disappointing. The containerized version couldn't tap into my machine's GPU, turning what should be snappy responses into sluggish waits. The native app, however, plays nicely with macOS's Metal framework and delivers the performance you'd expect.</p>
<h3>Choosing your model</h3>
<p>The model library at <a href="https://ollama.com/search">https://ollama.com/search</a> is extensive. You can pull any listed model with a simple command, or even create custom models using <a href="https://ollama.readthedocs.io/en/modelfile">Modelfiles</a>.</p>
<p>For this tutorial, I'm using qwen3:8b. It strikes a great balance between capability and resource requirements:</p>
<pre><code class="language-bash">ollama pull qwen3:8b
</code></pre>
<h3>Docker setup alternative</h3>
<p>If you prefer setting it up via Docker, here's an example setup:</p>
<pre><code class="language-yaml">services:
  ollama:
    image: docker.io/ollama/ollama:latest
    ports:
      - 7869:11434
    volumes:
      - ./ollama/ollama:/root/.ollama
    container_name: ollama
    pull_policy: always
    tty: true
    restart: always
    environment:
      - OLLAMA_KEEP_ALIVE=24h
      - OLLAMA_HOST=0.0.0.0
    networks:
      - ollama-docker

networks:
  ollama-docker:
    external: false
</code></pre>
<p><em>Source:</em> <a href="https://github.com/mythrantic/ollama-docker/blob/main/docker-compose.yml"><em>mythrantic/ollama-docker</em></a></p>
<h2>PostgreSQL &amp; pgadmin</h2>
<p>PostgreSQL becomes our data backbone, storing conversations, configurations, and eventually (in later parts) data for additional services we'll integrate. I went with PostgreSQL because it's bulletproof reliable and Open WebUI supports it excellently. pgadmin gives us a clean web interface for database exploration.</p>
<h3>The configuration</h3>
<p>Keeping it straightforward with just the essentials:</p>
<pre><code class="language-yaml">services:
  postgres:
    image: postgres:15-alpine
    container_name: postgres
    ports:
      - '5432:5432'
    environment:
      - POSTGRES_DB=openwebui
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=securepassword123
    volumes:
      - ./volumes/postgres/data:/var/lib/postgresql/data
    restart: unless-stopped
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres -d openwebui']
      interval: 10s
      timeout: 5s
      retries: 5

  pgadmin:
    image: dpage/pgadmin4:latest
    container_name: pgadmin
    ports:
      - '5050:80'
    environment:
      - PGADMIN_DEFAULT_EMAIL=admin@local.ai
      - PGADMIN_DEFAULT_PASSWORD=admin123
      - PGADMIN_CONFIG_SERVER_MODE=False
      - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False
    volumes:
      - ./volumes/pgadmin:/var/lib/pgadmin
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
</code></pre>
<p><strong>Configuration notes:</strong></p>
<ul>
<li><p>Using <code>postgres:15-alpine</code> for the right balance of size and features</p>
</li>
<li><p>The healthcheck prevents connection race conditions</p>
</li>
<li><p>pgadmin runs in desktop mode since this is local-only</p>
</li>
<li><p>Simple credentials for localhost. Change them if you expose this externally</p>
</li>
</ul>
<h2>Open WebUI</h2>
<p>Open WebUI delivers the ChatGPT experience, basically a nice chat interface for streamlined interaction with LLMs, but running entirely on your machine. After testing various interfaces (text-generation-webui, SillyTavern, and others), Open WebUI won me over with its clean design and incredible customization options.</p>
<h3>What makes it special</h3>
<ul>
<li><p><strong>Clean interface</strong> that actually works without fuss</p>
</li>
<li><p><strong>Built-in RAG support</strong> (we'll dig into this in Part 2)</p>
</li>
<li><p><strong>Seamless Ollama integration</strong></p>
</li>
<li><p><strong>Excellent APIs</strong> for integration with your own projects</p>
</li>
<li><p><strong>Active development</strong> with a supportive community</p>
</li>
</ul>
<h3>Setting it up</h3>
<p>Here's the configuration that ties everything together:</p>
<pre><code class="language-yaml">services:
  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: webui
    ports:
      - '3000:8080'
    volumes:
      - ./volumes/open-webui/data:/app/backend/data
    environment:
      # Ollama connection
      - OLLAMA_BASE_URL=http://host.docker.internal:11434
      
      # Database connection
      - DATABASE_URL=postgresql://postgres:securepassword123@postgres:5432/openwebui
      
      # Basic settings
      - WEBUI_SECRET_KEY=your-secret-key-here
      - WEBUI_AUTH=true
      - ENABLE_SIGNUP=true
      - DEFAULT_MODELS=qwen3:8b
      
    extra_hosts:
      - "host.docker.internal:host-gateway"
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
</code></pre>
<blockquote>
<p><strong>Note on extra_hosts</strong>: This setting is required because we're running Ollama as a native app (not in Docker). If you choose to run Ollama in Docker instead, remove the <code>extra_hosts</code> section and update <code>OLLAMA_BASE_URL</code> to use the container name (e.g., <code>http://ollama:11434</code>).</p>
</blockquote>
<p><strong>Key settings explained:</strong></p>
<ul>
<li><p><code>OLLAMA_BASE_URL</code> points to our native Ollama app via <code>host.docker.internal</code></p>
</li>
<li><p>Database URL connects to our PostgreSQL container</p>
</li>
<li><p>Auth is enabled. Disable for single-user setups if preferred</p>
</li>
<li><p><code>DEFAULT_MODELS</code> should match your downloaded model</p>
</li>
</ul>
<h1>Putting it all together</h1>
<p>Time to wire everything up. Here's the complete <code>docker-compose.yml</code> that orchestrates our entire self-hosted AI stack.</p>
<h2>The complete configuration</h2>
<p>Create a new directory for your project and drop in this <code>docker-compose.yml</code>:</p>
<pre><code class="language-yaml">version: '3.8'

services:
  postgres:
    image: postgres:15-alpine
    container_name: postgres
    ports:
      - '5432:5432'
    environment:
      - POSTGRES_DB=openwebui
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=securepassword123
    volumes:
      - ./volumes/postgres/data:/var/lib/postgresql/data
    restart: unless-stopped
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U postgres -d openwebui']
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - local-ai-network

  pgadmin:
    image: dpage/pgadmin4:latest
    container_name: pgadmin
    ports:
      - '5050:80'
    environment:
      - PGADMIN_DEFAULT_EMAIL=admin@local.ai
      - PGADMIN_DEFAULT_PASSWORD=admin123
      - PGADMIN_CONFIG_SERVER_MODE=False
      - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False
    volumes:
      - ./volumes/pgadmin:/var/lib/pgadmin
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - local-ai-network

  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: webui
    ports:
      - '3000:8080'
    volumes:
      - ./volumes/open-webui/data:/app/backend/data
    environment:
      # Ollama connection
      - OLLAMA_BASE_URL=http://host.docker.internal:11434

      # Database connection
      - DATABASE_URL=postgresql://postgres:securepassword123@postgres:5432/openwebui

      # Basic settings
      - WEBUI_SECRET_KEY=your-secret-key-here
      - WEBUI_AUTH=true
      - ENABLE_SIGNUP=true
      - DEFAULT_MODELS=qwen3:8b

    extra_hosts:
      - 'host.docker.internal:host-gateway'
    restart: unless-stopped
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - local-ai-network

networks:
  local-ai-network:
    driver: bridge
</code></pre>
<h2>Starting the stack</h2>
<p>Three simple steps:</p>
<ol>
<li><p><strong>Make sure Docker is running</strong></p>
</li>
<li><p><strong>Confirm Ollama is active</strong>: Run <code>ollama serve</code> in your terminal. If it starts, great! If you see "Error: listen tcp 127.0.0.1:11434: bind: address already in use", that means Ollama is already running</p>
</li>
<li><p><strong>Fire it up</strong> from your project directory:</p>
</li>
</ol>
<pre><code class="language-bash">docker compose up -d
</code></pre>
<p>The <code>-d</code> flag runs everything in the background. First startup takes a few minutes while Docker downloads the images.</p>
<p>Once everything is up and running, head to <a href="http://localhost:3000">http://localhost:3000</a> and you'll need to create an account before you can start using the interface. The first user to sign up automatically becomes the admin.</p>
<h1>Testing your setup</h1>
<p>Let's verify everything works. Here's my quick validation routine:</p>
<h2>1. Container health check</h2>
<pre><code class="language-bash">docker compose ps
</code></pre>
<p>All three containers should show as running. If any show "Exited", investigate with:</p>
<pre><code class="language-bash">docker compose logs [service-name]
</code></pre>
<h2>2. Verify Ollama connection</h2>
<p>Quick test to ensure Ollama responds:</p>
<p><strong>Mac/Linux/Windows Command Prompt:</strong></p>
<pre><code class="language-bash">curl http://localhost:11434/api/tags
</code></pre>
<p><strong>Windows PowerShell:</strong></p>
<pre><code class="language-powershell">Invoke-RestMethod http://localhost:11434/api/tags
</code></pre>
<p><strong>Browser fallback:</strong> Open <a href="http://localhost:11434/api/tags">http://localhost:11434/api/tags</a> in your browser</p>
<p>You should see your downloaded models in the response.</p>
<h2>3. Access the web interface</h2>
<ol>
<li><p>Navigate to <a href="http://localhost:3000">http://localhost:3000</a></p>
</li>
<li><p>Create an account (first user becomes admin automatically)</p>
</li>
<li><p>You should land on the clean chat interface</p>
</li>
</ol>
<h2>4. First conversation</h2>
<ol>
<li><p>The qwen3:8b model should be selected by default (since we set it as <code>DEFAULT_MODELS</code> in our docker-compose). If it isn't, you can find the model selector in the top-left corner</p>
</li>
<li><p>Choose qwen3:8b if not already selected</p>
</li>
<li><p>Send a test message and wait for the response</p>
</li>
</ol>
<p>Slow responses? Check that Ollama is running and your model downloaded completely.</p>
<h2>5. Peek at the database (optional)</h2>
<p>Curious about what's happening under the hood?</p>
<ol>
<li><p>Visit <a href="http://localhost:5050">http://localhost:5050</a></p>
</li>
<li><p>Login with <code>admin@local.ai</code> / <code>admin123</code></p>
</li>
<li><p>Add a server connection:</p>
<ol>
<li><p>Right click on <strong>Servers</strong> (in the left side menu) → <strong>Register</strong> → <strong>Server</strong></p>
</li>
<li><p>In the <strong>General</strong> tab → <strong>Name</strong>: <code>local-ai</code></p>
</li>
<li><p>Switch to the <strong>Connection</strong> tab and enter:</p>
<ul>
<li><p><strong>Host name/address</strong>: <code>postgres</code></p>
</li>
<li><p><strong>Port</strong>: <code>5432</code></p>
</li>
<li><p><strong>Maintenance database</strong>: <code>postgres</code></p>
</li>
<li><p><strong>Username</strong>: <code>postgres</code></p>
</li>
<li><p><strong>Password</strong>: <code>securepassword123</code></p>
</li>
</ul>
</li>
<li><p>Click <strong>Save</strong></p>
</li>
<li><p>Once connected, navigate to <strong>local-ai</strong> → <strong>Databases</strong> → <strong>openwebui</strong> → <strong>Schemas</strong> → <strong>public</strong> → <strong>Tables</strong> to see the OpenWebUI tables that were created automatically when it connected</p>
</li>
</ol>
</li>
</ol>
<h2>Troubleshooting common issues</h2>
<p><strong>"Cannot connect to Ollama"</strong></p>
<ul>
<li><p>Verify Ollama is actually running (check system tray)</p>
</li>
<li><p>If using Docker Ollama, double-check port mappings</p>
</li>
</ul>
<p><strong>"Database connection failed"</strong></p>
<ul>
<li><p>Give PostgreSQL more initialization time</p>
</li>
<li><p>Confirm postgres container health: <code>docker compose ps</code></p>
</li>
</ul>
<p><strong>"Port already in use"</strong></p>
<ul>
<li><p>Modify port mappings in docker-compose.yml</p>
</li>
<li><p>Stop whatever service is occupying those ports</p>
</li>
</ul>
<p><strong>"Performance is awful"</strong></p>
<ul>
<li><p>Usually means Ollama can't access your GPU</p>
</li>
<li><p>On Mac, stick with the native app over Docker</p>
</li>
<li><p>Try a smaller model if resources are tight</p>
</li>
</ul>
<h1>What's next</h1>
<p>Congratulations! You've built a solid foundation for your self-hosted AI stack. You're now running:</p>
<p>✅ <strong>Ollama</strong> serving your self-hosted LLM<br />✅ <strong>Open WebUI</strong> providing a beautiful chat interface<br />✅ <strong>PostgreSQL</strong> storing conversations and configurations<br />✅ <strong>pgadmin</strong> for database management</p>
<p>This foundation gives us a rock-solid base for the advanced capabilities we'll add in upcoming parts.</p>
<h2>Coming up in Part 2</h2>
<p>We'll supercharge our setup with <strong>RAG (Retrieval-Augmented Generation)</strong> by adding:</p>
<ul>
<li><p><strong>Apache Tika</strong> for document processing (PDFs, Word docs, images, etc.)</p>
</li>
<li><p><strong>Qdrant</strong> vector database for semantic search</p>
</li>
<li><p>Document upload and intelligent retrieval through Open WebUI</p>
</li>
</ul>
<p>Everything we've built today will integrate seamlessly with these new components.</p>
<h2>Homework before Part 2</h2>
<p>Take some time to explore what we've built:</p>
<ul>
<li><p>Try different models (<code>llama3.2</code>, <code>codellama</code>, <code>mistral</code>)</p>
</li>
<li><p>Experiment with Open WebUI's settings and themes</p>
</li>
<li><p>Upload some documents and see how basic file handling works</p>
</li>
<li><p>Poke around the conversation history in pgadmin</p>
</li>
</ul>
<h2>Helpful resources</h2>
<ul>
<li><p><a href="https://docs.openwebui.com/">Open WebUI Documentation</a></p>
</li>
<li><p><a href="https://ollama.com/library">Ollama Model Library</a></p>
</li>
<li><p><a href="https://www.postgresql.org/docs/current/tutorial.html">PostgreSQL Basics</a></p>
</li>
</ul>
<hr />
<p><em>This is part of my "Complete Self-Hosted AI Infrastructure" series. Follow along as we build increasingly sophisticated AI capabilities, all running self-hosted on your machine.</em></p>
]]></content:encoded></item><item><title><![CDATA[Blue-Green Deployments: Your Ticket to Stress-Free Software Releases]]></title><description><![CDATA[Introduction: The Art of Painless Deployments
Howdy! I'm back with some fresh insights and experiences to share. This time, we're diving deep into the world of release deployments, with a special focus on a strategy that might just revolutionize your...]]></description><link>https://triedandtestedbuilds.com/blue-green-deployments-your-ticket-to-stress-free-software-releases</link><guid isPermaLink="true">https://triedandtestedbuilds.com/blue-green-deployments-your-ticket-to-stress-free-software-releases</guid><category><![CDATA[Blue/Green deployment]]></category><category><![CDATA[Kubernetes]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[CI/CD]]></category><dc:creator><![CDATA[Farzam Mohammadi]]></dc:creator><pubDate>Sun, 25 Aug 2024 18:21:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724605042853/04efbb92-c094-4ae6-8eb3-f9f6b8c85b5a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction-the-art-of-painless-deployments">Introduction: The Art of Painless Deployments</h1>
<p>Howdy! I'm back with some fresh insights and experiences to share. This time, we're diving deep into the world of release deployments, with a special focus on a strategy that might just revolutionize your approach: Blue-Green deployments.</p>
<blockquote>
<p>Blue-green deployment is a release strategy used in software development to reduce downtime and risk during updates.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724605695127/3edcd592-b731-4c64-a4c0-a50ad74a6170.gif" alt class="image--center mx-auto" /></p>
<p>Source: <a target="_blank" href="https://medium.com/@aelson.alves/blue-green-deployment-142568063884"><strong>Blue-Green Deployment</strong></a></p>
</blockquote>
<p>But wait, there's more! I'm not flying solo on this mission. My trusty wingman and team lead extraordinaire, <a target="_blank" href="https://basictools.dev/">Jaime Valencia</a>, has joined forces with me to bring you this piece. Why, you ask? Well, we didn't just theorize about this strategy – we rolled up our sleeves, designed it, and implemented it in our own organization. And let me tell you, the benefits were so mind-blowing that we couldn't keep this knowledge to ourselves!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724701341710/1efee6e9-e29f-41c7-a35e-d7c987a68d73.png" alt class="image--center mx-auto" /></p>
<p>By going through the entire lifecycle of developing and implementing this strategy, we gained invaluable insights into its advantages, challenges, and important considerations. In the end, we completely transformed our deployment process, making life easier not just for ourselves, but for all teams involved in deployments. Oh, and did I mention we also supercharged the overall quality of our deliveries?</p>
<p>Now, I know what you're thinking: "Blue-Green deployments? That's not exactly breaking news!" And you're right. If you Google it right now, you'll find a smorgasbord of examples and implementations. But here's the kicker – most of them feel like they're missing something. They're often tailored for individual projects, showing you how to leverage specific containerization features or orchestration mechanisms to achieve a so-called Blue-Green strategy.</p>
<p>For us, the real challenge wasn't implementing this strategy for individual projects. No, no, no. Our Mount Everest was implementing a strategy that could cover our entire project ecosystem. We're talking about deploying multiple interconnected services together and creating a complete isolated testing environment – all without disturbing a single hair on your real users' heads.</p>
<p>So, buckle up! We're about to show you how to set up a Blue-Green release deployment strategy for your entire project suite. We'll walk you through deploying multiple interconnected services together, creating a completely isolated testing environment, and doing it all without your real users even noticing. This approach doesn't just let you test and validate your upcoming production release – it gives you the luxury of time to address any issues that pop up in testing, well ahead of your production release due date.</p>
<h2 id="heading-is-this-for-me-why-should-i-keep-reading">Is This for Me? Why Should I Keep Reading?</h2>
<p>Let's play a quick game of "Does This Sound Familiar?":</p>
<ul>
<li><p>Do your software releases sometimes feel like you're defusing a bomb while blindfolded?</p>
</li>
<li><p>Have you ever wished for just a little more time to test your changes in staging, convinced it would save you a ton of stress and trouble?</p>
</li>
<li><p>Does it seem like no matter how hard you try, you just can't create an environment that truly mimics production?</p>
</li>
<li><p>Do you break out in a cold sweat every time you hear the words "deployment day"?</p>
</li>
</ul>
<p>If you nodded your head to even one of these, then congratulations (or condolences?) – this article is definitely for you! And even if you're sitting there thinking, "Nah, my deployments are smooth as butter," I'd still encourage you to read on. Who knows? You might pick up a new trick or two, or at the very least, learn some fancy new deployment lingo to impress your colleagues at the next standup.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724598182691/0dbca747-9624-417b-9f31-8be7ef662d57.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-benefits-whats-in-it-for-me">Benefits: What's In It for Me?</h2>
<p>Alright, let's cut to the chase. Here's what you stand to gain by implementing a Blue-Green deployment strategy:</p>
<ol>
<li><p><strong>Zero User Impact:</strong> Your users won't even know you're updating. It's like changing the tires on a moving car, but way less dangerous.</p>
</li>
<li><p><strong>Zero Downtime:</strong> As soon as your new version is ready, traffic starts flowing to it immediately. It's like teleportation, but for your software.</p>
</li>
<li><p><strong>Multi-Release Flexibility:</strong> If your new deployment turns out to be a dud, no worries! You can redeploy a new version or wipe the slate clean without affecting a single user in your live environment. It's like having a time machine for your deployments.</p>
</li>
<li><p><strong>Stable and Secure Releases:</strong> By keeping your Beta environment separate from Live services, you can test thoroughly and securely. It's like having a safety net, a bulletproof vest, and a lucky charm all rolled into one.</p>
</li>
<li><p><strong>Efficient Management:</strong> You're in the driver's seat when it comes to how and when changes are promoted from Beta to Live. It's like being the air traffic controller for your software releases.</p>
</li>
<li><p><strong>Flexible Deployment Scheduling:</strong> Deploy to your Beta environment anytime, independent of your Live service schedule. Test and verify to your heart's content, well ahead of your live deployment date. It's like rehearsing for a play with an infinitely patient audience.</p>
</li>
</ol>
<h2 id="heading-challenge-if-it-was-that-easy-everyone-would-be-doing-it">Challenge: If It Was That Easy, Everyone Would Be Doing It</h2>
<p>Now, I know what you're thinking: "This sounds too good to be true!" And you're right to be skeptical. Implementing a Blue-Green deployment strategy isn't without its challenges. Let's take a look at some of the hurdles you might face:</p>
<h3 id="heading-shared-services-the-isolation-conundrum">Shared Services: The Isolation Conundrum</h3>
<p>One of the main challenges in preparing an isolated beta environment is, well, isolating all the services in the environment. The fewer shared services you have, the more isolated your beta environment becomes.</p>
<p>Take your system's message broker, for example. To isolate its communication solely to beta components, you need to allocate one exclusively for your beta resources. The same process applies to other shared services. Create separate ones for beta where you can and set your beta resources to use them instead of the production ones.</p>
<p>Once your beta environment is ready for promotion to live, you can bid farewell to these temporary services. We'll discuss the creation and removal process further under 'Ensuring consistent configuration across both environments' below.</p>
<p>It's important to note that some resources can't be easily created just for the sake of beta environment isolation. Even if they could be, integrating them into the beta resources isn't always straightforward. A common example is a database. This is where you need to consider your options carefully. If you decide to share them between both live and beta, do so with proper planning and precautions in mind.</p>
<h3 id="heading-near-zero-downtime-the-quest-for-seamlessness">Near-Zero Downtime: The Quest for Seamlessness</h3>
<p>While zero-downtime is the holy grail, the reality is that there's potential for brief service interruptions during the transition, depending on your configuration. You'll need to customize your setup based on your specific needs and the tools you're using.</p>
<p>If you take away one thing from this entire blog regarding Blue-Green deployments, let it be this: implementing the Blue-Green strategy isn't the toughest part – it's refining it to the point of seamless deployment that's the real challenge.</p>
<h3 id="heading-forwardbackward-compatibility-the-time-travelers-dilemma">Forward/Backward Compatibility: The Time-Traveler's Dilemma</h3>
<p>Managing database schema changes and migrations is another significant challenge. You and your team must always keep forward and backward compatibility in mind when making changes. This is particularly crucial when it comes to database changes.</p>
<blockquote>
<p><strong>Forward compatibility:</strong> A system's ability to accept input intended for a future version of itself without breaking.</p>
<p><strong>Backward compatibility:</strong> A system's ability to work with input from an older version of itself.</p>
<p>In database contexts:</p>
<ul>
<li><p>Forward: Schemas that can handle new fields added in future versions.</p>
</li>
<li><p>Backward: New schema changes that don't break existing queries or data structures.</p>
</li>
</ul>
<p>Maintaining both types of compatibility allows for smoother system evolution and reduces risks during updates or migrations.</p>
</blockquote>
<h3 id="heading-rollback-procedures-the-oops-button">Rollback Procedures: The "Oops" Button</h3>
<p>A contingency rollback plan must be in place in case of emergency. This can pose a challenge if the Blue-Green strategy you employ is only prepared to deploy forward. It's like having an ejector seat in your car – you hope you never have to use it, but you'll be glad it's there if you need it.</p>
<h3 id="heading-ensuring-consistent-configuration-the-clone-wars">Ensuring Consistent Configuration: The Clone Wars</h3>
<p>This is mainly an issue if you're not already using Infrastructure as Code (IaC). There's no doubt it's a learning curve, but to implement a Blue-Green strategy, I'd strongly advise you to invest the time. Once you set up one environment for deployment via IaC, you can mirror it by refactoring it into a template equivalent and use it for both Beta and Live deployment.</p>
<p>The best part? Your releases will always be uniform! It's a huge gain! Plus, it's great to add IaC to your toolbelt if you haven't already!</p>
<h3 id="heading-increased-infrastructure-and-hosting-costs-the-price-of-peace-of-mind">Increased Infrastructure and Hosting Costs: The Price of Peace of Mind</h3>
<p>You will have to maintain two full production environments simultaneously when both beta and live are up, there's no doubt about that. But given the benefits, I can promise you that any manager or higher-up who needs to approve your proposal will dive in headfirst when you show them the advantages. Nothing beats peace of mind, especially for the bigwigs. (Just kidding... sort of.)</p>
<h1 id="heading-implementation-enough-youve-sold-it-show-me-how">Implementation: Enough, You've Sold It, Show Me How!</h1>
<p>If you've read this far, then buckle up – you're in for a ride! First, we'll show you a few different methods of implementation. Then, we'll walk you through our approach. If you're in a hurry, you can check out the repository here: <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s">blue-green-with-k8s</a>. The README there contains all the commands needed to quickly set up this deployment strategy.</p>
<h2 id="heading-implementation-methods-how-many-ways-are-there">Implementation Methods: How Many Ways Are There?</h2>
<p>We personally implemented this strategy in two different methods before creating this third one for you.</p>
<h3 id="heading-services-based-approach-as-a-unit">Services Based Approach - As a Unit</h3>
<p>Our first implementation, which was really our introduction to Blue-Green deployments, revolved around our application's search functionality. We revamped our entire release deployment process and reduced its outage and preparation time by implementing a Blue-Green strategy for our Search Engine and its related services. We focused solely on three interconnected services, creating those 3 services as one unit (aka color) together.</p>
<p>In this approach, we would deploy an Azure Search Service (Azure's Search Engine service), a custom event-driven, container-based Search Engine indexer, and a Job Scheduler service that managed both the Azure Search Service and the custom indexer. Each new deployment would create this unit of resources as a new color.</p>
<p>This was our introduction to the potential of Blue-Green deployments!</p>
<h3 id="heading-entire-project-suite-individual-yet-interconnected-with-azure-container-apps">Entire Project Suite - Individual Yet Interconnected with Azure Container Apps</h3>
<p>Leveraging the knowledge gained in our first implementation, we implemented a similar approach for an entire new project suite that we got to work on and experiment with.</p>
<p>This time, we leveraged Azure Container Apps for Blue/Green version management. We would deploy new beta revisions to existing containers, to co-exist alongside stable revisions in use by users. This leverages the multi-revision feature of Azure Container Apps to allow us to have two revisions simultaneously in action. However, by always redirecting traffic solely to the Live aka Stable revision, live users only interacted with the Stable version of our applications. At the same time, we were able to test our Beta version!</p>
<p>This implementation was more intricate, and if you're planning to implement blue-green for your project suite in production, chances are something like this will be more appropriate for you, especially if you use or plan on using Azure Container Apps. A basic implementation can be found in Microsoft Docs here: <a target="_blank" href="https://learn.microsoft.com/en-us/azure/container-apps/blue-green-deployment?pivots=azure-cli">Blue-Green Deployment in Azure Container Apps</a>. I'd strongly suggest you use that solely for introductory purposes only. For a real implementation, make sure to test thoroughly and customize based on your needs.</p>
<p>It's worth noting that this implementation is on my todo list as well, which I'll be working on later.</p>
<h3 id="heading-entire-project-suite-individual-yet-interconnected-with-kubernetes">Entire Project Suite - Individual Yet Interconnected with Kubernetes</h3>
<p>This is the new implementation that we'll be walking you through today. A step-by-step implementation of a Blue-Green strategy via Kubernetes with three services: a database, a frontend UI service, and a backend API service.</p>
<p>The next section describes the step-by-step implementation.</p>
<h1 id="heading-step-by-step-implementation-show-me-the-money">Step-By-Step Implementation: Show Me the Money!</h1>
<p>We'll be deploying a suite of projects using Blue-Green deployment. In our example, we will first deploy a stable version, which will be our initial live deployment. Then, we'll make some small changes in a separate branch and deploy that as our Beta version. Once we see them side by side, both in use and action, we'll then promote our Beta to Live, emulating a real production deployment. In our example, the database will remain shared between both live and beta services, while the frontend and backend services will be created anew for each version.</p>
<p>To help visualize this process, let's look at a simplified illustration of our Blue-Green deployment strategy:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724605874413/bd0ed502-7140-4951-b3ba-9a2b84be69f5.gif" alt class="image--center mx-auto" /></p>
<p>Source: <a target="_blank" href="https://medium.com/@aelson.alves/blue-green-deployment-142568063884"><strong>Blue-Green Deployment</strong></a></p>
<p>This diagram shows how user traffic is initially directed to the "blue" (stable) environment. Once we're confident in our beta version, we can seamlessly switch traffic to the "green" (beta) environment, which then becomes our new stable version.</p>
<p>We've prepared all the steps for you and consolidated the deployment into one script. First, I'll walk you through our services. Then I'll show our deployment Kubernetes configuration files to see what we're deploying. Finally, I'll walk you through the actual deployment process. This is where we'll examine our implementation of Blue-Green and see for ourselves on our local machine how easy it is to get started!</p>
<p>The database has two tables:</p>
<ol>
<li><p>Greetings</p>
</li>
<li><p>Farewells</p>
</li>
</ol>
<p>First, we'll deploy our stable (starting) version of the services which retrieve and display the first record it finds in our Greetings table.</p>
<p>Then, emulating a feature version in a separate branch, we revise our backend and frontend services to also retrieve and display the first record it finds in the Farewells table, alongside the Greetings table.</p>
<p>After deploying our feature branch, we'll see the simultaneous deployment results first hand, and together, promote the feature branch (aka Beta environment) to our Live environment, overriding the initial version that solely displayed the first Greetings record.</p>
<h2 id="heading-assumptions">Assumptions</h2>
<p>I'm assuming you're familiar with the following technologies. If not, don't worry! A quick Google search should get you up to speed. If I can learn it, it'll be a piece of cake for you!</p>
<ul>
<li><p>Bash Scripting</p>
</li>
<li><p>Kubernetes</p>
</li>
<li><p>Docker</p>
<ul>
<li>Make sure to enable the Docker Kubernetes cluster</li>
</ul>
</li>
<li><p>YAML</p>
</li>
</ul>
<h2 id="heading-step-1-creation-of-dockerfiles">Step 1: Creation of Dockerfiles</h2>
<p>Containerization of your services is a vital step that cannot be bypassed. If containerization is new to you, you're in for a treat. Get busy learning it right now because it's a must-have in your toolbelt.</p>
<p>We created two Dockerfiles for our frontend and backend services. Nothing fancy about them – they containerize our backend and frontend services in the simplest manner possible.</p>
<p>Backend: <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/backend/Dockerfile">Dockerfile</a></p>
<p>Frontend: <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/frontend/Dockerfile">Dockerfile</a></p>
<p>Our database is MariaDB, an open-source database that's already available on Docker Hub, where we can simply pull and deploy it from. Shoutout to all the open-source contributors out there. You rock!</p>
<h3 id="heading-step-11-automating-image-preparation-with-build-images-bash-script">Step 1.1: Automating Image Preparation with build-images Bash Script</h3>
<p>For MariaDB, since the image is already available on Docker Hub, all we need to do is include the image path in our Kubernetes configuration file, which you'll see in the next step. However, for our own backend and frontend services, we must prepare and build them ourselves.</p>
<p>For building our backend and frontend images, we've created an automated bash script: <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/build-images.sh">build-images.sh</a>. This script will be utilized in our <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/deploy-manager.sh">deployment-manager</a> script later on, to build, tag, and push our images to our local Docker instance using random tags.</p>
<h2 id="heading-step-2-creation-of-kubernetes-config-files">Step 2: Creation of Kubernetes Config Files</h2>
<p>Our standard method of operation is to first create default deployment files. These will allow us to test our default non-Blue-Green deployments well before creating a template file to use for Blue-Green deployments.</p>
<p>Here are the default non-template deployment files:</p>
<p>Backend: <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/deployments/local/backend.yaml">backend.yaml</a></p>
<p>Frontend: <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/deployments/local/frontend.yaml">frontend.yaml</a></p>
<p>Database: <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/deployments/local/db.yaml">db.yaml</a></p>
<h3 id="heading-explanation">Explanation</h3>
<p><strong>Backend &amp; Frontend</strong></p>
<p>For both our backend and frontend services, we create two Kubernetes resources:</p>
<ol>
<li><p><strong>Deployment</strong>: This resource manages the creation and scaling of pods running our application.</p>
</li>
<li><p><strong>Service</strong>: This resource exposes our deployment, making it accessible both within the cluster and externally.</p>
</li>
</ol>
<p>These are configured as follows:</p>
<ul>
<li><p>Backend: Internally accessible at port 8080, externally at port 30001</p>
</li>
<li><p>Frontend: Internally accessible at port 3000, externally at port 30002</p>
</li>
</ul>
<p>This setup allows our services to communicate internally and be accessible from the outside world.</p>
<p>By creating these non-template files first, we can thoroughly test our basic setup before moving on to the more complex Blue-Green deployment configuration.</p>
<p><strong>Database</strong></p>
<p>For our database, we've created a more complex setup using MariaDB. Here's a breakdown of the resources:</p>
<ol>
<li><p><strong>ConfigMap (mariadb-init)</strong>: This contains our SQL initialization script. It creates our <code>blue_green</code> database and sets up two tables: <code>greetings</code> and <code>farewells</code>, each populated with one record.</p>
</li>
<li><p><strong>PersistentVolumeClaim</strong>: This requests 300MB of storage for our database, ensuring data persistence across pod restarts.</p>
</li>
<li><p><strong>Service</strong>: This exposes our MariaDB internally and externally:</p>
<ul>
<li><p>Internal port: 3306</p>
</li>
<li><p>NodePort (external access): 30306</p>
</li>
</ul>
</li>
<li><p><strong>Deployment</strong>: This deploys our MariaDB container:</p>
<ul>
<li><p>Uses MariaDB version 11.4</p>
</li>
<li><p>Sets up environment variables for the root password</p>
</li>
<li><p>Mounts volumes for data persistence and initialization</p>
</li>
</ul>
</li>
</ol>
<p>The initialization script (in the ConfigMap) runs on database creation, setting up our required schema:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">DATABASE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> blue_green;

<span class="hljs-keyword">USE</span> blue_green;

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> greetings (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">INT</span> PRIMARY <span class="hljs-keyword">KEY</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span> AUTO_INCREMENT, 
  greeting <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">100</span>)
);

<span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> greetings (greeting) <span class="hljs-keyword">VALUES</span> (<span class="hljs-string">'hello, hi, how are yah?'</span>);

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> farewells (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">INT</span> PRIMARY <span class="hljs-keyword">KEY</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span> AUTO_INCREMENT, 
  farewell <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">100</span>)
);

<span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> farewells (farewell) <span class="hljs-keyword">VALUES</span> (<span class="hljs-string">'adios, good bye, see yah?'</span>);
</code></pre>
<p>In short, we take care of the entire database creation, preparation, and setup in this deployment file. Absolutely no future changes are needed, allowing us to focus on learning the deployment processes instead of getting bogged down with database setup that's not relevant to our main goal.</p>
<h2 id="heading-step-3-creation-of-kubernetes-config-templates-from-config-files">Step 3: Creation of Kubernetes Config Templates from Config Files</h2>
<p>Since only one version of the database will be deployed, there's no need to create a database Kubernetes configuration template file. However, since we are deploying the backend and the frontend dynamically, we will need to leverage the benefits a template provides for the corresponding Kubernetes configuration files.</p>
<p>Backend: <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/deployments/local/backend.yaml.template">backend.yaml.template</a></p>
<p>Frontend: <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/deployments/local/frontend.yaml.template"><strong>frontend.yaml.template</strong></a></p>
<h3 id="heading-explanation-1">Explanation</h3>
<p><strong>Backend</strong></p>
<p>We add a <code>{{STATUS}}</code> variable that gets overwritten by our deployment type, i.e., beta or stable.</p>
<p>We replace the image tag with <code>{{IMAGE_TAG}}</code>, which will be overwritten by the randomly generated image tags we'll be creating for our beta and live services.</p>
<p><strong>Frontend</strong></p>
<p>Similar to the backend, we add <code>{{STATUS}}</code> and <code>{{IMAGE_TAG}}</code>, that override the deployment type and the image tag respectively.</p>
<p>For the frontend, however, we also added a ConfigMap that overrides our frontend's runtime environment variable. This overrides the <code>{{BACKEND_NODE_PORT}}</code> and dynamically sets the API URL that the frontend will be pointing to.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ConfigMap</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">frontend-config-{{STATUS}}</span>
  <span class="hljs-attr">labels:</span>
    <span class="hljs-attr">app:</span> <span class="hljs-string">b-g</span>
    <span class="hljs-attr">component:</span> <span class="hljs-string">frontend</span>
    <span class="hljs-attr">status:</span> {{<span class="hljs-string">STATUS</span>}}
<span class="hljs-attr">data:</span>
  <span class="hljs-attr">config.js:</span> <span class="hljs-string">|
    window.RUNTIME_CONFIG = {
      API_URL: "http://localhost:{{BACKEND_NODE_PORT}}",
    };</span>
</code></pre>
<h2 id="heading-step-4-deployment-of-live-services-starting-point-first-stable-deployment">Step 4: Deployment of Live Services (Starting Point - First Stable Deployment)</h2>
<h3 id="heading-execution">Execution</h3>
<p>From this step onward, we'll be using the <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/deploy-manager.sh">deploy-manager</a> bash script that automates our entire deployment process. We'll explain every single step to make sure you grasp its processes in depth as we go along.</p>
<p>To get started, make sure to have Docker running, with the Kubernetes cluster enabled. You must also have the ability to run bash scripts, for which plenty of documentation already exists.</p>
<p>At the root of our <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s">blue-green-with-k8s</a> repo, we run the following command to deploy our Live services:</p>
<pre><code class="lang-bash">./deploy-manager.sh deploy stable
</code></pre>
<h3 id="heading-result">Result</h3>
<p>The output will look something like this, with randomly allocated URLs where we can access our services on our local machine:</p>
<pre><code class="lang-plaintext">==============================
  Current Deployment Status
==============================

NAME                READY   UP-TO-DATE   AVAILABLE   AGE
b-g-backend-stable   1/1     1            1           2m13s
b-g-frontend-stable  1/1     1            1           2m11s
b-g-mariadb          1/1     1            1           2m14s

--- Current Services ---
NAME                         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)
AGE
b-g-backend-service-stable   NodePort    10.104.19.168   &lt;none&gt;        8080:30
951/TCP   2m13s
b-g-frontend-service-stable  NodePort    10.100.205.149  &lt;none&gt;        3000:30
183/TCP   2m11s
kubernetes                   ClusterIP   10.96.0.1       &lt;none&gt;        443/TCP
7m27s
mariadb-service              NodePort    10.109.117.1    &lt;none&gt;        3306:30
306/TCP   2m14s

--- Access URLs ---
Stable Backend: localhost:31543
Stable Frontend: localhost:32693
MariaDB: localhost:30306
</code></pre>
<h3 id="heading-stable-environment-visualization">Stable Environment Visualization</h3>
<p>When we access the stable frontend URL (in this case, localhost:32693), we see the following:</p>
<ul>
<li>A "Greeting" section displaying: "hello, hi, how are yah?"</li>
</ul>
<p>If we access the stable backend URLs directly, we see:</p>
<ul>
<li>At the /greeting endpoint (localhost:31543/greeting): "hello, hi, how are yah?"</li>
</ul>
<p>This demonstrates that our stable version is correctly retrieving and displaying the greeting message from the database.</p>
<h3 id="heading-explanation-2">Explanation</h3>
<p>The <code>./deploy-manager.sh deploy stable</code> command will trigger the following flow in the script:</p>
<pre><code class="lang-bash">deploy)
        <span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-variable">$#</span>"</span> -ne 2 ] || [[ ! <span class="hljs-string">"<span class="hljs-variable">$2</span>"</span> =~ ^(stable|beta)$ ]]; <span class="hljs-keyword">then</span>
            print_error <span class="hljs-string">"Usage: <span class="hljs-variable">$0</span> deploy &lt;stable|beta&gt;"</span>

            show_exit_prompt
        <span class="hljs-keyword">fi</span>

        handle_deployment <span class="hljs-string">"<span class="hljs-variable">$2</span>"</span>
        ;;
</code></pre>
<p>This calls the <code>handle_deployment</code> function to deploy our live services. The function looks like this:</p>
<pre><code class="lang-bash"><span class="hljs-function"><span class="hljs-title">handle_deployment</span></span>() {
    <span class="hljs-built_in">local</span> deployment_type=<span class="hljs-variable">$1</span>

    <span class="hljs-built_in">local</span> tag=$(generate_random_tag)

    build_images <span class="hljs-string">"<span class="hljs-variable">$tag</span>"</span>

    deploy_db

    deploy <span class="hljs-string">"<span class="hljs-variable">$deployment_type</span>"</span> <span class="hljs-string">"<span class="hljs-variable">$tag</span>"</span>

    check_env <span class="hljs-string">"<span class="hljs-variable">$deployment_type</span>"</span>
}
</code></pre>
<p>First, it creates two variables:</p>
<ol>
<li><p><code>deployment_type</code>, which is being set to <code>stable</code></p>
</li>
<li><p><code>tag</code>, a randomly generated string to be used as our image tag</p>
</li>
</ol>
<p>Then, we build, tag, and push our backend and frontend images using the <code>build_images "$tag"</code> command, which uses our automated build-images bash script. This overrides the tag for both images to this new randomly generated one.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">readonly</span> BUILD_SCRIPT=<span class="hljs-string">"./build-images.sh"</span>

<span class="hljs-function"><span class="hljs-title">build_images</span></span>() {
    <span class="hljs-built_in">local</span> tag=<span class="hljs-variable">$1</span>

    <span class="hljs-keyword">if</span> [[ ! -x <span class="hljs-string">"<span class="hljs-variable">$BUILD_SCRIPT</span>"</span> ]]; <span class="hljs-keyword">then</span>
        chmod +x <span class="hljs-string">"<span class="hljs-variable">$BUILD_SCRIPT</span>"</span>
    <span class="hljs-keyword">fi</span>

    print_header <span class="hljs-string">"Building Images"</span>

    print_info <span class="hljs-string">"Building images with tag: <span class="hljs-variable">$tag</span>"</span>

    <span class="hljs-string">"<span class="hljs-variable">$BUILD_SCRIPT</span>"</span> <span class="hljs-literal">true</span> <span class="hljs-string">"<span class="hljs-variable">$tag</span>"</span>

    print_success <span class="hljs-string">"Images successfully built with tag: <span class="hljs-variable">$tag</span>"</span>
}
</code></pre>
<p>Next, we deploy the database using the <code>deploy_db</code> function, which deploys our <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/deployments/local/db.yaml">db.yaml</a> via the Kubernetes CLI. It waits until the database status becomes ready.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">readonly</span> DB_DEPLOYMENT=<span class="hljs-string">"./deployments/local/db.yaml"</span>

<span class="hljs-function"><span class="hljs-title">deploy_db</span></span>() {
    print_header <span class="hljs-string">"Deploying Database"</span>
    kubectl apply -f <span class="hljs-string">"<span class="hljs-variable">$DB_DEPLOYMENT</span>"</span>

    print_info <span class="hljs-string">"Waiting for Database to be fully ready..."</span>

    <span class="hljs-keyword">if</span> kubectl <span class="hljs-built_in">wait</span> --<span class="hljs-keyword">for</span>=condition=ready --timeout=60s pod -l app=b-g,component=mariadb; <span class="hljs-keyword">then</span>
        print_success <span class="hljs-string">"Database is ready!"</span>
    <span class="hljs-keyword">else</span>
        print_error <span class="hljs-string">"Database is not ready. Please check the deployment."</span>
        show_exit_prompt
    <span class="hljs-keyword">fi</span>
}
</code></pre>
<p>Finally, we deploy our backend and frontend services, using <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/deployments/local/backend.yaml.template">backend.yaml.template</a> and <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/deployments/local/frontend.yaml.template">frontend.yaml.template</a> using the <code>deploy()</code> function. We pass our deployment type (<code>stable</code>) and a randomly generated image tag. This is where the magic happens! It's a bit verbose for debugging and UX sake, but still easy to follow.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">readonly</span> BACKEND_TEMPLATE=<span class="hljs-string">"./deployments/local/backend.yaml.template"</span>
<span class="hljs-built_in">readonly</span> FRONTEND_TEMPLATE=<span class="hljs-string">"./deployments/local/frontend.yaml.template"</span>

<span class="hljs-function"><span class="hljs-title">Deploy</span></span>() {
    <span class="hljs-built_in">local</span> status=<span class="hljs-variable">$1</span>
    <span class="hljs-built_in">local</span> image_tag=<span class="hljs-variable">$2</span>

    print_header <span class="hljs-string">"Deploying <span class="hljs-variable">$status</span> Environment"</span>

    print_subheader <span class="hljs-string">"Deploying <span class="hljs-variable">$status</span> backend"</span>

    sed -e <span class="hljs-string">"s/{{STATUS}}/<span class="hljs-variable">$status</span>/g"</span> -e <span class="hljs-string">"s/{{IMAGE_TAG}}/<span class="hljs-variable">$image_tag</span>/g"</span> <span class="hljs-string">"<span class="hljs-variable">$BACKEND_TEMPLATE</span>"</span> | kubectl apply -f -

    print_info <span class="hljs-string">"Waiting for <span class="hljs-variable">$status</span> backend deployment to be available..."</span>

    <span class="hljs-keyword">if</span> ! kubectl <span class="hljs-built_in">wait</span> --<span class="hljs-keyword">for</span>=condition=available --timeout=60s deployment/b-g-backend-<span class="hljs-variable">$status</span>; <span class="hljs-keyword">then</span>
        print_error <span class="hljs-string">"Backend deployment failed. Check the logs for more information."</span>

        kubectl get pods -l app=b-g,component=backend,status=<span class="hljs-variable">$status</span>

        kubectl describe deployment b-g-backend-<span class="hljs-variable">$status</span>

        show_exit_prompt
    <span class="hljs-keyword">fi</span>

    <span class="hljs-built_in">local</span> service_name=<span class="hljs-string">"b-g-backend-service-<span class="hljs-variable">$status</span>"</span>

    print_info <span class="hljs-string">"Checking if <span class="hljs-variable">$status</span> backend service exists..."</span>

    <span class="hljs-keyword">if</span> kubectl get service <span class="hljs-string">"<span class="hljs-variable">$service_name</span>"</span> &amp;&gt;/dev/null; <span class="hljs-keyword">then</span>
        print_success <span class="hljs-string">"<span class="hljs-variable">$status</span> backend service exists."</span>
    <span class="hljs-keyword">else</span>
        print_error <span class="hljs-string">"Backend service <span class="hljs-variable">$service_name</span> does not exist. Check the configuration."</span>

        show_exit_prompt
    <span class="hljs-keyword">fi</span>

    <span class="hljs-built_in">local</span> backend_node_port=$(kubectl get service <span class="hljs-string">"<span class="hljs-variable">$service_name</span>"</span> -o jsonpath=<span class="hljs-string">'{.spec.ports[0].nodePort}'</span>)

    print_subheader <span class="hljs-string">"Deploying <span class="hljs-variable">$status</span> frontend"</span>

    sed -e <span class="hljs-string">"s/{{STATUS}}/<span class="hljs-variable">$status</span>/g"</span> -e <span class="hljs-string">"s/{{IMAGE_TAG}}/<span class="hljs-variable">$image_tag</span>/g"</span> -e <span class="hljs-string">"s/{{BACKEND_NODE_PORT}}/<span class="hljs-variable">$backend_node_port</span>/g"</span> <span class="hljs-string">"<span class="hljs-variable">$FRONTEND_TEMPLATE</span>"</span> | kubectl apply -f -

    print_info <span class="hljs-string">"Waiting for <span class="hljs-variable">$status</span> frontend deployment to be available..."</span>

    <span class="hljs-keyword">if</span> ! kubectl <span class="hljs-built_in">wait</span> --<span class="hljs-keyword">for</span>=condition=available --timeout=60s deployment/b-g-frontend-<span class="hljs-variable">$status</span>; <span class="hljs-keyword">then</span>
        print_error <span class="hljs-string">"Frontend deployment failed. Check the logs for more information."</span>

        kubectl get pods -l app=b-g,component=frontend,status=<span class="hljs-variable">$status</span>

        kubectl describe deployment b-g-frontend-<span class="hljs-variable">$status</span>

        show_exit_prompt
    <span class="hljs-keyword">fi</span>

    print_success <span class="hljs-string">"<span class="hljs-variable">$status</span> deployment updated with image tag: <span class="hljs-variable">$image_tag</span>"</span>

    print_info <span class="hljs-string">"Frontend configured to use backend at http://localhost:<span class="hljs-variable">$backend_node_port</span>"</span>
}
</code></pre>
<p>In this function, we first deploy the backend template with stable status using the following line:</p>
<pre><code class="lang-bash">sed -e <span class="hljs-string">"s/{{STATUS}}/<span class="hljs-variable">$status</span>/g"</span> -e <span class="hljs-string">"s/{{IMAGE_TAG}}/<span class="hljs-variable">$image_tag</span>/g"</span> <span class="hljs-string">"<span class="hljs-variable">$BACKEND_TEMPLATE</span>"</span> | kubectl apply -f -
</code></pre>
<p>The usage of status and image tag can be seen in the template.</p>
<p>Backend: <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/deployments/local/backend.yaml.template">backend.yaml.template</a></p>
<p>In short, the status is being used as an identifier to indicate which type of environment the backend service will be. In this case, it's stable, which denotes our Live environment. The image tag is of course being used to pull the version of backend service we want to deploy.</p>
<p>We then await the backend service's deployment until it becomes available. Once available, we retrieve its access port. We will use this value to override our frontend service's environment settings to ensure it's pointing to the correct service.</p>
<p>Next, we repeat the same steps, this time for our frontend service via the following line:</p>
<pre><code class="lang-bash">sed -e <span class="hljs-string">"s/{{STATUS}}/<span class="hljs-variable">$status</span>/g"</span> -e <span class="hljs-string">"s/{{IMAGE_TAG}}/<span class="hljs-variable">$image_tag</span>/g"</span> -e <span class="hljs-string">"s/{{BACKEND_NODE_PORT}}/<span class="hljs-variable">$backend_node_port</span>/g"</span> <span class="hljs-string">"<span class="hljs-variable">$FRONTEND_TEMPLATE</span>"</span> | kubectl apply -f -
</code></pre>
<p>This time, we not only pass the deployment status and frontend service image tag but also pass the backend node port so that our frontend service can access our backend.</p>
<p>We once again wait until the deployment of frontend services is complete and becomes available.</p>
<p>Once all services are successfully created, the script ends by running a status check using the <code>show_status</code> function. The function uses the Kubernetes CLI (<code>kubectl</code>) to get and display the status of the resources we deployed, as well as their ports.</p>
<pre><code class="lang-bash"><span class="hljs-function"><span class="hljs-title">show_status</span></span>() {
    print_header <span class="hljs-string">"Current Deployment Status"</span>
    kubectl get deployments

    print_subheader <span class="hljs-string">"Current Services"</span>
    kubectl get services

    print_subheader <span class="hljs-string">"Access URLs"</span>

    <span class="hljs-built_in">local</span> services=(<span class="hljs-string">"b-g-backend-service-stable"</span> <span class="hljs-string">"b-g-backend-service-beta"</span> <span class="hljs-string">"b-g-frontend-service-stable"</span> <span class="hljs-string">"b-g-frontend-service-beta"</span> <span class="hljs-string">"mariadb-service"</span>)

    <span class="hljs-built_in">local</span> names=(<span class="hljs-string">"Stable Backend"</span> <span class="hljs-string">"Beta Backend"</span> <span class="hljs-string">"Stable Frontend"</span> <span class="hljs-string">"Beta Frontend"</span> <span class="hljs-string">"MariaDB"</span>)

    <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-string">"<span class="hljs-variable">${!services[@]}</span>"</span>; <span class="hljs-keyword">do</span>
        <span class="hljs-built_in">local</span> port=$(kubectl get service <span class="hljs-variable">${services[$i]}</span> -o jsonpath=<span class="hljs-string">'{.spec.ports[0].nodePort}'</span> 2&gt;/dev/null)

        <span class="hljs-keyword">if</span> [ -n <span class="hljs-string">"<span class="hljs-variable">$port</span>"</span> ]; <span class="hljs-keyword">then</span>
            print_info <span class="hljs-string">"<span class="hljs-variable">${names[$i]}</span>: http://localhost:<span class="hljs-variable">$port</span>"</span>
        <span class="hljs-keyword">fi</span>
    <span class="hljs-keyword">done</span>
}
</code></pre>
<p>And there you have it! That's how we deploy our Live services. In the next part, we'll look at deploying our Beta services and promoting them to Live.</p>
<h2 id="heading-step-5-deployment-of-beta-services-new-feature">Step 5: Deployment of Beta Services (New Feature)</h2>
<h3 id="heading-execution-1">Execution</h3>
<p>To deploy a new version of our services for a better demonstration of the promotion process, we've created a separate branch with additional changes on top of our Live services to emulate a new version deployment.</p>
<p>First, switch from the main branch to <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/tree/feat/display-farewell">feat/display-farewell</a>, then we once again repeat the steps we did in the previous Deployment of Live services step. The only difference is that we have to pass the beta argument instead of stable when running the <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/deploy-manager.sh">deploy-manager</a> script.</p>
<pre><code class="lang-bash">./deploy-manager.sh deploy beta
</code></pre>
<h3 id="heading-result-1">Result</h3>
<p>The output this time consists of both stable as well as beta services.</p>
<pre><code class="lang-bash">==============================
  Current Deployment Status
==============================

NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
b-g-backend-beta      1/1     1            1           5s
b-g-backend-stable    1/1     1            1           2m12s
b-g-frontend-beta     1/1     1            1           2s
b-g-frontend-stable   1/1     1            1           2m9s
b-g-mariadb           1/1     1            1           2m14s

--- Current Services ---
NAME                          TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)
          AGE
b-g-backend-service-beta      NodePort    10.100.39.121    &lt;none&gt;        8080:31
209/TCP   6s
b-g-backend-service-stable    NodePort    10.104.191.229   &lt;none&gt;        8080:31
543/TCP   2m13s
b-g-frontend-service-beta     NodePort    10.103.147.219   &lt;none&gt;        3000:30
984/TCP   3s
b-g-frontend-service-stable   NodePort    10.109.144.9     &lt;none&gt;        3000:32
693/TCP   2m10s
kubernetes                    ClusterIP   10.96.0.1        &lt;none&gt;        443/TCP
          5h3m
mariadb-service               NodePort    10.109.29.175    &lt;none&gt;        3306:30
306/TCP   2m15s

--- Access URLs ---
Stable Backend: http://localhost:31543
Beta Backend: http://localhost:31209

Stable Frontend: http://localhost:32693
Beta Frontend: http://localhost:30984

MariaDB: http://localhost:30306
</code></pre>
<p>We now have two environments running simultaneously in parallel. Each is fully accessible and isolated to its own domain.</p>
<h3 id="heading-beta-environment-visualization">Beta Environment Visualization</h3>
<p>When we access the beta frontend URL (in this case, localhost:30984), we see the following:</p>
<ol>
<li><p>A "Greeting" section displaying: "hello, hi, how are yah?"</p>
</li>
<li><p>A "Farewell" section displaying: "adios, good bye, see yah?"</p>
</li>
</ol>
<p>This demonstrates that our new feature branch has successfully added the farewell message to the frontend.</p>
<p>If we access the beta backend URLs directly, we see:</p>
<ol>
<li><p>At the /greeting endpoint (localhost:31209/greeting):</p>
<pre><code class="lang-plaintext"> "hello, hi, how are yah?"
</code></pre>
</li>
<li><p>At the /farewell endpoint (localhost:31209/farewell):</p>
<pre><code class="lang-plaintext"> "adios, good bye, see yah?"
</code></pre>
</li>
</ol>
<p>These backend responses confirm that our beta version is correctly fetching both the greeting and farewell messages from the database.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724602965130/cf499ed3-3356-4314-a8d8-2f78650adb85.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-explanation-3">Explanation</h3>
<p>The exact same processes take place as in the Deployment of Live services step, except a new image is created from our backend and frontend services, which contains the changes we made for our new feature - to retrieve and display not only the greeting but also the farewell from the database.</p>
<p>The services then become available on separate randomly generated URLs for accessing. This isolation allows us to thoroughly test our new feature in the beta environment without affecting the live services that our users are currently accessing.</p>
<p>By comparing the beta and stable versions side by side, we can easily verify that our new feature (adding the farewell message) is working as expected in the beta environment before we decide to promote it to live.</p>
<h2 id="heading-step-6-promotion-of-beta-to-live-overriding-live-services">Step 6: Promotion of Beta to Live (Overriding Live Services)</h2>
<p>In the real world, this would be where you'd test and validate the deployment of your new changes. At this point, we have two possible paths:</p>
<h3 id="heading-issues-found-in-beta-redeployment-plan">Issues Found In Beta - Redeployment Plan</h3>
<p>One of the great perks of the Blue-Green strategy (or at least the way we've implemented it, and you should try to do as well) is that we can deploy new beta services as many times as we want. We can continuously override our Beta environment with new changes without affecting our Live services. All that's needed is to run the same beta deployment command again. It's that easy!</p>
<pre><code class="lang-bash">./deploy-manager.sh deploy beta
</code></pre>
<h3 id="heading-validation-successful-promotion-plan">Validation Successful - Promotion Plan</h3>
<p><strong>Execution</strong></p>
<p>If all has gone well in beta validation and you're ready to move forward, we promote our beta services to the Live environment and override it. The <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/deploy-manager.sh">deploy-manager</a> script is once again easily capable of handling this via the following command:</p>
<pre><code class="lang-bash">./deploy-manager.sh promote
</code></pre>
<p><strong>Result</strong></p>
<p>The script once again displays the status of the promotion, as well as the resources we've deployed. This time, however, both the beta and live URLs will display the same content, because at this point, the initial Live services have been overwritten with Beta changes.</p>
<p><strong>Explanation</strong></p>
<p>The promote argument triggers the following flow:</p>
<pre><code class="lang-bash">promote)
        print_header <span class="hljs-string">"Promoting Beta to Stable"</span>

        beta_image=$(kubectl get deployment b-g-backend-beta -o jsonpath=<span class="hljs-string">'{.spec.template.spec.containers[0].image}'</span>)

        deploy <span class="hljs-string">"stable"</span> <span class="hljs-string">"<span class="hljs-variable">${beta_image##*:}</span>"</span> <span class="hljs-comment"># Extract tag from image</span>

        print_success <span class="hljs-string">"Beta promoted to stable. New stable image tag: <span class="hljs-variable">${beta_image##*:}</span>"</span>
        ;;
</code></pre>
<p>Here, we leverage the Kubernetes CLI (<code>kubectl</code>) to retrieve the image tag we deployed our beta services with. This tag is shared between both the backend and frontend. We then use this tag to call the <code>deploy</code> function using the <code>stable</code> tag, which overwrites the existing Live services with our beta ones. Essentially, we're repeating the Deployment of Live services step but with the beta image tag.</p>
<p><img src="https://www.abtasty.com/wp-content/uploads/bluegreen1.jpg" alt="A blue-green deployment diagram with a single database" /></p>
<p>Source: <a target="_blank" href="https://www.abtasty.com/blog/blue-green-deployment-pros-and-cons/">AB Tasty Blog</a></p>
<h2 id="heading-step-7-teardown-beta-services-finalize-live-deployment-amp-remove-unused-resources">Step 7: Teardown Beta Services (Finalize Live Deployment &amp; Remove Unused Resources)</h2>
<h3 id="heading-execution-2">Execution</h3>
<p>Once again emulating a practical example, this is where we would terminate our unused beta environment to save costs. The <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/deploy-manager.sh">deploy-manager</a> script can again be used to manage the teardown.</p>
<pre><code class="lang-bash">./deploy-manager.sh teardown beta
</code></pre>
<h3 id="heading-result-2">Result</h3>
<p>Once the teardown of beta resources is complete, all beta resources are removed.</p>
<pre><code class="lang-bash">==============================
  Tearing down beta resources
==============================
deployment.apps <span class="hljs-string">"b-g-backend-beta"</span> deleted
deployment.apps <span class="hljs-string">"b-g-frontend-beta"</span> deleted
service <span class="hljs-string">"b-g-backend-service-beta"</span> deleted
service <span class="hljs-string">"b-g-frontend-service-beta"</span> deleted
configmap <span class="hljs-string">"frontend-config-beta"</span> deleted

✔ Beta resources teardown complete.
</code></pre>
<h3 id="heading-explanation-4">Explanation</h3>
<p>Our <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/deploy-manager.sh">deploy-manager</a> script command triggers the following flow:</p>
<pre><code class="lang-bash">teardown)
        <span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-variable">$#</span>"</span> -ne 2 ] || [[ ! <span class="hljs-string">"<span class="hljs-variable">$2</span>"</span> =~ ^(all|beta)$ ]]; <span class="hljs-keyword">then</span>
            print_error <span class="hljs-string">"Usage: <span class="hljs-variable">$0</span> teardown &lt;all|beta&gt;"</span>

            show_exit_prompt
        <span class="hljs-keyword">fi</span>

        teardown_resources <span class="hljs-string">"<span class="hljs-variable">$2</span>"</span>
        ;;
</code></pre>
<p>This calls the <code>teardown_resources</code> function to remove beta resources.</p>
<pre><code class="lang-bash"><span class="hljs-function"><span class="hljs-title">teardown_resources</span></span>() {
    <span class="hljs-built_in">local</span> scope=<span class="hljs-variable">$1</span>

    <span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-variable">$scope</span>"</span> == <span class="hljs-string">"all"</span> ]; <span class="hljs-keyword">then</span>
        print_header <span class="hljs-string">"Tearing down all resources"</span>

        kubectl delete deployment,service,configmap,pvc -l app=b-g

        print_success <span class="hljs-string">"All resources teardown complete."</span>
    <span class="hljs-keyword">elif</span> [ <span class="hljs-string">"<span class="hljs-variable">$scope</span>"</span> == <span class="hljs-string">"beta"</span> ]; <span class="hljs-keyword">then</span>
        print_header <span class="hljs-string">"Tearing down beta resources"</span>

        kubectl delete deployment,service,configmap -l app=b-g,status=beta

        print_success <span class="hljs-string">"Beta resources teardown complete."</span>
}
</code></pre>
<h2 id="heading-step-8-teardown-live-services-cleanup">Step 8: Teardown Live Services (Cleanup)</h2>
<h3 id="heading-execution-3">Execution</h3>
<p>And to finalize your local machine cleanup, we run the teardown again, this time terminating Live services as well.</p>
<pre><code class="lang-bash">./deploy-manager.sh teardown all
</code></pre>
<h3 id="heading-result-3">Result</h3>
<pre><code class="lang-bash">==============================
  Tearing down all resources
==============================
deployment.apps <span class="hljs-string">"b-g-backend-stable"</span> deleted
deployment.apps <span class="hljs-string">"b-g-frontend-stable"</span> deleted
deployment.apps <span class="hljs-string">"b-g-mariadb"</span> deleted
service <span class="hljs-string">"b-g-backend-service-stable"</span> deleted
service <span class="hljs-string">"b-g-frontend-service-stable"</span> deleted
service <span class="hljs-string">"mariadb-service"</span> deleted
configmap <span class="hljs-string">"frontend-config-stable"</span> deleted
configmap <span class="hljs-string">"mariadb-init"</span> deleted
persistentvolumeclaim <span class="hljs-string">"mariadb-pv-claim"</span> deleted

✔ All resources teardown complete.
</code></pre>
<h3 id="heading-explanation-5">Explanation</h3>
<p>Our <a target="_blank" href="https://github.com/FarzamMohammadi/blue-green-with-k8s/blob/main/deploy-manager.sh">deploy-manager</a> script command triggers the following flow:</p>
<pre><code class="lang-bash">teardown)
        <span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-variable">$#</span>"</span> -ne 2 ] || [[ ! <span class="hljs-string">"<span class="hljs-variable">$2</span>"</span> =~ ^(all|beta)$ ]]; <span class="hljs-keyword">then</span>
            print_error <span class="hljs-string">"Usage: <span class="hljs-variable">$0</span> teardown &lt;all|beta&gt;"</span>

            show_exit_prompt
        <span class="hljs-keyword">fi</span>

        teardown_resources <span class="hljs-string">"<span class="hljs-variable">$2</span>"</span>
        ;;
</code></pre>
<p>Except this time, it enters the first condition in our if statement, which removes all blue-green related resources (regardless of environment type), based on resource tagging we did in our Kubernetes configuration files.</p>
<pre><code class="lang-bash"><span class="hljs-function"><span class="hljs-title">teardown_resources</span></span>() {
    <span class="hljs-built_in">local</span> scope=<span class="hljs-variable">$1</span>

    <span class="hljs-keyword">if</span> [ <span class="hljs-string">"<span class="hljs-variable">$scope</span>"</span> == <span class="hljs-string">"all"</span> ]; <span class="hljs-keyword">then</span>
        print_header <span class="hljs-string">"Tearing down all resources"</span>

        kubectl delete deployment,service,configmap,pvc -l app=b-g

        print_success <span class="hljs-string">"All resources teardown complete."</span>
    <span class="hljs-keyword">elif</span> [ <span class="hljs-string">"<span class="hljs-variable">$scope</span>"</span> == <span class="hljs-string">"beta"</span> ]; <span class="hljs-keyword">then</span>
        print_header <span class="hljs-string">"Tearing down beta resources"</span>

        kubectl delete deployment,service,configmap -l app=b-g,status=beta

        print_success <span class="hljs-string">"Beta resources teardown complete."</span>
}
</code></pre>
<p>And there you have it!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724603082104/4521e9e5-74e1-4e5d-a831-e1a1c566f5ad.png" alt class="image--center mx-auto" /></p>
<p>We've successfully implemented, deployed, promoted, and cleaned up our Blue-Green deployment strategy. This approach gives you the flexibility to:</p>
<ol>
<li><p>Test new versions of your services in isolation</p>
</li>
<li><p>Promote beta versions to live with minimal downtime</p>
</li>
<li><p>Easily redeploy new beta versions if issues are found during testing</p>
</li>
<li><p>Quickly roll back to the previous stable version if problems arise after promotion</p>
</li>
</ol>
<p>This multi-faceted flexibility is the true power of the Blue-Green deployment strategy, providing a safety net that allows for more confident and agile deployments.</p>
<h1 id="heading-final-thoughts-painting-your-deployments-blue-and-green">Final Thoughts: Painting Your Deployments Blue and Green</h1>
<p>As we wrap up this journey through the land of Blue-Green deployments, let's take a moment to reflect on what we've accomplished. We've not just talked about a deployment strategy – we've implemented it, tested it, and seen its benefits firsthand.</p>
<p>Here are the key takeaways from our Blue-Green adventure:</p>
<ol>
<li><p><strong>Reduced Risk</strong>: By creating an isolated beta environment, we can thoroughly test new features without affecting live users.</p>
</li>
<li><p><strong>Flexibility</strong>: The ability to easily rollback or redeploy gives us unprecedented control over our release process.</p>
</li>
<li><p><strong>Improved User Experience</strong>: With near-zero downtime deployments, our users can enjoy uninterrupted service.</p>
</li>
<li><p><strong>Better Testing</strong>: The isolated beta environment allows for more comprehensive testing in a production-like setting.</p>
</li>
<li><p><strong>Cost-Effective</strong>: While there is an initial increase in resources, the long-term benefits in terms of stability and reduced downtime far outweigh the costs.</p>
</li>
</ol>
<p>Implementing Blue-Green deployments might seem daunting at first, but as we've seen, with the right tools and approach, it's a achievable goal that can revolutionize your deployment process.</p>
<p>Remember, the journey doesn't end here. As you implement this strategy in your own projects, you'll undoubtedly encounter unique challenges and opportunities. Embrace them! Each obstacle is a chance to refine your process and make it even more robust.</p>
<p>So, are you ready to take your deployments to the next level? The blue (or green) sky's the limit!</p>
<p>Happy deploying, and may your releases always be smooth and your downtimes non-existent!</p>
]]></content:encoded></item><item><title><![CDATA[A Journey Through Software Engineering: Uncovering Insights and Embracing Growth]]></title><description><![CDATA[The Journey Begins
As I sit down to write this, I’ve been navigating the software engineering landscape for almost two years. Now, that might not sound like a lot, but the list of things I’ve picked up along the way could stretch on so long, you’d pr...]]></description><link>https://triedandtestedbuilds.com/a-journey-through-software-engineering-uncovering-insights-and-embracing-growth</link><guid isPermaLink="true">https://triedandtestedbuilds.com/a-journey-through-software-engineering-uncovering-insights-and-embracing-growth</guid><category><![CDATA[Software Engineering]]></category><category><![CDATA[software development]]></category><category><![CDATA[Software Testing]]></category><category><![CDATA[professional development]]></category><category><![CDATA[personal development]]></category><dc:creator><![CDATA[Farzam Mohammadi]]></dc:creator><pubDate>Wed, 14 Feb 2024 04:22:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1707885692511/57b1f6a3-97e1-4f36-a0cd-5c2780a833d1.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-the-journey-begins"><strong>The Journey Begins</strong></h1>
<p>As I sit down to write this, I’ve been navigating the software engineering landscape for almost two years. Now, that might not sound like a lot, but the list of things I’ve picked up along the way could stretch on so long, you’d probably give up scrolling. Today, though, I’m not here to dive into the deep technical weeds. Instead, I want to talk about my journey, share the insights I’ve gathered, and the tips I’ve picked up as a Software Engineer.</p>
<p>Recently, I started mentoring and training new engineers. Impressively, one of them hit me with, "Farzam, what tips do you have for me?" This got me thinking back to my own early days, scouring Google, Reddit, YouTube - you name it - for any golden nuggets on how to be the best software engineer possible. That’s when it clicked. It's time to put keyboard to monitor and share a post on my journey and what I've learned so far.</p>
<p>Some of the points I’m planning to hit might resonate across the board, regardless of your field. Then there are those bits that might seem more specific to software engineering, but here’s my take: try to grasp the essence behind them. Even if they don’t seem immediately relevant to what you do, there might be a way to adapt and apply these insights in ways you hadn’t considered before.</p>
<h2 id="heading-lets-get-acquainted"><strong>Let's Get Acquainted</strong></h2>
<p>Before diving deeper, I think it’s crucial you get to know me a bit better. Understand what drives me and, more importantly, the type of person I am. Getting a glimpse into my world will help you grasp my professional ethos and appreciate why my insights might be worth your while.</p>
<p>I’m a staunch believer in the principles of Kaizen and an unabashed advocate for continuous improvement and growth. Wondering what Kaizen is? Here’s my favorite definition:</p>
<p><img src="https://lawhimsy.files.wordpress.com/2013/04/kaizen-definition-word-nerd-via-lawhimsy.png?w=1086" alt="kaizen-definition-word-nerd-via-lawhimsy" /></p>
<p>Source: <a target="_blank" href="https://lawhimsy.com/2013/04/03/word-nerd-kaizen/">LaWhimsy</a></p>
<p>"Kaizen," initially just a cool term I'd use to sound impressive in job interviews, has genuinely become a cornerstone of my life. This post is saturated with Kaizen's principles, aiming to pass a bit of that mindset onto you. Through the upcoming discussions, my goal is to not only share the spirit of continuous improvement but also to steer you clear of surface-level development. With the content that follows, I'm committed to fostering a mindset of ongoing growth and meticulousness in our work. Together, let's dive deeper into professional development, bypassing the shortcuts that lead to shallow understanding.</p>
<h2 id="heading-finding-my-long-term-vision"><strong>Finding My Long-Term Vision</strong></h2>
<p>There was a time when I was just coasting through life, lacking any real drive or direction. It wasn't because things were going poorly or I didn't have aspirations; I was simply short-sighted. The turning point came in my early twenties, spurred by an insatiable curiosity for reading. Books became my window to new perspectives, prompting me to scrutinize my life and the world around me. They encouraged me to start thinking with a long-term perspective, dreaming about where I wanted to be in the distant future. Once I adopted this broader outlook, reverting to my previous short-term mindset was unthinkable.</p>
<p>To this day, I encounter many who are still viewing life through that narrow lens. Occasionally, I'll offer a sharp word or two, hoping to prompt a broader view, but realizing the importance of long-term thinking is a journey each person must undertake on their own. I'm convinced that we all eventually arrive at this perspective, though, for some, the realization comes perilously late. By then, the opportunity to meaningfully shape their future based on these expanded horizons may have significantly diminished.</p>
<h1 id="heading-the-journey-continues-embracing-growth"><strong>The Journey Continues: Embracing Growth</strong></h1>
<h2 id="heading-embracing-change"><strong>Embracing Change</strong></h2>
<p>I reached a point where I refused to let stagnation be part of my story, both personally and professionally. A guiding principle for me has become: "How you do anything is how you do everything." This mindset fuels my drive for excellence in all areas of life.</p>
<h2 id="heading-the-challenges"><strong>The Challenges</strong></h2>
<p>Pursuing this ideal of constant improvement, I've realized, comes with its own set of challenges. It involves a continuous search for areas to grow, not just within myself but also recognizing potential in others. This journey can be exhausting, as turning a blind eye to opportunities for enhancement is not in my nature.</p>
<p>In personal life, I've found that growth largely lies within our own control—our health, relationships, and self-care are ours to improve. Professional growth, however, introduces a more complex dynamic. The workplace brings together a diverse mix of individuals, each with their own backgrounds, cultures, and attitudes towards work. Striving to be the best version of oneself in such an environment can sometimes feel like a solo effort in a team sport.</p>
<p>Another maxim I hold dear is: "If you're going to do something, strive to do it better than anyone else. Do it all the way. If you're going to half-ass it, why bother?" This philosophy, especially when applied to one's career, doesn't always align with the outlook of everyone in the workplace. While some may be content with meeting the basic requirements, my approach has always been to push beyond these boundaries. Working alongside individuals who don’t share this commitment can be challenging.</p>
<h2 id="heading-the-reality-of-team-dynamics"><strong>The Reality of Team Dynamics</strong></h2>
<p>The essence of teamwork, for me, has always been about putting forth A-level quality effort. Witnessing anything less can be frustrating, draining one's energy. This isn’t to say that I haven’t encountered many astonishing colleagues; indeed, I've had the privilege of working with some truly impressive individuals. However, it's the inconsistency that poses the challenge.</p>
<p>Echoing a sentiment by Steve Jobs, the synergy of A-players working together can create an unparalleled dynamic. Jobs noted the transformative power of assembling a team of top performers who, once united, set a standard of excellence that becomes self-perpetuating. This ideal environment, where A-players thrive among their peers, is something I strive for.</p>
<p>The impact of diverse effort levels in a team setting is undeniable. Despite an individual’s best efforts, the collective output reflects the contribution of all members. The aspiration to maintain high standards can sometimes be diluted in a mixed-setting team, highlighting the importance of a unified drive towards excellence.</p>
<h2 id="heading-reflections-on-effort-and-passion"><strong>Reflections on Effort and Passion</strong></h2>
<p>In software engineering, recognizing the range of commitment levels in the workplace is crucial. While not everyone chooses to put in extra time to learn new skills, delve into emerging technologies, or expand their knowledge, some do. It's important to respect these differences. Yet, the real challenge is when this new knowledge isn't shared with the team. Sharing is key to avoid misunderstandings and ensure that the team grows together, not apart. It's about finding a balance that suits your lifestyle and benefits the team.</p>
<p>The core issue is how we integrate and share our expertise. Learning in isolation can lead to problems down the line. Sharing knowledge is essential for elevating everyone's skills and keeping the team united. It's critical that everyone is aligned on how to apply new insights to enhance our work. This not only prevents potential issues but also boosts our collective capabilities.</p>
<p>This approach isn't about pushing personal dedication onto others but about striking a balance that supports both individual ambitions and team growth. My goal is to emphasize my commitment to continuous learning and improvement, and to encourage a culture of sharing and collaboration. Together, we can achieve excellence as a cohesive, knowledgeable team.</p>
<p>Going forward, let's explore the insights and practical advice that have been key to this journey.</p>
<h1 id="heading-the-journeys-milestones-achieving-quality-and-team-synergy"><strong>The Journey's Milestones: Achieving Quality and Team Synergy</strong></h1>
<h2 id="heading-embracing-collective-success"><strong>Embracing Collective Success</strong></h2>
<p>Recognizing the importance of teamwork has been a crucial realization for me: You Are Your Team! This insight might not resonate if you're tackling projects on your own. However, in a team setting, our collective achievements are what truly matter, with trust being the cornerstone of outstanding results.</p>
<p>When rolling out new changes, remember, it’s a team effort. Completion isn’t just about your code landing in the main branch. It involves harmonizing every contribution to align seamlessly with the team’s vision for a successful release.</p>
<p>A key principle we adhere to in our discussions is clarifying what 'done' and 'ready for release' mean for us. In my perspective, a task isn't truly finished until it's received unanimous approval: development has wrapped up, QA has given its blessing, and the product owner is confident our solution meets the customer's needs. This collective agreement is vital, enabling us to thoroughly understand the development lifecycle and maintain our high standards across every project.</p>
<h2 id="heading-the-essence-of-care-in-development"><strong>The Essence of Care in Development</strong></h2>
<p>The level of care invested in the development process directly influences the quality of the outcome. Genuine passion and dedication lead to superior results. If your commitment is waning, consider seeking a role more aligned with your passions. When you work on projects you're truly passionate about, care and quality naturally follow.</p>
<p>Emphasizing care in our work is crucial; without it, we risk producing subpar results, lacking the quality, passion, and effort needed for excellence. This underscores the importance of Uncle Bob's Programmer's Oath, establishing clear standards to uphold daily as a guiding beacon for our profession.</p>
<h4 id="heading-the-programmers-oath"><strong>The Programmer's Oath</strong></h4>
<p>"<em>In order to defend and preserve the honor of the profession of computer programmers,</em></p>
<p><strong><em>I Promise that, to the best of my ability and judgement:</em></strong></p>
<ol>
<li><p><em>I will not produce harmful code.</em></p>
</li>
<li><p><em>The code that I produce will always be my best work. I will not knowingly allow code that is defective either in behavior or structure to accumulate.</em></p>
</li>
<li><p><em>I will produce, with each release, a quick, sure, and repeatable proof that every element of the code works as it should.</em></p>
</li>
<li><p><em>I will make frequent, small, releases so that I do not impede the progress of others.</em></p>
</li>
<li><p><em>I will fearlessly and relentlessly improve my creations at every opportunity. I will never degrade them.</em></p>
</li>
<li><p><em>I will do all that I can to keep the productivity of myself, and others, as high as possible. I will do nothing that decreases that productivity.</em></p>
</li>
<li><p><em>I will continuously ensure that others can cover for me, and that I can cover for them.</em></p>
</li>
<li><p><em>I will produce estimates that are honest both in magnitude and precision. I will not make promises without certainty.</em></p>
</li>
<li><p><em>I will never stop learning and improving my craft."</em></p>
</li>
</ol>
<p>Source: <a target="_blank" href="https://blog.cleancoder.com/uncle-bob/2015/11/18/TheProgrammersOath.html">The Clean Code Blog</a></p>
<p>Enforcing these aptly written standards underscores the importance of discipline, rules, and regulations in software engineering, akin to practices in law and medicine where strict guidelines govern professional conduct. The omnipresence of software in everything from daily items to complex systems highlights the critical need for robust standards and ethics in our field.</p>
<p>Although these guidelines may not be formally codified, passing them from mentors like Uncle Bob to me, from me to you, and from you to others through word of mouth is essential for our discipline's integrity and impact. I advocate for this tradition, aiming to inspire a future where these practices are more formally recognized and adopted.</p>
<h2 id="heading-perfecting-the-craft-of-software-engineering"><strong>Perfecting the Craft of Software Engineering</strong></h2>
<p>To me, software engineering transcends mere coding; it's an art form. My work embodies not just the interaction with a computer through words and symbols but an expression of special, intricate, and incredibly beautiful art. Each piece of software I create is a unique masterpiece, effortlessly communicating its needs and desires to other systems. Achieving this level of craftsmanship has been a journey of continuous study and practice, and I'm still on that path.</p>
<p>Just as artists like Picasso once started with works far from their later masterpieces, I view my early efforts in software development in a similar light. Initially, what we create may not meet our vision of beauty or function, but it's through relentless practice, study, and refinement that we inch closer to our ideals. I dedicate myself to not only improving my code output but enhancing my overall ability to engineer software solutions. This pursuit is a long-term commitment, a never-ending duel with the learning process.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1707870857022/6e0b39a0-6f23-4052-8aa0-99750b6d9f2a.png" alt class="image--center mx-auto" /></p>
<p>Source: <a target="_blank" href="https://tabithaannthelostsock.blogspot.com/2012/10/using-value-in-cubism.html">The Lost Sock</a></p>
<p>To continually refine our art, we must immerse ourselves in learning, studying every facet that contributes to the perfection of our craft, and persistently create, iterate, and evolve our work. It's a process of producing many iterations—some might call "failures"—but through this journey, we develop works of beauty that we can be truly proud of. Like the great artists who transformed their initial failures into revered art, we, too, can elevate software engineering to a form of art admired and respected for its beauty and impact.</p>
<h2 id="heading-cultivating-collaboration-beyond-pair-programming"><strong>Cultivating Collaboration Beyond Pair Programming</strong></h2>
<p>Collaboration fuels my enthusiasm. There’s a unique energy in brainstorming sessions, idea exchanges, and collaborative design efforts with my team. This collaborative spirit transcends mere teamwork; it involves mutual learning and significantly enhances our collective capabilities.</p>
<p>I advocate for embracing collaboration in all facets of our work as software engineers. This means not just cooperating on projects, but actively discussing ideas, topics, designs, and more in our daily tasks. Whether within your team or extending beyond, clear communication and open dialogue are key to innovation and effective problem-solving.</p>
<h3 id="heading-pair-programming-a-highlight-of-collaboration"><strong>Pair Programming: A Highlight of Collaboration</strong></h3>
<p>Within this culture of collaboration, pair programming stands out as a prime example, though it's just one aspect of our collective effort. Initially, I was skeptical about its efficacy—wondering why it would take two to do one person’s job. However, the experience was eye-opening.</p>
<p>Pair programming isn't merely about coding together; it's a deep dive into another engineer's thought process, showcasing the direct benefits of collaborative work. It's about experiencing firsthand how others approach problems and develop solutions.</p>
<p>Such collaboration, especially through pair programming, has been crucial to my learning. It encourages asking questions, no matter how simple they may seem, and actively seeking understanding. While pair programming is invaluable, it's important to recognize that it's part of a broader ethos of collaboration that permeates every aspect of our work.</p>
<p>Promoting a culture of collaboration—whether through pair programming or daily interactions—enhances our work and leads to personal and professional growth. It's about more than just specific practices; it's a fundamental approach to software engineering that encourages openness, shared learning, and continuous improvement.</p>
<h2 id="heading-navigating-pull-requests-maximizing-benefits-minimizing-pitfalls"><strong>Navigating Pull Requests: Maximizing Benefits, Minimizing Pitfalls</strong></h2>
<p>Pull requests are a pivotal aspect of modern software development, presenting a choice: engage with them thoroughly or not at all.</p>
<h3 id="heading-engaging-with-pull-requests"><strong>Engaging with Pull Requests</strong></h3>
<p>Choosing pull requests necessitates a commitment to thoroughness. When executed well, they elevate the quality of your work, leveraging the insights of knowledgeable reviewers to refine your code and guide corrections. This process isn't just about critique; it's a collaborative effort to enhance project outcomes.</p>
<h4 id="heading-enhancing-clarity-with-pair-reviews"><strong>Enhancing Clarity with Pair Reviews</strong></h4>
<p>For instances where a change's context might elude a reviewer, pair reviews offer a direct approach to resolve ambiguities and hone feedback, making sure misunderstandings are minimized and the review process is as effective as possible.</p>
<h4 id="heading-acknowledging-the-drawbacks"><strong>Acknowledging the Drawbacks</strong></h4>
<p>Yet, pull requests are not without their challenges. They can introduce delays, lead to merge conflicts, and sometimes suffer from less engagement or surface-level feedback from reviewers. Especially large changes might not receive the attention they require, and there's the potential for negative dynamics if feedback is not well-received.</p>
<h3 id="heading-alternatives-to-pull-requests"><strong>Alternatives to Pull Requests</strong></h3>
<h4 id="heading-continuous-integration-a-viable-path"><strong>Continuous Integration: A Viable Path</strong></h4>
<p>Opting out of pull requests often means embracing Continuous Integration (CI). This path requires a robust testing framework to detect and prevent the integration of errors, maintaining the stability of the main branch. Crucially, it also demands a high level of trust among team members and those involved in making changes, as the process relies on each individual's commitment to maintaining code quality without the immediate oversight of pull request reviews.</p>
<h4 id="heading-post-merge-reviews"><strong>Post-Merge Reviews</strong></h4>
<p>Another strategy involves merging changes immediately and conducting reviews post-merge. Platforms like Azure DevOps support this by allowing comments on commits after merging, mitigating the issues of delays and conflicts. This approach, however, may present challenges in achieving consistency across a team.</p>
<h3 id="heading-making-the-choice"><strong>Making the Choice</strong></h3>
<p>Ultimately, the decision to use or forego pull requests should be tailored to your team's dynamics, workflow, and project objectives. Each approach has its merits and challenges, and the right choice is the one that aligns with your team's needs, fostering a productive and collaborative software development environment.</p>
<h2 id="heading-move-it-along-mindset-quality-over-speed"><strong>"Move it Along" Mindset: Quality Over Speed</strong></h2>
<p>Lately, a phrase from a colleague has been echoing in my team: "Let's try to move it along. After all, it's only an MVP." I get where he's coming from—the push to speed things up, especially with all the big changes throwing us curveballs left and right. But here's the thing: I'm all for diving deep. When I'm getting my hands on a new library, I need to peek under the hood, do a bit of digging to really get what I'm dealing with. Blindly rushing through? Not my style.</p>
<p>This whole "move it along" vibe seems to push for speed over substance. Yet, as Uncle Bob puts it, "The only way to go fast is to go well." That hits home for me. I'm a firm believer in understanding the nitty-gritty, making sure the work I do isn't just quick, but solid. This approach has paid off big time for me, and I don't see that changing. In the end, knowing your stuff inside and out? That's the real shortcut.</p>
<h2 id="heading-surface-level-development-the-pitfalls-of-rushing"><strong>Surface-Level Development: The Pitfalls of Rushing</strong></h2>
<p>The rush to move quickly often leads to just skimming the surface of your work, a practice I've observed in colleagues who consistently make changes without delving into the details. When it comes time to review or discuss these changes, they find themselves at a loss to explain the actual impact of their work. This situation highlights a crucial point.</p>
<p>Understanding the context of your work is non-negotiable. Skipping this step can lead to repetitive corrections, especially in complex systems. Moreover, this surface-level approach often results in broken testing environments, adding layers of complexity to troubleshooting and delaying progress further. Deep engagement with your tasks not only prevents these setbacks but also contributes to more durable and impactful outcomes.</p>
<h2 id="heading-going-beyond-surface-level-development"><strong>Going Beyond Surface-Level Development</strong></h2>
<p>The essence of impactful work lies in a profound understanding of the task at hand. Start by thoroughly grasping all the requirements. Take the time to review the objectives and strategize your approach, considering both planning and design aspects. If there's an existing implementation, delve into it to understand its foundation and workings. Engage with other team members familiar with the project's context for discussions—not limited to developers. This holistic approach ensures a well-rounded understanding and paves the way for meaningful contributions.</p>
<h2 id="heading-copy-pasting-a-slippery-slope"><strong>Copy-Pasting: A Slippery Slope</strong></h2>
<p>In my early days of coding, just before the rise of advanced AI, mastering the art of searching for solutions was a critical skill for any software developer. This often led to discovering and repurposing solutions crafted by others. Initially, I saw copy-pasting as a standard practice, widely accepted and practiced amongst my peers. However, my perspective on this has drastically shifted.</p>
<p>Copy-pasting isn't limited to snagging answers off Stack Overflow. It extends to reusing code from your own projects, which I've come to view as a precarious shortcut.</p>
<p>The act of lifting code from external sources, whether it’s Stack Overflow, another project, code from the annals of programming history, or even the best that AI has to offer, opens the door to inadvertently introducing errors. Often, code is copied without a thorough line-by-line review, based on the assumption that if it worked elsewhere, it surely must work here as well. This mindset, however, can lead to significant oversights.</p>
<p>This realization hit me hard. In the beginning, I frequently borrowed from existing solutions, only to find that I missed critical details, even in projects with nearly identical requirements. Since then, I've made a concerted effort to minimize copy-pasting. On the rare occasions I do, I ensure I fully understand every aspect of the code I'm incorporating. Ultimately, the responsibility for the code's performance rests on my shoulders, regardless of its original author.</p>
<p>Falling into the trap of copy-pasting is yet another barrier to achieving the depth of understanding necessary for meaningful development. This follows naturally from our previous discussion on the importance of going beyond surface-level development.</p>
<h2 id="heading-lifelong-responsibility-for-your-code"><strong>Lifelong Responsibility for Your Code</strong></h2>
<p>Once you author a change, that piece of code is yours to back—forever. Adopting the mindset that you're accountable for your work indefinitely encourages you to tackle new tasks with the appropriate level of responsibility.</p>
<p>Don't fall into the trap of thinking that once QA has approved your code, the buck stops with them. The responsibility remains firmly on your shoulders. Maintaining this level of ownership ensures you avoid the pitfall of perpetually making only surface-level changes. This commitment to accountability is crucial for fostering a deeper, more meaningful approach to software development.</p>
<h2 id="heading-gathering-better-requirements"><strong>Gathering Better Requirements</strong></h2>
<p>The complexity of a task directly correlates with the necessity for thorough planning and design. Initially, writing code seems daunting, but with experience, you come to realize coding is often the easiest part. The challenge lies in ensuring the robustness and high quality of your changes, which demands meticulous planning and preparation.</p>
<h2 id="heading-putting-your-ego-aside"><strong>Putting Your Ego Aside</strong></h2>
<p>Mistakes are inevitable, and the sooner you accept this, the quicker you'll learn and improve. I'm opinionated, basing my knowledge on evidence and experience, ensuring my discussions are fact-driven. Yet, no matter how solid your knowledge base is, being proven wrong is part of the journey. A quote that resonates deeply with me is, "It ain’t what you don’t know that gets you into trouble. It’s what you know for sure that just ain’t so."</p>
<p>Strive for correctness, but don’t take it personally when corrected. Your capacity to acknowledge and learn from errors will set you apart in the long run.</p>
<h2 id="heading-knowing-when-to-seek-help"><strong>Knowing When to Seek Help</strong></h2>
<p>My relentless nature initially seemed like an asset. However, I've learned that persistence must be balanced. Here's what I mean:</p>
<p>Example of Misguided Persistence: Once, I spent days trying to fix a minor issue in a feature I was developing, only to realize after two days that I needed help. With assistance, the problem was resolved in an hour.</p>
<p>Example of Balanced Persistence: Facing another challenging issue, I sought help after an hour of solo effort. Together with a colleague, we found a solution within the next hour.</p>
<p>The distinction between stubbornness and productive perseverance is recognizing when to ask for help. While dedication is valuable, knowing when you've hit a wall and seeking assistance can save time and lead to better outcomes.</p>
<h2 id="heading-ensuring-excellence-the-vital-role-of-testing">Ensuring Excellence: The Vital Role of Testing</h2>
<h3 id="heading-adopting-a-trust-but-verify-approach-in-software-development"><strong>Adopting a "Trust but Verify" Approach in Software Development</strong></h3>
<p>Early in your career, it's tempting to take everything you hear as gospel, especially when it comes from those with more experience. This initial trust is crucial for building rapport within teams, yet in the nuanced field of software development, adopting a "trust but verify" approach is invaluable.</p>
<p>Consider this incident during a software release: the IT team needed to execute several database scripts in preparation for the update. As the person overseeing release support, I was informed that all scripts had run successfully, albeit with a minor anomaly - the customary confirmation message from the database was missing.</p>
<p>Despite assurances that everything was probably fine, I leaned on a lesson learned from past experience: always verify. We convened on a call to meticulously review the process and discovered that the script lacked the essential "commit transaction" step, preventing the system from saving the changes. This critical omission didn't prompt an error since the lack of a "commit" command isn't flagged as an error by the system, yet it meant our updates hadn't been properly applied to the database. By taking the time to verify, we rectified the issue, ensuring the release proceeded smoothly and with certainty.</p>
<h3 id="heading-the-reality-beyond-it-works-on-my-machine"><strong>The Reality Beyond "It Works on My Machine"</strong></h3>
<p>We’ve all heard it, or even said it: "It works on my machine!" But the reality is, the final deployment environment, whether it's an on-premises server or cloud-based services, operates under different conditions. Recognizing this is crucial for ensuring that our software performs as intended in the real world.</p>
<h3 id="heading-leverage-testing-environments-to-anticipate-real-world-scenarios"><strong>Leverage Testing Environments to Anticipate Real-World Scenarios</strong></h3>
<p>Access to testing environments that mimic the production setting is a golden opportunity. These environments are not merely features of your development toolkit; they're a necessity. By closely replicating the production environment, your deployments and updates in the testing environment let you reach production-level outcomes without the risks of directly impacting users.</p>
<p>This approach underscores a commitment to delivering quality software. By thoroughly testing in environments that closely replicate the production setting, you're not just preventing potential headaches for your team and users; you're actively contributing to a culture of reliability and excellence in your projects.</p>
<h3 id="heading-shaping-excellence-in-software-through-early-testing"><strong>Shaping Excellence in Software Through Early Testing</strong></h3>
<p>Deploying your changes to a testing environment, particularly one designated for engineers before QA review, is your initial shield against unexpected problems. This step lets you discover and solve issues early, ensuring your work is double-checked and refined by you before it gets to the quality team. By this stage, it's polished and almost ready for production. This proactive approach demonstrates a deep commitment to the reliability and excellence of your work.</p>
<h3 id="heading-test-driven-development-tdd"><strong>Test-Driven Development (TDD)</strong></h3>
<p><a target="_blank" href="https://www.browserstack.com/guide/what-is-test-driven-development#:~:text=In%20layman's%20terms%2C%20Test%20Driven,unit%20test%20creation%2C%20and%20refactoring.">Test Drive Development</a> is a powerful approach to software design and implementation. Although initially challenging to adopt, its value becomes immediately apparent once mastered. Start slow, set aside the learning curve, and focus on becoming the best engineer possible.</p>
<h3 id="heading-beyond-unit-tests"><strong>Beyond Unit Tests</strong></h3>
<p>After mastering key unit testing skills such as mocking, assertion techniques, test-driven development (TDD), and code coverage analysis, move on to more advanced testing strategies like integration, end-to-end (E2E), and acceptance tests to further enhance your capabilities. This not only improves your coding skills but also exposes you to new testing methodologies that enhance your abilities further than what you'd achieve without testing.</p>
<h3 id="heading-continuously-verifying-system-needs"><strong>Continuously Verifying System Needs</strong></h3>
<p>Well-crafted tests do more than address immediate concerns; they continuously validate your changes, ensuring robust and reliable updates no matter who makes them. Our tests are our safety net.</p>
<h3 id="heading-the-importance-of-testing"><strong>The Importance of Testing</strong></h3>
<p>Estimates suggest that for every 1,000 lines of code delivered, there are typically 15 to 50 bugs, meaning 1.5% to 5% of all code may contain errors. These numbers can fluctuate but emphasize a constant reality: writing code always means creating bugs, as the two go hand in hand. By employing a variety of tests, emulating production environments closely, and thoroughly verifying changes, we can further reduce these numbers.</p>
<p>For me, the ultimate goal is receiving minimal feedback from QA. It's not that I don't value collaboration with them; on the contrary, I appreciate working together on changes. However, minimal feedback post-commit is my indicator of success, signifying no further refinements or issues were found. It's always a challenging goal, regardless of experience level. And even if QA and I find no issues, it doesn't mean there are none; it just indicates thorough anticipation of desired behaviors. Never underestimate the power of testing; it enhances not only the quality of outcomes but also your skills, making you a more well-rounded engineer.</p>
<h1 id="heading-advancing-the-journey-strategies-for-professional-growth-in-software-engineering">Advancing the Journey: Strategies for Professional Growth in Software Engineering</h1>
<p>Shifting gears from the intricacies of coding and the quest for quality, let's dive into the broader horizon of long-term career development. I've picked up a few strategies along the way that have been game-changers for climbing the career ladder.</p>
<h2 id="heading-grab-the-reins-of-your-future"><strong>Grab the Reins of Your Future</strong></h2>
<p>The best piece of advice? Don’t sit around waiting for a nudge forward. Taking the initiative is key. While it might seem like your manager or mentors should be paving your path, in reality, they're often swamped or might not fully grasp what you need. It's on you to pinpoint your improvement areas and bring them into the conversation. Remember, you're the captain of your ship.</p>
<h2 id="heading-voice-your-needs"><strong>Voice Your Needs</strong></h2>
<p>When you stumble upon something that sparks an idea or a potential growth avenue, bring it up with your mentors and manager. What works for me might not be the silver bullet for you, given our unique learning curves. For instance, craving more exposure to high-level discussions, I asked to shadow sessions with senior engineers, promising to blend into the background. It's about finding what accelerates your own growth and making it known.</p>
<h2 id="heading-embrace-feedback"><strong>Embrace Feedback</strong></h2>
<p>Make it a ritual, maybe every half year, to gather the folks you work closely with and seek their honest insights. There's always something to polish or a new angle to consider.</p>
<h2 id="heading-chart-your-learning-journey"><strong>Chart Your Learning Journey</strong></h2>
<p>Identify the skills that will propel you forward and seek out resources tailored to these areas. This strategic approach to learning can dramatically shorten the path to your goals.</p>
<h2 id="heading-when-in-doubt-reach-out"><strong>When in Doubt, Reach Out</strong></h2>
<p>Asking for help isn't just about overcoming immediate hurdles; it's about broadening your understanding of what it takes to level up. Dive into discussions about the skills and experiences you need, and don't shy away from seeking guidance.</p>
<h2 id="heading-know-and-surpass-your-roles-expectations"><strong>Know and Surpass Your Role's Expectations</strong></h2>
<p>Understanding your role's expectations is the first step. Next, identify ways to surpass those expectations. Engage with your team and manager to clarify any uncertainties, and then aim to exceed them.</p>
<h2 id="heading-initiative-is-your-best-friend"><strong>Initiative Is Your Best Friend</strong></h2>
<p>Whenever you spot an opportunity to add value, jump on it. Whether it's a small gesture or spearheading a major project, taking action without waiting for an invitation speaks volumes. My journey with <a target="_blank" href="https://github.com/FarzamMohammadi/ado-express">ADO Express</a>, born from spotting a process gap, highlights the impact of proactive problem-solving.</p>
<h2 id="heading-find-your-side-hustle"><strong>Find Your Side Hustle</strong></h2>
<p>Keep an eye out for side projects that pique your interest or passion. They're not just a break from the routine; they're a bridge to new skills and technologies. Diversify your learning to keep your passion alive and kicking.</p>
<h2 id="heading-stay-curious"><strong>Stay Curious</strong></h2>
<p>Adopt a habit of researching every new term or technology you encounter. A quick Google search can either satisfy your curiosity or lead you down a rabbit hole of fascinating discoveries. Either way, you're broadening your horizon with every search.</p>
<p>In weaving these strategies into your professional fabric, you're not merely preparing for the future; you're actively shaping it to fit your vision of success in software engineering. This journey is yours to command, with curiosity, initiative, and continuous learning as your guiding stars.</p>
<h2 id="heading-tackle-the-tough-stuff"><strong>Tackle the Tough Stuff</strong></h2>
<p>The most significant growth stems from taking on the tough challenges. It's tempting to stick to familiar territory, relying on the skills that got us to where we are. Yet, from my experience, the most substantial, transformative growth, especially in software engineering, comes from embracing challenges that stretch your boundaries—the kind that ventures into the unknown. Pushing beyond comfort zones not only sharpens your technical abilities but also fosters resilience and innovation, making you a true standout in the field.</p>
<p>Opting for the more challenging project, delving into unfamiliar code, and navigating complex problems—these endeavors are essential for elevating our capabilities. It transcends mere technical skill enhancement, fostering resilience, honing problem-solving acumen, and developing the perseverance needed to overcome obstacles.</p>
<h3 id="heading-why-tackling-the-tough-stuff-matters">Why Tackling the Tough Stuff Matters</h3>
<p>Embracing difficult tasks isn’t just good for your skill set; it's crucial for innovation and staying ahead in the fast-paced tech landscape. It forces you to think creatively, to be resourceful, and to find solutions where others might hit a wall.</p>
<p>So, as we move forward, remember: opting for the path with more resistance is not about making things hard for yourself. It’s about growth, learning, and setting yourself up for long-term success in your career. Let’s not shy away from the tough stuff. It’s where we find our greatest potential.</p>
<h1 id="heading-wrapping-up-the-journey"><strong>Wrapping Up The Journey</strong></h1>
<h2 id="heading-key-insights"><strong>Key Insights</strong></h2>
<p>Throughout this journey, you've encountered the theme of continuous improvement time and again. The essence of consistently choosing the more challenging path, actively pursuing your goals, and pushing beyond expectations has been underscored. The key takeaway is the importance of taking charge of both your personal and professional life, as ultimately, no one else will. Embrace this mindset, and you'll see a transformative shift in your outlook on life, fostering an insatiable hunger to learn, improve, and strive to be the best version of yourself.</p>
<h2 id="heading-stay-consistent-amp-enjoy-the-journey"><strong>Stay Consistent &amp; Enjoy The Journey</strong></h2>
<p>Adopting a lifestyle of continuous improvement means accepting that we are always a work in progress. The journey might get tough, and there may be moments when you feel utterly stuck. The crucial part is navigating through these challenges with a steadfast commitment to improvement. The image below captures the essence of maintaining consistency, even as your intensity may fluctuate. Real life is full of ups and downs, but as long as you keep the flame of consistency alive, you're on the right path.</p>
<p><img src="https://miro.medium.com/v2/resize:fit:700/1*KsFe9p5bLRlxxcu2KghUlQ.jpeg" alt="consistency over intensity" /></p>
<p>Source: <a target="_blank" href="https://medium.com/@adbeelomars3.0/consistency-over-intensity-8217f359bf9">Consistency Over Intensity</a></p>
<h2 id="heading-start-on-what-you-need-right-now"><strong>Start On What You Need Right Now!</strong></h2>
<p>I urge you to reflect on your career aspirations and identify areas needing the most attention. Develop a pragmatic action plan using the strategies discussed and transition those plans into tangible actions. The time for procrastination is over; seize the day with unparalleled zeal, for the future is unwritten, and only you hold the pen.</p>
<h2 id="heading-prepare-to-be-the-black-sheep"><strong>Prepare To Be The Black Sheep</strong></h2>
<p>Consider the individuals you admire, those who have achieved greatness. A common trait among them is their uniqueness—they're the black sheep. They dare to think differently and act distinctively. Embracing this mindset is challenging but crucial for those aiming to stand out. If you blend in with the crowd, what sets you apart?</p>
<h2 id="heading-engage-your-long-term-vision-goggles"><strong>Engage Your Long-Term Vision Goggles</strong></h2>
<p>Imagine your future. That typical question, "Where do you see yourself in 5 years?" isn't just small talk; it's a prompt to envision your path forward. Don't stop at five years; think ten, twenty years ahead. Determine your goals and pursue them with relentless passion, knowing that the only one who can truly propel you forward is yourself.</p>
<h2 id="heading-imagine-endless-possibilities-for-yourself"><strong>Imagine Endless Possibilities For Yourself</strong></h2>
<p>Consider the power of small habits, as eloquently captured by a favorite saying: "Reading 20 pages per day equals 30 books a year. Saving $10 per day accumulates to $3650 a year. Running 1 mile a day totals 365 miles a year." These small, daily actions compound into significant achievements over time. Envision where these habits could take you in just a year, and remember, time flies.</p>
<h2 id="heading-the-journey-will-be-tough-but-youll-be-even-tougher"><strong>The Journey Will Be Tough, But You'll Be Even Tougher</strong></h2>
<p>Embracing these principles will undoubtedly challenge you, but your resilience and consistency will see you through. The path to greatness is never easy, but it's always worth it.</p>
<h2 id="heading-thankful-for-the-amazing-team-i-work-with">Thankful For The Amazing Team I Work With</h2>
<p>My journey has been deeply influenced by the incredible team and mentors around me. Their guidance has been instrumental in my rapid growth from a novice to a role where I'm viewed as moving towards an intermediate/senior level. This progression, from taking on side projects to initiating blogs, reflects a journey of hard work and dedication. I hope my experiences inspire and guide you in your own path to success.</p>
<h1 id="heading-its-a-never-ending-journey">It's a Never Ending Journey</h1>
<p>Thank you for investing your time in reading this post. My aim was to inspire reflection on these topics and wish you the best of luck on your journey of career development. The road ahead is perpetual, filled with learning and growth at every turn. Keep moving forward, and may your path be ever upward.</p>
]]></content:encoded></item><item><title><![CDATA[Elevate Your App's Search Game: A Beginner's Guide to Supercharging Search Capabilities with Azure AI Search]]></title><description><![CDATA[Introduction to Azure AI Search
Azure AI Search, formerly Azure Cognitive Search, is your versatile toolkit for data management. It's an AI-driven platform that optimizes data accessibility while easing database load. My first encounter with Azure AI...]]></description><link>https://triedandtestedbuilds.com/elevate-your-apps-search-game-a-beginners-guide-to-supercharging-search-capabilities-with-azure-ai-search</link><guid isPermaLink="true">https://triedandtestedbuilds.com/elevate-your-apps-search-game-a-beginners-guide-to-supercharging-search-capabilities-with-azure-ai-search</guid><category><![CDATA[Azure Ai Search]]></category><category><![CDATA[azure-devops]]></category><category><![CDATA[Azure-Cognitive-Search]]></category><category><![CDATA[Search engine optimization]]></category><category><![CDATA[C#]]></category><dc:creator><![CDATA[Farzam Mohammadi]]></dc:creator><pubDate>Sat, 23 Dec 2023 23:28:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1703302718010/02e5af73-158c-4ced-8f0b-cd48c65ed93b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction-to-azure-ai-search"><strong>Introduction to Azure AI Search</strong></h2>
<p>Azure AI Search, formerly Azure Cognitive Search, is your versatile toolkit for data management. It's an AI-driven platform that optimizes data accessibility while easing database load. My first encounter with Azure AI Search was as a solution for database overload. Its capacity for handling and rapidly retrieving vast data sets, coupled with a user-friendly search process, makes it a standout choice.</p>
<p>But there's more to it than just easing database load. Azure AI Search acts like a digital librarian, swiftly and efficiently organizing and pulling up information. The AI-powered search not only improves accuracy but also enhances the user experience. Plus, it integrates smoothly with other Azure services, adding flexibility across various applications.</p>
<p>Azure AI Search revolutionizes the way we handle, search, and interact with large data sets. It has become an indispensable tool in application development, especially crucial in scenarios where enhanced search functionality plays a pivotal role.</p>
<h3 id="heading-key-features"><strong>Key Features</strong></h3>
<ul>
<li><p><strong>Efficient Data Handling</strong>: Acts as your digital librarian, swiftly organizing and retrieving large datasets.</p>
</li>
<li><p><strong>AI-Enhanced Search Precision</strong>: Boosts search accuracy for an improved user experience.</p>
</li>
<li><p><strong>Seamless Azure Integration</strong>: Ideal for integration within the Azure ecosystem.</p>
</li>
<li><p><strong>Advanced Data Querying</strong>: Features like searchable, filterable, sortable, and facetable fields enable sophisticated data queries.</p>
</li>
<li><p><strong>Database Relief:</strong> It significantly lightens the load on your databases, particularly in search-heavy applications, by efficiently managing complex queries that would traditionally tax your database resources.</p>
</li>
<li><p><strong>Versatile Search Capabilities:</strong> Excels in text and vector similarity searches, ideal for pattern analysis in large datasets.</p>
</li>
<li><p><strong>Complex Data Type Modeling</strong>: Handles intricate data structures with ease. For instance, it can manage complex objects like a <a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/search-howto-complex-data-types?tabs=portal#example-of-a-complex-structure">hotel's 'Room' attributes,</a> where each room is characterized by its description, room number, and rate.</p>
</li>
<li><p><strong>Content Transformation:</strong> Converts text or image files into searchable content, ideal for assets stored in Azure Blob Storage or Azure Cosmos DB.</p>
</li>
<li><p><strong>Customizable Text Analysis:</strong> Supports various linguistic tools and customizes text analysis for multi-language and format content.</p>
</li>
</ul>
<p>Now that we've covered the features, let's delve into understanding a Search Index in Azure AI Search.</p>
<h3 id="heading-understanding-a-search-index"><strong>Understanding a Search Index</strong></h3>
<p>A Search Index is like the index of a book but for digital data. It lists topics and keywords, guiding you to the information you need. This specialized database is tailored for large data sets, organizing data for efficient searching and retrieval. It's the go-to tool for finding that 'needle in the digital haystack'.</p>
<h3 id="heading-azure-ai-search-enhanced-features-and-application-scenarios"><strong>Azure AI Search Enhanced Features and Application Scenarios</strong></h3>
<p>Azure AI Search elevates search functionality with features for full-text searches (searchable fields), refining results (filterable fields), organizing results (sortable fields), and aiding in search refinement (facetable fields). It excels in various application scenarios, including full-text and vector similarity searches, making it suitable for generative AI applications. Additionally, it streamlines the implementation of search features like relevance tuning and faceted navigation and transforms extensive files into searchable formats. It also accommodates diverse linguistic and custom text analyses, ensuring versatility in content processing.</p>
<p>With an understanding of Azure AI Search's capabilities, it's time to turn theory into action by building our own Search Index.</p>
<h3 id="heading-whats-the-plan"><strong>What's the Plan?</strong></h3>
<p>I am a firm believer in learning by doing. We're diving into the process of building a Search Index in C#. I'll take you through each step, breaking down the code along the way. After we explore each part, we'll run the project and check out what we've accomplished. Here’s what we’ll be doing:</p>
<ol>
<li><p><strong>Creating the Azure Data Source (Via Azure Portal):</strong> We'll start by creating the data source for our Search Index, using Azure Table Storage.</p>
</li>
<li><p><strong>Creating the Azure AI Search Service (Via Azure Portal):</strong> Next, we establish the Azure Search Service, the home for our Search Index and its components.</p>
</li>
<li><p><strong>Cloning the Repository &amp; Project Preparation (Optional):</strong> For hands-on experience, clone the repository and align your appsettings.json with your Azure setup.</p>
</li>
<li><p><strong>Populating the Data Source (Via Code):</strong> With Azure Table Storage ready, we'll populate it with 'Books' table data, perfect for filtering, sorting, and searching.</p>
</li>
<li><p><strong>Building the Search Index Infrastructure (Via Code):</strong> We then shift our focus to creating a Search Index that aligns with our 'Books' table's fields.</p>
</li>
<li><p><strong>Populating the Search Index (Via Code):</strong> Now, we're at the data loading phase. Here, we'll set up an Indexer within the Azure Search Service. Think of the Indexer as a bridge: it's provided by Azure free of charge, and it connects two key points. On one end, we have our Data Source Connection, our source, where we've got our 'Books' table data. On the other end, there's our Search Index, the target, ready to receive and organize this data. Once we link the Indexer to our data source, it'll funnel data into the Search Index, transforming raw data into a structured, searchable format.</p>
</li>
<li><p><strong>Querying Data from the Search Index (Via Code):</strong> Finally, with our Search Index fully stocked, we'll run various queries to explore Azure AI's querying capabilities, giving us a practical understanding of its potential.</p>
</li>
</ol>
<h3 id="heading-im-assuming"><strong>I'm Assuming</strong></h3>
<ol>
<li><p><strong>You Have an Azure Subscription:</strong> To create resources, an Azure Subscription is required. If you don't have one yet, <a target="_blank" href="https://azure.microsoft.com/en-ca/free/"><strong>signing up</strong></a> is straightforward, and you'll immediately receive $200 in credits, which is more than enough for this project.</p>
</li>
<li><p><strong>A Prepared Resource Group:</strong> This is where we'll host our resources. If you’re not familiar with setting up a resource group in Azure, you can follow the steps outlined <a target="_blank" href="https://triedandtestedbuilds.com/easy-guide-to-creating-a-resource-group-in-azure"><strong>here</strong></a>.</p>
</li>
<li><p><strong>Basic Programming Knowledge (Optional):</strong> It's helpful to know some programming basics, especially how to use languages with external libraries. We'll be using C# and SDKs here. If not, don't worry, I'll guide you through everything.</p>
</li>
<li><p><strong>.NET IDE Ready to Go (Optional):</strong> Ideal to have Visual Studio or Rider installed.</p>
</li>
</ol>
<blockquote>
<p><strong>Note: If coding isn't your thing or you prefer to learn passively, you can skip the optional steps and still grasp the key concepts.</strong></p>
</blockquote>
<h2 id="heading-setting-up-the-foundations"><strong>Setting Up the Foundations</strong></h2>
<h3 id="heading-azure-data-source-creation"><strong>Azure Data Source Creation</strong></h3>
<p>We begin by establishing a data source for our Azure Search Index. Azure supports a variety of them, including Azure Blob Storage, Cosmos DB, Data Lake Storage Gen2, and several others, with some like Azure Files and Azure MySQL currently in preview. You can dive deeper into these options <a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/search-indexer-overview#supported-data-sources">here</a>.</p>
<p>For simplicity, we're going with <a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/search-howto-indexing-azure-tables">Azure Table Storage</a>. It's straightforward to set up. Just head to your Resource Group, click '+ Create', and find 'Storage Account' in the Azure Marketplace.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703108282291/2388d258-2a9c-44e9-9ec3-7f3ea8175e34.png" alt class="image--center mx-auto" /></p>
<p>Choose the same Region as your Resource Group and opt for 'Locally-redundant storage' under Redundancy to minimize costs. The rest? Just leave it as is and hit 'Create'.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703115280100/3b2d8456-ed66-443a-a994-a6c90de7d7d8.png" alt class="image--center mx-auto" /></p>
<p>Feel free to explore the <a target="_blank" href="https://learn.microsoft.com/en-us/azure/storage/tables/table-storage-quickstart-portal">official documentation</a> for more details.</p>
<h3 id="heading-azure-ai-search-service-creation"><strong>Azure AI Search Service Creation</strong></h3>
<p>Next, we set up the Azure AI Search Service. In your Resource Group, select '+ Create' and search for 'Azure AI Search' in the Azure Marketplace.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703108999024/5f541c90-bd8c-46d1-aebd-4b14b2370086.png" alt class="image--center mx-auto" /></p>
<p>Again select the same Region as your Resource Group, but this time, pick the 'Free' Pricing Tier. Leave the other settings default and proceed with 'Review + Create'. This setup won't cost you a dime.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703115478581/7a51812f-727d-4c74-bc43-771a16a7e5a6.png" alt class="image--center mx-auto" /></p>
<p>Feel free to explore the <a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/search-create-service-portal">official documentation</a> for more details.</p>
<h2 id="heading-preparing-for-coding"><strong>Preparing for Coding</strong></h2>
<h3 id="heading-cloning-the-repository-amp-project-preparation-optional"><strong>Cloning the Repository &amp; Project Preparation (Optional)</strong></h3>
<p>For those who prefer hands-on coding, clone the <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder"><strong>azure-search-index-builder</strong></a> repository. Make sure to align the settings in the <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/appsettings.json"><strong>appsettings.json file</strong></a> with your newly created Storage Account and Search Service.</p>
<p>Here's how the <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/appsettings.json"><strong>appsettings.json file</strong></a> should look based on my previous steps:</p>
<pre><code class="lang-json">﻿{
  <span class="hljs-attr">"AzureStorageAccount"</span>: {
    <span class="hljs-attr">"Name"</span>: <span class="hljs-string">"mystorageaccount9999"</span>, <span class="hljs-comment">// Your Azure Storage Account name - Can be found on the very top of the Storage Account overview</span>
    <span class="hljs-attr">"Key"</span>: <span class="hljs-string">"xxxxxxx"</span> <span class="hljs-comment">// Your Azure Storage Account key - Can be found in the Storage Account settings, under "Access keys"</span>
  },
  <span class="hljs-attr">"AzureSearchService"</span>: {
    <span class="hljs-attr">"Url"</span>: <span class="hljs-string">"https://my-searchservice9999.search.windows.net/"</span>, <span class="hljs-comment">// Your Azure Search Service URL, in the format of https://&lt;service name&gt;.search.windows.net</span>
    <span class="hljs-attr">"AdminApiKey"</span>: <span class="hljs-string">"xxxxxxx"</span> <span class="hljs-comment">// Your Azure Search Service Admin API key, can be found in the Azure Search Service settings, under "Keys"</span>
  }
}
</code></pre>
<p>If you're more inclined to learn passively, feel free to skip this step.</p>
<h2 id="heading-diving-into-the-code"><strong>Diving into the Code</strong></h2>
<h3 id="heading-populating-the-data-source"><strong>Populating the Data Source</strong></h3>
<p>Now, we dive into the code. In the <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Program.cs">Program.cs</a> file, you'll find the <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Program.cs#L37-L50"><code>CreateAndPopulateBooksTable()</code></a> method where the Data Source preparation happens. We set up and fill the 'Books' table in Azure Table Storage, using data from the <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Assets/books.json">books.json file</a>.</p>
<p>Here’s a quick look at what we're doing:</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">string</span> <span class="hljs-title">CreateAndPopulateBooksTable</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> azureStorageAccountName, <span class="hljs-keyword">string</span> azureStorageAccountKey</span>)</span>
{
    <span class="hljs-comment">// Create Books table in Azure Storage</span>
    <span class="hljs-keyword">const</span> <span class="hljs-keyword">string</span> books = <span class="hljs-string">"Books"</span>;

    <span class="hljs-keyword">var</span> tableServiceManager = <span class="hljs-keyword">new</span> TableServiceManager(azureStorageAccountName, azureStorageAccountKey);

    tableServiceManager.CreateTable(books);

    <span class="hljs-comment">// Insert data from JSON file into Books table</span>
    tableServiceManager.InsertRecordsIntoTable(books);

    <span class="hljs-keyword">return</span> books;
}
</code></pre>
<p>We kick things off by creating an instance of the <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/TableServiceManager.cs"><code>TableServiceManager</code></a> class, which sets up our connection to Azure Table Storage. This connection is crucial as it's our gateway to interacting with the storage service.</p>
<p>Here's how the <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/TableServiceManager.cs#L13-L21">Table Service Client</a> looks like:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> TableServiceClient _tableServiceClient = <span class="hljs-keyword">new</span>
(
    <span class="hljs-keyword">new</span> Uri(<span class="hljs-string">$"https://<span class="hljs-subst">{storageAccountName}</span>.table.core.windows.net/"</span>),
    <span class="hljs-keyword">new</span> TableSharedKeyCredential
    (
        storageAccountName,
        storageAccountKey
    )
);
</code></pre>
<p>We then call <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/TableServiceManager.cs#L23-L26"><code>CreateTable()</code></a> to create our 'Books' table, followed by <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/TableServiceManager.cs#L28-L47"><code>InsertRecordsIntoTable()</code></a> to populate it.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">CreateTable</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> tableName</span>)</span>
{
    _tableServiceClient.CreateTableIfNotExists(tableName);
}
</code></pre>
<p>The <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/TableServiceManager.cs#L28-L47"><code>InsertRecordsIntoTable</code></a> method is where we read the <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Assets/books.json">books.json file</a>, convert its contents into <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Models/Book.cs"><code>Book</code></a> objects, and then insert them into our table.</p>
<p>Here's the <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/TableServiceManager.cs#L28-L47"><code>InsertRecordsIntoTable</code></a> method:</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">InsertRecordsIntoTable</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> tableName</span>)</span>
{
    <span class="hljs-keyword">var</span> tableClient = _tableServiceClient.GetTableClient(tableName);

    <span class="hljs-keyword">var</span> jsonFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, <span class="hljs-string">"Assets"</span>, <span class="hljs-string">"books.json"</span>);
    <span class="hljs-keyword">var</span> jsonFileContent = File.ReadAllText(jsonFilePath);

    <span class="hljs-keyword">var</span> books =
        JsonConvert.DeserializeObject&lt;List&lt;Book&gt;&gt;(jsonFileContent)
        ??
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> Exception(<span class="hljs-string">"Unable to deserialize books. Check JSON file and make sure it was included in the build."</span>);

    <span class="hljs-keyword">foreach</span> (<span class="hljs-keyword">var</span> book <span class="hljs-keyword">in</span> books)
    {
        book.PartitionKey = book.Genre;
        book.RowKey = book.Id;

        tableClient.AddEntity(book);
    }
}
</code></pre>
<p>The <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Models/Book.cs"><code>Book</code></a> class is straightforward. It inherits from <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Models/Book.cs#L9"><code>ITableEntity</code></a>, a requirement for any entity you want to store in Azure Table Storage. Here's what our <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Models/Book.cs"><code>Book</code></a> class looks like:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Book</span> : <span class="hljs-title">ITableEntity</span>
{
    [<span class="hljs-meta">JsonIgnore</span>] <span class="hljs-comment">// Ignored by Index &amp; Indexer.</span>
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> PartitionKey { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-literal">null</span>!;

    [<span class="hljs-meta">JsonIgnore</span>] <span class="hljs-comment">// Ignored by Index &amp; Indexer.</span>
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> RowKey { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-literal">null</span>!;

    [<span class="hljs-meta">JsonIgnore</span>] <span class="hljs-comment">// Ignored by Index &amp; Indexer.</span>
    <span class="hljs-keyword">public</span> DateTimeOffset? Timestamp { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    [<span class="hljs-meta">JsonIgnore</span>] <span class="hljs-comment">// Ignored by Index &amp; Indexer.</span>
    <span class="hljs-keyword">public</span> ETag ETag { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    [<span class="hljs-meta">SimpleField(IsKey = true)</span>]
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Id { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-literal">null</span>!;

    [<span class="hljs-meta">SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnLucene)</span>]
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Title { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-literal">null</span>!;

    [<span class="hljs-meta">SearchableField(IsFilterable = true)</span>]
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Author { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-literal">null</span>!;

    [<span class="hljs-meta">SimpleField(IsFilterable = true)</span>]
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Genre { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-literal">null</span>!;

    [<span class="hljs-meta">SimpleField(IsFilterable = true, IsSortable = true)</span>]
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> PublishedYear { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    [<span class="hljs-meta">SimpleField(IsSortable = true, IsFilterable = true)</span>]
    <span class="hljs-keyword">public</span> <span class="hljs-keyword">double</span> Price { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }

    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">override</span> <span class="hljs-keyword">string</span> <span class="hljs-title">ToString</span>(<span class="hljs-params"></span>)</span>
    {
        <span class="hljs-keyword">return</span> <span class="hljs-string">$"Id: <span class="hljs-subst">{Id}</span>, Title: <span class="hljs-subst">{Title}</span>, Author: <span class="hljs-subst">{Author}</span>, Genre: <span class="hljs-subst">{Genre}</span>, PublishedYear: <span class="hljs-subst">{PublishedYear}</span>, Price: <span class="hljs-subst">{Price}</span>"</span>;
    }
}
</code></pre>
<p>And that's the walkthrough of the setup! By following these steps in the project, you will have created a table in Azure Table Storage and populated it with data, setting the stage for indexing once you run the project.</p>
<p>Once you've executed the code, you can verify the results in your Storage Account. Simply navigate to the 'Books' table by going to your Storage Account &gt; Storage Browser &gt; Table &gt; Books. There, you should find 30 Book records, showcasing the successful data population.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703281778176/a3c6f3b8-b238-4479-9a1c-3a0354271670.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-build-the-search-index-infrastructure"><strong>Build the Search Index Infrastructure</strong></h3>
<p>We then focus on constructing the Search Index infrastructure. The trickiest part? Defining the Index fields.</p>
<h4 id="heading-index-field-preparation"><strong>Index Field Preparation</strong></h4>
<p>Imagine a Search Index as similar to a database table, where fields in the index parallel the columns in a database. Just like defining column types in a database, we define field types in a Search Index. However, there's an additional layer in Search Indexes as we also need to specify the query attributes for each field. This distinction is what sets the Search Index fields apart and tailors them for efficient searching.</p>
<p>Now, let's circle back to our <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Models/Book.cs"><code>Book</code></a> class. The fields in this class come with specific annotations, demonstrating how Azure AI Search interprets and utilizes each field. Here’s a quick glance at some of these annotations:</p>
<p><strong>JsonIgnore</strong>: This annotation tells Azure to exclude specific fields during index creation. For instance, <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Models/Book.cs#L14-L15"><code>RowKey</code></a> is not necessary for our querying purposes and can be overlooked.</p>
<pre><code class="lang-csharp">[<span class="hljs-meta">JsonIgnore</span>] <span class="hljs-comment">// Ignored by Index &amp; Indexer.</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> RowKey { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-literal">null</span>!;
</code></pre>
<p><strong>SimpleField</strong>: Used for fields that we want to retrieve but not necessarily search through. The <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Models/Book.cs#L23-L24"><code>Id</code></a> field is a perfect example; it's retrievable but not intended for searching.</p>
<pre><code class="lang-csharp">[<span class="hljs-meta">SimpleField(IsKey = true)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Id { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-literal">null</span>!;
</code></pre>
<p><strong>SearchableField</strong>: Assigned to fields that should be both retrievable and searchable. Consider the <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Models/Book.cs#L29-L30"><code>Author</code></a> field, which is also marked as filterable.</p>
<pre><code class="lang-csharp">[<span class="hljs-meta">SearchableField(IsFilterable = true)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">string</span> Author { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; } = <span class="hljs-literal">null</span>!;
</code></pre>
<p>These examples illustrate just a few of the annotations and field setups used in Search Indexes. For a comprehensive understanding of how to set field attributes and to explore all the options available, I recommend diving into the documentation on <a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/search-what-is-an-index#field-definitions">Field definitions</a> and <a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/search-what-is-an-index#field-attributes">Field attributes</a>.</p>
<h4 id="heading-index-construction"><strong>Index Construction</strong></h4>
<p>In <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Program.cs">Program.cs</a>, we use the <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Program.cs#L52-L74"><code>CreateAndPopulateBooksSearchIndex</code></a> method to build our Search Index and its components. The <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/SearchServiceManager.cs"><code>SearchServiceManager</code></a> class sets up the connection to our Azure Cognitive AI Search service. In this section, we focus on the Index creation, happening in <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/SearchServiceManager.cs#L20-L34"><code>CreateIndex</code></a> method, called from <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Program.cs#L62">Program.cs on line 62</a>.</p>
<p>The process:</p>
<ol>
<li><p>Initialize the Search Index Client.</p>
</li>
<li><p>Build the index fields using FieldBuilder.</p>
</li>
<li><p>Define the index name and fields.</p>
</li>
<li><p>Either create a new index or update an existing one using CreateOrUpdateIndex.</p>
</li>
</ol>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> SearchIndex <span class="hljs-title">CreateIndex</span>(<span class="hljs-params"></span>)</span>
{
    <span class="hljs-keyword">var</span> searchIndexClient = <span class="hljs-keyword">new</span> SearchIndexClient(_searchServiceAuthentication.Uri, _searchServiceAuthentication.Credentials);

    <span class="hljs-comment">// Define &amp; build the index fields</span>
    <span class="hljs-keyword">var</span> fieldBuilder = <span class="hljs-keyword">new</span> FieldBuilder();
    <span class="hljs-keyword">var</span> searchFields = fieldBuilder.Build(<span class="hljs-keyword">typeof</span>(Book));

    <span class="hljs-comment">// Set the index name and fields</span>
    <span class="hljs-keyword">var</span> searchIndex = <span class="hljs-keyword">new</span> SearchIndex(<span class="hljs-string">"my-index"</span>, searchFields);

    searchIndexClient.CreateOrUpdateIndex(searchIndex);

    <span class="hljs-keyword">return</span> searchIndex;
}
</code></pre>
<h3 id="heading-populate-the-search-index"><strong>Populate the Search Index</strong></h3>
<p>Next up: transferring data from our Azure Storage Book table to the Search Index.</p>
<p>We start by setting up a Data Source Connection for our Indexer. This connection links our data source (the Azure Storage Book table) to the Indexer. The setup happens in <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Program.cs#L65">Program.cs, line 65</a> when we run <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/SearchServiceManager.cs#L36-L57"><code>CreateIndexerDataSource</code></a>.</p>
<p>The process involves:</p>
<ol>
<li><p>Creating a Search Indexer Client.</p>
</li>
<li><p>Building the Data Source Connection.</p>
</li>
<li><p>Either creating a new Data Source Connection or updating the existing one using CreateOrUpdateDataSourceConnection.</p>
</li>
</ol>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> SearchIndexerClient <span class="hljs-title">CreateIndexerDataSource</span>
(<span class="hljs-params">
    <span class="hljs-keyword">string</span> storageAccountName,
    <span class="hljs-keyword">string</span> storageAccountKey,
    <span class="hljs-keyword">string</span> tableName,
    <span class="hljs-keyword">out</span> SearchIndexerDataSourceConnection searchIndexerDataSourceConnection
</span>)</span>
{
    <span class="hljs-keyword">var</span> searchIndexerClient = <span class="hljs-keyword">new</span> SearchIndexerClient(_searchServiceAuthentication.Uri, _searchServiceAuthentication.Credentials);

    searchIndexerDataSourceConnection = <span class="hljs-keyword">new</span> SearchIndexerDataSourceConnection
    (
        <span class="hljs-string">"my-index-data-source"</span>,
        SearchIndexerDataSourceType.AzureTable,
        <span class="hljs-string">$"DefaultEndpointsProtocol=https;AccountName=<span class="hljs-subst">{storageAccountName}</span>;AccountKey=<span class="hljs-subst">{storageAccountKey}</span>;EndpointSuffix=core.windows.net"</span>,
        <span class="hljs-keyword">new</span> SearchIndexerDataContainer(tableName)
    );

    searchIndexerClient.CreateOrUpdateDataSourceConnection(searchIndexerDataSourceConnection);

    <span class="hljs-keyword">return</span> searchIndexerClient;
}
</code></pre>
<p>After establishing the Data Source Connection, we create the Indexer by calling the <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/SearchServiceManager.cs#L59-L66"><code>CreateIndexer</code></a> method from <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Program.cs#L68">Program.cs, line 68</a>. This sets up where the data is pulled from and where it's pushed to through the <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/SearchServiceManager.cs#L61C33-L61C47">SearchIndexer class instantiation</a>.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> SearchIndexer <span class="hljs-title">CreateIndexer</span>(<span class="hljs-params">SearchIndexerDataSourceConnection dataSourceConnection, SearchIndex searchIndex, SearchIndexerClient searchIndexerClient</span>)</span>
{
    <span class="hljs-keyword">var</span> searchIndexer = <span class="hljs-keyword">new</span> SearchIndexer(<span class="hljs-string">"my-index-indexer"</span>, dataSourceConnection.Name, searchIndex.Name);

    searchIndexerClient.CreateOrUpdateIndexer(searchIndexer);

    <span class="hljs-keyword">return</span> searchIndexer;
}
</code></pre>
<p>Finally, to start the data transfer, we run the <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/SearchServiceManager.cs#L68-L73"><code>PopulateIndex</code></a> method in <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Program.cs#L71">Program.cs, line 71</a>, which <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/SearchServiceManager.cs#L70">resets</a> and then <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/SearchServiceManager.cs#L72">runs</a> the Indexer.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">PopulateIndex</span>(<span class="hljs-params">SearchIndexerClient searchIndexerClient, SearchIndexer searchIndexer</span>)</span>
{
    searchIndexerClient.ResetIndexer(searchIndexer.Name);

    searchIndexerClient.RunIndexer(searchIndexer.Name);
}
</code></pre>
<p>This function does two key things:</p>
<ol>
<li><p>Resets the change tracking state of indexed documents.</p>
</li>
<li><p>Runs the Indexer to begin transferring data into our Search Index.</p>
</li>
</ol>
<p>The Indexer may take around 30 seconds to finish transferring data to the Index. To view results in your Cognitive AI Search service navigate to Indexes &gt; my-index and hit Search.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703281535588/10087229-a2fe-4330-9968-7b0fc64facd2.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703299304424/faca9366-b175-4a70-a10e-a30d043ab524.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-querying-data-from-the-search-index"><strong>Querying Data From the Search Index</strong></h3>
<p>Before we run the code, let's take a look at some queries that highlight the capabilities of the Search Index. I've set up four distinct queries for us to execute. They're called from <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/Program.cs#L85-L95">Program.cs (lines 85-95)</a>, each designed to demonstrate the versatility of our Search Index.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">RunQueriesOnSearchIndexAndPrintResults</span>(<span class="hljs-params"><span class="hljs-keyword">string</span> azureSearchServiceEndpoint, <span class="hljs-keyword">string</span> azureSearchServiceAdminApiKey, SearchIndex azureSearchIndex</span>)</span>
{
    <span class="hljs-keyword">var</span> searchIndexDataRetriever = <span class="hljs-keyword">new</span> SearchIndexDataRetriever
    (
        azureSearchServiceEndpoint,
        azureSearchServiceAdminApiKey,
        azureSearchIndex
    );

    <span class="hljs-keyword">var</span> queryOneResults = searchIndexDataRetriever.SearchBooksForGeorgeOrwell();
    queryOneResults.PrintValue();

    <span class="hljs-keyword">var</span> queryTwoResults = searchIndexDataRetriever.FilterBooksCheaperThanTwentyFiveDollarsInDescendingOrder();
    queryTwoResults.PrintValue();

    <span class="hljs-keyword">var</span> queryThreeResults = searchIndexDataRetriever.SortBooksByPublishedYearInAscendingOrderAndGrabTopFive();
    queryThreeResults.PrintValue();

    <span class="hljs-keyword">var</span> queryFourResults = searchIndexDataRetriever.SearchBooksForTitleMatchingMockingBird();
    queryFourResults.PrintValue();
}
</code></pre>
<p>Let's break down these queries:</p>
<p><strong>Query 1 - Searching for 'George Orwell'</strong>: This query rummages through all fields for any mention of 'George Orwell'.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> SearchResults&lt;Book&gt; <span class="hljs-title">SearchBooksForGeorgeOrwell</span>(<span class="hljs-params"></span>)</span>
{
    Console.WriteLine(<span class="hljs-string">"Query 1: Search for 'George Orwell':"</span>);

    <span class="hljs-keyword">var</span> options = <span class="hljs-keyword">new</span> SearchOptions();

    GrabAllBookFieldsFromTheIndex(options);

    <span class="hljs-keyword">var</span> results = _searchClient.Search&lt;Book&gt;(<span class="hljs-string">"George Orwell"</span>, options);

    <span class="hljs-keyword">return</span> results;
}
</code></pre>
<p><strong>Query 2 - Filtering Books Under $25</strong>: Here, we filter for books priced below $25 and sort them by price in descending order.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> SearchResults&lt;Book&gt; <span class="hljs-title">FilterBooksCheaperThanTwentyFiveDollarsInDescendingOrder</span>(<span class="hljs-params"></span>)</span>
{
    Console.WriteLine(<span class="hljs-string">"Query 2: Apply a filter to find books cheaper than $25, order by Price in descending order:"</span>);

    <span class="hljs-keyword">var</span> options = <span class="hljs-keyword">new</span> SearchOptions
    {
        Filter = <span class="hljs-string">"Price lt 25"</span>,
        OrderBy = { <span class="hljs-string">"Price desc"</span> }
    };

    GrabAllBookFieldsFromTheIndex(options);

    <span class="hljs-keyword">var</span> results = _searchClient.Search&lt;Book&gt;(<span class="hljs-string">"*"</span>, options);

    <span class="hljs-keyword">return</span> results;
}
</code></pre>
<p><strong>Query 3 - Sorting by Published Year, Top 5</strong>: This query sorts books by their publication year in ascending order, grabbing the first five.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> SearchResults&lt;Book&gt; <span class="hljs-title">SortBooksByPublishedYearInAscendingOrderAndGrabTopFive</span>(<span class="hljs-params"></span>)</span>
{
    Console.WriteLine(<span class="hljs-string">"Query 3: Search all the books, order by published year in ascending order, take the top 5 results:"</span>);

    <span class="hljs-keyword">var</span> options = <span class="hljs-keyword">new</span> SearchOptions
    {
        Size = <span class="hljs-number">5</span>,
        OrderBy = { <span class="hljs-string">"PublishedYear asc"</span> }
    };

    GrabAllBookFieldsFromTheIndex(options);

    <span class="hljs-keyword">var</span> results = _searchClient.Search&lt;Book&gt;(<span class="hljs-string">"*"</span>, options);

    <span class="hljs-keyword">return</span> results;
}
</code></pre>
<p><strong>Query 4 - Title Search for 'Mockingbird'</strong>: We focus on the 'Title' field to find books with 'Mockingbird' in their title.</p>
<pre><code class="lang-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> SearchResults&lt;Book&gt; <span class="hljs-title">SearchBooksForTitleMatchingMockingBird</span>(<span class="hljs-params"></span>)</span>
{
    Console.WriteLine(<span class="hljs-string">"Query 4: Search the Title field for the term 'Mockingbird':"</span>);

    <span class="hljs-keyword">var</span> options = <span class="hljs-keyword">new</span> SearchOptions
    {
        SearchFields = { <span class="hljs-string">"Title"</span> }
    };

    GrabAllBookFieldsFromTheIndex(options);

    <span class="hljs-keyword">var</span> results = _searchClient.Search&lt;Book&gt;(<span class="hljs-string">"Mockingbird"</span>, options);

    <span class="hljs-keyword">return</span> results;
}
</code></pre>
<h2 id="heading-running-the-project-and-viewing-results"><strong>Running the Project and Viewing Results</strong></h2>
<h3 id="heading-executing-the-project"><strong>Executing the Project</strong></h3>
<p>After configuring the variables in your <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/appsettings.json"><strong>appsettings.json file</strong></a>, simply run the project. That's all there is to it. Should you encounter any issues, refer to the <a class="post-section-overview" href="#heading-troubleshooting-amp-additional-information">Troubleshooting &amp; Additional Information</a> section for guidance.</p>
<h3 id="heading-observing-the-query-outputs"><strong>Observing the Query Outputs</strong></h3>
<p>After running the project, If everything has gone smoothly, we should now see the following results from our queries:</p>
<p><strong>Query 1</strong>: Finds '1984' by George Orwell.</p>
<pre><code class="lang-plaintext">Id: 2, Title: 1984, Author: George Orwell, Genre: Dystopian, PublishedYear: 1949, Price: 19.99
</code></pre>
<p><strong>Query 2</strong>: Lists books under $25, like 'The Great Gatsby' by F. Scott Fitzgerald and 'Moby Dick' by Herman Melville, sorted by price.</p>
<pre><code class="lang-plaintext">Id: 1, Title: The Great Gatsby, Author: F. Scott Fitzgerald, Genre: Fiction, PublishedYear: 1925, Price: 24.99
Id: 9, Title: Book Title 9, Author: Author 9, Genre: Other Genre, PublishedYear: 1959, Price: 24.99           
Id: 8, Title: Moby Dick, Author: Herman Melville, Genre: Adventure, PublishedYear: 1851, Price: 22.99         
Id: 5, Title: Brave New World, Author: Aldous Huxley, Genre: Dystopian, PublishedYear: 1932, Price: 22.99     
Id: 6, Title: The Hobbit, Author: J.R.R. Tolkien, Genre: Fantasy, PublishedYear: 1937, Price: 21.99           
Id: 4, Title: The Catcher in the Rye, Author: J.D. Salinger, Genre: Fiction, PublishedYear: 1951, Price: 20.99
Id: 2, Title: 1984, Author: George Orwell, Genre: Dystopian, PublishedYear: 1949, Price: 19.99                
Id: 3, Title: To Kill a Mockingbird, Author: Harper Lee, Genre: Fiction, PublishedYear: 1960, Price: 18.99    
Id: 7, Title: Pride and Prejudice, Author: Jane Austen, Genre: Romance, PublishedYear: 1813, Price: 18.99
</code></pre>
<p><strong>Query 3</strong>: Brings up the top 5 books sorted by year, starting with the oldest, like 'Pride and Prejudice' by Jane Austen.</p>
<pre><code class="lang-plaintext">Id: 7, Title: Pride and Prejudice, Author: Jane Austen, Genre: Romance, PublishedYear: 1813, Price: 18.99
Id: 8, Title: Moby Dick, Author: Herman Melville, Genre: Adventure, PublishedYear: 1851, Price: 22.99         
Id: 1, Title: The Great Gatsby, Author: F. Scott Fitzgerald, Genre: Fiction, PublishedYear: 1925, Price: 24.99
Id: 5, Title: Brave New World, Author: Aldous Huxley, Genre: Dystopian, PublishedYear: 1932, Price: 22.99     
Id: 6, Title: The Hobbit, Author: J.R.R. Tolkien, Genre: Fantasy, PublishedYear: 1937, Price: 21.99
</code></pre>
<p><strong>Query 4</strong>: Successfully locates 'To Kill a Mockingbird' by Harper Lee.</p>
<pre><code class="lang-plaintext">Id: 3, Title: To Kill a Mockingbird, Author: Harper Lee, Genre: Fiction, PublishedYear: 1960, Price: 18.99
</code></pre>
<h3 id="heading-query-outputs-explained"><strong>Query Outputs Explained</strong></h3>
<p>Alright, let's break down what's happening in our query method calls. It's simpler than it looks, I promise!</p>
<p>We're basically doing two things in each query:</p>
<ol>
<li><p><strong>Setting Search Options</strong>: We tailor these options to meet our specific query needs. This is where we decide how we want to search, filter, sort or in other ways, find the data we need.</p>
</li>
<li><p><strong>Selecting Fields to Receive</strong>: We select which Search Index fields we want to receive in our results.</p>
</li>
</ol>
<p>Each query is crafted to showcase a different aspect of Azure Search's data retrieval prowess. Let me walk you through a couple of examples:</p>
<p><strong>Filtering by Price, Sorted in Descending Order</strong></p>
<p>Consider this query:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> options = <span class="hljs-keyword">new</span> SearchOptions
{
    Filter = <span class="hljs-string">"Price lt 25"</span>,
    OrderBy = { <span class="hljs-string">"Price desc"</span> }
};

<span class="hljs-keyword">var</span> results = _searchClient.Search&lt;Book&gt;(<span class="hljs-string">"*"</span>, options);
</code></pre>
<p>Here, we're implementing a filter to identify books priced below (less than, denoted as 'lt') $25, and we're sorting these results in descending (abbreviated as 'desc') order by price. The asterisk '*', used as a wildcard, signifies a broad search without pinpointing a specific term. This approach depends on the 'Price' field being set as both filterable and sortable in our Book class:</p>
<pre><code class="lang-csharp">[<span class="hljs-meta">SimpleField(IsSortable = true, IsFilterable = true)</span>]
<span class="hljs-keyword">public</span> <span class="hljs-keyword">double</span> Price { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
</code></pre>
<p><strong>Searching by Title for 'Mockingbird'</strong></p>
<p>Now, let's look at a title-specific search:</p>
<pre><code class="lang-csharp"><span class="hljs-keyword">var</span> options = <span class="hljs-keyword">new</span> SearchOptions
{
    SearchFields = { <span class="hljs-string">"Title"</span> }
};

<span class="hljs-keyword">var</span> results = _searchClient.Search&lt;Book&gt;(<span class="hljs-string">"Mockingbird"</span>, options);
</code></pre>
<p>In this query, we're zeroing in on the 'Title' field to find matches for 'Mockingbird'. This approach narrows our search scope to titles, seeking the best match for the given term.</p>
<p>If you're still feeling a bit fuzzy on these concepts, I recommend running the code yourself and tweaking the queries. It's a hands-on way to see how flexible and powerful Azure Search can be. For further reading and more examples, check out the <a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/search-howto-dotnet-sdk#run-queries">official Azure Search documentation</a>.</p>
<p>By following these steps, you're well-equipped to embark on your own Proof of Concept (POC) projects. The best way to learn is by doing, so dive in and experiment! And hey, once you've mastered it, feel free to flaunt 'Expert Azure Search Index Developer' on your resume – you've earned it!</p>
<h3 id="heading-a-quick-tour-of-our-index-indexer-and-data-source-connection-optional"><strong>A Quick Tour of Our Index, Indexer, and Data Source Connection (Optional)</strong></h3>
<p>Let's briefly navigate the key components you've set up in Azure AI Search. This guide is focused on where to find these elements and what to expect when you get there.</p>
<h4 id="heading-take-a-closer-look-at-your-index"><strong>Take a Closer Look at Your Index</strong></h4>
<p>After checking the search results in the Cognitive AI Search service, why not explore a bit more? In your Azure Search Service, navigate to the 'Indexes' section and select 'my-index' once more. Here, you can casually browse through the layout and settings. It's a good spot to see how your data is structured and get familiar with the finer details of your index setup.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703281690400/0e180b96-494a-42d8-8b09-c78b80eb8e09.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-finding-the-indexer"><strong>Finding the Indexer</strong></h4>
<p>For the Indexer, head to the 'Indexers' section within your Search Service. Click on 'my-index-indexer' to see the indexing status and details. This is where the process of feeding data into your index takes place.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703281165015/7fc1cb42-77b8-4b62-ae21-e3dda0003c6f.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703281443380/4510423e-c502-4670-9983-c67a408d1fa4.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-locating-the-data-source-connection"><strong>Locating the Data Source Connection</strong></h4>
<p>To check your Data Source Connection, navigate to 'Data Sources' in the Azure Search Service. Select 'my-index-data-source' to view the configuration that connects your data source to the Indexer.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703281404127/708a4087-71e2-4000-9e25-a78313641cf8.png" alt class="image--center mx-auto" /></p>
<p>This is a quick orientation to help you locate and recognize the components you've configured. Each plays a vital role in your Azure AI Search setup, and knowing where to find them is key to managing your search capabilities efficiently.</p>
<h2 id="heading-navigating-challenges-and-opportunities-with-azure-ai-search-indexes"><strong>Navigating Challenges and Opportunities with Azure AI Search Indexes</strong></h2>
<h3 id="heading-balancing-pros-and-cons-of-azure-indexer"><strong>Balancing Pros and Cons of Azure Indexer</strong></h3>
<p>Azure Indexers are a double-edged sword. On the one hand, they offer a quick start, letting you dive into the Search Service without getting bogged down in the complexities of index population. However, as you scale, limitations and errors can become prominent, especially with multiple indexes handling extensive data. While perfect for beginners, for larger-scale operations involving millions of records across numerous indexes, consider investing time in developing a custom indexer.</p>
<h3 id="heading-caution-with-index-infrastructure-changes"><strong>Caution with Index Infrastructure Changes</strong></h3>
<p>Modifying the Search Index Infrastructure requires careful planning. Changes, especially altering field properties, often require a complete index rebuild from scratch. This means losing all data and needing a full re-index, particularly challenging with large datasets. Adding new fields is generally safer, but always plan your index structure with future changes in mind. For detailed guidance, see <a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/search-howto-reindex">Azure's index rebuild documentation</a>.</p>
<h3 id="heading-utilizing-azure-ai-search-thoughtfully"><strong>Utilizing Azure AI Search Thoughtfully</strong></h3>
<p>Azure AI Search is indeed a powerful tool, yet it's essential to approach its integration thoughtfully. Building your entire application around it can introduce risks. While it's not discouraged, it's crucial to proceed with awareness of these potential challenges.</p>
<h3 id="heading-preparedness-and-backup-strategies"><strong>Preparedness and Backup Strategies</strong></h3>
<p>Having backup strategies and contingency plans is sensible, especially for scenarios where the service might face issues. This doesn't diminish the tool's value but highlights the importance of a well-rounded approach to application architecture.</p>
<h3 id="heading-informed-integration-and-exploration"><strong>Informed Integration and Exploration</strong></h3>
<p>From my own positive experiences with Azure AI Search, I've learned the importance of understanding both its strengths and complexities. It's beneficial to explore Microsoft's documentation in depth and also consider other search service providers. This broader perspective helps in making an informed decision, ensuring Azure AI Search is integrated into your application in a way that maximizes its advantages while being mindful of potential risks.</p>
<h2 id="heading-essential-tips-amp-best-practices"><strong>Essential Tips &amp; Best Practices</strong></h2>
<h3 id="heading-start-slow-and-steady"><strong>Start Slow and Steady</strong></h3>
<p>Begin with small-scale Proof of Concept (POC) projects. This approach allows you to test different configurations and understand the tool's capabilities before fully integrating it into your system. As Robert C. Martin aptly puts it, 'The only way to <strong><em>go fast</em></strong>, is to <strong><em>go well</em></strong>.'</p>
<h3 id="heading-deep-dive-into-documentation"><strong>Deep Dive into Documentation</strong></h3>
<p>Familiarizing yourself with Azure's documentation is key. It helps you understand not just the basics but also the underlying principles of the tool. Don't hesitate to explore offerings from other providers and compare their features and pricing.</p>
<h3 id="heading-comprehensive-testing-is-key"><strong>Comprehensive Testing is Key</strong></h3>
<p>Thorough testing is crucial for long-term success with Search Indexes. This includes validating Search Index data, ensuring smooth index creation during deployments, handling updates, managing indexer timeouts, and more. A well-planned and rigorously tested system can significantly enhance your search capabilities.</p>
<h2 id="heading-troubleshooting-amp-additional-information"><strong>Troubleshooting &amp; Additional Information</strong></h2>
<h3 id="heading-dealing-with-deployment-issues"><strong>Dealing with Deployment Issues</strong></h3>
<p>If you run into deployment problems, start by checking the deployment logs in your Resource Group. Ensure you're following all the prerequisites as outlined in the documentation for each resource.</p>
<h3 id="heading-solving-code-related-problems"><strong>Solving Code-Related Problems</strong></h3>
<p>The repository provided has been extensively tested. Should you face any issues, please report them on the <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/issues">GitHub issues page</a>, and I'll assist as soon as possible. Remember to configure the <a target="_blank" href="https://github.com/FarzamMohammadi/azure-search-index-builder/blob/main/AzureSearchIndexBuilder/appsettings.json">appsettings.json file</a> correctly before running the project.</p>
<blockquote>
<p>A key point to note: the Books table only supports unique identifiers, so you'll need to delete it if you need to rerun the project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1703281866701/690c4656-f1b3-482d-8afa-11c382fdad5a.png" alt class="image--center mx-auto" /></p>
</blockquote>
<h2 id="heading-looking-ahead-advanced-search-index-management"><strong>Looking Ahead: Advanced Search Index Management</strong></h2>
<p>In my upcoming posts, I will guide you through the advanced elements of managing Azure AI Search Indexes, catering to those who are incorporating this tool into their systems. We'll navigate together through topics like custom data indexing, complex data modeling, strategies for updating Search Index data, and effective testing methods for Search Indexes and their various components. And there’s more to it. Considering that each business faces unique challenges, we will also explore diverse and complex scenarios. My aim is to provide a path based on my experiences, enabling you to achieve a comprehensive understanding of Search Index management and beyond. And remember, there's always more to learn, so keep exploring on your own!</p>
<h2 id="heading-wrapping-up-our-journey-with-azure-ai-search"><strong>Wrapping Up Our Journey with Azure AI Search</strong></h2>
<p>Our exploration of Azure AI Search has opened a window into efficient, intelligent data management. From creating data sources to building, populating, and querying Search Indexes, we’ve laid the groundwork for you to harness this powerful tool effectively.</p>
<p>The hands-on experience with C# is just the beginning. As we progress, we'll delve into more complex topics such as custom data indexing and dynamic Search Index updates. This exploration will tailor Azure AI Search to suit your specific needs, enhancing your data management strategies.</p>
<p>The road to expertise in Azure AI Search, like any other tool, is a continuous learning process. Embrace the challenges, dive into the documentation, and don't shy away from thorough testing. Each step is an opportunity to grow and innovate.</p>
<p>Thank you for joining this journey. Keep learning and experimenting with Azure AI Search. It's a versatile tool for data management and you're just scratching the surface. Here's to your success in harnessing its full potential!</p>
<h2 id="heading-additional-resources-and-further-reading"><strong>Additional Resources and Further Reading</strong></h2>
<h3 id="heading-general-microsoft-azure-ai-search-documentation">General Microsoft Azure AI Search Documentation</h3>
<ul>
<li><p><a target="_blank" href="https://azure.microsoft.com/en-us/products/ai-services/ai-search">Azure AI Search Service Overview</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/search-what-is-an-index">Indexes in Azure AI Search</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/search-indexer-overview">Indexers in Azure AI Search</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/search-howto-complex-data-types?tabs=portal">Modeling Complex Data Types in Azure AI Search</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/search-security-overview">Security overview for Azure AI Search</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/search-get-started-portal">Create a search index in the Azure portal</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/search-explorer">Query with Search Explorer</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/storage/tables/table-storage-overview">What is Azure Table Storage?</a></p>
</li>
</ul>
<h3 id="heading-best-practices-amp-improving-performance">Best Practices &amp; Improving Performance</h3>
<ul>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/search/search-performance-tips">Tips for better performance</a></p>
</li>
<li><p><a target="_blank" href="https://dibranmulder.github.io/2020/09/22/Improving-your-Azure-Search-performance/">Improving your Azure Cognitive Search performance</a></p>
</li>
<li><p><a target="_blank" href="https://techcommunity.microsoft.com/t5/ai-azure-ai-services-blog/azure-cognitive-search-performance-setting-yourself-up-for/ba-p/2324037">Azure Cognitive Search performance: Setting yourself up for success</a></p>
</li>
</ul>
<h3 id="heading-visual-resources-and-tutorials">Visual Resources and Tutorials</h3>
<ul>
<li><p><a target="_blank" href="https://youtu.be/ChWdqQnrmjs?si=1fHqCpIfTx4JEllH">Azure Cognitive Search</a> (more of an overview)</p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=6kw8SHwxp9c&amp;ab_channel=MicrosoftAzure">Cognitive Search - Azure Search with AI</a> (decent demo)</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Getting Started with Azure Bicep: Deploying a "Hello World" Web Application]]></title><description><![CDATA[Biceps? But I'm Not Into Weightlifting...
Don’t worry, there’s no heavy lifting required – only some clever coding. With Azure Bicep, your computer and a willingness to learn are all you need. Let’s dive in.
What is a Bicep File?
Bicep files are your...]]></description><link>https://triedandtestedbuilds.com/getting-started-with-azure-bicep-deploying-a-hello-world-web-application</link><guid isPermaLink="true">https://triedandtestedbuilds.com/getting-started-with-azure-bicep-deploying-a-hello-world-web-application</guid><category><![CDATA[azure-bicep]]></category><category><![CDATA[Bicep]]></category><category><![CDATA[Azure]]></category><category><![CDATA[azure-devops]]></category><category><![CDATA[Hello World]]></category><dc:creator><![CDATA[Farzam Mohammadi]]></dc:creator><pubDate>Sun, 26 Nov 2023 01:03:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1700418403087/af9793db-d27a-45f6-a5da-53f6de7f7048.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-biceps-but-im-not-into-weightlifting"><strong>Biceps? But I'm Not Into Weightlifting...</strong></h3>
<p>Don’t worry, there’s no heavy lifting required – only some clever coding. With Azure Bicep, your computer and a willingness to learn are all you need. Let’s dive in.</p>
<h3 id="heading-what-is-a-bicep-file"><strong>What is a Bicep File?</strong></h3>
<p>Bicep files are your gateway to deploying resources in Azure, offering a more straightforward approach than <a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/templates/overview">ARM Template files</a>. If you're not familiar with ARM Templates, don't worry. It's helpful to focus solely on Bicep files for now. ARM templates can be challenging to understand and develop. Meanwhile, Microsoft's Bicep files provide equivalent functionality and capabilities but are notably easier to understand and work with, simplifying the experience of managing deployments in Azure.</p>
<h4 id="heading-bicep-deployment-overview"><strong>Bicep Deployment Overview:</strong></h4>
<ol>
<li><p><strong>Writing the Code</strong>: Begin by writing the deployment code in a Bicep file.</p>
</li>
<li><p><strong>Deployment to Azure</strong>: Use tools like the Azure CLI or the Azure portal to deploy the Bicep file to Azure.</p>
</li>
<li><p><strong>Conversion to ARM Template Code</strong>: Azure automatically converts the Bicep file's code into ARM template code upon deployment.</p>
</li>
<li><p><strong>Resource Creation</strong>: Finally, the ARM template code is used to create the necessary resources within Azure.</p>
</li>
</ol>
<h3 id="heading-benefits-of-bicep"><strong>Benefits of Bicep</strong></h3>
<p>Wondering about the perks of Bicep files? Let’s look at it this way: Do you prefer automated processes over manual ones? If yes, we’re on the same page.</p>
<p>The primary advantage of using Bicep files is automation in resource deployments. Once you’ve crafted your Bicep file with the desired setup, you can reuse it across various platforms, like the Azure CLI, Azure Releases Pipeline, or even the Azure Portal.</p>
<p>Moreover, Bicep allows for parameter variation in each deployment. This flexibility is invaluable for systems with different environments. You can customize setups for testing and have entirely different configurations for production, ensuring each environment gets precisely what it needs.</p>
<h3 id="heading-whats-the-plan"><strong>What's the plan?</strong></h3>
<p>We're going to deploy a "Hello World" Web Application to Azure’s Web App service. It's not just about getting the app up and running; you'll pick up some cool skills along the way, such as:</p>
<ul>
<li><p><strong>Setting up an Azure Resource Group</strong> – think of this as your project’s home base in Azure.</p>
</li>
<li><p><strong>Writing a Bicep file</strong> – the blueprint of our deployment plan.</p>
</li>
<li><p><strong>Discovering Resource Templates for Bicep Deployments</strong> – I'll guide you through finding and using resource templates, essential in shaping your Bicep files.</p>
</li>
<li><p><strong>Crafting a Bicep parameter file</strong> – this is where we get clever with our configuration, making our deployment adaptable.</p>
</li>
<li><p><strong>Deploying your Bicep file using the Azure CLI</strong> – you’ll learn not only how to execute the deployment but also how to preview it first to ensure everything goes smoothly.</p>
</li>
</ul>
<p>By the end of this, you’ll not only have your "Hello World" Web Application running on Azure but you'll also gain a comprehensive understanding of resource groups, Bicep files, App Service Plans, and Web App Services. Plus, you’ll become adept at deploying with different parameters. Excited to begin? Let's get started!</p>
<h3 id="heading-im-assuming"><strong>I'm Assuming</strong></h3>
<ol>
<li><p><strong>You Have an Azure Subscription</strong>: To create resources, an Azure Subscription is required. If you don't have one yet, <a target="_blank" href="https://azure.microsoft.com/en-ca/free/">signing up</a> is straightforward, and you'll immediately receive $200 in credits, which is more than enough for this project.</p>
</li>
<li><p><strong>A Prepared Resource Group</strong>: This is where we'll host our resources. If you’re not familiar with setting up a resource group in Azure, you can follow the steps outlined <a target="_blank" href="https://triedandtestedbuilds.com/easy-guide-to-creating-a-resource-group-in-azure">here</a>.</p>
</li>
<li><p><strong>Basic Programming Knowledge</strong>: A fundamental understanding of programming will help you follow along more easily.</p>
</li>
<li><p><strong>IDE Installed</strong>: Something like VSCode should be on your machine, ready to go.</p>
</li>
<li><p><strong>Azure CLI</strong>: It's essential for our work. If it's not already set up, you can follow the download and install instructions in this <a target="_blank" href="https://learn.microsoft.com/en-us/cli/azure/install-azure-cli">link</a>.</p>
</li>
<li><p><strong>Azure CLI - Bicep</strong>: Let's make sure you've got the Azure CLI's Bicep extension ready to use. Double-check by following the instructions <a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/install#azure-cli">here</a>.</p>
</li>
</ol>
<h3 id="heading-lets-get-started"><strong>Let's Get Started!</strong></h3>
<p>We're kicking off by setting up our environment. Start by creating a new workspace folder, and inside that, create a file named <code>deploy-app.bicep</code>.</p>
<p>To deploy an App Service, we first need to prepare an App Service plan. This plan acts as a hosting environment for the App Service and defines the region, features, cost, and compute resources. Service plans are referred to as <code>Microsoft.Web/serverfarms</code> in Bicep. You can easily find the template from Microsoft with a quick Google search, and then copy it into our Bicep file. For your convenience, <a target="_blank" href="https://learn.microsoft.com/en-us/azure/templates/microsoft.web/serverfarms?pivots=deployment-language-bicep">here's the link</a> for easy access.</p>
<p>After trimming down the unnecessary configurations, our code snippet looks like this:</p>
<pre><code class="lang-typescript">resource symbolicname <span class="hljs-string">'Microsoft.Web/serverfarms@2022-09-01'</span> = {
  name: <span class="hljs-string">'string'</span>
  location: <span class="hljs-string">'string'</span>
  properties: {
    reserved: <span class="hljs-literal">true</span>
  }
  sku: {
    name: <span class="hljs-string">'string'</span>
  }
  kind: <span class="hljs-string">'string'</span>
}
</code></pre>
<p>Next, let's focus on the App Service Bicep template itself. This service is where our site will live. In Bicep, App Services are known as <code>Microsoft.Web/sites</code>. Following the same steps as before, we search, find, and refine the <a target="_blank" href="https://learn.microsoft.com/en-us/azure/templates/microsoft.web/sites?pivots=deployment-language-bicep">template</a>. Don't worry about the plethora of configuration options; here's a streamlined version for our purpose:</p>
<pre><code class="lang-typescript">resource appService <span class="hljs-string">'Microsoft.Web/sites@2020-12-01'</span> = {
  name: <span class="hljs-string">'string'</span>
  location: <span class="hljs-string">'string'</span>
  kind: <span class="hljs-string">'string'</span>
  properties: {
    serverFarmId: <span class="hljs-string">'string'</span>
    httpsOnly: bool
    siteConfig: {
      appSettings: [
        {
          name: <span class="hljs-string">'string'</span>
          value: <span class="hljs-string">'string'</span>
        }
      ]
      publicNetworkAccess: bool
    }
  }
}
</code></pre>
<p>Now, let's tackle the code deployment to our new App Service. For this, we use a template called <code>Microsoft.Web/sites/sourcecontrols</code>. This <a target="_blank" href="https://learn.microsoft.com/en-us/azure/templates/microsoft.web/sites/sourcecontrols?pivots=deployment-language-bicep">template</a> allows us to deploy our code directly from a public GitHub repository. In our case, we'll use it to deploy a simple "Hello World" application. For newcomers, this means that our App Service will automatically pull the code from the specified repository and branch, making the deployment process smooth and automated.</p>
<p>Here's how that part of the Bicep file should look:</p>
<pre><code class="lang-typescript">resource srcControls <span class="hljs-string">'Microsoft.Web/sites/sourcecontrols@2021-01-01'</span> = {
  parent: <span class="hljs-string">'string'</span>
  name: <span class="hljs-string">'string'</span>
  properties: {
    repoUrl: <span class="hljs-string">'string'</span>
    branch: <span class="hljs-string">'string'</span>
    isManualIntegration: bool
  }
}
</code></pre>
<p>By the end of this setup, your <code>deploy-app.bicep</code> file will be a robust blueprint for your Azure deployment. It should look something like this: <a target="_blank" href="https://github.com/FarzamMohammadi/bicep-tutorial-helloworld/blob/main/base%20file/deploy-app.bicep">deploy-app.bicep</a>.</p>
<h3 id="heading-preparing-your-bicep-file-structuring-for-success"><strong>Preparing Your Bicep File: Structuring for Success</strong></h3>
<p>A quick note before we continue: If you're eager to get straight to the action, you're welcome to skip ahead to the <a class="post-section-overview" href="#heading-creating-parameter-files-for-different-environments-testing-vs-production">Creating Parameter Files for Different Environments (Testing vs. Production)</a> section. But if you're here for some handy tips on structuring your Bicep file for success, let's dive in!</p>
<h4 id="heading-tips-on-structuring-your-code">Tips on Structuring Your Code</h4>
<ol>
<li><p><strong>Logical Grouping</strong>: Organize related resources together. For instance, if you're creating a web app, group all resources related to the web app (like App Service, App Service Plan, and Storage Account) in one section.</p>
</li>
<li><p><strong>Use Comments Wisely</strong>: Comments are crucial for explaining why something is done a certain way, especially for complex logic. However, avoid over-commenting obvious things as it can clutter your code.</p>
</li>
<li><p><strong>Consistent Naming Conventions</strong>: Adopt a clear and consistent naming strategy for resources and variables. This not only makes your code more readable but also eases collaboration with others.</p>
</li>
<li><p><strong>Modular Approach</strong>: Break down your code into modules for reusability and simplicity. Each module should ideally represent a logical unit of deployment, like a network module, a storage module, etc.</p>
</li>
</ol>
<h4 id="heading-best-practices-for-scalability-and-maintenance">Best Practices for Scalability and Maintenance</h4>
<ol>
<li><p><strong>Parameterization</strong>: Make use of parameters to avoid hard-coding values, especially those that might change (like environment names, sizes, and SKUs). This practice makes your Bicep files adaptable and easier to scale.</p>
</li>
<li><p><strong>Leverage Resource Dependencies</strong>: Bicep automatically handles resource dependencies, but understanding and defining these properly ensures that resources are deployed in the correct order.</p>
</li>
<li><p><strong>Version Control</strong>: Use version control systems like Git to track changes, collaborate with others, and maintain a history of your deployments. This is crucial for any scalable project.</p>
</li>
<li><p><strong>Testing</strong>: Regularly test your Bicep files in different environments. Testing helps to identify issues early and ensures that your deployments are reliable.</p>
</li>
<li><p><strong>Stay Updated:</strong> Make sure to keep up with the latest Azure Bicep updates. Regular releases bring new features, improvements, and bug fixes, all of which can greatly enhance your deployment experience.</p>
</li>
</ol>
<p>By following these guidelines, you’ll set a strong foundation for your Azure Bicep files, ensuring they are not only effective and efficient but also scalable and easy to maintain.</p>
<h3 id="heading-bicep-refinement"><strong>Bicep Refinement</strong></h3>
<p>To enhance readability, maintainability, and overall deployment efficiency, let's fine-tune the base template by:</p>
<ol>
<li><p><strong>Extracting Change-Expected Values into Parameters</strong>: We're moving values that are likely to change into parameters for easy adjustments.</p>
<pre><code class="lang-typescript"> param location <span class="hljs-built_in">string</span> = resourceGroup().location <span class="hljs-comment">// Bicep function returning the resource group location</span>
 param textToReplaceSubtitleWith <span class="hljs-built_in">string</span>
 param repositoryBranch <span class="hljs-built_in">string</span>
</code></pre>
</li>
<li><p><strong>Adding Clear and Concise Comments</strong>: Brief yet informative comments are key to understanding our parameters.</p>
<pre><code class="lang-typescript"> <span class="hljs-meta">@description</span>(<span class="hljs-string">'Azure resource deployment location.'</span>)
 param location <span class="hljs-built_in">string</span>

 <span class="hljs-meta">@description</span>(<span class="hljs-string">'The text to replace the default subtitle with.'</span>)
 param textToReplaceSubtitleWith <span class="hljs-built_in">string</span>

 <span class="hljs-meta">@description</span>(<span class="hljs-string">'Branch of the repository for deployment.'</span>)
 param repositoryBranch <span class="hljs-built_in">string</span>
</code></pre>
</li>
<li><p><strong>Setting Defaults for Some Parameters</strong>: We'll assign default values to parameters that might not get values during deployment.</p>
<pre><code class="lang-typescript"> <span class="hljs-meta">@description</span>(<span class="hljs-string">'Azure resource deployment location.'</span>)
 param location <span class="hljs-built_in">string</span> = resourceGroup().location

 <span class="hljs-meta">@description</span>(<span class="hljs-string">'The text to replace the default subtitle with.'</span>)
 param textToReplaceSubtitleWith <span class="hljs-built_in">string</span> = <span class="hljs-string">'This is my default subtitle text. Boring, right?'</span>

 <span class="hljs-meta">@description</span>(<span class="hljs-string">'Branch of the repository for deployment.'</span>)
 param repositoryBranch <span class="hljs-built_in">string</span> = <span class="hljs-string">'main'</span>
</code></pre>
</li>
<li><p><strong>Finalizing the Bicep File</strong>: We'll now incorporate our variables and set the remaining values and configurations.</p>
<pre><code class="lang-typescript"> <span class="hljs-comment">// App Service Plan Creation</span>
 resource appServicePlan <span class="hljs-string">'Microsoft.Web/serverfarms@2020-12-01'</span> = {
   name: <span class="hljs-string">'myAppServicePlan'</span>
   location: location
   sku: {
     name: <span class="hljs-string">'F1'</span>
   }
   kind: <span class="hljs-string">'app'</span>
   properties: {
     reserved: <span class="hljs-literal">false</span>
   }
 }

 <span class="hljs-comment">// Web App Creation</span>
 resource appService <span class="hljs-string">'Microsoft.Web/sites@2020-12-01'</span> = {
   name: <span class="hljs-string">'this-is-a-unique-name-for-the-web-app'</span>
   location: location
   properties: {
     serverFarmId: appServicePlan.id
     httpsOnly: <span class="hljs-literal">true</span>
     siteConfig: {
       appSettings: [
         {
           name: <span class="hljs-string">'TEXT_TO_REPLACE_SUBTITLE_WITH'</span> <span class="hljs-comment">// This value needs to match the name of the environment variable in the application code</span>
           value: textToReplaceSubtitleWith
         }
         {
           name: <span class="hljs-string">'SCM_DO_BUILD_DURING_DEPLOYMENT'</span> <span class="hljs-comment">// Build the application during deployment</span>
           value: <span class="hljs-string">'true'</span>
         }
         {
           name: <span class="hljs-string">'WEBSITE_NODE_DEFAULT_VERSION'</span> <span class="hljs-comment">// Set the default node version</span>
           value: <span class="hljs-string">'~20'</span>
         }
       ]
       publicNetworkAccess: <span class="hljs-string">'Enabled'</span>
     }
   }
 }

 <span class="hljs-comment">// Source Control Integration</span>
 resource srcControls <span class="hljs-string">'Microsoft.Web/sites/sourcecontrols@2021-01-01'</span> = {
   parent: appService
   name: <span class="hljs-string">'web'</span>
   properties: {
     repoUrl: <span class="hljs-string">'https://github.com/FarzamMohammadi/hello-world'</span>
     branch: repositoryBranch
     isManualIntegration: <span class="hljs-literal">true</span>
   }
 }
</code></pre>
</li>
</ol>
<p>The final result should align with this <a target="_blank" href="https://github.com/FarzamMohammadi/bicep-tutorial-helloworld/blob/main/deploy-webapp.bicep"><code>deploy-webapp.bicep</code> file</a>. Wondering how I know which configurations to use? Well, it's a mix of scouring documentation and good old Google searches. Often, someone else's shared solution is the key to unlocking your deployment puzzles.</p>
<h3 id="heading-creating-parameter-files-for-different-environments-testing-vs-production"><strong>Creating Parameter Files for Different Environments (Testing vs. Production)</strong></h3>
<p>When deploying applications in Azure, it's common to have separate settings for different environments, such as testing and production. This is where parameter files come into play. They allow you to define environment-specific values without altering the core Bicep file. This approach not only simplifies management but also reduces the risk of errors when moving from testing to production.</p>
<p>For our "Hello World" web app, we've prepared two parameter files: <code>parameters.test.json</code> for testing and <code>parameters.json</code> for production. The primary distinction between these files is the source code branch they target.</p>
<h4 id="heading-parametersjson-production-environment">parameters.json (Production Environment)</h4>
<p>This file, used for the production environment, adheres to the default branch specified in the Bicep file. It doesn't require an explicit <code>repositoryBranch</code> parameter.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"$schema"</span>: <span class="hljs-string">"https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#"</span>,
  <span class="hljs-attr">"contentVersion"</span>: <span class="hljs-string">"1.0.0.0"</span>,
  <span class="hljs-attr">"parameters"</span>: {
    <span class="hljs-attr">"textToReplaceSubtitleWith"</span>: {
      <span class="hljs-attr">"value"</span>: <span class="hljs-string">"This site was released to Azure using a Bicep file. Biceps are better than ARM templates!"</span>
    }
  }
}
</code></pre>
<h4 id="heading-parameterstestjson-testing-environment">parameters.test.json (Testing Environment)</h4>
<p>In this file, we've included the <code>repositoryBranch</code> parameter. This simple yet effective change specifies that Azure should use code from the "test" branch for deployment. It's a practical way to ensure that during the testing phase, we are deploying exactly what's intended for testing, separate from our production environment.</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"$schema"</span>: <span class="hljs-string">"https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#"</span>,
  <span class="hljs-attr">"contentVersion"</span>: <span class="hljs-string">"1.0.0.0"</span>,
  <span class="hljs-attr">"parameters"</span>: {
    <span class="hljs-attr">"textToReplaceSubtitleWith"</span>: {
      <span class="hljs-attr">"value"</span>: <span class="hljs-string">"This site is being tested and was released to Azure using a Bicep file. Let's see if Biceps are better than ARM templates."</span>
    },
    <span class="hljs-attr">"repositoryBranch"</span>: {
      <span class="hljs-attr">"value"</span>: <span class="hljs-string">"test"</span>
    }
  }
}
</code></pre>
<p>The <code>repositoryBranch</code> parameter in the test file is a key example of how changing one parameter can shift the deployment from production to testing. This helps demonstrate the real-world effects of such variations.</p>
<h2 id="heading-time-to-deploy"><strong>Time to Deploy</strong></h2>
<h3 id="heading-azure-cli-preparation"><strong>Azure CLI Preparation</strong></h3>
<p>First things first, we need to authenticate ourselves with the Azure CLI before deploying our Bicep file. It's simple: just run <code>az login</code>. You'll be taken to the Azure portal for a quick sign-in.</p>
<p>Once authenticated, a confirmation message will appear, indicating we're ready to proceed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700789788536/cc82edef-d80a-4cbe-a86b-cc79a79c6cde.png" alt class="image--center mx-auto" /></p>
<p>Now, let's set the Azure account subscription. Just run <code>az account set --subscription &lt;SUBSCRIPTION_ID&gt;</code>, replacing <code>&lt;SUBSCRIPTION_ID&gt;</code> with your actual subscription ID. And with that, we're set for deployment.</p>
<h3 id="heading-deploying-bicep-with-test-parameters"><strong>Deploying Bicep With Test Parameters</strong></h3>
<p>We'll start by deploying our Bicep file using the <code>parameters.test.json</code> file. Before deploying, it's wise to preview our changes. Using the <code>--confirm-with-what-if</code> option in Azure CLI, we can get a sneak peek of what our deployment will entail.</p>
<p>Run this command in the same directory as your Bicep file:</p>
<pre><code class="lang-bash">az deployment group create --resource-group my-rg --template-file deploy-webapp.bicep --parameters parameters.test.json --confirm-with-what-if
</code></pre>
<p><strong>Understanding the Command:</strong></p>
<ul>
<li><p><strong>Resource Group</strong>: Use the <code>--resource-group</code> tag to specify the Resource Group where your resources will be deployed.</p>
</li>
<li><p><strong>Bicep File</strong>: The <code>--template-file</code> tag is followed by the name of your Bicep file.</p>
</li>
<li><p><strong>Parameter File</strong>: Use the <code>--parameters</code> tag to specify the Parameter JSON file that will override your Bicep parameters.</p>
</li>
<li><p><strong>Confirm Before Deployment</strong>: The <code>--confirm-with-what-if</code> tag gives you a preview of all the changes about to be made. It's your chance to review everything and confirm whether to proceed or not.</p>
</li>
</ul>
<p>This preview helps identify any potential issues. Once confident, proceed with deployment by pressing <code>y</code> and enter.</p>
<blockquote>
<p><strong><em>Note:</em></strong> You can skip the preview and deploy directly using the same command without <code>--confirm-with-what-if</code>.</p>
</blockquote>
<h3 id="heading-monitoring-the-deployment-progress"><strong>Monitoring the Deployment Progress</strong></h3>
<p>During deployment, check out "Deployments" in your resource group. This area lets you monitor the progress, review the status, and troubleshoot if needed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700794201862/98c5ac51-16bb-4c0e-a693-732e087a6bb4.png" alt class="image--center mx-auto" /></p>
<p>After deployment, delve into the details by selecting your deployment, then your uniquely named App Service under "Resources".</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700798396618/9527d528-9d52-42d8-9845-46c0ce35c81d.png" alt class="image--center mx-auto" /></p>
<p>Clicking the "Default domain" URL in the overview will take you to your newly deployed site.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700866543631/b29ee0f5-3793-453f-90e3-ea2a97f2545f.png" alt class="image--center mx-auto" /></p>
<p>If things went smoothly, you should see something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700798565474/81689272-e4ab-40fe-9b0f-56f54bfea375.png" alt class="image--center mx-auto" /></p>
<p>Keep this tab open and let's discuss why we're seeing this "From the Test branch" text.</p>
<h3 id="heading-executing-the-steps-video-demonstration"><strong>Executing the Steps: Video Demonstration</strong></h3>
<p>For a visual guide on deploying with test parameters, watch the first video. It details each step from "Azure CLI Preparation" to "Understanding the Deployment of the Test Branch."</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/sgp2vl7tzPM">https://youtu.be/sgp2vl7tzPM</a></div>
<p> </p>
<h3 id="heading-understanding-the-test-branch-deployment"><strong>Understanding the Test Branch Deployment</strong></h3>
<p>Our deployment targeted the test branch due to the <code>parameters.test.json</code> file. This file specifies the <code>repositoryBranch</code> as 'test', directing Azure to pull code from this branch.</p>
<h3 id="heading-deploying-bicep-with-production-parameters"><strong>Deploying Bicep With Production Parameters</strong></h3>
<p>Next, let’s deploy using the <code>parameters.json</code> file for a production setup.</p>
<blockquote>
<p><strong>Important Step: Disconnect Source Control in App Service</strong></p>
<p>After testing your deployment, there's a crucial step to ensure everything works smoothly. I encountered a bug with the "Source Control" feature that prevented it from updating the branch name to "main" during redeployment. To avoid this issue and proceed successfully, manually disconnect the source control settings:</p>
<ol>
<li><p>Go to Your App Service: Open the Azure portal and select your App Service created for Bicep deployment.</p>
</li>
<li><p>Find Deployment Center: Inside your App Service, locate and click on "Deployment Center."</p>
</li>
<li><p>Disconnect: In the Deployment Center, simply click the "Disconnect" button to remove the source control link.</p>
</li>
</ol>
<p>This step is vital for the next part of our deployment process to work correctly.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700801041869/c223c1de-ec05-4622-8c72-3bbe2e49e511.png" alt class="image--center mx-auto" /></p>
<p>If you're interested in the exact details of this bug, take a look at the issue I created in the Azure Bicep repository. Visit <a target="_blank" href="https://github.com/Azure/bicep/issues/12544">Azure Bicep Issue #12544</a> for more information.</p>
</blockquote>
<p>Deploying the production code is similar to the test deployment. Use this command:</p>
<pre><code class="lang-typescript">az deployment group create --resource-group my-rg --template-file deploy-webapp.bicep --parameters parameters.json
</code></pre>
<p>With <code>parameters.json</code> missing the <code>repositoryBranch</code> property, deployment defaults to the previously set "main" branch in our Bicep file.</p>
<pre><code class="lang-typescript"><span class="hljs-meta">@description</span>(<span class="hljs-string">'Branch of the repository for deployment.'</span>)
param repositoryBranch <span class="hljs-built_in">string</span> = <span class="hljs-string">'main'</span>
</code></pre>
<p>After the deployment is complete, the view should now reflect the <code>main</code> branch code, as shown here, without any mentions of the <code>Test</code> branch:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700801187341/de20053d-210b-4020-ae66-20b88c913dd2.png" alt class="image--center mx-auto" /></p>
<p>And just like that, you’ve completed not one, but two Bicep deployments, and your website is live for the world to see. Congratulations!!</p>
<h3 id="heading-additional-video-production-parameter-deployment-walkthrough"><strong>Additional Video: Production Parameter Deployment Walkthrough</strong></h3>
<p>For insights into deploying with the "production" parameters, check out this second video. It illustrates the use of <code>parameters.json</code> file, to simulate various deployment scenarios. This video serves as a practical complement to the first, enhancing your understanding of Bicep deployment flexibility.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/InmOKz3vdgw?si=TLdfOxpl6ScOOwlO">https://youtu.be/InmOKz3vdgw?si=TLdfOxpl6ScOOwlO</a></div>
<p> </p>
<h3 id="heading-troubleshooting-common-issues"><strong>Troubleshooting Common Issues</strong></h3>
<h4 id="heading-deployment-troubleshooting">Deployment Troubleshooting</h4>
<p>If you encounter non-syntax related issues during deployment, a good starting point is the Deployment logs in your Resource Group. These logs can help pinpoint which resources are causing trouble and clarify error messages.</p>
<h4 id="heading-azure-cli-issues">Azure CLI Issues</h4>
<p>For Azure CLI-related problems, the official documentation is a reliable resource. It offers detailed solutions and step-by-step guidance for many common issues.</p>
<h4 id="heading-additional-tips">Additional Tips:</h4>
<ul>
<li><p><strong>Check Parameter Mismatches</strong>: Ensure your parameter file correctly matches the parameters in your Bicep file. Even small mismatches or typos can lead to unexpected issues.</p>
</li>
<li><p><strong>Resource Dependencies</strong>: The order of resource deployment can be crucial. Review your Bicep file to ensure dependencies are appropriately managed.</p>
</li>
<li><p><strong>API Version Compatibility</strong>: Keep your API versions in Bicep templates up-to-date. Older versions may lead to unexpected behaviors.</p>
</li>
<li><p><strong>Community Forums and Support</strong>: Azure forums and platforms like Stack Overflow are valuable for finding solutions to complex problems.</p>
</li>
<li><p><strong>Local Validation and Testing</strong>: It's important to validate your Bicep files locally before deploying. You can use the Bicep CLI for this. The <code>--confirm-with-what-if</code> tag in Azure CLI is also useful for previewing changes and catching potential issues early.</p>
</li>
</ul>
<h3 id="heading-expanding-beyond-hello-world"><strong>Expanding Beyond "Hello World"</strong></h3>
<p>Next on the agenda is something a bit more advanced: a guide on deploying containerized applications to Azure using Bicep. This is for those looking to take their Azure skills up a notch, so stay tuned!</p>
<p>Got any Bicep or Azure topics you're itching to learn more about? Send me a message or leave a comment below. I'm always keen to collaborate and explore new ideas!</p>
<h3 id="heading-conclusion-wrapping-up-and-looking-ahead"><strong>Conclusion: Wrapping Up and Looking Ahead</strong></h3>
<p>That’s it for our Azure Bicep guide! We’ve tackled everything from Bicep basics to getting a "Hello World" Web Application up and running on Azure. The goal was to make things straightforward and practical, and I hope you found it useful.</p>
<p>Along the way, we’ve seen how to write, refine, and deploy with Bicep, not to mention a little bit of Azure CLI in action. Hopefully, the video demo made things even clearer.</p>
<p>Up next, I’m planning to cover the deployment of containerized applications using Bicep, so there’s more useful content on the way. If you have any Bicep or Azure topics you’re curious about, feel free to let me know – your input could guide what comes next.</p>
<p>Thanks for joining me on this journey. Here’s to creating killer software solutions, and successful Azure projects! Keep an eye out for what’s next!</p>
<h3 id="heading-additional-resources-and-further-reading"><strong>Additional Resources and Further Reading</strong></h3>
<p>General Microsoft Bicep Documentation:</p>
<ul>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/overview?tabs=bicep">Bicep Overview and Basics</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/">Azure Bicep Official Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/Azure/bicep">Azure Bicep GitHub Repository</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/app-service/overview">Azure App Service</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans">Azure App Serice Plan</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/app-service/deploy-continuous-deployment?tabs=github">Continuous deployment to Azure App Service</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-cli">Bicep CLI commands</a></p>
</li>
<li><p><a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/deploy-cli">How to deploy resources with Bicep and Azure CLI</a></p>
</li>
</ul>
<p>Best Practices:</p>
<ul>
<li><a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/best-practices">Azure Bicep Best Practices Guide</a></li>
</ul>
<p>Visual Resources and Tutorials:</p>
<ul>
<li><p><a target="_blank" href="https://youtu.be/77AfsFzTsI4?si=54x3A7l0cxqH4rB-">Getting Started with Azure Bicep (Video)</a></p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=wevlRsVxsUw&amp;ab_channel=MicrosoftReactor">Bicep Advanced Deployments - Part 1 (Video)</a></p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/live/6QLkSRc7tgM?si=DFsBqA0okZanAJCe">Bicep Advanced Deployments - Part 2 (Video)</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Easy Guide to Creating a Resource Group in Azure]]></title><description><![CDATA[Introduction
Setting up a Resource Group in Azure is easier than you think, but first, what exactly is a Resource Group? In Azure, a Resource Group is a container that holds related resources for an Azure solution. Think of it as a folder where you c...]]></description><link>https://triedandtestedbuilds.com/easy-guide-to-creating-a-resource-group-in-azure</link><guid isPermaLink="true">https://triedandtestedbuilds.com/easy-guide-to-creating-a-resource-group-in-azure</guid><category><![CDATA[Azure Resource Group]]></category><category><![CDATA[Azure]]></category><category><![CDATA[azure-devops]]></category><category><![CDATA[how-to]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Farzam Mohammadi]]></dc:creator><pubDate>Sun, 19 Nov 2023 21:48:51 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1700429346250/4e5dd95e-6d3b-4acf-9502-0b4f0c0c914d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-introduction"><strong>Introduction</strong></h3>
<p>Setting up a Resource Group in Azure is easier than you think, but first, what exactly is a Resource Group? In Azure, a Resource Group is a container that holds related resources for an Azure solution. Think of it as a folder where you can group your Azure services (like databases, web apps, and storage accounts) together, making them easier to manage and monitor. This guide will walk you through each step to create your own Resource Group, simplifying the process to make it straightforward and hassle-free.</p>
<h3 id="heading-what-you-need"><strong>What You Need</strong></h3>
<ul>
<li>An Azure Subscription</li>
</ul>
<h3 id="heading-steps-for-setting-up-your-azure-resource-group"><strong>Steps for Setting Up Your Azure Resource Group</strong></h3>
<ol>
<li><p><strong>Get Started</strong>: First, log into your Azure account. This is where everything begins.</p>
</li>
<li><p><strong>Find Your Way</strong>: Click on "Create a resource" in the Azure dashboard. This is where you can search for all Azure resources.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700427578715/15284813-8579-4fec-a471-cc1497844dcb.png" alt class="image--center mx-auto" /></p>
</li>
<li><p><strong>Search and Select:</strong> Type “Resource Group” in the search bar and hit "Create".</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700427734354/08863045-c4ca-48a5-b997-3abe8e2e0365.png" alt class="image--center mx-auto" /></p>
<p> <strong>Name Your Group:</strong> Choose a name for your Resource Group, as this will be its identifier within Azure. Additionally, you can select the Region to specify the geographical location where your data and deployment metadata will be stored. This helps in optimizing the deployment according to your desired location.</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700429409378/58a59f0c-e0b7-41fd-b67f-209900cae8fb.png" alt class="image--center mx-auto" /></p>
<p> <strong>Check Everything:</strong> Go to the bottom of the page and click "Review + create". Just to make sure all looks good.</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700428489036/9a0b6388-589e-4f75-813c-fb6987055868.png" alt class="image--center mx-auto" /></p>
<p> <strong>Hit Create:</strong> After your settings pass the validation check, click "Create" to start building your Resource Group.</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700428585167/c88a037a-37e0-4ce6-b2dd-e3d12f4cc1fc.png" alt class="image--center mx-auto" /></p>
<p> <strong>Confirmation Time:</strong> Once Azure tells you it's done, click "Go to resource group". This takes you to your new group.</p>
</li>
<li><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700428709129/ba9559e6-42c4-4fec-a01b-02252e016639.png" alt class="image--center mx-auto" /></p>
<p> <strong>Take a Look Around:</strong> You're now in your Resource Group. Check out its details under "Overview".</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700429041372/2755935b-ce34-4e14-aba4-0705e144d6c9.png" alt class="image--center mx-auto" /></p>
<p>Your Resource Group is all set to accommodate new resources, forming an organized hub for managing your Azure solution. To learn more and explore advanced capabilities, refer to the official documentation here: <a target="_blank" href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/manage-resource-groups-portal">Manage Azure resource groups by using the Azure portal</a>.</p>
]]></content:encoded></item></channel></rss>