<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shaikh Al Amin</title>
    <description>The latest articles on DEV Community by Shaikh Al Amin (@shaikhalamin).</description>
    <link>https://dev.to/shaikhalamin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F486178%2F52a795b9-8d19-4e12-ad5e-654be9daa74e.jpeg</url>
      <title>DEV Community: Shaikh Al Amin</title>
      <link>https://dev.to/shaikhalamin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shaikhalamin"/>
    <language>en</language>
    <item>
      <title>Install playwrite cli with claude code for any project with more accurate result</title>
      <dc:creator>Shaikh Al Amin</dc:creator>
      <pubDate>Tue, 07 Apr 2026 19:43:12 +0000</pubDate>
      <link>https://dev.to/shaikhalamin/install-playwrite-cli-with-claude-code-for-any-project-with-more-accurate-result-3716</link>
      <guid>https://dev.to/shaikhalamin/install-playwrite-cli-with-claude-code-for-any-project-with-more-accurate-result-3716</guid>
      <description>&lt;p&gt;First install playwright globally in PC&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; @playwright/cli@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;cd into any project directory and then install&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;playwright-cli &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and now install playwright skills under claude code&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;playwright-cli &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--skills&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
    </item>
    <item>
      <title>Building a Reliable Job Scheduler with Cloudflare Durable Objects and Queues</title>
      <dc:creator>Shaikh Al Amin</dc:creator>
      <pubDate>Sat, 07 Mar 2026 16:11:35 +0000</pubDate>
      <link>https://dev.to/shaikhalamin/building-a-reliable-job-scheduler-with-cloudflare-durable-objects-and-queues-3d27</link>
      <guid>https://dev.to/shaikhalamin/building-a-reliable-job-scheduler-with-cloudflare-durable-objects-and-queues-3d27</guid>
      <description>&lt;p&gt;&lt;strong&gt;A beginner-friendly deep dive into scheduling repeatable jobs on the edge — no cron servers, no Redis, no infrastructure to manage.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: "I Need Something to Happen Later"
&lt;/h2&gt;

&lt;p&gt;Imagine you're building an HR/Payroll SaaS app. Your users want things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"Send me a &lt;strong&gt;daily reminder&lt;/strong&gt; at 9 AM to fill my timesheet"&lt;/li&gt;
&lt;li&gt;"Generate a &lt;strong&gt;weekly report&lt;/strong&gt; every Monday at noon"&lt;/li&gt;
&lt;li&gt;"Remind me &lt;strong&gt;once&lt;/strong&gt; on March 15th about the tax deadline"&lt;/li&gt;
&lt;li&gt;"Check for new payslips &lt;strong&gt;every 30 minutes&lt;/strong&gt;"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are all &lt;strong&gt;scheduled jobs&lt;/strong&gt; — things that need to happen at a specific time in the future, and sometimes repeatedly.&lt;/p&gt;

&lt;p&gt;In a traditional setup, you'd spin up a cron server, or use something like Redis + BullMQ. But we're running on &lt;strong&gt;Cloudflare Workers&lt;/strong&gt; — there are no long-running servers. Workers are stateless and short-lived. So how do we schedule things?&lt;/p&gt;

&lt;p&gt;The answer: &lt;strong&gt;Durable Objects&lt;/strong&gt; + &lt;strong&gt;Queues&lt;/strong&gt; + &lt;strong&gt;KV Storage&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Big Picture (Start Here)
&lt;/h2&gt;

&lt;p&gt;Before diving into code, let's understand the system with a real-world analogy.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Alarm Clock Analogy
&lt;/h3&gt;

&lt;p&gt;Think of the system as a &lt;strong&gt;hotel wake-up call service&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F56yknutmdmngr3s2lo0a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F56yknutmdmngr3s2lo0a.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now map this to our system:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Analogy&lt;/th&gt;
&lt;th&gt;Real System&lt;/th&gt;
&lt;th&gt;What It Does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;You&lt;/strong&gt; (the guest)&lt;/td&gt;
&lt;td&gt;User calling the API&lt;/td&gt;
&lt;td&gt;Requests a schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Front Desk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Auth Worker (Notifications API)&lt;/td&gt;
&lt;td&gt;Validates request, saves to database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Your Alarm Clock&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scheduler Worker (Durable Object)&lt;/td&gt;
&lt;td&gt;Keeps time, fires at the right moment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Room Service&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cloudflare Queue + Notify Worker&lt;/td&gt;
&lt;td&gt;Delivers the actual job (email, report, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Guest Registry&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;PostgreSQL Database&lt;/td&gt;
&lt;td&gt;Permanent record of all schedules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Wake-up Call Logbook&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;KV Storage&lt;/td&gt;
&lt;td&gt;Quick lookup of upcoming alarms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key insight: &lt;strong&gt;each guest gets their own personal alarm clock&lt;/strong&gt;. That's exactly how Durable Objects work — each schedule gets its own isolated instance with its own alarm.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;Here's the complete system:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fimpqo6fdru774oyf0m5o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fimpqo6fdru774oyf0m5o.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's break down each piece.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: The Three Types of Schedules
&lt;/h2&gt;

&lt;p&gt;Our scheduler supports three ways to define "when":&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Cron Schedule (Repeating Pattern)
&lt;/h3&gt;

&lt;p&gt;Cron is a time expression format that describes recurring patterns. It has 5 fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; *    *    *    *    *
 |    |    |    |    |
 |    |    |    |    +--- Day of week (0=Sun, 1=Mon, ..., 6=Sat)
 |    |    |    +-------- Month (1-12)
 |    |    +------------- Day of month (1-31)
 |    +------------------ Hour (0-23)
 +----------------------- Minute (0-59)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"0 9 * * *"      = Every day at 9:00 AM
"0 9 * * 1"      = Every Monday at 9:00 AM
"30 14 15 * *"   = 15th of every month at 2:30 PM
"*/30 * * * *"   = Every 30 minutes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our system, users don't write raw cron. They provide friendly inputs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"dailyTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"hour"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"minute"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And we convert it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// "0 9 * * *" means: at minute 0, hour 9, every day, every month, every weekday&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateDailyCron&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;minute&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; * * *`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Interval Schedule (Every N Minutes)
&lt;/h3&gt;

&lt;p&gt;Simpler than cron — just "run this every 30 minutes" or "run this every 2 hours":&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The scheduler adds the interval to the current time each time it fires:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Now: 10:00 AM
  -&amp;gt; Next run: 10:30 AM
     -&amp;gt; Next run: 11:00 AM
        -&amp;gt; Next run: 11:30 AM
           -&amp;gt; ... forever until stopped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Once Schedule (Fire and Forget)
&lt;/h3&gt;

&lt;p&gt;Run exactly one time at a specific moment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"onceAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-03-15T14:00:00Z"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After it fires once, it marks itself as inactive. Done.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: The API Layer (Auth Worker — Notifications Module)
&lt;/h2&gt;

&lt;p&gt;This is where users interact with the system. It's a standard REST API built with Hono.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Happens When a User Creates a Schedule
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /notifications/schedules
{
  "type": "profile_reminder",
  "timezone": "America/New_York",
  "dailyTime": { "hour": 14, "minute": 0 }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the step-by-step:&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Validate the Input
&lt;/h4&gt;

&lt;p&gt;The Zod schema enforces that the user provides &lt;strong&gt;exactly one&lt;/strong&gt; timing option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// You must provide ONE of these — not zero, not two&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intervalMinutes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// every N minutes&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dailyTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// daily at specific time&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;weeklyTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// weekly on specific day/time&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;monthlyTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// monthly on specific day/time&lt;/span&gt;
  &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onceAt&lt;/span&gt;            &lt;span class="c1"&gt;// one-time at specific datetime&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;// Exactly one must be provided&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why? Because it doesn't make sense to say "run daily at 9 AM AND every 30 minutes." Pick one.&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 2: Convert to Internal Format
&lt;/h4&gt;

&lt;p&gt;The service layer (&lt;code&gt;NotificationsService&lt;/code&gt;) translates the user-friendly input into the internal format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// User sends:  { "dailyTime": { "hour": 14, "minute": 0 } }&lt;/span&gt;
&lt;span class="c1"&gt;// We convert:  scheduleType = "cron", cronExpression = "0 14 * * *"&lt;/span&gt;

&lt;span class="c1"&gt;// User sends:  { "intervalMinutes": 30 }&lt;/span&gt;
&lt;span class="c1"&gt;// We convert:  scheduleType = "interval", intervalMinutes = 30&lt;/span&gt;

&lt;span class="c1"&gt;// User sends:  { "onceAt": "2026-03-15T14:00:00Z" }&lt;/span&gt;
&lt;span class="c1"&gt;// We convert:  scheduleType = "once", onceAt = "2026-03-15T14:00:00Z"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 3: Save to Database
&lt;/h4&gt;

&lt;p&gt;The schedule is saved to PostgreSQL. This is the &lt;strong&gt;permanent record&lt;/strong&gt; — it tracks who owns the schedule, what type it is, and its current state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schedule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createSchedule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;profile_reminder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scheduleType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cron&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cronExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0 14 * * *&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;America/New_York&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;isActive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;nextRun&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="cm"&gt;/* calculated */&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;lastRun&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 4: Register with the Scheduler Worker
&lt;/h4&gt;

&lt;p&gt;This is the critical bridge. The auth-worker tells the scheduler-worker: "Hey, I need you to fire a job on this schedule."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;schedulerPublisher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createSchedule&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// same UUID from the database&lt;/span&gt;
  &lt;span class="na"&gt;targetQueue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notification-queue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// which queue should receive the job&lt;/span&gt;
  &lt;span class="na"&gt;jobPayload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;              &lt;span class="c1"&gt;// the actual message to deliver&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notification&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;scheduleId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;userEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;currentUser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;notificationType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;profile_reminder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;profile_reminder notification&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Scheduled profile_reminder for user@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;scheduleType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cron&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cronExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0 14 * * *&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;America/New_York&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice: the &lt;code&gt;jobPayload&lt;/code&gt; is the &lt;strong&gt;exact message&lt;/strong&gt; that will be delivered to the queue every time the alarm fires. It's stored once and sent repeatedly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: The Scheduler Publisher (The Bridge)
&lt;/h2&gt;

&lt;p&gt;How does auth-worker talk to scheduler-worker? Through a &lt;strong&gt;Cloudflare Service Binding&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a Service Binding?
&lt;/h3&gt;

&lt;p&gt;Think of it like a &lt;strong&gt;private internal phone line&lt;/strong&gt; between two workers. No internet, no public URL, no latency of a network call. It's a direct in-memory connection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;auth-worker  ---[Service Binding / Fetcher]---&amp;gt; scheduler-worker
             (private, fast, no public URL)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;SchedulerPublisher&lt;/code&gt; is a tiny HTTP client that uses this binding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SchedulerPublisher&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;fetcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Fetcher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;createSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// This looks like an HTTP call, but it's actually a direct&lt;/span&gt;
    &lt;span class="c1"&gt;// worker-to-worker call through Cloudflare's internal network.&lt;/span&gt;
    &lt;span class="c1"&gt;// The "https://scheduler" URL is fake — it's just required by the Fetch API.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fetcher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://scheduler/schedules&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The URL &lt;code&gt;https://scheduler/schedules&lt;/code&gt; might look confusing. The hostname &lt;code&gt;scheduler&lt;/code&gt; doesn't actually matter — Cloudflare ignores it because the &lt;code&gt;Fetcher&lt;/code&gt; already knows which worker to talk to. Only the path &lt;code&gt;/schedules&lt;/code&gt; matters for routing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4: The Scheduler Worker (The Brain)
&lt;/h2&gt;

&lt;p&gt;This is where the magic happens. The scheduler-worker has two parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hono routes&lt;/strong&gt; — receives requests and delegates to Durable Objects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SchedulerDO&lt;/strong&gt; — the Durable Object that actually manages timers&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What is a Durable Object?
&lt;/h3&gt;

&lt;p&gt;A normal Cloudflare Worker is &lt;strong&gt;stateless&lt;/strong&gt; — it handles a request and forgets everything. A Durable Object is different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It has &lt;strong&gt;persistent storage&lt;/strong&gt; (survives restarts, deploys, crashes)&lt;/li&gt;
&lt;li&gt;It can set &lt;strong&gt;alarms&lt;/strong&gt; (wake me up at a specific time)&lt;/li&gt;
&lt;li&gt;Each instance is &lt;strong&gt;unique&lt;/strong&gt; and &lt;strong&gt;single-threaded&lt;/strong&gt; (no race conditions)&lt;/li&gt;
&lt;li&gt;It lives as long as it has data or a pending alarm&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it as a tiny, immortal server dedicated to one specific task.&lt;/p&gt;

&lt;h3&gt;
  
  
  One Durable Object Per Schedule
&lt;/h3&gt;

&lt;p&gt;This is the most important design decision. When you create a schedule with ID &lt;code&gt;abc-123&lt;/code&gt;, the system creates a Durable Object instance &lt;strong&gt;just for that schedule&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In routes.ts — the Hono API layer&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/schedules&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// Create a unique Durable Object for this schedule ID&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SCHEDULER_DO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;idFromName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// "abc-123" -&amp;gt; unique DO ID&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SCHEDULER_DO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;doId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;// get the DO instance&lt;/span&gt;

  &lt;span class="c1"&gt;// Forward the request to the DO&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://do/init&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why one DO per schedule?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Schedule "abc-123" -&amp;gt; DO instance A (has its own alarm, its own storage)
Schedule "def-456" -&amp;gt; DO instance B (completely independent)
Schedule "ghi-789" -&amp;gt; DO instance C (completely independent)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If schedule A fails, B and C are completely unaffected. If schedule A needs to change its timing, only DO instance A is touched. &lt;strong&gt;Total isolation.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 5: Inside the Durable Object (The Timer Engine)
&lt;/h2&gt;

&lt;p&gt;This is the heart of the system. Let's trace exactly what happens inside a DO.&lt;/p&gt;

&lt;h3&gt;
  
  
  Initialization: Setting the First Alarm
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;initSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Build the schedule data&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;targetQueue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hr-queue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;jobPayload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notification&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;scheduleType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cron&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;cronExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0 14 * * *&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;America/New_York&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;isActive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2026-03-07T...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Save to DO's own persistent storage&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;schedule&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Calculate when to fire next&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;calculateNextRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// e.g., today at 2:00 PM + random offset&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. SET THE ALARM -- this is the key line&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAlarm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextRun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

  &lt;span class="c1"&gt;// 5. Write to KV for external queries&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeKvRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextRun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;setAlarm()&lt;/code&gt; call is the magic. You're telling Cloudflare: "Wake this Durable Object up at exactly this timestamp." Cloudflare guarantees it will call the &lt;code&gt;alarm()&lt;/code&gt; method at that time (within a few seconds of accuracy).&lt;/p&gt;

&lt;h3&gt;
  
  
  How &lt;code&gt;calculateNextRun&lt;/code&gt; Works
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;calculateNextRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// ONCE: fire at the specified time (or immediately if it's in the past)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scheduleType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;once&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onceAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onceTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onceAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;onceTime&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;onceTime&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// INTERVAL: fire after N minutes + random 30-50s offset&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scheduleType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;interval&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intervalMinutes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;intervalMinutes&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;randomOffset&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// CRON: use the croner library to compute the next matching time&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scheduleType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cron&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cronExpression&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextCron&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getNextCronRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cronExpression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextCron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;randomOffset&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Fallback: 1 minute from now&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why the random offset of 30-50 seconds?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine 5,000 users all set a "daily at 9 AM" schedule. Without the offset, all 5,000 alarms fire at exactly 9:00:00 AM, hitting the queue simultaneously. The random offset spreads them between 9:00:30 and 9:00:50, preventing a stampede. This is called &lt;strong&gt;jitter&lt;/strong&gt; and it's a common pattern in distributed systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Alarm Fires: Delivering the Job
&lt;/h3&gt;

&lt;p&gt;When the alarm goes off, Cloudflare calls the &lt;code&gt;alarm()&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Load the schedule from storage&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schedule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;schedule&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isActive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;  &lt;span class="c1"&gt;// bail if deleted/paused&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Pick the right queue&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTargetQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;targetQueue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;//    "hr-queue"      -&amp;gt; this.env.HR_QUEUE&lt;/span&gt;
  &lt;span class="c1"&gt;//    "auth-queue"    -&amp;gt; this.env.AUTH_QUEUE&lt;/span&gt;
  &lt;span class="c1"&gt;//    "payroll-queue" -&amp;gt; this.env.PAYROLL_QUEUE&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. SEND THE JOB TO THE QUEUE&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jobPayload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// This delivers exactly the jobPayload that was stored during init:&lt;/span&gt;
  &lt;span class="c1"&gt;// { type: "notification", scheduleId: "abc-123", userId: "...", ... }&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. Handle what comes next...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  After the Job is Sent: The Repeat Loop
&lt;/h3&gt;

&lt;p&gt;This is how repeatable schedules work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ... (job sent successfully) ...&lt;/span&gt;

  &lt;span class="c1"&gt;// ONE-TIME: deactivate and stop&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scheduleType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;once&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isActive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;schedule&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;// No new alarm set — this DO is done forever&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// CRON or INTERVAL: calculate next time and set a new alarm&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextRun&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;calculateNextRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAlarm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextRun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeKvRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;nextRun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="c1"&gt;// The cycle continues...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visualizing the repeating cycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CRON SCHEDULE: "0 14 * * *" (daily at 2 PM)

Day 1:
  init() -&amp;gt; setAlarm(Day 1, 2:00:37 PM)
                |
                v
  alarm() fires at 2:00:37 PM -&amp;gt; sends job to queue
                                -&amp;gt; calculateNextRun() = Day 2, 2:00:42 PM
                                -&amp;gt; setAlarm(Day 2, 2:00:42 PM)
                                            |
                                            v
  Day 2: alarm() fires at 2:00:42 PM -&amp;gt; sends job to queue
                                      -&amp;gt; calculateNextRun() = Day 3, 2:00:35 PM
                                      -&amp;gt; setAlarm(Day 3, 2:00:35 PM)
                                                  |
                                                  v
  Day 3: ... and so on, forever, until the user stops it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each alarm sets the &lt;strong&gt;next&lt;/strong&gt; alarm. It's a self-perpetuating chain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retry Logic: What If the Queue Send Fails?
&lt;/h3&gt;

&lt;p&gt;Network issues happen. The scheduler handles this gracefully:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jobPayload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// Success! Reset retry counter&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;retryCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryCount&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;retryCount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Try again in 60 seconds&lt;/span&gt;
    &lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;retryCount&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAlarm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;  &lt;span class="c1"&gt;// don't schedule the next regular run yet&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// 3 retries failed — give up, mark as inactive&lt;/span&gt;
  &lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isActive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="c1"&gt;// The schedule is now dead. A human needs to investigate.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visualized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alarm() fires -&amp;gt; queue.send() FAILS
                   |
                   v
              retry 1 (wait 60s) -&amp;gt; queue.send() FAILS
                                       |
                                       v
                                  retry 2 (wait 60s) -&amp;gt; queue.send() FAILS
                                                           |
                                                           v
                                      retry 3 (wait 60s) -&amp;gt; queue.send() FAILS
                                                               |
                                                               v
                                                          GIVE UP
                                                          isActive = false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 6: The Three Storage Layers (Why Three?)
&lt;/h2&gt;

&lt;p&gt;The system uses three different storage mechanisms. Each serves a distinct purpose:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;th&gt;What it stores&lt;/th&gt;
&lt;th&gt;Who reads it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PostgreSQL (DB)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;User-facing schedule records with ownership (userId), notification type, active status, next/last run times&lt;/td&gt;
&lt;td&gt;Auth Worker API (user CRUD operations)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DO Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The "live" schedule state: job payload, cron expression, retry count, active flag (private to each DO instance)&lt;/td&gt;
&lt;td&gt;The Durable Object itself (timer engine)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;KV Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Read-only index of all schedules with their next run times (queryable, fast)&lt;/td&gt;
&lt;td&gt;Admin/debug queries, listing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why can't we just use one?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; is in the auth-worker. The scheduler-worker can't access it (different worker, no DB connection).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DO Storage&lt;/strong&gt; is private to each DO instance. You can't query across all DOs ("show me all active schedules"). That's by design — DOs are isolated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KV&lt;/strong&gt; fills the gap: it's globally readable, fast, and lets you list/filter schedules without waking up every single DO.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it this way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PostgreSQL = "What schedules does USER X have?"       (user-facing)
DO Storage = "What should I do when my alarm rings?"  (execution engine)
KV         = "What are ALL the upcoming schedules?"   (admin overview)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 7: Complete End-to-End Example
&lt;/h2&gt;

&lt;p&gt;Let's trace a complete flow from user request to job delivery.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenario: "Send me a daily stock price notification at 2 PM New York time"
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TIME: March 7, 2026 at 10:00 AM

STEP 1: User calls the API
=========================================================
POST /notifications/schedules
Authorization: Bearer &amp;lt;jwt-token&amp;gt;
{
  "type": "stock_price",
  "timezone": "America/New_York",
  "dailyTime": { "hour": 14, "minute": 0 }
}

STEP 2: Auth Worker processes
=========================================================
  a) Zod validates: type is valid, exactly one timing option provided  [OK]
  b) Convert: dailyTime {14, 0} -&amp;gt; cronExpression "0 14 * * *"
  c) Calculate next run: March 7, 2:00 PM ET (today! it's before 2 PM)
  d) Save to PostgreSQL:
     {
       id: "schedule-uuid-abc",
       userId: "user-uuid-123",
       type: "stock_price",
       scheduleType: "cron",
       cronExpression: "0 14 * * *",
       timezone: "America/New_York",
       isActive: true,
       nextRun: "2026-03-07T19:00:00Z"    // 2 PM ET = 7 PM UTC
     }

STEP 3: Auth Worker -&amp;gt; Scheduler Worker (via service binding)
=========================================================
  SchedulerPublisher sends:
  POST https://scheduler/schedules
  {
    id: "schedule-uuid-abc",
    targetQueue: "notification-queue",
    jobPayload: {
      type: "notification",
      scheduleId: "schedule-uuid-abc",
      userId: "user-uuid-123",
      userEmail: "alice@company.com",
      notificationType: "stock_price",
      title: "stock_price notification",
      message: "Scheduled stock_price for alice@company.com"
    },
    scheduleType: "cron",
    cronExpression: "0 14 * * *",
    timezone: "America/New_York"
  }

STEP 4: Scheduler Worker routes to Durable Object
=========================================================
  a) doId = SCHEDULER_DO.idFromName("schedule-uuid-abc")
     -&amp;gt; A unique DO instance is created (or resumed) for this ID
  b) stub.fetch("https://do/init", { body: config })

STEP 5: Durable Object initializes
=========================================================
  a) Saves full schedule data to DO storage
  b) Calculates next run: March 7, 2:00:37 PM ET (with 37s random jitter)
  c) Sets alarm: setAlarm(1741374037000)  // Unix timestamp in milliseconds
  d) Writes to KV:
     Key: "schedule:schedule-uuid-abc"
     Value: { ...full record, nextRun: "2026-03-07T19:00:37Z" }

STEP 6: Response flows back to user
=========================================================
  201 Created
  {
    "id": "schedule-uuid-abc",
    "type": "stock_price",
    "scheduleType": "cron",
    "cronExpression": "0 14 * * *",
    "timezone": "America/New_York",
    "isActive": true,
    "nextRun": "2026-03-07T19:00:37Z",
    "lastRun": null,
    "createdAt": "2026-03-07T15:00:00Z",
    "updatedAt": "2026-03-07T15:00:00Z"
  }

  === TIME PASSES... it's now March 7, 2:00:37 PM ET ===

STEP 7: Cloudflare wakes up the Durable Object
=========================================================
  alarm() method is called automatically by Cloudflare's runtime.

  a) Reads schedule from DO storage
  b) Checks isActive === true  [OK]
  c) Resolves target queue: "hr-queue" -&amp;gt; this.env.HR_QUEUE
  d) Sends message to queue:
     HR_QUEUE.send({
       type: "notification",
       scheduleId: "schedule-uuid-abc",
       userId: "user-uuid-123",
       userEmail: "alice@company.com",
       notificationType: "stock_price",
       title: "stock_price notification",
       message: "Scheduled stock_price for alice@company.com"
     })

STEP 8: Schedule the next run (THE REPEAT)
=========================================================
  a) scheduleType is "cron", not "once" -&amp;gt; keep going!
  b) calculateNextRun("0 14 * * *", "America/New_York")
     -&amp;gt; March 8, 2:00:42 PM ET (tomorrow, with new random jitter)
  c) setAlarm(March 8, 2:00:42 PM ET)
  d) Update KV with new nextRun

STEP 9: Queue consumer processes the job
=========================================================
  HR Worker receives the message from hr-queue.
  It sees type: "notification" and handles it
  (e.g., sends an email, pushes to websocket, etc.)

  === NEXT DAY: March 8, 2:00:42 PM ET ===

STEP 10: The cycle repeats (back to Step 7)
=========================================================
  alarm() fires again -&amp;gt; sends job -&amp;gt; calculates March 9 -&amp;gt; sets alarm
  ... and so on, every day, until the user deletes or pauses the schedule.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 8: Update and Delete Flows
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Updating a Schedule
&lt;/h3&gt;

&lt;p&gt;User wants to change from daily 2 PM to weekly Monday 9 AM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PUT /notifications/schedules/schedule-uuid-abc
{ "weeklyTime": { "dayOfWeek": 1, "hour": 9, "minute": 0 } }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Auth Worker:
  1. Load from DB, verify ownership
  2. Convert weeklyTime -&amp;gt; cronExpression "0 9 * * 1"
  3. Update PostgreSQL record
  4. Call schedulerPublisher.updateSchedule("schedule-uuid-abc", {
       scheduleType: "cron",
       cronExpression: "0 9 * * 1",
       timezone: "America/New_York"
     })

Scheduler Worker -&amp;gt; Durable Object:
  1. Load current schedule from DO storage
  2. Merge new fields over old fields
  3. Cancel old alarm (implicitly — setAlarm overwrites)
  4. Calculate new next run (next Monday at 9 AM)
  5. Set new alarm
  6. Update KV
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deleting a Schedule
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DELETE /notifications/schedules/schedule-uuid-abc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Auth Worker:
  1. Verify ownership
  2. Call schedulerPublisher.stopSchedule("schedule-uuid-abc")
  3. Delete from PostgreSQL

Scheduler Worker -&amp;gt; Durable Object:
  1. Delete the alarm (no more wake-ups)
  2. Delete schedule from DO storage
  3. Mark as inactive in KV
  4. Clean up secondary KV index
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The DO effectively becomes empty. Cloudflare will garbage-collect it eventually.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 9: The Queue System (How Jobs Get Delivered)
&lt;/h2&gt;

&lt;p&gt;Cloudflare Queues work like a conveyor belt:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjuxrrwnr7winv5es65ps.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjuxrrwnr7winv5es65ps.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The scheduler-worker has &lt;strong&gt;three queue bindings&lt;/strong&gt; — it can send jobs to any of the three workers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In the Durable Object&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;getTargetQueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetQueue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TargetQueue&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Queue&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;targetQueue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auth-queue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AUTH_QUEUE&lt;/span&gt;     &lt;span class="c1"&gt;// -&amp;gt; auth-worker&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notification-queue&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NOTIFICATION_QUEUE&lt;/span&gt;       &lt;span class="c1"&gt;// -&amp;gt; notification-worker&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each queue has &lt;strong&gt;typed messages&lt;/strong&gt;. The &lt;code&gt;notification-queue&lt;/code&gt; accepts &lt;code&gt;NotificationJob&lt;/code&gt; which includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;NotificationJob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;NotificationJob&lt;/span&gt;       &lt;span class="c1"&gt;// { type: "notification", scheduleId, userId, ... }&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;ReportGenerationJob&lt;/span&gt;   &lt;span class="c1"&gt;// { type: "report_generation", reportType, ... }&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;DataSyncJob&lt;/span&gt;           &lt;span class="c1"&gt;// { type: "data_sync", source, entityType, ... }&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;OnboardingReminderJob&lt;/span&gt; &lt;span class="c1"&gt;// { type: "onboarding_reminder", employeeId, ... }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The consumer uses the &lt;code&gt;type&lt;/code&gt; field to decide what to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Conceptual queue consumer in hr-worker&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;notification&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;report_generation&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
      &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 10: Why This Architecture Works Well
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Reliability
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Durable Object alarms&lt;/strong&gt; survive crashes and deploys. If Cloudflare restarts your DO, the alarm is still set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue delivery&lt;/strong&gt; has at-least-once guarantees. If the consumer crashes, the message is redelivered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retry logic&lt;/strong&gt; in the DO handles transient queue failures (3 retries with 60s backoff).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Scalability
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Each schedule is its own DO — &lt;strong&gt;no shared state&lt;/strong&gt;, no database locks, no contention.&lt;/li&gt;
&lt;li&gt;100,000 schedules = 100,000 independent DOs, each with its own alarm. Cloudflare handles the distribution.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Simplicity
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;No cron servers to manage. No Redis. No "is the scheduler process still running?" anxiety.&lt;/li&gt;
&lt;li&gt;The entire scheduler-worker is ~260 lines of TypeScript.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;DOs only consume resources when they're active (during alarm handling). A DO sitting idle waiting for its alarm costs nothing.&lt;/li&gt;
&lt;li&gt;Queues charge per message, not per connection.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User -&amp;gt; Auth Worker API -&amp;gt; validates &amp;amp; saves to PostgreSQL
                        -&amp;gt; tells Scheduler Worker via service binding
                              -&amp;gt; creates a Durable Object for this schedule
                              -&amp;gt; DO sets an alarm (Cloudflare's built-in timer)
                              -&amp;gt; DO writes to KV for queryability

         ... time passes ...

Cloudflare wakes the DO -&amp;gt; alarm() fires
                        -&amp;gt; DO sends jobPayload to the target queue
                        -&amp;gt; Queue delivers to the consumer worker
                        -&amp;gt; Consumer processes the job (sends email, etc.)

         ... for repeating schedules ...

                        -&amp;gt; DO calculates next run time
                        -&amp;gt; DO sets a new alarm
                        -&amp;gt; The cycle repeats forever
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. An alarm clock that never forgets, never crashes, and scales to millions of schedules — all without a single server to manage.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>jobscheduler</category>
      <category>durableobject</category>
      <category>cloudflarequeues</category>
    </item>
    <item>
      <title>Building Resilient, Scalable Apps with Bun, Hono, and Cloudflare</title>
      <dc:creator>Shaikh Al Amin</dc:creator>
      <pubDate>Fri, 27 Feb 2026 21:31:47 +0000</pubDate>
      <link>https://dev.to/shaikhalamin/building-resilient-scalable-apps-with-bun-hono-and-cloudflare-4jj4</link>
      <guid>https://dev.to/shaikhalamin/building-resilient-scalable-apps-with-bun-hono-and-cloudflare-4jj4</guid>
      <description>&lt;p&gt;When building modern web applications, handling background jobs, managing state, and ensuring fast database access are critical. Recently, I migrated my Bun + Hono project to Cloudflare’s ecosystem, and the combination of Cloudflare Queues, Durable Objects, KV, R2, D1, and Hyperdrive with Planetscale turned out to be a game-changer—especially for managing repeatable jobs and dead-letter queues (DLQ).&lt;/p&gt;

&lt;p&gt;The Stack at a Glance:&lt;/p&gt;

&lt;p&gt;Bun + Hono: Lightweight, fast runtime and web framework running on Cloudflare Workers.&lt;/p&gt;

&lt;p&gt;Cloudflare Queues: Reliable message passing for background tasks.&lt;br&gt;
Durable Objects + D1: Stateful processing with persistent SQL storage (via D1) for job metadata and retry logic.&lt;/p&gt;

&lt;p&gt;KV &amp;amp; R2: Ultra-low-latency key-value cache and object storage for files.&lt;br&gt;
Planetscale + Hyperdrive: MySQL-compatible database with Hyperdrive’s connection pooling and query caching, accessed through Drizzle ORM.&lt;/p&gt;

&lt;p&gt;How It All Flows:&lt;/p&gt;

&lt;p&gt;Request Handling: An incoming HTTP request hits the Hono app. For simple reads/writes, the app uses Drizzle ORM to query Planetscale through Hyperdrive. Hyperdrive caches frequent queries, dramatically reducing latency and database load.&lt;/p&gt;

&lt;p&gt;Job Orchestration: For tasks that need background processing (e.g., sending emails, image processing), the app enqueues a message into Cloudflare Queue. This guarantees delivery and decouples the request from heavy work.&lt;/p&gt;

&lt;p&gt;Durable Object Processing: A Durable Object consumes messages from the queue. It maintains job state in its attached D1 database (for persistence across failures) and uses KV for ephemeral data like rate-limit counters. If the job involves files, it streams them to/from R2.&lt;/p&gt;

&lt;p&gt;Database Interactions: The Durable Object also updates the Planetscale database via Drizzle + Hyperdrive. Since Hyperdrive pools connections and caches read results, repeated lookups (e.g., checking user quotas) are lightning fast.&lt;/p&gt;

&lt;p&gt;Resilience &amp;amp; DLQ Handling: Failed jobs are retried according to logic inside the Durable Object. If all retries fail, the job can be moved to a dead-letter queue (simulated using D1 or KV), allowing manual inspection without losing data.&lt;/p&gt;

&lt;p&gt;Why This Rocks:&lt;/p&gt;

&lt;p&gt;Performance: Hyperdrive’s cache reduces database round trips; KV gives microsecond access to hot data; R2 handles large files without bloating the DB.&lt;/p&gt;

&lt;p&gt;Reliability: Queues and Durable Objects ensure exactly-once processing semantics and state persistence—perfect for financial transactions or idempotent jobs.&lt;/p&gt;

&lt;p&gt;Developer Experience: Bun’s speed, Hono’s simplicity, and Drizzle’s type-safe SQL make development a joy. Everything deploys seamlessly with Wrangler.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8m0rp6duawgpirwvpi1h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8m0rp6duawgpirwvpi1h.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>queue</category>
      <category>durableobject</category>
      <category>hyperdrive</category>
    </item>
    <item>
      <title>Build Production-Ready GCP Infrastructure from Scratch Part 04</title>
      <dc:creator>Shaikh Al Amin</dc:creator>
      <pubDate>Wed, 04 Feb 2026 12:49:58 +0000</pubDate>
      <link>https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-04-43i8</link>
      <guid>https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-04-43i8</guid>
      <description>&lt;h1&gt;
  
  
  Build Production-Ready GCP Infrastructure from Scratch: A Complete Console Guide
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;A 4-Part Series for Complete Beginners&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-01-3c2n"&gt;Part 1: Foundation - Project Setup, VPC &amp;amp; Networking&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-02-542o"&gt;Part 2: Security Services - Secrets, Bastion &amp;amp; IAM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-03-34ld"&gt;Part 3: Database &amp;amp; Compute Resources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-04-43i8"&gt;Part 4: Observability &amp;amp; Load Balancer&lt;/a&gt; ← &lt;strong&gt;You are here&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Part 4: Observability &amp;amp; Load Balancer
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;In this final part, you'll complete your infrastructure with observability and external access. We'll create Prometheus for metrics, Loki for logs, Grafana for dashboards, and an Application Load Balancer for external traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll build:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prometheus VM for metrics collection (7-day retention)&lt;/li&gt;
&lt;li&gt;Loki VM for log aggregation with Grafana&lt;/li&gt;
&lt;li&gt;External Application Load Balancer with SSL&lt;/li&gt;
&lt;li&gt;End-to-end health checks and monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Estimated time:&lt;/strong&gt; 45-60 minutes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Estimated cost:&lt;/strong&gt; ~$64/month&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final cumulative cost:&lt;/strong&gt; ~$301/month&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before continuing, ensure you've completed &lt;strong&gt;Parts 1-3&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] VPC and 5 subnets exist (including &lt;code&gt;private-obs&lt;/code&gt; subnet)&lt;/li&gt;
&lt;li&gt;[ ] Cloud SQL with private IP is running&lt;/li&gt;
&lt;li&gt;[ ] Backend MIG has 2+ healthy VMs&lt;/li&gt;
&lt;li&gt;[ ] Cache VM with Redis/PgBouncer is running&lt;/li&gt;
&lt;li&gt;[ ] Firewall rules allow health check IPs (35.191.0.0/16, 130.211.0.0/22)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you missed Parts 1-3:&lt;/strong&gt; &lt;a href="//01-foundation-project-vpc-networking.md"&gt;Start with Part 1: Foundation →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 1: Create Static Internal IPs for Observability
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What are Static Internal IPs?
&lt;/h3&gt;

&lt;p&gt;Static IPs ensure observability VMs have predictable IP addresses. This makes configuration easier (no need to update configs if VMs are recreated).&lt;/p&gt;

&lt;h3&gt;
  
  
  Prometheus Static IP
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Navigation Path
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;VPC networks&lt;/strong&gt; → &lt;strong&gt;Internal IP addresses&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Reserve static internal IP address"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  IP Configuration
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-prometheus-ip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Descriptive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Our VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Subnetwork&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;private-obs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Observability subnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP address&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.0.5.10&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Manual assignment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-50-reserve-ip.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-50-reserve-ip.png" alt="Screenshot: Reserve Internal IP" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Reserve"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Loki Static IP
&lt;/h3&gt;

&lt;p&gt;Repeat the process:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-loki-ip&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Subnetwork&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;private-obs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP address&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.0.5.11&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Verify IPs
&lt;/h3&gt;

&lt;p&gt;You should see 2 reserved IPs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;IP Address&lt;/th&gt;
&lt;th&gt;Subnetwork&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dev-prometheus-ip&lt;/td&gt;
&lt;td&gt;10.0.5.10&lt;/td&gt;
&lt;td&gt;private-obs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dev-loki-ip&lt;/td&gt;
&lt;td&gt;10.0.5.11&lt;/td&gt;
&lt;td&gt;private-obs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 2: Create Prometheus VM
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is Prometheus?
&lt;/h3&gt;

&lt;p&gt;Prometheus is a metrics collection and storage system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scrapes metrics from Node Exporter (every VM)&lt;/li&gt;
&lt;li&gt;Stores 7 days of metrics data&lt;/li&gt;
&lt;li&gt;Provides query API for Grafana&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why self-hosted:&lt;/strong&gt; Full control, no vendor lock-in, predictable costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Navigation Path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Compute Engine&lt;/strong&gt; → &lt;strong&gt;VM instances&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create instance"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  VM Configuration
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Basic Settings
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-prometheus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Descriptive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same as VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Zone&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1-b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zone b&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Machine Type
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Machine type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;e2-medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 vCPU, 4GB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Boot Disk
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ubuntu 22.04 LTS Minimal&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disk type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pd-balanced&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;50 GB&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why 50GB:&lt;/strong&gt; 7 days of metrics at 15s interval requires ~30-40GB. 50GB provides headroom.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Network Interface
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Our VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Subnetwork&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;private-obs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Observability subnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary internal IP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Static&lt;/code&gt; → &lt;code&gt;dev-prometheus-ip&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Use static IP&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;External IPv4 address&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;None&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No public IP needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-51-prometheus-network.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-51-prometheus-network.png" alt="Screenshot: Prometheus Network" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Service Account
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service account&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;observability-dev-sa&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Metadata - Startup Script
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Key:&lt;/strong&gt; &lt;code&gt;startup-script&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Value:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Prometheus Startup Script&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== Prometheus Startup Script Begin &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; ==="&lt;/span&gt;

&lt;span class="c"&gt;# Install Docker&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.docker.com &lt;span class="nt"&gt;-o&lt;/span&gt; get-docker.sh
sh get-docker.sh

&lt;span class="c"&gt;# Install Node Exporter for self-monitoring&lt;/span&gt;
&lt;span class="nv"&gt;NODE_EXPORTER_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1.6.1"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Installing Node Exporter &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_EXPORTER_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;

useradd &lt;span class="nt"&gt;--no-create-home&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; /bin/false node_exporter &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true

&lt;/span&gt;wget &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/prometheus/node_exporter/releases/download/v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_EXPORTER_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/node_exporter-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_EXPORTER_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.linux-amd64.tar.gz"&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; /tmp/node_exporter.tar.gz
&lt;span class="nb"&gt;tar &lt;/span&gt;xzf /tmp/node_exporter.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /tmp
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/node_exporter-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_EXPORTER_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.linux-amd64/node_exporter /usr/local/bin/
&lt;span class="nb"&gt;chown &lt;/span&gt;node_exporter:node_exporter /usr/local/bin/node_exporter
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/local/bin/node_exporter

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/systemd/system/node_exporter.service &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Unit]
Description=Prometheus Node Exporter
After=network.target

[Service]
Type=simple
User=node_exporter
ExecStart=/usr/local/bin/node_exporter --web.listen-address=:9100
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemctl daemon-reload
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;node_exporter
systemctl start node_exporter

&lt;span class="c"&gt;# Create Prometheus configuration&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /opt/prometheus.yml &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
global:
  scrape_interval: 15s
  retention: 7d

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'node-exporter'
    static_configs:
      - targets: ['localhost:9100']

  # Backend VMs (update IPs after MIG creation)
  - job_name: 'backend'
    static_configs:
      - targets: ['10.0.2.2:9100', '10.0.2.3:9100']
        labels:
          tier: backend

  # Cache VM
  - job_name: 'cache'
    static_configs:
      - targets: ['10.0.4.2:9100']
        labels:
          tier: cache
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Run Prometheus&lt;/span&gt;
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; prometheus &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--restart&lt;/span&gt; unless-stopped &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 9090:9090 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; /opt/prometheus.yml:/etc/prometheus/prometheus.yml &lt;span class="se"&gt;\&lt;/span&gt;
  prom/prometheus:latest

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== Prometheus Startup Script Complete &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; ==="&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Prometheus running on port 9090"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Node Exporter running on port 9100"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create the VM
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VM Creation Time:&lt;/strong&gt; 2-3 minutes&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify Prometheus
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name:&lt;/strong&gt; dev-prometheus&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status:&lt;/strong&gt; Running&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal IP:&lt;/strong&gt; 10.0.5.10&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External IP:&lt;/strong&gt; None&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 3: Create Loki VM (with Grafana)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is Loki and Grafana?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Loki:&lt;/strong&gt; Log aggregation system (like Prometheus, but for logs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grafana:&lt;/strong&gt; Visualization dashboard for metrics and logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why combined:&lt;/strong&gt; Cost optimization. Single VM runs both services (~$23/month).&lt;/p&gt;

&lt;h3&gt;
  
  
  Navigation Path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Compute Engine&lt;/strong&gt; → &lt;strong&gt;VM instances&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create instance"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  VM Configuration
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Basic Settings
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-loki&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Descriptive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same as VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Zone&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1-b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zone b&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Machine Type
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Machine type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;e2-medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 vCPU, 4GB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Boot Disk
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ubuntu 22.04 LTS Minimal&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disk type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pd-balanced&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;50 GB&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Network Interface
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Our VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Subnetwork&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;private-obs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Observability subnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary internal IP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Static&lt;/code&gt; → &lt;code&gt;dev-loki-ip&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Use static IP&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;External IPv4 address&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ephemeral&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Enable for Grafana access&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why public IP:&lt;/strong&gt; Grafana needs to be accessible from your browser. In production, use IAP instead of public IP.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-52-loki-network.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-52-loki-network.png" alt="Screenshot: Loki Network" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Service Account
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service account&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;observability-dev-sa&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Metadata - Startup Script
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Key:&lt;/strong&gt; &lt;code&gt;startup-script&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Value:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Loki and Grafana Startup Script&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== Loki/Grafana Startup Script Begin &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; ==="&lt;/span&gt;

&lt;span class="c"&gt;# Install Docker&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://get.docker.com &lt;span class="nt"&gt;-o&lt;/span&gt; get-docker.sh
sh get-docker.sh

&lt;span class="c"&gt;# Install Docker Compose&lt;/span&gt;
apt-get update
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker-compose

&lt;span class="c"&gt;# Create docker-compose.yml&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /opt/docker-compose.yml &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
version: '3.8'

services:
  loki:
    image: grafana/loki:latest
    ports:
      - "3100:3100"
    volumes:
      - /opt/loki-config.yml:/etc/loki/local-config.yaml
      - loki-data:/loki
    command: -config.file=/etc/loki/local-config.yaml
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin123
      - GF_USERS_ALLOW_SIGN_UP=false
      - GF_SERVER_ROOT_URL=http://localhost:3000
    volumes:
      - grafana-storage:/var/lib/grafana
      - /opt/grafana-provisioning:/etc/grafana/provisioning
    restart: unless-stopped

volumes:
  loki-data:
  grafana-storage:
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Create Loki config&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /opt/loki-config.yml &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
auth_enabled: false

server:
  http_listen_port: 3100

ingester:
  lifecycler:
    address: 127.0.0.1
    ring:
      kvstore:
        store: inmemory
      replication_factor: 1
    final_sleep: 0s
  chunk_idle_period: 1h
  max_chunk_age: 1h
  chunk_target_size: 1048576
  chunk_retain_period: 30s

limits_config:
  enforce_metric_name: false
  reject_old_samples: true
  reject_old_samples_max_age: 168h

schema_config:
  configs:
    - from: 2020-10-24
      store: boltdb-shipper
      object_store: filesystem
      schema: v11
      index:
        prefix: index_
        period: 24h

storage_config:
  boltdb_shipper:
    active_index_directory: /loki/index
    cache_location: /loki/cache
    shared_store: filesystem
  filesystem:
    directory: /loki/chunks

chunk_store_config:
  max_look_back_period: 168h

table_manager:
  retention_deletes_enabled: false
  retention_period: 0s
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Create Grafana provisioning&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /opt/grafana-provisioning/datasources

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /opt/grafana-provisioning/datasources/loki.yml &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
apiVersion: 1

datasources:
  - name: Loki
    type: loki
    access: proxy
    url: http://loki:3100
    isDefault: false
    editable: false
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /opt/grafana-provisioning/datasources/prometheus.yml &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    access: proxy
    url: http://10.0.5.10:9090
    isDefault: true
    editable: false
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Start services&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt
docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== Loki/Grafana Startup Script Complete &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; ==="&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Loki running on port 3100"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Grafana running on port 3000 (http://EXTERNAL_IP:3000)"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Login: admin / admin123"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create the VM
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VM Creation Time:&lt;/strong&gt; 2-3 minutes&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify Loki VM
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name:&lt;/strong&gt; dev-loki&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status:&lt;/strong&gt; Running&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal IP:&lt;/strong&gt; 10.0.5.11&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External IP:&lt;/strong&gt; (Assigned IP)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Copy the external IP&lt;/strong&gt; - we'll need it for Grafana access.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Access Grafana Dashboard
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Get Loki VM External IP
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Compute Engine&lt;/strong&gt; → &lt;strong&gt;VM instances&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Find &lt;code&gt;dev-loki&lt;/code&gt; VM&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;External IP&lt;/strong&gt; address&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Access Grafana
&lt;/h3&gt;

&lt;p&gt;Open your browser and navigate to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://[LOKI_EXTERNAL_IP]:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-53-grafana-login.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-53-grafana-login.png" alt="Screenshot: Grafana Login" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Grafana Login
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Email or username&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;admin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Password&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;admin123&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Click &lt;strong&gt;"Log in"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify Datasources
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;"Configuration"&lt;/strong&gt; (gear icon) → &lt;strong&gt;Data sources&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Verify &lt;strong&gt;Prometheus&lt;/strong&gt; shows "Healthy" (green checkmark)&lt;/li&gt;
&lt;li&gt;Verify &lt;strong&gt;Loki&lt;/strong&gt; shows "Healthy"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-54-grafana-datasources.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-54-grafana-datasources.png" alt="Screenshot: Grafana Datasources" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Troubleshooting:&lt;/strong&gt; If datasources are unhealthy:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check Prometheus is running: &lt;code&gt;docker ps&lt;/code&gt; on Loki VM&lt;/li&gt;
&lt;li&gt;Verify network connectivity: &lt;code&gt;curl http://10.0.5.10:9090/-/healthy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check Docker logs: &lt;code&gt;docker logs loki&lt;/code&gt; or &lt;code&gt;docker logs grafana&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 5: Update Prometheus Targets
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What are Prometheus Targets?
&lt;/h3&gt;

&lt;p&gt;Prometheus "scrapes" metrics from targets. We need to add our backend and cache VMs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update Prometheus Configuration
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;SSH to Prometheus VM via bastion:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# From your local machine&lt;/span&gt;
gcloud compute ssh dev-bastion &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt;

&lt;span class="c"&gt;# From bastion, SSH to Prometheus&lt;/span&gt;
ssh 10.0.5.10

&lt;span class="c"&gt;# Edit Prometheus config&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /opt/prometheus.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Update the targets with actual backend VM IPs:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;scrape_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;backend'&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;10.0.2.2:9100'&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;10.0.2.3:9100'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Update with actual IPs&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;job_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cache'&lt;/span&gt;
    &lt;span class="na"&gt;static_configs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;10.0.4.2:9100'&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Update with actual IP&lt;/span&gt;
        &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;tier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Restart Prometheus:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker restart prometheus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Verify Targets
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;code&gt;http://[LOKI_EXTERNAL_IP]:3000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Explore&lt;/strong&gt; → Select &lt;strong&gt;Prometheus&lt;/strong&gt; datasource&lt;/li&gt;
&lt;li&gt;Query: &lt;code&gt;up{job="backend"}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You should see metrics from backend VMs&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 6: Create External Application Load Balancer
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is an ALB?
&lt;/h3&gt;

&lt;p&gt;Application Load Balancer (ALB):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Distributes traffic across backend VMs&lt;/li&gt;
&lt;li&gt;Provides single public IP for external access&lt;/li&gt;
&lt;li&gt;Handles SSL termination&lt;/li&gt;
&lt;li&gt;Health checks for backend availability&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 6a: Create Health Check
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Navigation Path
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Compute Engine&lt;/strong&gt; → &lt;strong&gt;Health checks&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create health check"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Health Check Configuration
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-lb-health-check&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Descriptive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Protocol&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HTTP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTTP health check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Port&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NestJS app port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Request path&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;App must implement this&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Check interval&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;20&lt;/code&gt; seconds&lt;/td&gt;
&lt;td&gt;How often to check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Timeout&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;5&lt;/code&gt; seconds&lt;/td&gt;
&lt;td&gt;Response timeout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Healthy threshold&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 successes = healthy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unhealthy threshold&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5 failures = unhealthy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-55-lb-health-check.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-55-lb-health-check.png" alt="Screenshot: LB Health Check" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6b: Add Named Port to MIG
&lt;/h3&gt;

&lt;h4&gt;
  
  
  What is a Named Port?
&lt;/h4&gt;

&lt;p&gt;Named ports link a name (like "http") to a port number (3000). The load balancer uses named ports for routing.&lt;/p&gt;

&lt;h4&gt;
  
  
  Navigation Path
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Compute Engine&lt;/strong&gt; → &lt;strong&gt;Instance groups&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click on &lt;strong&gt;&lt;code&gt;dev-backend-mig&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Edit group"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Edit MIG
&lt;/h4&gt;

&lt;p&gt;Scroll to &lt;strong&gt;"Named ports"&lt;/strong&gt; section:&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Add item"&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;http&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Port&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-56-mig-named-port.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-56-mig-named-port.png" alt="Screenshot: MIG Named Port" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Save"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6c: Create Load Balancer
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Navigation Path
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Network Services&lt;/strong&gt; → &lt;strong&gt;Load balancing&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create load balancer"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Select LB Type
&lt;/h4&gt;

&lt;p&gt;Click &lt;strong&gt;"Application Load Balancer (HTTP/S)"&lt;/strong&gt; → Click &lt;strong&gt;"Configure"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-57-lb-type.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-57-lb-type.png" alt="Screenshot: LB Type Selection" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Basic Configuration
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-lb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Environment-prefixed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same as VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Our VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Backend Configuration
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Backend type:&lt;/strong&gt; Instance group&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend configuration:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backend&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-backend-mig&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Our MIG&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Balancing mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Rate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rate-based balancing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maximum RPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;100&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per instance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Health check:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Health check&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-lb-health-check&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Session affinity:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Session affinity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;None&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stateless app&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-58-lb-backend.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-58-lb-backend.png" alt="Screenshot: LB Backend" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advanced settings:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Timeout&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;30&lt;/code&gt; seconds&lt;/td&gt;
&lt;td&gt;Connection timeout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enable Cloud CDN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Off&lt;/td&gt;
&lt;td&gt;Not needed for now&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Click &lt;strong&gt;"Done"&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Frontend Configuration
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Protocol:&lt;/strong&gt; HTTPS (we'll add HTTP redirect)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTPS Frontend:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Add frontend IP and port"&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Protocol&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HTTPS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Secure traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP address&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Reserve new static IP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create new IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP address name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-lb-ip&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Descriptive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Port&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;443&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTTPS port&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Certificate:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Certificate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Create a new certificate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;For SSL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Create Certificate:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-lb-cert&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Descriptive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Google-managed certificate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Auto-renew&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Domains:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Domains&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(Skip for now)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; For testing, you can use HTTP only (skip certificate). For production, add your domain and create Google-managed certificate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP Frontend (Redirect):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Add frontend IP and port"&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Protocol&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HTTP&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP address&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-lb-ip&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Port&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;80&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enable redirect to HTTPS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-59-lb-frontend.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-59-lb-frontend.png" alt="Screenshot: LB Frontend" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Routing Rules
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Host rules:&lt;/strong&gt; Leave default (all hosts)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path matcher:&lt;/strong&gt; Leave default (all paths)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backend service:&lt;/strong&gt; Select the backend created above&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Done"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the Load Balancer
&lt;/h3&gt;

&lt;p&gt;Review configuration and click &lt;strong&gt;"Create load balancer"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creation Time:&lt;/strong&gt; 5-10 minutes&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify Load Balancer
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name:&lt;/strong&gt; dev-lb&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status:&lt;/strong&gt; (Checkmark) - Active&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP address:&lt;/strong&gt; (Reserved IP)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backends:&lt;/strong&gt; dev-backend-mig (healthy)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 7: Test End-to-End Connectivity
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Test 1: Load Balancer Health Check
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get LB IP&lt;/span&gt;
gcloud compute forwarding-rules list &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"name=dev-lb*"&lt;/span&gt;

&lt;span class="c"&gt;# Test health endpoint&lt;/span&gt;
curl http://[LB_IP]/api/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expected:&lt;/strong&gt; HTTP 200 with response like &lt;code&gt;{"status":"ok"}&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 2: Full Request Flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Test through load balancer&lt;/span&gt;
curl http://[LB_IP]/api/test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expected:&lt;/strong&gt; Response from backend application&lt;/p&gt;

&lt;h3&gt;
  
  
  Test 3: Prometheus Metrics
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open Grafana: &lt;code&gt;http://[LOKI_EXTERNAL_IP]:3000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Explore&lt;/strong&gt; → &lt;strong&gt;Prometheus&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Query: &lt;code&gt;rate(http_requests_total[5m])&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You should see request metrics&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Test 4: Loki Logs
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open Grafana: &lt;code&gt;http://[LOKI_EXTERNAL_IP]:3000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Explore&lt;/strong&gt; → &lt;strong&gt;Loki&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Query: &lt;code&gt;{job="nestjs"}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You should see application logs&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Part 4 Verification Checklist
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Final Verification
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Prometheus VM running at 10.0.5.10:9090&lt;/li&gt;
&lt;li&gt;[ ] Loki VM running with external IP assigned&lt;/li&gt;
&lt;li&gt;[ ] Grafana accessible at http://[LOKI_EXTERNAL_IP]:3000&lt;/li&gt;
&lt;li&gt;[ ] Prometheus datasource shows "Healthy"&lt;/li&gt;
&lt;li&gt;[ ] Loki datasource shows "Healthy"&lt;/li&gt;
&lt;li&gt;[ ] Prometheus scraping backend and cache VMs&lt;/li&gt;
&lt;li&gt;[ ] Load balancer has reserved static IP&lt;/li&gt;
&lt;li&gt;[ ] Backend service shows healthy instances&lt;/li&gt;
&lt;li&gt;[ ] curl http://[LB_IP]/api/health returns 200&lt;/li&gt;
&lt;li&gt;[ ] Full request flow works (LB → MIG → App)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-60-part4-complete.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-60-part4-complete.png" alt="Screenshot: Completed Part 4" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost Summary - Part 4
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prometheus VM&lt;/td&gt;
&lt;td&gt;~$23&lt;/td&gt;
&lt;td&gt;e2-medium, 50GB disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loki VM&lt;/td&gt;
&lt;td&gt;~$23&lt;/td&gt;
&lt;td&gt;e2-medium, 50GB disk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load Balancer&lt;/td&gt;
&lt;td&gt;~$18&lt;/td&gt;
&lt;td&gt;ALB forwarding rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total Part 4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$64&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$64/month&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Final Infrastructure Cost
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Part 1 (VPC, NAT, Firewall)&lt;/td&gt;
&lt;td&gt;~$42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 2 (Bastion)&lt;/td&gt;
&lt;td&gt;~$11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 3 (Cloud SQL, MIG, Cache)&lt;/td&gt;
&lt;td&gt;~$184&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 4 (Observability, LB)&lt;/td&gt;
&lt;td&gt;~$64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$301/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cost Optimization Tips:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use preemptible VMs for non-critical workloads (save 80%)&lt;/li&gt;
&lt;li&gt;Reduce Cloud SQL tier to db-g1-small for dev (save ~$70/month)&lt;/li&gt;
&lt;li&gt;Scale MIG to 0 during off-hours (save ~$46/month)&lt;/li&gt;
&lt;li&gt;Disable Flow Logs on non-critical subnets (save ~$5-10/month)&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Comprehensive Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Issue: Grafana Cannot Connect to Datasources
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Datasource shows "Could not connect"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagnosis:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check Prometheus is running&lt;/li&gt;
&lt;li&gt;Verify network connectivity&lt;/li&gt;
&lt;li&gt;Check firewall rules&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# From Loki VM&lt;/span&gt;
docker ps  &lt;span class="c"&gt;# Check containers running&lt;/span&gt;

&lt;span class="c"&gt;# Test Prometheus&lt;/span&gt;
curl http://10.0.5.10:9090/-/healthy

&lt;span class="c"&gt;# Test Loki&lt;/span&gt;
curl http://localhost:3100/ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Issue: Load Balancer Shows 0/0 Healthy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; No healthy backend instances&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagnosis:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check health check configuration&lt;/li&gt;
&lt;li&gt;Verify app is running on port 3000&lt;/li&gt;
&lt;li&gt;Check firewall for health check IPs&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# From backend VM&lt;/span&gt;
curl localhost:3000/health
netstat &lt;span class="nt"&gt;-tlnp&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Increase health check unhealthy threshold to 5&lt;/li&gt;
&lt;li&gt;Increase initial delay for MIG autohealing to 300&lt;/li&gt;
&lt;li&gt;Verify firewall rule allows 35.191.0.0/16 and 130.211.0.0/22&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Issue: High CPU on Backend VMs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; VMs constantly at 90%+ CPU&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagnosis:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check application logs&lt;/li&gt;
&lt;li&gt;Verify autoscaling thresholds&lt;/li&gt;
&lt;li&gt;Profile application performance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check CPU usage&lt;/span&gt;
top &lt;span class="nt"&gt;-bn1&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;

&lt;span class="c"&gt;# Check Node.js process&lt;/span&gt;
pm2 monit

&lt;span class="c"&gt;# Check autoscaler status&lt;/span&gt;
gcloud compute instance-groups managed describe dev-backend-mig &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; europe-west1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fixes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Increase machine type (e2-medium → e2-highcpu-4)&lt;/li&gt;
&lt;li&gt;Optimize application code&lt;/li&gt;
&lt;li&gt;Increase max replicas to 6 or 8&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Issue: Secrets Not Accessible from VMs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; "Permission denied" accessing secrets&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diagnosis:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify service account has Secret Accessor role&lt;/li&gt;
&lt;li&gt;Check secret exists and has versions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# From VM with correct SA&lt;/span&gt;
gcloud secrets versions list db-credentials-dev

&lt;span class="c"&gt;# Add IAM role if missing&lt;/span&gt;
gcloud secrets add-iam-policy-binding db-credentials-dev &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--member&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'serviceAccount:backend-dev-sa@PROJECT_ID.iam.gserviceaccount.com'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'roles/secretmanager.secretAccessor'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Next Steps Beyond This Guide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Domain &amp;amp; SSL
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Purchase domain from registrar&lt;/li&gt;
&lt;li&gt;Configure DNS to point to LB IP&lt;/li&gt;
&lt;li&gt;Update Load Balancer certificate with your domain&lt;/li&gt;
&lt;li&gt;Enable Google-managed certificate&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2. CI/CD Pipeline
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Setup GitHub Actions&lt;/li&gt;
&lt;li&gt;Auto-deploy on push&lt;/li&gt;
&lt;li&gt;Run tests in container&lt;/li&gt;
&lt;li&gt;Automated rollback on failure&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Monitoring Enhancements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Add alert rules in Prometheus&lt;/li&gt;
&lt;li&gt;Configure PagerDuty/Slack integration&lt;/li&gt;
&lt;li&gt;Create custom Grafana dashboards&lt;/li&gt;
&lt;li&gt;Set up uptime monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Security Hardening
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Enable VPC Service Controls&lt;/li&gt;
&lt;li&gt;Configure Organization Policies&lt;/li&gt;
&lt;li&gt;Setup Security Command Center&lt;/li&gt;
&lt;li&gt;Implement workload identity&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Multi-Environment
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create staging environment&lt;/li&gt;
&lt;li&gt;Use Shared VPC&lt;/li&gt;
&lt;li&gt;Implement environment isolation&lt;/li&gt;
&lt;li&gt;Setup service directory&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  End-to-End Verification Test
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Test 1: Full Request Flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Get Load Balancer IP&lt;/span&gt;
gcloud compute forwarding-rules list &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"name=dev-lb*"&lt;/span&gt;

&lt;span class="c"&gt;# 2. Send test request&lt;/span&gt;
curl http://[LB_IP]/api/health

&lt;span class="c"&gt;# Expected: {"status":"ok","timestamp":"..."}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test 2: Database Connectivity
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# From backend VM (via bastion)&lt;/span&gt;
gcloud compute ssh dev-bastion &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt;
ssh 10.0.2.2  &lt;span class="c"&gt;# Backend VM IP&lt;/span&gt;

&lt;span class="c"&gt;# Test Cloud SQL connection&lt;/span&gt;
psql &lt;span class="nt"&gt;-h&lt;/span&gt; 10.100.0.2 &lt;span class="nt"&gt;-U&lt;/span&gt; backend-dev-sa &lt;span class="nt"&gt;-d&lt;/span&gt; appdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test 3: Cache Connectivity
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# From backend VM&lt;/span&gt;
redis-cli &lt;span class="nt"&gt;-h&lt;/span&gt; 10.0.4.2 PING
&lt;span class="c"&gt;# Expected: PONG&lt;/span&gt;

&lt;span class="c"&gt;# Test PgBouncer&lt;/span&gt;
psql &lt;span class="nt"&gt;-h&lt;/span&gt; 10.0.4.2 &lt;span class="nt"&gt;-p&lt;/span&gt; 6432 &lt;span class="nt"&gt;-U&lt;/span&gt; app_admin &lt;span class="nt"&gt;-d&lt;/span&gt; appdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test 4: Monitoring Pipeline
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Generate traffic: &lt;code&gt;ab -n 1000 http://[LB_IP]/api/health&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Open Grafana&lt;/li&gt;
&lt;li&gt;Check dashboards for metrics&lt;/li&gt;
&lt;li&gt;Verify Loki has logs&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Test 5: Disaster Recovery
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Simulate instance failure&lt;/span&gt;
gcloud compute instances delete &lt;span class="o"&gt;[&lt;/span&gt;ONE_BACKEND_INSTANCE] &lt;span class="nt"&gt;--quiet&lt;/span&gt;

&lt;span class="c"&gt;# Verify:&lt;/span&gt;
&lt;span class="c"&gt;# - MIG auto-heals (new instance appears)&lt;/span&gt;
&lt;span class="c"&gt;# - Load balancer continues serving&lt;/span&gt;
&lt;span class="c"&gt;# - No data loss&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Congratulations!
&lt;/h2&gt;

&lt;p&gt;You've built a complete, production-ready GCP infrastructure:&lt;/p&gt;

&lt;p&gt;✅ &lt;strong&gt;Network:&lt;/strong&gt; VPC with 5 subnets, Cloud NAT, firewall rules&lt;br&gt;
✅ &lt;strong&gt;Security:&lt;/strong&gt; Secret Manager, bastion with IAP, OS Login&lt;br&gt;
✅ &lt;strong&gt;Data:&lt;/strong&gt; Cloud SQL PostgreSQL with regional HA&lt;br&gt;
✅ &lt;strong&gt;Compute:&lt;/strong&gt; Managed Instance Group with autoscaling&lt;br&gt;
✅ &lt;strong&gt;Cache:&lt;/strong&gt; Redis + PgBouncer&lt;br&gt;
✅ &lt;strong&gt;Observability:&lt;/strong&gt; Prometheus, Loki, Grafana&lt;br&gt;
✅ &lt;strong&gt;Access:&lt;/strong&gt; External Application Load Balancer&lt;/p&gt;

&lt;p&gt;Your infrastructure is ready for production deployment!&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/docs" rel="noopener noreferrer"&gt;GCP Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://prometheus.io/docs" rel="noopener noreferrer"&gt;Prometheus Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs" rel="noopener noreferrer"&gt;Grafana Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/docs/loki/latest" rel="noopener noreferrer"&gt;Loki Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/load-balancing/docs" rel="noopener noreferrer"&gt;Load Balancer Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Series Complete!&lt;/strong&gt; You now have a fully functional, production-ready GCP infrastructure. From here, you can deploy your NestJS application and scale as needed.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>googlecloud</category>
      <category>monitoring</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Build Production-Ready GCP Infrastructure from Scratch Part 03</title>
      <dc:creator>Shaikh Al Amin</dc:creator>
      <pubDate>Wed, 04 Feb 2026 12:49:25 +0000</pubDate>
      <link>https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-03-34ld</link>
      <guid>https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-03-34ld</guid>
      <description>&lt;h1&gt;
  
  
  Build Production-Ready GCP Infrastructure from Scratch: A Complete Console Guide
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;A 4-Part Series for Complete Beginners&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-01-3c2n"&gt;Part 1: Foundation - Project Setup, VPC &amp;amp; Networking&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-02-542o"&gt;Part 2: Security Services - Secrets, Bastion &amp;amp; IAM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-03-34ld"&gt;Part 3: Database &amp;amp; Compute Resources&lt;/a&gt; ← &lt;strong&gt;You are here&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-04-43i8"&gt;Part 4: Observability &amp;amp; Load Balancer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Part 3: Database &amp;amp; Compute Resources
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;In this part, you'll build the data and compute layers of your infrastructure. We'll create a Cloud SQL PostgreSQL instance with high availability, set up a Managed Instance Group (MIG) for backend VMs, and deploy a cache VM with Redis and PgBouncer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll build:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Private Service Connection for Cloud SQL private IP&lt;/li&gt;
&lt;li&gt;Cloud SQL PostgreSQL with regional HA&lt;/li&gt;
&lt;li&gt;Managed Instance Group (MIG) with 2-4 backend VMs&lt;/li&gt;
&lt;li&gt;Cache VM with Redis and PgBouncer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Estimated time:&lt;/strong&gt; 60-90 minutes (includes Cloud SQL provisioning time)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Estimated cost:&lt;/strong&gt; ~$184/month&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cumulative cost:&lt;/strong&gt; ~$237/month&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before continuing, ensure you've completed &lt;strong&gt;Parts 1 &amp;amp; 2&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] VPC and 5 subnets exist (including &lt;code&gt;private-data&lt;/code&gt; subnet)&lt;/li&gt;
&lt;li&gt;[ ] Cloud NAT gateway is running&lt;/li&gt;
&lt;li&gt;[ ] Service accounts created (including &lt;code&gt;backend-dev-sa&lt;/code&gt;, &lt;code&gt;cache-dev-sa&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] Firewall rules created (health check, backend-to-cache)&lt;/li&gt;
&lt;li&gt;[ ] Bastion host is running&lt;/li&gt;
&lt;li&gt;[ ] Secrets created in Secret Manager&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you missed Parts 1-2:&lt;/strong&gt; &lt;a href="//01-foundation-project-vpc-networking.md"&gt;Start with Part 1: Foundation →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 1: Setup Private Service Connection
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is Private Service Connection?
&lt;/h3&gt;

&lt;p&gt;Cloud SQL requires a private IP address for secure access. Private Service Connection allocates a CIDR range (10.100.0.0/16) that Google-managed services (like Cloud SQL) use for private IPs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You allocate an IP range (10.100.0.0/16)&lt;/li&gt;
&lt;li&gt;Google creates a VPC peering connection&lt;/li&gt;
&lt;li&gt;Cloud SQL gets a private IP from this range&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why private IP:&lt;/strong&gt; More secure than public IP. No exposure to internet. Lower latency.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Navigation Path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;VPC networks&lt;/strong&gt; → &lt;strong&gt;Private service connection&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Set up private service connection"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-31-psc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-31-psc.png" alt="Screenshot: Private Service Connection" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Allocation Settings
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Allocate an IP range:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;google-managed-services-dev-network&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Descriptive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP range&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.100.0.0/16&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Standard range for PSA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Purpose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;VPC peering&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Required for Cloud SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-32-psa-ip.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-32-psa-ip.png" alt="Screenshot: PSA IP Range" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why 10.100.0.0/16:&lt;/strong&gt; This range doesn't overlap with our VPC (10.0.0.0/16). Google-managed services will allocate IPs from this range.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Create Connection
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;"Connect"&lt;/strong&gt; and wait 1-2 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify Connection
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Status:&lt;/strong&gt; Connected&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP range:&lt;/strong&gt; 10.100.0.0/16&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Peering:&lt;/strong&gt; &lt;code&gt;servicenetworking-googleapis-com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 2: Create Cloud SQL PostgreSQL Instance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is Cloud SQL?
&lt;/h3&gt;

&lt;p&gt;Cloud SQL is Google's managed PostgreSQL service:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatic backups&lt;/li&gt;
&lt;li&gt;High availability (regional)&lt;/li&gt;
&lt;li&gt;Point-in-time recovery&lt;/li&gt;
&lt;li&gt;Automatic patching&lt;/li&gt;
&lt;li&gt;99.99% SLA (regional)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cost Alert:&lt;/strong&gt; Cloud SQL is the most expensive component (~$115/month for regional HA). Consider db-g1-small (~$45/month) for non-critical workloads.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Navigation Path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;SQL&lt;/strong&gt; → &lt;strong&gt;SQL&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create Instance"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Choose PostgreSQL"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-33-cloudsql-create.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-33-cloudsql-create.png" alt="Screenshot: Create Cloud SQL" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Instance Settings
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Basic Information
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Instance ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;app-dev-db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Must be globally unique&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Password&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(Generate strong password)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Save this password!&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PostgreSQL version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;PostgreSQL 16&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Latest stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same as VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Password Generation:&lt;/strong&gt; Use a password manager or generate with: &lt;code&gt;openssl rand -base64 32&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Zone selection:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Zone&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Automatically select high availability zone&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Let GCP choose for HA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why automatic zone:&lt;/strong&gt; GCP selects the zone with best availability. Regional HA will create a standby in another zone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Database Configuration
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Machine Type
&lt;/h4&gt;

&lt;p&gt;Click &lt;strong&gt;"Change"&lt;/strong&gt; to customize:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Instance type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;db-n1-standard-2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 vCPU, 8GB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Availability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Regional (High availability)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Recommended for production&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-34-cloudsql-machine.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-34-cloudsql-machine.png" alt="Screenshot: Cloud SQL Machine Type" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cost Impact:&lt;/strong&gt; Regional HA adds ~$70/month over single zone. Critical for production (99.99% vs 99.95% SLA).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Storage
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SSD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fast I/O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage capacity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;100 GB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sufficient for most apps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Automatic storage increases&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Prevent disk full issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage autoincrease limit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1000 GB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maximum auto-increase&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Connectivity
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Private IP
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Private IP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Critical - no public IP&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Our VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP allocation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Use automatically allocated IP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Easier than manual&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-35-cloudsql-private.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-35-cloudsql-private.png" alt="Screenshot: Cloud SQL Private IP" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Associated allocation:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Allocation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;google-managed-services-dev-network&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;CRITICAL - No Public IP:&lt;/strong&gt; Do NOT enable public IP. Private IP with PSA is more secure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Ensure:&lt;/strong&gt; Public IP checkbox is &lt;strong&gt;unchecked&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Automatic Backups
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Automated backups&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Required for production&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3:00 AM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Low-traffic window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Retention&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;7 days&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Standard retention&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Point-in-Time Recovery
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Point-in-time recovery&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Allows restore to any point in time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why PITR:&lt;/strong&gt; If you accidentally delete data, you can restore to any second within the retention period.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Deletion Protection
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deletion protection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;CRITICAL - prevents accidental deletion&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why Deletion Protection:&lt;/strong&gt; Prevents accidental data loss. You must disable this before deleting the instance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  IAM Authentication
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IAM authentication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Passwordless auth via IAM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why IAM Auth:&lt;/strong&gt; Backend VMs can connect using service account (no passwords in secrets).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Force SSL
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Force SSL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Encrypt database connections&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; SSL is optional for private IP (traffic stays within Google's network), but recommended for defense in depth.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Customize Your Instance
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Database Flags
&lt;/h4&gt;

&lt;p&gt;Click &lt;strong&gt;"Add flag"&lt;/strong&gt; to add performance tuning flags:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flag 1: Max Connections&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flag name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;max_connections&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Value&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;100&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Flag 2: Shared Buffers&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flag name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;shared_buffers&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Value&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;256MB&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Flag 3: Effective Cache Size&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flag name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;effective_cache_size&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Value&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1GB&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Flag 4: IAM Authentication&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flag name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cloudsql.iam_authentication&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Value&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;on&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-36-cloudsql-flags.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-36-cloudsql-flags.png" alt="Screenshot: Cloud SQL Flags" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Maintenance
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Preferred window&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Sunday 3:00 AM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Low-traffic window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maintenance channel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Preview channel&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get new features early (optional)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Create the Instance
&lt;/h3&gt;

&lt;p&gt;Review all settings and click &lt;strong&gt;"Create instance"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provisioning Time:&lt;/strong&gt; 10-15 minutes&lt;/p&gt;

&lt;p&gt;You can monitor progress in the SQL instances list.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify Cloud SQL Creation
&lt;/h3&gt;

&lt;p&gt;Once ready, you should see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Instance ID:&lt;/strong&gt; app-dev-db&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status:&lt;/strong&gt; (Checkmark) - Runnable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Region:&lt;/strong&gt; europe-west1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP addresses:&lt;/strong&gt; Private IP assigned (e.g., 10.100.0.2)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-37-cloudsql-ready.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-37-cloudsql-ready.png" alt="Screenshot: Cloud SQL Ready" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Copy the private IP&lt;/strong&gt; - we'll need it for the secret update in Step 5.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Create Database
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a Database?
&lt;/h3&gt;

&lt;p&gt;A Cloud SQL instance can host multiple databases. We'll create one for our application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Navigation Path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Click on &lt;strong&gt;&lt;code&gt;app-dev-db&lt;/code&gt;&lt;/strong&gt; instance&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;"Databases"&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create database"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Database Configuration
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Database name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;appdb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Our application database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Charset&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(Default)&lt;/td&gt;
&lt;td&gt;UTF-8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Collation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(Default)&lt;/td&gt;
&lt;td&gt;Default collation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-38-create-db.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-38-create-db.png" alt="Screenshot: Create Database" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify Database
&lt;/h3&gt;

&lt;p&gt;You should see &lt;code&gt;appdb&lt;/code&gt; in the databases list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Database name:&lt;/strong&gt; appdb&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size:&lt;/strong&gt; ~8MB (empty database)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 4: Create IAM Database User
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is IAM Authentication?
&lt;/h3&gt;

&lt;p&gt;IAM authentication allows VMs to connect to Cloud SQL using their service account (no passwords needed).&lt;/p&gt;

&lt;h3&gt;
  
  
  Navigation Path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Still on &lt;strong&gt;&lt;code&gt;app-dev-db&lt;/code&gt;&lt;/strong&gt; instance page&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;"Users"&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Add user account"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  User Configuration
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IAM service account&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Use IAM, not password&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service account&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend-dev-sa@PROJECT_ID.iam.gserviceaccount.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Our backend SA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-39-iam-user.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-39-iam-user.png" alt="Screenshot: IAM User" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Add"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify IAM User
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Username:&lt;/strong&gt; backend-dev-sa@PROJECT_ID&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; IAM service account&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why IAM User:&lt;/strong&gt; Backend VMs can now connect without storing passwords in secrets. They use their service account identity.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 5: Update Secret with Cloud SQL Connection Details
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Update the Secret?
&lt;/h3&gt;

&lt;p&gt;Now that Cloud SQL has a private IP, we need to update the &lt;code&gt;db-credentials-dev&lt;/code&gt; secret with the correct connection details.&lt;/p&gt;

&lt;h3&gt;
  
  
  Navigation Path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Security&lt;/strong&gt; → &lt;strong&gt;Secret Manager&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click on &lt;strong&gt;&lt;code&gt;db-credentials-dev&lt;/code&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create new version"&lt;/strong&gt; at the top&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Update Secret Value
&lt;/h3&gt;

&lt;p&gt;Replace the secret value with the updated JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"backend-dev-sa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"use-iam-authentication"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.100.0.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"database"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"appdb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5432"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Replace &lt;code&gt;host&lt;/code&gt; value:&lt;/strong&gt; Use the private IP you copied from Cloud SQL (Step 2).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note on credentials:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;username&lt;/code&gt;: Uses IAM service account name&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;password&lt;/code&gt;: Not needed with IAM auth (placeholder)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;host&lt;/code&gt;: Cloud SQL private IP from PSA range&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click &lt;strong&gt;"Create new version"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify Secret Update
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Version 1:&lt;/strong&gt; Original (with placeholder host)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Version 2:&lt;/strong&gt; Updated (with Cloud SQL private IP)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 6: Create Backend Instance Template
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is an Instance Template?
&lt;/h3&gt;

&lt;p&gt;An instance template defines VM configuration for a Managed Instance Group (MIG). It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Machine type&lt;/li&gt;
&lt;li&gt;Boot disk image&lt;/li&gt;
&lt;li&gt;Startup script&lt;/li&gt;
&lt;li&gt;Service account&lt;/li&gt;
&lt;li&gt;Network settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why Templates:&lt;/strong&gt; Ensure all VMs in the MIG have identical configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Navigation Path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Compute Engine&lt;/strong&gt; → &lt;strong&gt;Instance templates&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create instance template"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Template Configuration
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Basic Settings
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-backend-template&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Descriptive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Machine type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;e2-medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 vCPU, 4GB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Boot disk type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pd-balanced&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Balance cost/performance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Boot disk size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;20 GB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sufficient for app&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-40-template.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-40-template.png" alt="Screenshot: Instance Template" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Boot Disk
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;"Change"&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ubuntu&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ubuntu 22.04 LTS Minimal&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disk type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pd-balanced&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;20 GB&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Network Interface
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Our VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Subnetwork&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;private-backend&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Private subnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network interface type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IPv4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IPv4 only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;External IPv4 address&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;None&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;No public IP!&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;CRITICAL - No Public IP:&lt;/strong&gt; Backend VMs must NOT have public IP. They access internet via Cloud NAT.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-41-template-network.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-41-template-network.png" alt="Screenshot: Template Network" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Service Account
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service account&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend-dev-sa&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Metadata - Startup Script
&lt;/h3&gt;

&lt;p&gt;This script runs automatically when the VM starts. It:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Installs dependencies (nginx, Node.js)&lt;/li&gt;
&lt;li&gt;Clones your application&lt;/li&gt;
&lt;li&gt;Fetches secrets from Secret Manager&lt;/li&gt;
&lt;li&gt;Starts the application with PM2&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  Add Metadata Item
&lt;/h4&gt;

&lt;p&gt;Expand &lt;strong&gt;"Metadata"&lt;/strong&gt; section:&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Add item"&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;startup-script&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;(See script below)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Startup Script:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Backend VM Startup Script&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;  &lt;span class="c"&gt;# Exit on error&lt;/span&gt;

&lt;span class="c"&gt;# Logging&lt;/span&gt;
&lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nb"&gt;tee&lt;/span&gt; /var/log/startup-script.log&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;exec &lt;/span&gt;2&amp;gt;&amp;amp;1

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== Backend Startup Script Begin &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; ==="&lt;/span&gt;

&lt;span class="c"&gt;# Install dependencies&lt;/span&gt;
apt-get update
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nginx git curl wget

&lt;span class="c"&gt;# Install Node Exporter for metrics&lt;/span&gt;
&lt;span class="nv"&gt;NODE_EXPORTER_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1.6.1"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Installing Node Exporter &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_EXPORTER_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;

useradd &lt;span class="nt"&gt;--no-create-home&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; /bin/false node_exporter &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true

&lt;/span&gt;wget &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/prometheus/node_exporter/releases/download/v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_EXPORTER_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/node_exporter-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_EXPORTER_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.linux-amd64.tar.gz"&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; /tmp/node_exporter.tar.gz
&lt;span class="nb"&gt;tar &lt;/span&gt;xzf /tmp/node_exporter.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /tmp
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/node_exporter-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_EXPORTER_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.linux-amd64/node_exporter /usr/local/bin/
&lt;span class="nb"&gt;chown &lt;/span&gt;node_exporter:node_exporter /usr/local/bin/node_exporter
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/local/bin/node_exporter
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /tmp/node_exporter&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="c"&gt;# Create systemd service for Node Exporter&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/systemd/system/node_exporter.service &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Unit]
Description=Prometheus Node Exporter
After=network.target

[Service]
Type=simple
User=node_exporter
ExecStart=/usr/local/bin/node_exporter --web.listen-address=:9100
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemctl daemon-reload
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;node_exporter
systemctl start node_exporter

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Node Exporter running on port 9100"&lt;/span&gt;

&lt;span class="c"&gt;# Install nvm and Node.js&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.nvm"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;curl &lt;span class="nt"&gt;-o-&lt;/span&gt; https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;NVM_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.nvm"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NVM_DIR&lt;/span&gt;&lt;span class="s2"&gt;/nvm.sh"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NVM_DIR&lt;/span&gt;&lt;span class="s2"&gt;/nvm.sh"&lt;/span&gt;

nvm &lt;span class="nb"&gt;install &lt;/span&gt;20
nvm use 20
nvm &lt;span class="nb"&gt;alias &lt;/span&gt;default 20

&lt;span class="c"&gt;# Clone application (replace with your repo)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"/opt/app"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Cloning application..."&lt;/span&gt;
  git clone https://github.com/your-org/your-repo.git /opt/app
  &lt;span class="nb"&gt;cd&lt;/span&gt; /opt/app
  npm &lt;span class="nb"&gt;install
&lt;/span&gt;&lt;span class="k"&gt;else
  &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/app
  git pull
  npm &lt;span class="nb"&gt;install
&lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Fetch secrets from Secret Manager&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Fetching secrets..."&lt;/span&gt;
&lt;span class="nv"&gt;DB_CREDENTIALS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gcloud secrets versions access latest &lt;span class="nt"&gt;--secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"db-credentials-dev"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gcloud secrets versions access latest &lt;span class="nt"&gt;--secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"api-key-dev"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Create .env file&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /opt/app/.env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
DATABASE_URL=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DB_CREDENTIALS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
API_KEY=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;API_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;
PORT=3000
NODE_ENV=production
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Configure nginx as reverse proxy&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/nginx/sites-available/default &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
server {
  listen 80;
  server_name _;

  location / {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade &lt;/span&gt;&lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="sh"&gt;;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host &lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="sh"&gt;;
    proxy_set_header X-Real-IP &lt;/span&gt;&lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="sh"&gt;;
    proxy_set_header X-Forwarded-For &lt;/span&gt;&lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="sh"&gt;;
    proxy_set_header X-Forwarded-Proto &lt;/span&gt;&lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="sh"&gt;;
    proxy_cache_bypass &lt;/span&gt;&lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="sh"&gt;;
  }
}
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemctl restart nginx

&lt;span class="c"&gt;# Install and start PM2&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; pm2
&lt;span class="nb"&gt;cd&lt;/span&gt; /opt/app
pm2 delete nestjs-app 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;pm2 start npm &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"nestjs-app"&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; start
pm2 save
pm2 startup systemd

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== Startup Script Complete &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; ==="&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Application running on port 3000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-42-startup-script.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-42-startup-script.png" alt="Screenshot: Startup Script" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Modify the script:&lt;/strong&gt; Replace &lt;code&gt;git clone&lt;/code&gt; URL with your actual repository.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Management - Automation
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enable autohealing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(Configure in MIG)&lt;/td&gt;
&lt;td&gt;Health check based&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Create the Template
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Creation Time:&lt;/strong&gt; 1-2 minutes&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Create Managed Instance Group (MIG)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a MIG?
&lt;/h3&gt;

&lt;p&gt;A Managed Instance Group (MIG):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ensures a specified number of VMs are running&lt;/li&gt;
&lt;li&gt;Auto-heals unhealthy VMs&lt;/li&gt;
&lt;li&gt;Auto-scales based on CPU/load&lt;/li&gt;
&lt;li&gt;Performs rolling updates&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Navigation Path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Compute Engine&lt;/strong&gt; → &lt;strong&gt;Instance groups&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create instance group"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Group Configuration
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Group Type
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Group type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Regional MIG&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;High availability across zones&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-backend-mig&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Descriptive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same as VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why Regional MIG:&lt;/strong&gt; Spreads VMs across multiple zones for HA. If one zone fails, VMs in other zones continue serving traffic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Instance Template
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Instance template&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-backend-template&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Group Size
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Group size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Minimum for HA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-43-mig-create.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-43-mig-create.png" alt="Screenshot: MIG Creation" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Autoscaling
&lt;/h3&gt;

&lt;p&gt;Expand &lt;strong&gt;"Autoscaling policy"&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Autoscaling mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;On&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enable autoscaling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Minimum instances&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Always have 2 VMs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maximum instances&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scale up to 4 VMs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Autoscaling metric&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CPU utilization&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scale based on CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Target CPU utilization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;70%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Scale out when CPU &amp;gt; 70%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cooldown period&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;300&lt;/code&gt; seconds&lt;/td&gt;
&lt;td&gt;Wait 5 min between scaling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-44-mig-autoscaling.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-44-mig-autoscaling.png" alt="Screenshot: MIG Autoscaling" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Autohealing
&lt;/h3&gt;

&lt;p&gt;Expand &lt;strong&gt;"Autohealing policy"&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Health check&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Create health check&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Click link to create&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Health Check Configuration
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-backend-http-health-check&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Descriptive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Protocol&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;HTTP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTTP health check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Port&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NestJS app port&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Request path&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Your app must implement this&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Check interval&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;30&lt;/code&gt; seconds&lt;/td&gt;
&lt;td&gt;How often to check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Timeout&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;10&lt;/code&gt; seconds&lt;/td&gt;
&lt;td&gt;Response timeout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Healthy threshold&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1&lt;/code&gt; consecutive success&lt;/td&gt;
&lt;td&gt;Mark healthy after 1 success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unhealthy threshold&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;3&lt;/code&gt; consecutive failures&lt;/td&gt;
&lt;td&gt;Mark unhealthy after 3 failures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-45-health-check.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-45-health-check.png" alt="Screenshot: Health Check" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;CRITICAL - /health endpoint:&lt;/strong&gt; Your NestJS application MUST implement a &lt;code&gt;/health&lt;/code&gt; endpoint that returns HTTP 200. Without this, health checks will fail and VMs will be constantly recreated.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Back to MIG Autohealing:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Initial delay&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;300&lt;/code&gt; seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why 5 min delay:&lt;/strong&gt; The startup script takes time (nvm install, npm install can take 3-5 minutes). If health checks start too early, VMs will be marked unhealthy and recreated (infinite loop).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Create the MIG
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VM Creation Time:&lt;/strong&gt; 5-10 minutes per VM&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify MIG Creation
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Group name:&lt;/strong&gt; dev-backend-mig&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status:&lt;/strong&gt; Running&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instances:&lt;/strong&gt; 2/2 (healthy)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autoscaling:&lt;/strong&gt; Enabled (2-4 VMs)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click on the MIG to see individual instances:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dev-backend-xxxxx&lt;/code&gt; (europe-west1-b)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dev-backend-yyyyy&lt;/code&gt; (europe-west1-c)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Zones:&lt;/strong&gt; Regional MIG spreads VMs across zones for HA.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 8: Create Cache VM (Redis + PgBouncer)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the Cache VM?
&lt;/h3&gt;

&lt;p&gt;A single VM running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis:&lt;/strong&gt; In-memory cache for session data, query results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PgBouncer:&lt;/strong&gt; Connection pooler for Cloud SQL (reduces connections)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why combine:&lt;/strong&gt; Cost optimization. A single VM handles both services (~$23/month vs ~$46/month for separate VMs).&lt;/p&gt;

&lt;h3&gt;
  
  
  Navigation Path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Compute Engine&lt;/strong&gt; → &lt;strong&gt;VM instances&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create instance"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  VM Configuration
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Basic Settings
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-cache-vm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Descriptive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same as VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Zone&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1-b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zone b&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Machine Type
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Machine type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;e2-medium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 vCPU, 4GB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Boot Disk
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ubuntu 22.04 LTS Minimal&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disk type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pd-balanced&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;20 GB&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Network Interface
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Our VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Subnetwork&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;private-cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Cache subnet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;External IPv4 address&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;None&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No public IP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Service Account
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service account&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cache-dev-sa&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Metadata - Startup Scripts
&lt;/h3&gt;

&lt;p&gt;We'll add 3 metadata items for Redis, PgBouncer, and observability agents.&lt;/p&gt;

&lt;h4&gt;
  
  
  Metadata Item 1: Redis Startup Script
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Key:&lt;/strong&gt; &lt;code&gt;redis-startup-script&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Value:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Redis Startup Script&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Configuring Redis..."&lt;/span&gt;

&lt;span class="c"&gt;# Install Redis&lt;/span&gt;
apt-get update
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; redis-server

&lt;span class="c"&gt;# Configure Redis to listen on all interfaces&lt;/span&gt;
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/^bind 127.0.0.1/bind 0.0.0.0/'&lt;/span&gt; /etc/redis/redis.conf

&lt;span class="c"&gt;# Set maxmemory policy&lt;/span&gt;
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/^# maxmemory/maxmemory/'&lt;/span&gt; /etc/redis/redis.conf
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/^maxmemory .*/maxmemory 512mb/'&lt;/span&gt; /etc/redis/redis.conf

&lt;span class="c"&gt;# Set eviction policy&lt;/span&gt;
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/^# maxmemory-policy/maxmemory-policy/'&lt;/span&gt; /etc/redis/redis.conf
&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'s/^maxmemory-policy .*/maxmemory-policy allkeys-lru/'&lt;/span&gt; /etc/redis/redis.conf

&lt;span class="c"&gt;# Restart Redis&lt;/span&gt;
systemctl restart redis-server

&lt;span class="c"&gt;# Enable Redis on boot&lt;/span&gt;
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;redis-server

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Redis configured and running on port 6379"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Metadata Item 2: PgBouncer Startup Script
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Key:&lt;/strong&gt; &lt;code&gt;pgbouncer-startup-script&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Value:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# PgBouncer Startup Script&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="c"&gt;# Replace with your Cloud SQL private IP&lt;/span&gt;
&lt;span class="nv"&gt;CLOUD_SQL_IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"10.100.0.2"&lt;/span&gt;  &lt;span class="c"&gt;# From Step 2&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Configuring PgBouncer for Cloud SQL at &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CLOUD_SQL_IP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;

&lt;span class="c"&gt;# Install PgBouncer&lt;/span&gt;
apt-get update
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; pgbouncer

&lt;span class="c"&gt;# Configure PgBouncer&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/pgbouncer/pgbouncer.ini &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
[databases]
appdb = host=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CLOUD_SQL_IP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt; port=5432 dbname=appdb

[pgbouncer]
pool_mode = transaction
max_client_conn = 100
default_pool_size = 50
reserve_pool = 5
reserve_pool_timeout = 3
listen_port = 6432
listen_addr = 0.0.0.0
auth_type = any
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Create user list (for auth_type=any)&lt;/span&gt;
&lt;span class="c"&gt;# In production, use proper authentication&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;app_admin&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;any_password&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/pgbouncer/userlist.txt

&lt;span class="c"&gt;# Restart PgBouncer&lt;/span&gt;
systemctl restart pgbouncer

&lt;span class="c"&gt;# Enable PgBouncer on boot&lt;/span&gt;
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;pgbouncer

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PgBouncer configured and running on port 6432"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Replace CLOUD_SQL_IP:&lt;/strong&gt; Use the private IP from Step 2.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Metadata Item 3: Observability Agents
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Key:&lt;/strong&gt; &lt;code&gt;observability-agents-script&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Value:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Observability Agents (Node Exporter + Promtail)&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Installing observability agents..."&lt;/span&gt;

&lt;span class="c"&gt;# Install Node Exporter&lt;/span&gt;
&lt;span class="nv"&gt;NODE_EXPORTER_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"1.6.1"&lt;/span&gt;

useradd &lt;span class="nt"&gt;--no-create-home&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; /bin/false node_exporter &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true

&lt;/span&gt;wget &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/prometheus/node_exporter/releases/download/v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_EXPORTER_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/node_exporter-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_EXPORTER_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.linux-amd64.tar.gz"&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; /tmp/node_exporter.tar.gz
&lt;span class="nb"&gt;tar &lt;/span&gt;xzf /tmp/node_exporter.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /tmp
&lt;span class="nb"&gt;cp&lt;/span&gt; /tmp/node_exporter-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;NODE_EXPORTER_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.linux-amd64/node_exporter /usr/local/bin/
&lt;span class="nb"&gt;chown &lt;/span&gt;node_exporter:node_exporter /usr/local/bin/node_exporter
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/local/bin/node_exporter

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/systemd/system/node_exporter.service &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Unit]
Description=Prometheus Node Exporter
After=network.target

[Service]
Type=simple
User=node_exporter
ExecStart=/usr/local/bin/node_exporter --web.listen-address=:9100
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemctl daemon-reload
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;node_exporter
systemctl start node_exporter

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Node Exporter installed on port 9100"&lt;/span&gt;

&lt;span class="c"&gt;# Install Promtail ( Loki URL will be updated in Part 4)&lt;/span&gt;
&lt;span class="nv"&gt;PROMTAIL_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"2.9.0"&lt;/span&gt;
&lt;span class="nv"&gt;LOKI_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://10.0.5.11:3100"&lt;/span&gt;  &lt;span class="c"&gt;# Loki VM IP&lt;/span&gt;

useradd &lt;span class="nt"&gt;--no-create-home&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; /bin/false promtail &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true

&lt;/span&gt;wget &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/grafana/loki/releases/download/v&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROMTAIL_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/promtail-linux-amd64.zip"&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; /tmp/promtail.zip
unzip &lt;span class="nt"&gt;-o&lt;/span&gt; /tmp/promtail.zip &lt;span class="nt"&gt;-d&lt;/span&gt; /tmp
&lt;span class="nb"&gt;mv&lt;/span&gt; /tmp/promtail-linux-amd64/promtail /usr/local/bin/
&lt;span class="nb"&gt;chown &lt;/span&gt;promtail:promtail /usr/local/bin/promtail
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/local/bin/promtail

&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/promtail
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/promtail/config.yml &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
server:
  http_listen_port: 9080

positions:
  filename: /tmp/positions.yaml

clients:
  - url: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LOKI_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;/loki/api/v1/push

scrape_configs:
  - job_name: cache
    static_configs:
      - targets:
          - localhost
        labels:
          job: cache
          env: dev
          host: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;chown &lt;/span&gt;promtail:promtail /etc/promtail/config.yml

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/systemd/system/promtail.service &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Unit]
Description=Promtail Log Agent
After=network.target

[Service]
Type=simple
User=promtail
ExecStart=/usr/local/bin/promtail -config.file /etc/promtail/config.yml
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;systemctl daemon-reload
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;promtail
systemctl start promtail

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Promtail installed and shipping logs to &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LOKI_URL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create the VM
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VM Creation Time:&lt;/strong&gt; 2-3 minutes&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify Cache VM
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name:&lt;/strong&gt; dev-cache-vm&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status:&lt;/strong&gt; Running&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal IP:&lt;/strong&gt; 10.0.4.x&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zone:&lt;/strong&gt; europe-west1-b&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-46-cache-vm-ready.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-46-cache-vm-ready.png" alt="Screenshot: Cache VM Ready" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Test Redis Connectivity
&lt;/h3&gt;

&lt;p&gt;From the bastion host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# SSH to bastion&lt;/span&gt;
gcloud compute ssh dev-bastion &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt;

&lt;span class="c"&gt;# From bastion, SSH to cache VM (via internal IP)&lt;/span&gt;
ssh 10.0.4.2

&lt;span class="c"&gt;# Test Redis&lt;/span&gt;
redis-cli PING
&lt;span class="c"&gt;# Expected: PONG&lt;/span&gt;

&lt;span class="c"&gt;# Test PgBouncer&lt;/span&gt;
psql &lt;span class="nt"&gt;-h&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;-p&lt;/span&gt; 6432 &lt;span class="nt"&gt;-U&lt;/span&gt; app_admin &lt;span class="nt"&gt;-d&lt;/span&gt; appdb
&lt;span class="c"&gt;# Expected: psql (16.x) connection&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 3 Verification Checklist
&lt;/h2&gt;

&lt;p&gt;Before moving to Part 4, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Private Service Connection is active (10.100.0.0/16)&lt;/li&gt;
&lt;li&gt;[ ] Cloud SQL instance is running with private IP&lt;/li&gt;
&lt;li&gt;[ ] Database "appdb" exists&lt;/li&gt;
&lt;li&gt;[ ] IAM user created for backend-dev-sa&lt;/li&gt;
&lt;li&gt;[ ] Secret updated with Cloud SQL private IP&lt;/li&gt;
&lt;li&gt;[ ] Backend MIG has 2 running VMs (healthy)&lt;/li&gt;
&lt;li&gt;[ ] Autohealing health check is configured&lt;/li&gt;
&lt;li&gt;[ ] Cache VM is running&lt;/li&gt;
&lt;li&gt;[ ] Redis is listening on port 6379&lt;/li&gt;
&lt;li&gt;[ ] PgBouncer is listening on port 6432&lt;/li&gt;
&lt;li&gt;[ ] Firewall rule allows backend to cache (ports 6379, 6432)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-47-part3-complete.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-47-part3-complete.png" alt="Screenshot: Completed Part 3" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost Summary - Part 3
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud SQL (regional HA)&lt;/td&gt;
&lt;td&gt;~$115&lt;/td&gt;
&lt;td&gt;db-n1-standard-2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend MIG (2-4 VMs)&lt;/td&gt;
&lt;td&gt;~$46-92&lt;/td&gt;
&lt;td&gt;e2-medium, autoscaling 2-4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache VM&lt;/td&gt;
&lt;td&gt;~$23&lt;/td&gt;
&lt;td&gt;e2-medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total Part 3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$184-230&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$184/month (2 VMs)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Cumulative Cost (Part 1 + 2 + 3):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Part 1 (VPC, NAT, etc.)&lt;/td&gt;
&lt;td&gt;~$42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 2 (Bastion)&lt;/td&gt;
&lt;td&gt;~$11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 3 (Cloud SQL, MIG, Cache)&lt;/td&gt;
&lt;td&gt;~$184&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$237/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Troubleshooting - Part 3
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Issue: Private Service Connection Fails
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; "Allocation failed" error&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify 10.100.0.0/16 doesn't overlap with VPC&lt;/li&gt;
&lt;li&gt;Check VPC peering status&lt;/li&gt;
&lt;li&gt;Ensure you're in the correct project
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check PSA status&lt;/span&gt;
gcloud compute networks peerings list &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--network&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev-network
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Issue: Cloud SQL Creation Fails
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; "Instance creation failed"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify Private Service Connection is active&lt;/li&gt;
&lt;li&gt;Check subnet has Private Google Access enabled&lt;/li&gt;
&lt;li&gt;Ensure sufficient quota (SQL instances)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Issue: Backend MIG VMs Unhealthy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; 0/2 VMs healthy&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check health check configuration (/health endpoint)&lt;/li&gt;
&lt;li&gt;Increase initial delay to 300+ seconds&lt;/li&gt;
&lt;li&gt;Increase unhealthy threshold to 5&lt;/li&gt;
&lt;li&gt;Check VM serial port logs for startup script errors
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# View VM logs&lt;/span&gt;
gcloud compute instances get-serial-port-output INSTANCE_NAME &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;europe-west1-b &lt;span class="nt"&gt;--port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Issue: Cache VM Cannot Connect to Cloud SQL
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; PgBouncer connection timeout&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify Cloud SQL private IP is correct&lt;/li&gt;
&lt;li&gt;Check firewall allows cache VM to Cloud SQL&lt;/li&gt;
&lt;li&gt;Ensure Private Service Connection is active&lt;/li&gt;
&lt;li&gt;Verify PgBouncer configuration
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# From cache VM, test Cloud SQL connectivity&lt;/span&gt;
ping 10.100.0.2
telnet 10.100.0.2 5432
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What's Next - Part 4?
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;Part 4: Observability &amp;amp; Load Balancer&lt;/strong&gt;, you'll build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prometheus VM for metrics collection&lt;/li&gt;
&lt;li&gt;Loki VM for log aggregation&lt;/li&gt;
&lt;li&gt;Grafana dashboards for visualization&lt;/li&gt;
&lt;li&gt;External Application Load Balancer&lt;/li&gt;
&lt;li&gt;End-to-end testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="//04-observability-load-balancer.md"&gt;Continue to Part 4: Observability &amp;amp; Load Balancer →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/sql/docs" rel="noopener noreferrer"&gt;Cloud SQL Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/compute/docs/instance-groups" rel="noopener noreferrer"&gt;MIG Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/architecture/redis-for-gcp" rel="noopener noreferrer"&gt;Redis on GCP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pgbouncer.org/usage.html" rel="noopener noreferrer"&gt;PgBouncer Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Data and Compute Layers Complete!&lt;/strong&gt; Your Cloud SQL database, backend MIG, and cache VM are ready. Next, we'll add observability (Prometheus, Loki, Grafana) and the load balancer for external access.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>database</category>
      <category>googlecloud</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Build Production-Ready GCP Infrastructure from Scratch Part 02</title>
      <dc:creator>Shaikh Al Amin</dc:creator>
      <pubDate>Wed, 04 Feb 2026 12:45:14 +0000</pubDate>
      <link>https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-02-542o</link>
      <guid>https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-02-542o</guid>
      <description>&lt;h1&gt;
  
  
  Build Production-Ready GCP Infrastructure from Scratch: A Complete Console Guide
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;A 4-Part Series for Complete Beginners&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-01-3c2n"&gt;Part 1: Foundation - Project Setup, VPC &amp;amp; Networking&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-02-542o"&gt;Part 2: Security Services - Secrets, Bastion &amp;amp; IAM&lt;/a&gt; ← &lt;strong&gt;You are here&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-03-34ld"&gt;Part 3: Database &amp;amp; Compute Resources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-04-43i8"&gt;Part 4: Observability &amp;amp; Load Balancer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Part 2: Security Services - Secrets, Bastion &amp;amp; IAM
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;In this part, you'll build the security layer for your infrastructure. We'll create secrets for sensitive data, set up a bastion host for secure SSH access, configure IAP (Identity-Aware Proxy) for zero-trust access, and establish IAM permissions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll build:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secret Manager secrets for database credentials and API keys&lt;/li&gt;
&lt;li&gt;Bastion host with IAP tunneling&lt;/li&gt;
&lt;li&gt;OS Login for IAM-managed SSH authentication&lt;/li&gt;
&lt;li&gt;IAM roles for service account and user access&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Estimated time:&lt;/strong&gt; 30-45 minutes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Estimated cost:&lt;/strong&gt; ~$11/month (Bastion VM only)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cumulative cost:&lt;/strong&gt; ~$53/month&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before continuing, ensure you've completed &lt;strong&gt;Part 1&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] VPC &lt;code&gt;dev-network&lt;/code&gt; exists&lt;/li&gt;
&lt;li&gt;[ ] 5 subnets created (including &lt;code&gt;public-subnet&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] Cloud NAT gateway is running&lt;/li&gt;
&lt;li&gt;[ ] 4 service accounts created (including &lt;code&gt;bastion-dev-sa&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[ ] Firewall rules created&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you missed Part 1:&lt;/strong&gt; &lt;a href="//01-foundation-project-vpc-networking.md"&gt;Start with Part 1: Foundation →&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 1: Create Secret Manager Secrets
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is Secret Manager?
&lt;/h3&gt;

&lt;p&gt;Secret Manager is Google Cloud's secure storage for sensitive data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database credentials&lt;/li&gt;
&lt;li&gt;API keys&lt;/li&gt;
&lt;li&gt;Certificates&lt;/li&gt;
&lt;li&gt;Tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Secrets are encrypted at rest and accessed via IAM permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Not Hardcode Secrets?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Security Risk:&lt;/strong&gt; Hardcoding secrets in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Configuration files (committed to Git)&lt;/li&gt;
&lt;li&gt;Startup scripts (visible to anyone with VM access)&lt;/li&gt;
&lt;li&gt;Environment variables (visible in process lists)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Best Practice:&lt;/strong&gt; Store secrets in Secret Manager and fetch at runtime.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; Secret Manager is free for active secrets. Storage costs ~$0.03/GB after the free tier.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Secret 1: Database Credentials
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Navigation Path
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Security&lt;/strong&gt; → &lt;strong&gt;Secret Manager&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create Secret"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-20-secret-create.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-20-secret-create.png" alt="Screenshot: Secret Manager Create" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Secret Configuration
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Basic Settings:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;db-credentials-dev&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Environment-suffixed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Secret value&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(See JSON below)&lt;/td&gt;
&lt;td&gt;Click "Create or upload"&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Secret Value (JSON format):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"app_admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"password"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REPLACE_WITH_SECURE_PASSWORD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"host"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.100.0.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"database"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"appdb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"port"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5432"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The &lt;code&gt;host&lt;/code&gt; IP will be updated after we create Cloud SQL in Part 3. For now, use the placeholder.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Password Generation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Generate a secure password&lt;/span&gt;
openssl rand &lt;span class="nt"&gt;-base64&lt;/span&gt; 32
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Replication:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Replication&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Automatic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Replicates across regions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Rotation:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rotation period&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Leave blank&lt;/td&gt;
&lt;td&gt;No auto-rotation for now&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Version Access:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Version access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Enable secret version&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Access immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Click &lt;strong&gt;"Create secret"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secret 2: API Key
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Create API Key Secret
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;In Secret Manager, click &lt;strong&gt;"Create Secret"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api-key-dev&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Secret value&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;your-api-key-here&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Replace with actual value:&lt;/strong&gt; Use your actual API key or generate a placeholder for testing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Replication:&lt;/strong&gt; Automatic (same as above)&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Create secret"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify Secrets Created
&lt;/h3&gt;

&lt;p&gt;You should see 2 secrets in Secret Manager:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret Name&lt;/th&gt;
&lt;th&gt;Created&lt;/th&gt;
&lt;th&gt;Versions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;db-credentials-dev&lt;/td&gt;
&lt;td&gt;(timestamp)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;api-key-dev&lt;/td&gt;
&lt;td&gt;(timestamp)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-21-secret-list.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-21-secret-list.png" alt="Screenshot: Secret Manager List" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Grant Secret Access to Service Accounts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is IAM Access to Secrets?
&lt;/h3&gt;

&lt;p&gt;Service accounts need permission to access secrets. Without this, VMs cannot fetch secrets at runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grant Access to Backend SA
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Click on &lt;strong&gt;&lt;code&gt;db-credentials-dev&lt;/code&gt;&lt;/strong&gt; secret&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;"Permissions"&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Grant access"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-22-secret-iam.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-22-secret-iam.png" alt="Screenshot: Secret IAM" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add Principal:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;New principals&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend-dev-sa@PROJECT_ID.iam.gserviceaccount.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Select Role:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Secret Manager Secret Accessor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Can read secret values&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Click &lt;strong&gt;"Save"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grant Access to Cache and Observability SAs
&lt;/h3&gt;

&lt;p&gt;Repeat for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;api-key-dev&lt;/code&gt;&lt;/strong&gt; secret → Grant access to &lt;code&gt;backend-dev-sa&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;db-credentials-dev&lt;/code&gt;&lt;/strong&gt; secret → Grant access to &lt;code&gt;cache-dev-sa&lt;/code&gt; (for PgBouncer)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;api-key-dev&lt;/code&gt;&lt;/strong&gt; secret → Grant access to &lt;code&gt;observability-dev-sa&lt;/code&gt; (if needed)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Verify IAM Bindings
&lt;/h3&gt;

&lt;p&gt;For &lt;code&gt;db-credentials-dev&lt;/code&gt;, you should see:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Principal&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;backend-dev-sa@PROJECT_ID&lt;/td&gt;
&lt;td&gt;Secret Manager Secret Accessor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cache-dev-sa@PROJECT_ID&lt;/td&gt;
&lt;td&gt;Secret Manager Secret Accessor&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 3: Enable IAP (Identity-Aware Proxy)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is IAP?
&lt;/h3&gt;

&lt;p&gt;IAP provides zero-trust access to your VMs without:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public IPs&lt;/li&gt;
&lt;li&gt;VPN connections&lt;/li&gt;
&lt;li&gt;SSH keys in metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User authenticates with Google Cloud&lt;/li&gt;
&lt;li&gt;IAP establishes a secure tunnel&lt;/li&gt;
&lt;li&gt;Traffic flows through Google's network (not public internet)&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why IAP is more secure:&lt;/strong&gt; SSH traffic never traverses the public internet. Access is controlled by IAM, not SSH keys.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Navigation Path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Security&lt;/strong&gt; → &lt;strong&gt;Identity-Aware Proxy&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Go to Identity-Aware Proxy"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Tick the checkbox"&lt;/strong&gt; to enable IAP&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-23-enable-iap.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-23-enable-iap.png" alt="Screenshot: Enable IAP" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure IAP for SSH
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;"Configure SSH and TCP Resources"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Enable IAP"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;SSH and TCP forwarding:&lt;/strong&gt; ✓ Enable&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; IAP for TCP forwarding is required for SSH tunneling.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Verify IAP Enabled
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Status:&lt;/strong&gt; Enabled&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources:&lt;/strong&gt; (None yet - bastion will be added)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 4: Create IAP Firewall Rule
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the IAP Firewall Rule?
&lt;/h3&gt;

&lt;p&gt;This rule allows IAP's infrastructure to connect to your VM. IAP uses IP range &lt;code&gt;35.235.240.0/20&lt;/code&gt; for tunneling.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If using the Terraform bastion module, this rule is created automatically. For console setup, we create it manually.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Navigation Path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;VPC networks&lt;/strong&gt; → &lt;strong&gt;Firewall&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create firewall rule"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Firewall Configuration
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-firewall-allow-iap&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Descriptive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Our VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Priority&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Standard allow priority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Direction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ingress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inbound traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Action&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Allow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allow traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Target service accounts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bastion-dev-sa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Only bastion host&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-24-iap-firewall.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-24-iap-firewall.png" alt="Screenshot: IAP Firewall Rule" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Source filter:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Source IPv4 ranges&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;35.235.240.0/20&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why this range:&lt;/strong&gt; This is Google's IAP forwarding range. Without this rule, IAP cannot tunnel to your VM.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Protocols and ports:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TCP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ Checked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ports&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;22&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Logging:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Log config&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;On&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Create Bastion Host VM
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a Bastion Host?
&lt;/h3&gt;

&lt;p&gt;A bastion host is a secure entry point to your private network. It's the only VM with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public IP (optional)&lt;/li&gt;
&lt;li&gt;SSH access enabled&lt;/li&gt;
&lt;li&gt;Access to private subnets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security Model:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users SSH to bastion via IAP&lt;/li&gt;
&lt;li&gt;From bastion, SSH to backend VMs&lt;/li&gt;
&lt;li&gt;Backend VMs have no public IP&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cost Alert:&lt;/strong&gt; Bastion VM costs ~$11/month (e2-small). You can stop it when not in use to save costs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Navigation Path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Compute Engine&lt;/strong&gt; → &lt;strong&gt;VM instances&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create instance"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-25-create-vm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-25-create-vm.png" alt="Screenshot: Create VM" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Basic Settings
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-bastion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Environment-prefixed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Belgium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Zone&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1-b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Zone b&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Machine Configuration
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Machine type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;e2-small&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2 vCPU, 2GB RAM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CPU platform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Intel/AMD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Default (Automatic)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; e2-small ≈ $11/month. Sufficient for bastion (just forwards SSH).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Boot Disk
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;"Change"&lt;/strong&gt; to configure:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ubuntu&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Popular Linux distribution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Version&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ubuntu 22.04 LTS Minimal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Long-term support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Disk type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pd-balanced&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Balance cost/performance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;20 GB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sufficient for bastion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-26-boot-disk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-26-boot-disk.png" alt="Screenshot: Boot Disk" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Select"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Network Interface
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Network settings:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Our VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Subnetwork&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public-subnet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Must be public subnet&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network interface type&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;IPv4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IPv4 only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;External IPv4 address:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network Service Tiers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Premium&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Default (recommended)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;External IPv4 address&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ephemeral&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Do NOT create static IP&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why ephemeral IP:&lt;/strong&gt; Bastion uses IAP for access, so public IP is not directly accessed. Save money by using ephemeral IP.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-27-network-interface.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-27-network-interface.png" alt="Screenshot: Network Interface" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Network Tags
&lt;/h3&gt;

&lt;p&gt;Add network tag:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network tags&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bastion-host&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Network tags are used for the IP whitelist backup firewall rule (IAP is primary).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Identity and API Access
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Service account:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service account&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bastion-dev-sa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Our bastion SA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Access scopes:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Access scopes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Allow full access to all Cloud APIs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why full access:&lt;/strong&gt; Bastion needs to access Secret Manager (for credentials) and other services. In production, restrict to specific scopes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-28-service-account.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-28-service-account.png" alt="Screenshot: Service Account" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Security - OS Login
&lt;/h3&gt;

&lt;p&gt;Expand &lt;strong&gt;"Security"&lt;/strong&gt; section:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enable OS Login&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Critical for IAM-based SSH&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is OS Login:&lt;/strong&gt; OS Login allows you to manage SSH access via IAM. No SSH key distribution needed. Users log in with their Google account.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Advanced Options - Metadata
&lt;/h3&gt;

&lt;p&gt;No custom metadata needed for bastion (OS Login handles authentication).&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the VM
&lt;/h3&gt;

&lt;p&gt;Review all settings and click &lt;strong&gt;"Create"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Wait 2-3 minutes for VM creation. You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Instance 'dev-bastion' is RUNNING
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 6: Grant IAP Access to Users
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What IAM Roles Are Needed?
&lt;/h3&gt;

&lt;p&gt;Users need two roles to SSH via IAP:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;IAP-secured Tunnel User&lt;/strong&gt; - Permission to use IAP tunnel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute OS Login&lt;/strong&gt; - Permission to log in to VMs&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; These roles are granted at the project level, not per VM.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Grant IAP Tunnel Access
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;IAM &amp;amp; Admin&lt;/strong&gt; → &lt;strong&gt;IAM&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Grant access"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Add principal:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;New principals&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;your-email@example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Select role:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;IAP-secured Tunnel User&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Identity-Aware Proxy → IAP-secured Tunnel User&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-29-grant-iap-access.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-29-grant-iap-access.png" alt="Screenshot: Grant IAP Access" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Save"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grant OS Login Access
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;"Grant access"&lt;/strong&gt; again:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add principal:&lt;/strong&gt; Same email as above&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Select role:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Compute OS Login&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Compute Engine → Compute OS Login&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Click &lt;strong&gt;"Save"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify IAM Bindings
&lt;/h3&gt;

&lt;p&gt;You should see your user with these roles:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Principal&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:your-email@example.com"&gt;your-email@example.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;IAP-secured Tunnel User&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="mailto:your-email@example.com"&gt;your-email@example.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Compute OS Login&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 7: Add SSH Key for OS Login (Optional)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is OS Login SSH Key?
&lt;/h3&gt;

&lt;p&gt;OS Login can use IAM-managed SSH keys. These are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stored in Google Cloud&lt;/li&gt;
&lt;li&gt;Automatically rotated&lt;/li&gt;
&lt;li&gt;Managed via IAM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Alternative:&lt;/strong&gt; You can add your own SSH key to VM metadata.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Best Practice:&lt;/strong&gt; Use OS Login with IAM for production. No SSH key management overhead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Add SSH Key to Project (Optional Backup)
&lt;/h3&gt;

&lt;p&gt;If OS Login fails, you can use SSH keys:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Compute Engine&lt;/strong&gt; → &lt;strong&gt;Metadata&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"SSH Keys"&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Edit"&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Add item"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Generate SSH key locally:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; rsa &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/gcp_bastion &lt;span class="nt"&gt;-C&lt;/span&gt; your-email@example.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Copy public key:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.ssh/gcp_bastion.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste the public key into the SSH Keys field:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SSH key&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ssh-rsa AAAA... your-email@example.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Click &lt;strong&gt;"Save"&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Security Note:&lt;/strong&gt; Metadata SSH keys are less secure than OS Login. Use only as backup.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 2 Verification Checklist
&lt;/h2&gt;

&lt;p&gt;Before moving to Part 3, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] 2 secrets created (db-credentials-dev, api-key-dev)&lt;/li&gt;
&lt;li&gt;[ ] Backend, cache, and observability SAs have Secret Accessor role&lt;/li&gt;
&lt;li&gt;[ ] IAP is enabled for TCP forwarding&lt;/li&gt;
&lt;li&gt;[ ] IAP firewall rule created (allows 35.235.240.0/20)&lt;/li&gt;
&lt;li&gt;[ ] Bastion VM is RUNNING&lt;/li&gt;
&lt;li&gt;[ ] Bastion has bastion-dev-sa attached&lt;/li&gt;
&lt;li&gt;[ ] OS Login is enabled on bastion&lt;/li&gt;
&lt;li&gt;[ ] Your user has IAP-secured Tunnel User role&lt;/li&gt;
&lt;li&gt;[ ] Your user has Compute OS Login role&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-30-part2-complete.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-30-part2-complete.png" alt="Screenshot: Completed Part 2" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Test SSH via IAP
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Verify IAP Access
&lt;/h3&gt;

&lt;p&gt;Test SSH connection to bastion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute ssh dev-bastion &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;PROJECT_ID &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;europe-west1-b &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expected output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Welcome to Ubuntu 22.04 LTS
your-email@dev-bastion:~$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Troubleshooting:&lt;/strong&gt; If SSH fails:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify IAP is enabled&lt;/li&gt;
&lt;li&gt;Check IAP firewall rule exists&lt;/li&gt;
&lt;li&gt;Verify your user has IAP-secured Tunnel User role&lt;/li&gt;
&lt;li&gt;Check OS Login is enabled on bastion&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Cost Summary - Part 2
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bastion Host (e2-small)&lt;/td&gt;
&lt;td&gt;~$11&lt;/td&gt;
&lt;td&gt;24/7 operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret Manager&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Within free tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAP&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;No additional cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total Part 2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$11&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$11/month&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Cumulative Cost (Part 1 + 2):&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Part 1 (VPC, NAT, etc.)&lt;/td&gt;
&lt;td&gt;~$42&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Part 2 (Bastion)&lt;/td&gt;
&lt;td&gt;~$11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$53/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cost Optimization:&lt;/strong&gt; Stop bastion VM when not in use to save ~$11/month. Start it only when needed for SSH access.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Troubleshooting - Part 2
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Issue: Cannot Access Secret
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; "Permission denied" when accessing secret&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check service account has Secret Manager Secret Accessor role&lt;/li&gt;
&lt;li&gt;Verify secret name matches exactly (case-sensitive)&lt;/li&gt;
&lt;li&gt;Check secret has at least 1 version
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List secret versions&lt;/span&gt;
gcloud secrets versions list db-credentials-dev

&lt;span class="c"&gt;# Access secret&lt;/span&gt;
gcloud secrets versions access latest &lt;span class="nt"&gt;--secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;db-credentials-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Issue: IAP Connection Fails
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; "IAP does not have permission" error&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify IAP is enabled&lt;/li&gt;
&lt;li&gt;Check your user has IAP-secured Tunnel User role&lt;/li&gt;
&lt;li&gt;Verify IAP firewall rule exists (allows 35.235.240.0/20)
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check IAP status&lt;/span&gt;
gcloud compute ssh dev-bastion &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Issue: OS Login Not Working
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; "OS Login is not enabled" error&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify OS Login is enabled on bastion VM&lt;/li&gt;
&lt;li&gt;Check your user has Compute OS Login role&lt;/li&gt;
&lt;li&gt;Ensure no conflicting SSH keys in metadata
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Enable OS Login on existing VM&lt;/span&gt;
gcloud compute instances add-metadata dev-bastion &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metadata&lt;/span&gt; enable-oslogin&lt;span class="o"&gt;=&lt;/span&gt;TRUE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Issue: Bastion Cannot Access Secrets
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Secret access denied from bastion&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify bastion-dev-sa has Secret Manager Secret Accessor role&lt;/li&gt;
&lt;li&gt;Check service account is attached to VM&lt;/li&gt;
&lt;li&gt;Ensure VM has access scopes for Secret Manager&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What's Next - Part 3?
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;Part 3: Database &amp;amp; Compute&lt;/strong&gt;, you'll build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Private Service Connection for Cloud SQL&lt;/li&gt;
&lt;li&gt;Cloud SQL PostgreSQL instance with HA&lt;/li&gt;
&lt;li&gt;Managed Instance Group (MIG) for backend VMs&lt;/li&gt;
&lt;li&gt;Cache VM with Redis and PgBouncer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-03-34ld"&gt;Continue to Part 3: Database &amp;amp; Compute →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/secret-manager/docs" rel="noopener noreferrer"&gt;Secret Manager Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/iap/docs" rel="noopener noreferrer"&gt;IAP Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/compute/docs/oslogin" rel="noopener noreferrer"&gt;OS Login Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/architecture/bastion-host" rel="noopener noreferrer"&gt;Bastion Host Best Practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Security Layer Complete!&lt;/strong&gt; Your secrets are securely stored, bastion host is ready for secure SSH access, and IAM permissions are configured. Next, we'll add the data and compute layers (Cloud SQL, MIG, Cache).&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>googlecloud</category>
      <category>networking</category>
      <category>vpc</category>
    </item>
    <item>
      <title>Build Production-Ready GCP Infrastructure from Scratch Part 01</title>
      <dc:creator>Shaikh Al Amin</dc:creator>
      <pubDate>Wed, 04 Feb 2026 12:42:35 +0000</pubDate>
      <link>https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-01-3c2n</link>
      <guid>https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-01-3c2n</guid>
      <description>&lt;h1&gt;
  
  
  Build Production-Ready GCP Infrastructure from Scratch: A Complete Console Guide
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;A 4-Part Series for Complete Beginners&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-01-3c2n"&gt;Part 1: Foundation - Project Setup, VPC &amp;amp; Networking&lt;/a&gt; ← &lt;strong&gt;You are here&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-02-542o"&gt;Part 2: Security Services - Secrets, Bastion &amp;amp; IAM&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-03-34ld"&gt;Part 3: Database &amp;amp; Compute Resources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-04-43i8"&gt;Part 4: Observability &amp;amp; Load Balancer&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Part 1: Foundation - Project Setup, VPC &amp;amp; Networking
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;In this first part of our series, you'll build the networking foundation for a production-ready GCP infrastructure. We'll create a custom VPC with 5 subnets, configure Cloud NAT for internet access, set up service accounts, and implement service account-targeted firewall rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you'll build:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom VPC (10.0.0.0/16) in europe-west1 region&lt;/li&gt;
&lt;li&gt;5 subnets for different application tiers&lt;/li&gt;
&lt;li&gt;Cloud NAT Gateway for outbound internet access&lt;/li&gt;
&lt;li&gt;4 Service Accounts for IAM-based security&lt;/li&gt;
&lt;li&gt;Firewall rules with service account targeting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Estimated time:&lt;/strong&gt; 45-60 minutes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Estimated cost:&lt;/strong&gt; ~$32/month (Cloud NAT only)&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we begin, ensure you have:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;GCP Account&lt;/strong&gt; - Sign up at &lt;a href="https://console.cloud.google.com" rel="noopener noreferrer"&gt;console.cloud.google.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Billing Account&lt;/strong&gt; - Required for creating resources&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Free Tier Credit&lt;/strong&gt; - New accounts get $300 credit for 90 days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Required Permissions&lt;/strong&gt; - Project Owner or Editor role&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cost Alert:&lt;/strong&gt; This tutorial will incur charges. With the free tier credit, you can follow along for free. After that, expect ~$350/month total for all infrastructure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Why GCP for Your Infrastructure?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EU Data Residency&lt;/strong&gt; - Host in europe-west1 (Belgium) for compliance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private Network&lt;/strong&gt; - No public IPs needed for database and application servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed Services&lt;/strong&gt; - Focus on code, not infrastructure management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in Security&lt;/strong&gt; - IAM-based access control, VPC Service Controls&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: Create or Select GCP Project
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Navigation Path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://console.cloud.google.com" rel="noopener noreferrer"&gt;console.cloud.google.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click the project dropdown at the top (next to "Google Cloud Platform")&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"New Project"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-01-console-homepage.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-01-console-homepage.png" alt="Screenshot: GCP Console Homepage" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Project Configuration
&lt;/h3&gt;

&lt;p&gt;Fill in the form:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Project name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;My GCP Infrastructure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Display name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Project ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;my-gcp-project-id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Must be globally unique&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Organization&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(Your organization)&lt;/td&gt;
&lt;td&gt;Leave blank for personal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Location&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;No organization&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;For personal projects&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; Project IDs cannot be changed after creation. Choose carefully and avoid spaces.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-02-create-project.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-02-create-project.png" alt="Screenshot: Create Project Form" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt; and wait 1-2 minutes for project creation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify Project Creation
&lt;/h3&gt;

&lt;p&gt;You should see a notification: "Project 'My GCP Infrastructure' is ready."&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Select project"&lt;/strong&gt; to switch to your new project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enable Required APIs
&lt;/h3&gt;

&lt;p&gt;Some resources require specific APIs to be enabled:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;APIs &amp;amp; Services&lt;/strong&gt; → &lt;strong&gt;Library&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Search for and enable these APIs:

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Compute Engine API&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloud SQL API&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secret Manager API&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Identity-Aware Proxy API&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-03-enable-apis.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-03-enable-apis.png" alt="Screenshot: Enable APIs" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; Enabling APIs early prevents "API not enabled" errors during resource creation.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 2: Create Custom VPC (10.0.0.0/16)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a VPC?
&lt;/h3&gt;

&lt;p&gt;A Virtual Private Cloud (VPC) is your isolated network in Google Cloud. Think of it as your own data center network in the cloud.&lt;/p&gt;

&lt;h3&gt;
  
  
  Navigation Path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;From the main menu (hamburger icon ≡), navigate to:
&lt;strong&gt;Networking&lt;/strong&gt; → &lt;strong&gt;VPC networks&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-04-vpc-navigation.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-04-vpc-navigation.png" alt="Screenshot: VPC Networks Navigation" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;"Create VPC network"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  VPC Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-05-vpc-creation.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-05-vpc-creation.png" alt="Screenshot: VPC Creation Form" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fill in the form:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Environment-prefixed for clarity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Development environment VPC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional but recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom VPC creation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;CRITICAL:&lt;/strong&gt; Uncheck "Automatic subnet creation"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Routing mode&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Regional&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Recommended for most cases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MTU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1460&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Default (leave unchanged)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  VPC Flow Logs Configuration
&lt;/h3&gt;

&lt;p&gt;Scroll down to &lt;strong&gt;"VPC flow logs"&lt;/strong&gt; section:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Off/On&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;On&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enable for security monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sampling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;50%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Balance cost vs visibility&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Aggregation interval&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5 sec&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Most granular option&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Metadata&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Include all&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full logging for forensics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cost Alert:&lt;/strong&gt; VPC Flow Logs cost ~$0.005/GB. With 50% sampling, expect ~$5-10/month for moderate traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Flow Logs:&lt;/strong&gt; Essential for troubleshooting network issues and security audits. They show all traffic flows through your VPC.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Dynamic Routing Mode
&lt;/h3&gt;

&lt;p&gt;Leave as &lt;strong&gt;"Regional"&lt;/strong&gt; (default).&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the VPC
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt; at the bottom.&lt;/p&gt;

&lt;p&gt;Wait 30-60 seconds for creation. You should see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VPC network "dev-network" was created successfully.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Verify VPC Creation
&lt;/h3&gt;

&lt;p&gt;You should see &lt;code&gt;dev-network&lt;/code&gt; in your VPC networks list with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Subnets:&lt;/strong&gt; 0 (we'll add these next)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Routing mode:&lt;/strong&gt; Regional&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flow logs:&lt;/strong&gt; On&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 3: Create 5 Subnets (Sequential /24)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What are Subnets?
&lt;/h3&gt;

&lt;p&gt;Subnets divide your VPC into smaller networks. Each subnet hosts a specific tier of your application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Public subnet (load balancer, bastion)&lt;/li&gt;
&lt;li&gt;Private subnets (backend, database, cache, observability)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why europe-west1?
&lt;/h3&gt;

&lt;p&gt;We're using &lt;strong&gt;europe-west1 (Belgium)&lt;/strong&gt; for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EU Data Residency&lt;/strong&gt; - Data stays within Europe&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low Latency&lt;/strong&gt; - Good for European users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt; - Competitive pricing compared to other regions&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; For production, consider multi-region deployment for disaster recovery.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Subnet Overview
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Subnet Name&lt;/th&gt;
&lt;th&gt;CIDR&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;public-subnet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10.0.1.0/24&lt;/td&gt;
&lt;td&gt;Bastion, Load Balancer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;private-backend&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10.0.2.0/24&lt;/td&gt;
&lt;td&gt;Backend VMs (NestJS app)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;private-data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10.0.3.0/24&lt;/td&gt;
&lt;td&gt;Cloud SQL PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;private-cache&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10.0.4.0/24&lt;/td&gt;
&lt;td&gt;Redis + PgBouncer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;private-obs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10.0.5.0/24&lt;/td&gt;
&lt;td&gt;Prometheus, Loki, Grafana&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why /24:&lt;/strong&gt; Provides 251 usable IP addresses per subnet (sufficient for most applications).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Create Each Subnet
&lt;/h3&gt;

&lt;p&gt;We'll create all 5 subnets. The process is identical for each, just with different values.&lt;/p&gt;

&lt;h4&gt;
  
  
  Subnet 1: public-subnet (10.0.1.0/24)
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Click on &lt;strong&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/strong&gt; in your VPC list&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;"SUBNETS"&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create subnet"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-06-create-subnet.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-06-create-subnet.png" alt="Screenshot: Create Subnet" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fill in the form:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;public-subnet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Descriptive name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Public subnet for bastion and load balancer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Belgium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP address range&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.0.1.0/24&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Manual CIDR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Private Google Access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;CRITICAL&lt;/strong&gt; - see below&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;CRITICAL - Private Google Access:&lt;/strong&gt; This allows VMs without public IPs to reach Google services (Cloud SQL, Secret Manager, etc.). Without this, private VMs cannot access GCP services!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Flow Logs for Subnet 1
&lt;/h4&gt;

&lt;p&gt;Scroll down to &lt;strong&gt;"Flow logs"&lt;/strong&gt; section:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Off/On&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;On&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Same as VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sampling&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;50%&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Consistent with VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Aggregation interval&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5 sec&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Consistent with VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Metadata&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Include all&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Consistent with VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Click &lt;strong&gt;"Add"&lt;/strong&gt; to create the subnet.&lt;/p&gt;

&lt;h4&gt;
  
  
  Subnet 2: private-backend (10.0.2.0/24)
&lt;/h4&gt;

&lt;p&gt;Repeat the process with these values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;private-backend&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP address range&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.0.2.0/24&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Private Google Access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flow logs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;On&lt;/strong&gt; (same settings)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Subnet 3: private-data (10.0.3.0/24)
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;private-data&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP address range&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.0.3.0/24&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Private Google Access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flow logs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;On&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why separate subnet:&lt;/strong&gt; Isolates database layer for security. Database VMs have different firewall rules than backend VMs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Subnet 4: private-cache (10.0.4.0/24)
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;private-cache&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP address range&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.0.4.0/24&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Private Google Access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flow logs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;On&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Subnet 5: private-obs (10.0.5.0/24)
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;private-obs&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IP address range&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10.0.5.0/24&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Private Google Access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ &lt;strong&gt;Enable&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Flow logs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;On&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Verify All Subnets
&lt;/h3&gt;

&lt;p&gt;You should now see 5 subnets under &lt;strong&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Subnet&lt;/th&gt;
&lt;th&gt;Region&lt;/th&gt;
&lt;th&gt;CIDR&lt;/th&gt;
&lt;th&gt;Private IP Access&lt;/th&gt;
&lt;th&gt;Flow Logs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;public-subnet&lt;/td&gt;
&lt;td&gt;europe-west1&lt;/td&gt;
&lt;td&gt;10.0.1.0/24&lt;/td&gt;
&lt;td&gt;On&lt;/td&gt;
&lt;td&gt;On&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;private-backend&lt;/td&gt;
&lt;td&gt;europe-west1&lt;/td&gt;
&lt;td&gt;10.0.2.0/24&lt;/td&gt;
&lt;td&gt;On&lt;/td&gt;
&lt;td&gt;On&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;private-data&lt;/td&gt;
&lt;td&gt;europe-west1&lt;/td&gt;
&lt;td&gt;10.0.3.0/24&lt;/td&gt;
&lt;td&gt;On&lt;/td&gt;
&lt;td&gt;On&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;private-cache&lt;/td&gt;
&lt;td&gt;europe-west1&lt;/td&gt;
&lt;td&gt;10.0.4.0/24&lt;/td&gt;
&lt;td&gt;On&lt;/td&gt;
&lt;td&gt;On&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;private-obs&lt;/td&gt;
&lt;td&gt;europe-west1&lt;/td&gt;
&lt;td&gt;10.0.5.0/24&lt;/td&gt;
&lt;td&gt;On&lt;/td&gt;
&lt;td&gt;On&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step 4: Create Cloud Router
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a Cloud Router?
&lt;/h3&gt;

&lt;p&gt;Cloud Router manages dynamic routing for your Cloud NAT Gateway. It's required for NAT functionality.&lt;/p&gt;

&lt;h3&gt;
  
  
  Navigation Path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Hybrid Connectivity&lt;/strong&gt; → &lt;strong&gt;Cloud Routers&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create Router"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-07-cloud-router.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-07-cloud-router.png" alt="Screenshot: Cloud Router Creation" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Router Configuration
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-nat-router&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Descriptive name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Our VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same as subnets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google ASN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;65000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Default (leave unchanged)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why ASN:&lt;/strong&gt; Autonomous System Number uniquely identifies your router. 65000 is the default for private networks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt; and wait for creation (should be instant).&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Create Cloud NAT Gateway
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is Cloud NAT?
&lt;/h3&gt;

&lt;p&gt;Cloud NAT allows VMs &lt;strong&gt;without public IPs&lt;/strong&gt; to access the internet. This is essential for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Downloading packages (apt, npm)&lt;/li&gt;
&lt;li&gt;Calling external APIs&lt;/li&gt;
&lt;li&gt;Software updates&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Navigation Path
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Network Services&lt;/strong&gt; → &lt;strong&gt;Cloud NAT&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Get started"&lt;/strong&gt; or &lt;strong&gt;"Create Cloud NAT Gateway"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-08-cloud-nat.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-08-cloud-nat.png" alt="Screenshot: Cloud NAT Creation" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Gateway Configuration
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Basic Settings
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gateway name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-nat-gateway&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Descriptive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloud Router&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-nat-router&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Router we created&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;europe-west1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same region&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  NAT Mapping (Critical Section)
&lt;/h4&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-09-nat-mapping.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-09-nat-mapping.png" alt="Screenshot: NAT Mapping Configuration" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom single region NAT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Select&lt;/td&gt;
&lt;td&gt;For single region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloud Router&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-nat-router&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Already populated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Source subnetworks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✓ Select all 5 subnets&lt;/td&gt;
&lt;td&gt;All subnets need internet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NAT IP allocation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Manual&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Critical for HA&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why Manual IP Allocation:&lt;/strong&gt; Gives us control over outbound IPs. We'll create 2 IPs for high availability.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  NAT IP Allocation
&lt;/h4&gt;

&lt;p&gt;Click &lt;strong&gt;"Add reserved IPs"&lt;/strong&gt; twice:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IP 1:&lt;/strong&gt;&lt;br&gt;
| Field | Value |&lt;br&gt;
|-------|-------|&lt;br&gt;
| &lt;strong&gt;Name&lt;/strong&gt; | &lt;code&gt;dev-nat-ip-1&lt;/code&gt; |&lt;br&gt;
| &lt;strong&gt;Static IP address&lt;/strong&gt; | (Leave blank - auto-allocate) |&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IP 2:&lt;/strong&gt;&lt;br&gt;
| Field | Value |&lt;br&gt;
|-------|-------|&lt;br&gt;
| &lt;strong&gt;Name&lt;/strong&gt; | &lt;code&gt;dev-nat-ip-2&lt;/code&gt; |&lt;br&gt;
| &lt;strong&gt;Static IP address&lt;/strong&gt; | (Leave blank - auto-allocate) |&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why 2 IPs:&lt;/strong&gt; High availability. If one IP fails, the other handles traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost Alert:&lt;/strong&gt; ~$0.045/hr per NAT gateway = ~$32/month. Static IPs are free when attached to NAT.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Logging
&lt;/h4&gt;

&lt;p&gt;Leave &lt;strong&gt;"Log and monitor"&lt;/strong&gt; disabled (not needed for NAT).&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the NAT Gateway
&lt;/h3&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt; and wait 1-2 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify Cloud NAT
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Status:&lt;/strong&gt; Running&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Router:&lt;/strong&gt; dev-nat-router&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External IPs:&lt;/strong&gt; 2 IPs assigned&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subnets:&lt;/strong&gt; All 5 subnets mapped&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 6: Create Service Accounts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What are Service Accounts?
&lt;/h3&gt;

&lt;p&gt;Service accounts are identities for applications (not humans). They allow VMs to authenticate with Google Cloud services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Service Account Targeting?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Security Best Practice:&lt;/strong&gt; Instead of using network tags (which can be spoofed), we use service accounts to target firewall rules. This provides IAM-based security.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why SA-targeted is more secure:&lt;/strong&gt; If someone compromises your VM, they can't change network tags to bypass firewall rules. Service account targeting is enforced at the IAM level.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Create Service Accounts
&lt;/h3&gt;

&lt;p&gt;We'll create 4 service accounts for different tiers.&lt;/p&gt;

&lt;h4&gt;
  
  
  Service Account 1: Backend SA
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;IAM &amp;amp; Admin&lt;/strong&gt; → &lt;strong&gt;Service Accounts&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create Service Account"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-10-create-sa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-10-create-sa.png" alt="Screenshot: Create Service Account" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fill in the form:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service account name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend-dev-sa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tier-environment pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service account description&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Service account for backend VMs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Documentation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service account ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;(Auto-filled)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend-dev-sa@PROJECT_ID.iam&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Click &lt;strong&gt;"Create and continue"&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Skip the &lt;strong&gt;"Grant this service account access to project"&lt;/strong&gt; step (we'll add roles later).&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Done"&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Service Account 2: Cache SA
&lt;/h4&gt;

&lt;p&gt;Repeat the process:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cache-dev-sa&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Service account for Redis/PgBouncer VM&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Service Account 3: Observability SA
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;observability-dev-sa&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Service account for monitoring tools&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Service Account 4: Bastion SA
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;bastion-dev-sa&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Service account for bastion host&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Add IAM Roles to Service Accounts
&lt;/h3&gt;

&lt;p&gt;Now we'll add roles to allow VMs to write logs and metrics.&lt;/p&gt;

&lt;h4&gt;
  
  
  For Backend, Cache, and Observability SAs:
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Click on the service account (e.g., &lt;code&gt;backend-dev-sa&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Click the &lt;strong&gt;"Permissions"&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Grant access"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Add these roles:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Logs Writer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allow VMs to write to Cloud Logging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Monitoring Metric Writer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allow VMs to send metrics to Cloud Monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-11-add-iam-roles.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-11-add-iam-roles.png" alt="Screenshot: Add IAM Roles" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add Principal:&lt;/strong&gt; &lt;code&gt;backend-dev-sa@PROJECT_ID.iam.gserviceaccount.com&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Select Role:&lt;/strong&gt; &lt;code&gt;Logs Writer&lt;/code&gt; → &lt;code&gt;Monitoring Metric Writer&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Repeat for &lt;code&gt;cache-dev-sa&lt;/code&gt; and &lt;code&gt;observability-dev-sa&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  For Bastion SA:
&lt;/h4&gt;

&lt;p&gt;Bastion only needs &lt;code&gt;Logs Writer&lt;/code&gt; role.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Create Firewall Rules (Service Account Targeted)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Firewall Rule Priority
&lt;/h3&gt;

&lt;p&gt;GCP evaluates firewall rules by priority number:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lower numbers = Higher priority&lt;/strong&gt; (evaluated first)&lt;/li&gt;
&lt;li&gt;Deny rules typically use priority 500&lt;/li&gt;
&lt;li&gt;Allow rules typically use priority 1000+&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-12-firewall-priority.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-12-firewall-priority.png" alt="Screenshot: Firewall Priority" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why this order:&lt;/strong&gt; Deny rules (higher priority) are evaluated before allow rules. This ensures we can block specific traffic before allowing general traffic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Rule 1: Deny SMTP Egress (Priority 500)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Purpose:&lt;/strong&gt; Prevent compromised VMs from sending spam.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to &lt;strong&gt;VPC networks&lt;/strong&gt; → &lt;strong&gt;Firewall&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Create firewall rule"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fill in the form:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-firewall-deny-smtp-egress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Descriptive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Our VPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Priority&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;500&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;High priority (deny)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Direction of traffic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Egress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Outbound&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Action on match&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Deny&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Block traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Target service accounts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Select all 4 SAs&lt;/td&gt;
&lt;td&gt;Apply to all private VMs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-13-firewall-deny-smtp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-13-firewall-deny-smtp.png" alt="Screenshot: Firewall Rule Deny SMTP" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filters&lt;/strong&gt; section:&lt;br&gt;
| Field | Value |&lt;br&gt;
|-------|-------|&lt;br&gt;
| &lt;strong&gt;Destination filter&lt;/strong&gt; | &lt;code&gt;IPv4 range&lt;/code&gt; |&lt;br&gt;
| &lt;strong&gt;IPv4 range&lt;/strong&gt; | &lt;code&gt;0.0.0.0/0&lt;/code&gt; | All destinations |&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Protocols and ports&lt;/strong&gt;:&lt;br&gt;
| Field | Value |&lt;br&gt;
|-------|-------|&lt;br&gt;
| &lt;strong&gt;Allow protocols&lt;/strong&gt; | &lt;code&gt;Specified protocols and ports&lt;/code&gt; |&lt;br&gt;
| &lt;strong&gt;TCP&lt;/strong&gt; | ✓ Checked |&lt;br&gt;
| &lt;strong&gt;Ports&lt;/strong&gt; | &lt;code&gt;25&lt;/code&gt; | SMTP port |&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logging&lt;/strong&gt;:&lt;br&gt;
| Field | Value |&lt;br&gt;
|-------|-------|&lt;br&gt;
| &lt;strong&gt;Log config&lt;/strong&gt; | &lt;code&gt;On&lt;/code&gt; | &lt;strong&gt;Enable for security&lt;/strong&gt; |&lt;br&gt;
| &lt;strong&gt;Metadata&lt;/strong&gt; | &lt;code&gt;Include all&lt;/code&gt; | Full logging |&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why log deny rules:&lt;/strong&gt; Detect potential compromise. If a VM tries to send SMTP traffic, it's suspicious.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Rule 2: Allow SSH to Backend (Priority 1000)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Purpose:&lt;/strong&gt; Allow SSH access to backend VMs for troubleshooting.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-firewall-allow-ssh-backend&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Priority&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1000&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Direction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ingress&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Action&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Allow&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Target service accounts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend-dev-sa&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Source filter&lt;/strong&gt;:&lt;br&gt;
| Field | Value |&lt;br&gt;
|-------|-------|&lt;br&gt;
| &lt;strong&gt;Source IPv4 ranges&lt;/strong&gt; | &lt;code&gt;0.0.0.0/0&lt;/code&gt; | All sources (restrict in prod!) |&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Security Warning:&lt;/strong&gt; In production, restrict SSH to specific IPs or VPN ranges.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Protocols and ports&lt;/strong&gt;:&lt;br&gt;
| Field | Value |&lt;br&gt;
|-------|-------|&lt;br&gt;
| &lt;strong&gt;TCP&lt;/strong&gt; | ✓ Checked |&lt;br&gt;
| &lt;strong&gt;Ports&lt;/strong&gt; | &lt;code&gt;22&lt;/code&gt; | SSH |&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logging&lt;/strong&gt;:&lt;br&gt;
| Field | Value |&lt;br&gt;
|-------|-------|&lt;br&gt;
| &lt;strong&gt;Log config&lt;/strong&gt; | &lt;code&gt;On&lt;/code&gt; | &lt;strong&gt;Critical - log SSH access&lt;/strong&gt; |&lt;br&gt;
| &lt;strong&gt;Metadata&lt;/strong&gt; | &lt;code&gt;Include all&lt;/code&gt; | Full metadata |&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 3: Allow Internal HTTP (Priority 1001)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Purpose:&lt;/strong&gt; Allow internal HTTP/HTTPS traffic between services (load balancer → backend).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-firewall-allow-http-backend&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Priority&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1001&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Direction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ingress&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Action&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Allow&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Target service accounts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend-dev-sa&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Source filter&lt;/strong&gt;:&lt;br&gt;
| Field | Value |&lt;br&gt;
|-------|-------|&lt;br&gt;
| &lt;strong&gt;Source IPv4 ranges&lt;/strong&gt; | &lt;code&gt;10.0.0.0/8&lt;/code&gt; | Internal VPC traffic only |&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why 10.0.0.0/8:&lt;/strong&gt; Covers all our subnets (10.0.1.0-10.0.5.0). More secure than 0.0.0.0/0.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Protocols and ports&lt;/strong&gt;:&lt;br&gt;
| Field | Value |&lt;br&gt;
|-------|-------|&lt;br&gt;
| &lt;strong&gt;TCP&lt;/strong&gt; | ✓ Checked |&lt;br&gt;
| &lt;strong&gt;Ports&lt;/strong&gt; | &lt;code&gt;80, 443&lt;/code&gt; | HTTP and HTTPS |&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logging&lt;/strong&gt;:&lt;br&gt;
| Field | Value |&lt;br&gt;
|-------|-------|&lt;br&gt;
| &lt;strong&gt;Log config&lt;/strong&gt; | &lt;code&gt;Off&lt;/code&gt; | Too noisy for production |&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 4: Allow Health Checks (Priority 1002)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Purpose:&lt;/strong&gt; Allow GCP health checkers to probe backend VMs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-firewall-allow-health-checks&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Priority&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1002&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Direction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ingress&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Action&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Allow&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Target service accounts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend-dev-sa&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Source filter&lt;/strong&gt;:&lt;br&gt;
| Field | Value |&lt;br&gt;
|-------|-------|&lt;br&gt;
| &lt;strong&gt;Source IPv4 ranges&lt;/strong&gt; | &lt;code&gt;35.191.0.0/16, 130.211.0.0/22&lt;/code&gt; | &lt;strong&gt;GCP health check ranges&lt;/strong&gt; |&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why these IPs:&lt;/strong&gt; These are GCP's health checker IP ranges. Without this rule, health checks fail and load balancer won't route traffic.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Protocols and ports&lt;/strong&gt;:&lt;br&gt;
| Field | Value |&lt;br&gt;
|-------|-------|&lt;br&gt;
| &lt;strong&gt;TCP&lt;/strong&gt; | ✓ Checked |&lt;br&gt;
| &lt;strong&gt;Ports&lt;/strong&gt; | &lt;code&gt;80, 443&lt;/code&gt; | HTTP and HTTPS |&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logging&lt;/strong&gt;:&lt;br&gt;
| Field | Value |&lt;br&gt;
|-------|-------|&lt;br&gt;
| &lt;strong&gt;Log config&lt;/strong&gt; | &lt;code&gt;Off&lt;/code&gt; | Health checks are too noisy |&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 5: Allow Backend to Cache (Priority 1003)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Purpose:&lt;/strong&gt; Allow backend VMs to connect to Redis (6379) and PgBouncer (6432).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-allow-backend-to-cache&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Network&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dev-network&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Priority&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1003&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Direction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ingress&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Action&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Allow&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Source service accounts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend-dev-sa&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Target service accounts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cache-dev-sa&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Service Account Source:&lt;/strong&gt; This is the key to SA-targeted firewall. We're allowing traffic FROM backend SA TO cache SA.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Protocols and ports&lt;/strong&gt;:&lt;br&gt;
| Field | Value |&lt;br&gt;
|-------|-------|&lt;br&gt;
| &lt;strong&gt;TCP&lt;/strong&gt; | ✓ Checked |&lt;br&gt;
| &lt;strong&gt;Ports&lt;/strong&gt; | &lt;code&gt;6379, 6432&lt;/code&gt; | Redis and PgBouncer |&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logging&lt;/strong&gt;:&lt;br&gt;
| Field | Value |&lt;br&gt;
|-------|-------|&lt;br&gt;
| &lt;strong&gt;Log config&lt;/strong&gt; | &lt;code&gt;On&lt;/code&gt; | Useful for debugging |&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Create"&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verify All Firewall Rules
&lt;/h3&gt;

&lt;p&gt;You should see 5 firewall rules:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule Name&lt;/th&gt;
&lt;th&gt;Priority&lt;/th&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;dev-firewall-deny-smtp-egress&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;Egress&lt;/td&gt;
&lt;td&gt;Deny&lt;/td&gt;
&lt;td&gt;All SAs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dev-firewall-allow-ssh-backend&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;Ingress&lt;/td&gt;
&lt;td&gt;Allow&lt;/td&gt;
&lt;td&gt;backend-dev-sa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dev-firewall-allow-http-backend&lt;/td&gt;
&lt;td&gt;1001&lt;/td&gt;
&lt;td&gt;Ingress&lt;/td&gt;
&lt;td&gt;Allow&lt;/td&gt;
&lt;td&gt;backend-dev-sa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dev-firewall-allow-health-checks&lt;/td&gt;
&lt;td&gt;1002&lt;/td&gt;
&lt;td&gt;Ingress&lt;/td&gt;
&lt;td&gt;Allow&lt;/td&gt;
&lt;td&gt;backend-dev-sa&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dev-allow-backend-to-cache&lt;/td&gt;
&lt;td&gt;1003&lt;/td&gt;
&lt;td&gt;Ingress&lt;/td&gt;
&lt;td&gt;Allow&lt;/td&gt;
&lt;td&gt;cache-dev-sa&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Part 1 Verification Checklist
&lt;/h2&gt;

&lt;p&gt;Before moving to Part 2, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] VPC &lt;code&gt;dev-network&lt;/code&gt; exists in europe-west1&lt;/li&gt;
&lt;li&gt;[ ] 5 subnets created with correct CIDRs (10.0.1.0/24 - 10.0.5.0/24)&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Private Google Access&lt;/strong&gt; enabled on all subnets&lt;/li&gt;
&lt;li&gt;[ ] Cloud NAT &lt;code&gt;dev-nat-gateway&lt;/code&gt; has 2 external IPs&lt;/li&gt;
&lt;li&gt;[ ] Cloud Router &lt;code&gt;dev-nat-router&lt;/code&gt; status is "Ready"&lt;/li&gt;
&lt;li&gt;[ ] 4 service accounts created (backend, cache, observability, bastion)&lt;/li&gt;
&lt;li&gt;[ ] Service accounts have Logs Writer role&lt;/li&gt;
&lt;li&gt;[ ] All 5 firewall rules visible in VPC → Firewall&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-14-part1-complete.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/screenshot-14-part1-complete.png" alt="Screenshot: Completed Part 1" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost Summary - Part 1
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Monthly Cost&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud NAT Gateway&lt;/td&gt;
&lt;td&gt;~$32&lt;/td&gt;
&lt;td&gt;2 static IPs + NAT gateway fee&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VPC Flow Logs&lt;/td&gt;
&lt;td&gt;~$5-10&lt;/td&gt;
&lt;td&gt;Depends on traffic volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total Part 1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$37-42&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~$42/month&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cost Optimization:&lt;/strong&gt; In production, consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reducing Flow Logs sampling to 25%&lt;/li&gt;
&lt;li&gt;Using 1 NAT IP instead of 2 (lower HA)&lt;/li&gt;
&lt;li&gt;Disabling Flow Logs on non-critical subnets&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Troubleshooting - Part 1
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Issue: VPC Creation Fails
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; "API not enabled" error&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate to APIs &amp;amp; Services → Dashboard&lt;/li&gt;
&lt;li&gt;Search for "Compute Engine API"&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;"Enable"&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Issue: Subnet Creation Fails
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; "IP range overlaps with existing subnet"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check existing subnet CIDRs in VPC details&lt;/li&gt;
&lt;li&gt;Ensure each subnet has unique /24 range&lt;/li&gt;
&lt;li&gt;Verify you're using 10.0.1.0 through 10.0.5.0 (not overlapping)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Issue: Cloud NAT Shows "Failed"
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; NAT gateway status is "Failed"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify Cloud Router exists and is in same region&lt;/li&gt;
&lt;li&gt;Check NAT IP allocation (should show 2 IPs)&lt;/li&gt;
&lt;li&gt;Ensure subnets have no external IP (Cloud NAT only for private IPs)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Issue: Firewall Rule Not Working
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Traffic blocked unexpectedly&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check rule priority (lower = higher priority)&lt;/li&gt;
&lt;li&gt;Verify service account email matches exactly&lt;/li&gt;
&lt;li&gt;Check implicit deny: If no rule allows, traffic is blocked by default&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What's Next - Part 2?
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;Part 2: Security Services&lt;/strong&gt;, you'll build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secret Manager for secure credential storage&lt;/li&gt;
&lt;li&gt;Bastion host with IAP tunneling for secure SSH&lt;/li&gt;
&lt;li&gt;IAM permissions for access control&lt;/li&gt;
&lt;li&gt;OS Login for keyless SSH authentication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://dev.to/shaikhalamin/build-production-ready-gcp-infrastructure-from-scratch-part-02-542o"&gt;Continue to Part 2: Security Services →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/vpc/docs" rel="noopener noreferrer"&gt;GCP VPC Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/nat/docs" rel="noopener noreferrer"&gt;Cloud NAT Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/vpc/docs/firewalls" rel="noopener noreferrer"&gt;Firewall Rules Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/iam/docs/service-accounts" rel="noopener noreferrer"&gt;Service Account Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Foundational Infrastructure Complete!&lt;/strong&gt; Your VPC, subnets, NAT gateway, service accounts, and firewall rules are ready. Next, we'll add security services (Secrets, Bastion, IAM).&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>googlecloud</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to fix Ubuntu 24.04 NVIDIA RTX 4050 graphics driver issue in ASUS TUF A15</title>
      <dc:creator>Shaikh Al Amin</dc:creator>
      <pubDate>Sat, 25 Oct 2025 14:57:35 +0000</pubDate>
      <link>https://dev.to/shaikhalamin/how-to-fix-ubuntu-2404-nvidia-4050-graphics-driver-issue-in-asus-tuf-a15-34ic</link>
      <guid>https://dev.to/shaikhalamin/how-to-fix-ubuntu-2404-nvidia-4050-graphics-driver-issue-in-asus-tuf-a15-34ic</guid>
      <description>&lt;p&gt;&lt;strong&gt;Make sure you have secure boot off from BIOS&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt purge 'nvidia-*' -y
sudo apt autoremove -y
sudo apt update
sudo apt install nvidia-driver-580 -y
sudo update-initramfs -u
sudo update-grub
sudo reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify after reboot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nvidia-smi
xrandr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>linux</category>
      <category>tutorial</category>
      <category>ubuntu</category>
    </item>
    <item>
      <title>How to set up nats with docker and docker compose with token</title>
      <dc:creator>Shaikh Al Amin</dc:creator>
      <pubDate>Wed, 23 Jul 2025 10:36:23 +0000</pubDate>
      <link>https://dev.to/shaikhalamin/how-to-set-up-nats-with-docker-and-docker-compose-with-token-2ddb</link>
      <guid>https://dev.to/shaikhalamin/how-to-set-up-nats-with-docker-and-docker-compose-with-token-2ddb</guid>
      <description>&lt;p&gt;&lt;strong&gt;Docker compose file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;version: "3.9"

services:
  nats:
    image: nats:latest
    container_name: nats_main
    restart: always
    ports:
      - "4222:4222"
      - "8222:8222"
    volumes:
      - ./nats.conf:/etc/nats/nats.conf
    command: ["-c", "/etc/nats/nats.conf"]

  nats-client:
    image: natsio/nats-box
    container_name: nats_client
    restart: always
    depends_on:
      - nats
    entrypoint: /bin/sh
    tty: true

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Config file:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
listen: 0.0.0.0:4222
http: 8222

jetstream {
  store_dir: /data/jetstream
  max_mem_store: 1Gb
  max_file_store: 10Gb
}

authorization {
  token: dfji348934jdd0i24uhjd29834ijrr0345jo0r3j034n
}


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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Run:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker compose up --build -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Test:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker compose exec nats-client nats sub -s nats://dfji348934jdd0i24uhjd29834ijrr0345jo0r3j034n@nats:4222 test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fron another terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker compose exec nats-client nats pub -s nats://dfji348934jdd0i24uhjd29834ijrr0345jo0r3j034n@nats:4222 test "Hello via Docker Compose!"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setup JetStream:&lt;/p&gt;

&lt;p&gt;Verify JetStream is running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker compose exec nats-client nats server check jetstream -s nats://dfji348934jdd0i24uhjd29834ijrr0345jo0r3j034n@nats:4222

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Create the TIXIO Stream (Manual CLI) and follow default command&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker compose exec nats-client nats stream add TIXIO \
  --subjects "tixio.&amp;gt;" \
  --storage file \
  --retention limits \
  -s nats://dfji348934jdd0i24uhjd29834ijrr0345jo0r3j034n@nats:4222
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>nats</category>
      <category>docker</category>
      <category>token</category>
    </item>
    <item>
      <title>How to deploy Node.js Nest.js project on AWS lamda serverless</title>
      <dc:creator>Shaikh Al Amin</dc:creator>
      <pubDate>Thu, 23 Jan 2025 13:40:33 +0000</pubDate>
      <link>https://dev.to/shaikhalamin/how-to-integrate-aws-lamda-serverless-with-nodejs-nestjs-5060</link>
      <guid>https://dev.to/shaikhalamin/how-to-integrate-aws-lamda-serverless-with-nodejs-nestjs-5060</guid>
      <description>&lt;p&gt;N:B Setup necessary VPC configurations, public and private subnet, provide necessary permission of your AWS access and secret key and more importantly the lambda security group outbound rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;443 Outbound rules should be forwarded to 0.0.0.0/0&lt;/li&gt;
&lt;li&gt;Any 5432/Postgres Outbound rules should be forwarded be to postgres-security-group&lt;/li&gt;
&lt;li&gt;For Postgres security group inbound rule accepts incoming from Lamda security groups&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Create Serverless yml in project root:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service: backend-api

frameworkVersion: '^3.0.0'

useDotenv: true
configValidationMode: error

provider:
  name: aws
  region: us-east-2
  runtime: nodejs20.x
  stage: ${opt:stage, 'dev'}
  deploymentMethod: direct
  logRetentionInDays: 7

  vpc:
    securityGroupIds:
      - sg-13f39e24a9d
      - sg-87037c0f81
    subnetIds:
      - subnet-d40bc57
      - subnet-45gf564g3454

  ecr:
    scanOnPush: true
    images:
      backend-api-image:
        path: .
        file: Dockerfile

  environment:
    NODE_ENV: ${env:NODE_ENV}
    DATABASE_URL: ${env:DATABASE_URL}
    JWT_TOKEN_SECRET: ${env:JWT_TOKEN_SECRET}
    API_BACKEND_AWS_REGION: ${env:STUDY_BUDS_AWS_REGION}
    API_BACKEND_AWS_ACCESS_KEY: ${env:STUDY_BUDS_AWS_ACCESS_KEY}
    API_BACKEND_AWS_SECRETE_KEY: ${env:STUDY_BUDS_AWS_SECRETE_KEY}
    AWS_BUCKET_NAME: ${env:AWS_BUCKET_NAME}
    AWS_CLOUD_FRONT_URL: ${env:AWS_CLOUD_FRONT_URL}
    AWS_SMTP_HOST: ${env:AWS_SMTP_HOST}
    AWS_SMTP_USER: ${env:AWS_SMTP_USER}
    AWS_SMTP_PASS: ${env:AWS_SMTP_PASS}
    AWS_FROM_EMAIL: ${env:AWS_FROM_EMAIL}
    AI_BACKEND_URL: ${env:AI_BACKEND_URL}
    CORS_ALLOWED_HOSTS: ${env:CORS_ALLOWED_HOSTS}
    FRONTEND_BASE_URL: ${env:FRONTEND_BASE_URL}
    MAIL_HOST: ${env:MAIL_HOST}
    MAIL_USER: ${ env:MAIL_USER }
    MAIL_PASSWORD: ${env:MAIL_PASSWORD}
    MAIL_FROM_NAME: ${env:MAIL_FROM_NAME}
    MAIL_FROM_ADDRESS: ${env:MAIL_FROM_ADDRESS}

  apiGateway:
    binaryMediaTypes:
      - '*/*'
custom:
  prune:
    automatic: true
    number: 2

functions:
  main:
    image:
      name: backend-api-image
      command:
        - dist/src/serverless.handler
      entryPoint:
        - '/lambda-entrypoint.sh'
    memorySize: 1024
    timeout: 330
    url: true
    provisionedConcurrency: 0
    events:
      - http:
          method: ANY
          path: /
      - http:
          method: ANY
          path: '{proxy+}'

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Install below two packages:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i @codegenie/serverless-express
npm i -D @types/aws-lambda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Create serverless.ts inside src/serverless.ts&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import serverlessExpress from '@codegenie/serverless-express';
import { Callback, Context, Handler } from 'aws-lambda';
import { RequestMethod, ValidationPipe } from '@nestjs/common';
import helmet from 'helmet';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { ResponseTransformInterceptor } from './common/interceptor/global-response-interceptor';
import { RolePermissionsSeederService } from './modules/v1/user/seed/role-permissions.seeder.service';
import { SeedService } from './modules/v1/character/seeder/seeder.service';
let server: Handler;

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const allowedHosts = (process.env.CORS_ALLOWED_HOSTS as string) || '*';

  app.enableCors({
    origin: allowedHosts.split(','),
    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
    credentials: true,
  });

  app.setGlobalPrefix('api/v1', {
    exclude: [{ path: '/', method: RequestMethod.GET }],
  });

  app.use(helmet());

  app.useGlobalPipes(
    new ValidationPipe({
      transform: true,
      whitelist: true,
    }),
  );

  const reflector = app.get(Reflector);
  app.useGlobalInterceptors(new ResponseTransformInterceptor(reflector));

  //const rolePermissionsService = app.get(RolePermissionsSeederService);

  //await Promise.all([rolePermissionsService.insertRolePermissions()]);

  const rolePermissionsService = app.get(RolePermissionsSeederService);
  const seedsService = app.get(SeedService);

  const config = new DocumentBuilder()
    .setTitle('Backend api')
    .setDescription('Swagger docs for backend apis')
    .setVersion('1.0')
    .addBearerAuth()
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('/api-docs', app, document);

  await app.init();

  const expressApp = app.getHttpAdapter().getInstance();
  return serverlessExpress({
    app: expressApp,
  });
}

export const handler: Handler = async (
  event: any,
  context: Context,
  callback: Callback,
) =&amp;gt; {
  try {
    server = server ?? (await bootstrap());
    return server(event, context, callback);
  } catch (error) {
    console.error('Error during Lambda execution:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({
        error: 'Internal Server Error',
        message: error.message,
      }),
    };
  }
};

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Create Dockerfile inside root directory :&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Use the AWS Lambda base image for Node.js
FROM public.ecr.aws/lambda/nodejs:22

# Set the working directory in the container
WORKDIR ${LAMBDA_TASK_ROOT}

# Copy only package files first for dependency installation
COPY package*.json ${LAMBDA_TASK_ROOT}/

# Install dependencies (both production and development for the build phase)
RUN npm install

# Copy the rest of the application code (excluding files in .dockerignore)
COPY . ${LAMBDA_TASK_ROOT}/

# Build the NestJS application
RUN npm run build

# Set the Lambda function handler
CMD ["dist/src/serverless.handler"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Create .env.dev in project root&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NODE_ENV=development
PORT=8035
DATABASE_URL=postgresql://postgres:postgres@database_container:5432/nestjs
REDIS_HOST=redis_container
REDIS_PORT=6379
JWT_TOKEN_SECRET=dkfjkjdfkjdfkdfjkjdfkjkdkdfjk
AWS_BUCKET_NAME=store-backend
API_BACKEND_AWS_REGION=us-east-2
API_BACKEND_AWS_ACCESS_KEY=JKDIUENDIUEKNIUEIJKIWJKWJOUIW
API_BACKEND_AWS_SECRETE_KEY=LKFLKORJKIJKUFKJFKJKFJKFKFJ
AWS_CLOUD_FRONT_URL=https://your.cloudfront.net
AWS_SMTP_HOST=email-smtp.ap-southeast-1.amazonaws.com
AWS_SMTP_USER=AJKDIUENSDUJEHUSDJBUE
AWS_SMTP_PASS=fkdjkjrtiu93j934jkjf98493ikjkjrjii4ju5
AWS_FROM_EMAIL=alamin.cse15@gmail.com
AI_BACKEND_URL=https://aibackend.io/api/v2
CORS_ALLOWED_HOSTS='http://localhost:3000'
FRONTEND_BASE_URL="http://localhost:3000"

MAIL_HOST=smtp.gmail.com
MAIL_USER=alamin.cse15@gmail.com
MAIL_PASSWORD='sdjkdfjkdfkdfhjdfhdjfhj'
MAIL_FROM_NAME=Shaikh
MAIL_FROM_ADDRESS=alamin.cse15@gmail.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;*&lt;em&gt;Install serverless framework globally: *&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i -g serverless@3.39.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Setup AWS access and secret key using &lt;a href="https://dev.to/shaikhalamin/install-aws-cli-in-ubuntu-5a8a"&gt;AWS CLI (setup in ubuntu)&lt;/a&gt;:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;**&lt;br&gt;
Now navigate to project root and run **&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sls deploy --stage dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make sure you have .env.dev exists in your project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;**CI/CD: Deploy from github actions:**
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: Deploy Serverless App to Dev Environment

on:
  push:
    branches:
      - dev
    pull_request:
      types: [closed]
      branches:
        - dev

jobs:
  deploy:
    if: github.event.pull_request.merged == true || github.event_name == 'push'
    runs-on: ubuntu-latest

    steps:
      # 1. Checkout code
      - name: Checkout code
        uses: actions/checkout@v3

      # 3. Configure AWS Credentials
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v3
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-2

      # 4. Setup Node.js
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 20.x

      - name: Clear Serverless Cache
        run: |
          rm -rf .serverless

      # 5. Map environment variables manually
      - name: Set Environment Variables
        run: |
          rm -f .env.dev
          touch .env.dev
          echo "NODE_ENV=${{ secrets.NODE_ENV }}" &amp;gt;&amp;gt; .env.dev
          echo "DATABASE_URL=${{ secrets.DEV_DATABASE_URL }}" &amp;gt;&amp;gt; .env.dev
          echo "JWT_TOKEN_SECRET=${{ secrets.DEV_JWT_TOKEN_SECRET }}" &amp;gt;&amp;gt; .env.dev
          echo "AWS_BUCKET_NAME=${{ secrets.DEV_AWS_BUCKET_NAME }}" &amp;gt;&amp;gt; .env.dev
          echo "BACKEND_AWS_REGION=${{ secrets.DEV_BACKEND_AWS_REGION }}" &amp;gt;&amp;gt; .env.dev
          echo "BACKEND_AWS_ACCESS_KEY=${{ secrets.BACKEND_AWS_ACCESS_KEY }}" &amp;gt;&amp;gt; .env.dev
          echo "BACKEND_AWS_SECRETE_KEY=${{ secrets.DEV_BACKEND_AWS_SECRETE_KEY }}" &amp;gt;&amp;gt; .env.dev
          echo "AWS_CLOUD_FRONT_URL=${{ secrets.DEV_AWS_CLOUD_FRONT_URL }}" &amp;gt;&amp;gt; .env.dev
          echo "AWS_SMTP_HOST=${{ secrets.AWS_SMTP_HOST }}" &amp;gt;&amp;gt; .env.dev
          echo "AWS_SMTP_USER=${{ secrets.AWS_SMTP_USER }}" &amp;gt;&amp;gt; .env.dev
          echo "AWS_SMTP_PASS=${{ secrets.AWS_SMTP_PASS }}" &amp;gt;&amp;gt; .env.dev
          echo "AWS_FROM_EMAIL=${{ secrets.AWS_FROM_EMAIL }}" &amp;gt;&amp;gt; .env.dev
          echo "AI_BACKEND_URL=${{ secrets.DEV_AI_BACKEND_URL }}" &amp;gt;&amp;gt; .env.dev
          echo "CORS_ALLOWED_HOSTS=${{ secrets.DEV_CORS_ALLOWED_HOSTS }}" &amp;gt;&amp;gt; .env.dev
          echo "FRONTEND_BASE_URL=${{ secrets.DEV_FRONTEND_BASE_URL }}" &amp;gt;&amp;gt; .env.dev
          echo "MAIL_HOST=${{ secrets.MAIL_HOST }}" &amp;gt;&amp;gt; .env.dev
          echo "MAIL_USER=${{ secrets.MAIL_USER }}" &amp;gt;&amp;gt; .env.dev
          echo "MAIL_PASSWORD=${{ secrets.MAIL_PASSWORD }}" &amp;gt;&amp;gt; .env.dev
          echo "MAIL_FROM_NAME=${{ secrets.MAIL_FROM_NAME }}" &amp;gt;&amp;gt; .env.dev
          echo "MAIL_FROM_ADDRESS=${{ secrets.MAIL_FROM_ADDRESS }}" &amp;gt;&amp;gt; .env.dev


      # 6. Install Serverless Framework v3
      - name: Install Serverless Framework v3
        run: npm i -g serverless@3.39.0

        # 7. Deploy to the specified stage
      - name: Deploy to dev
        run: sls deploy --stage dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>aws</category>
      <category>serverless</category>
      <category>nestjs</category>
      <category>lambda</category>
    </item>
    <item>
      <title>How to deploy fastAPI app with postgreSQL database in AWS EC2</title>
      <dc:creator>Shaikh Al Amin</dc:creator>
      <pubDate>Thu, 07 Nov 2024 03:42:13 +0000</pubDate>
      <link>https://dev.to/shaikhalamin/how-to-deploy-fastapi-app-with-postgresql-database-in-aws-ec2-2hn5</link>
      <guid>https://dev.to/shaikhalamin/how-to-deploy-fastapi-app-with-postgresql-database-in-aws-ec2-2hn5</guid>
      <description>&lt;h1&gt;
  
  
  SSH into your AWS cli
&lt;/h1&gt;

&lt;h1&gt;
  
  
  System update and install Postgres, Nginx, python3 pip, and venv
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;curl ca-certificates
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; /usr/share/postgresql-common/pgdg
&lt;span class="nb"&gt;sudo &lt;/span&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc &lt;span class="nt"&gt;--fail&lt;/span&gt; https://www.postgresql.org/media/keys/ACCC4CF8.asc
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; /usr/share/postgresql-common/pgdg
&lt;span class="nb"&gt;sudo &lt;/span&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" &amp;gt; /etc/apt/sources.list.d/pgdg.list'&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;postgresql-14 postgresql-client-14
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;python3-pip python3-venv 
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Complete Postgres setup and create a database
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; postgres
psql &lt;span class="nt"&gt;-U&lt;/span&gt; postgres &lt;span class="nt"&gt;-h&lt;/span&gt; localhost

postgres@shaikh:~&lt;span class="nv"&gt;$ &lt;/span&gt;psql
&lt;span class="nv"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# create database local_test;&lt;/span&gt;
CREATE DATABASE
&lt;span class="nv"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# grant all privileges on database local_test to postgres;&lt;/span&gt;
GRANT
&lt;span class="nv"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# ALTER USER postgres WITH PASSWORD 'postgres';&lt;/span&gt;
ALTER ROLE
&lt;span class="nv"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="c"&gt;# exit()&lt;/span&gt;

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

&lt;/div&gt;



&lt;h1&gt;
  
  
  Generate ssh profile to pull the project from github
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/.ssh/
ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"amin@test.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Copy the public key and put inside your github setting SSH keys with a valid name
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cat id_ed25519.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Create a folder for git projects and clone the repo
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/
&lt;span class="nb"&gt;mkdir &lt;/span&gt;projects
&lt;span class="nb"&gt;cd &lt;/span&gt;projects/
git clone git@github.com:shaikhalamin/backend-project.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Create a virtual environment and activate the venv
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3.10 &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
&lt;span class="nb"&gt;source &lt;/span&gt;venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Now CD into the project directory and install the requirements along with Gunicorn server
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;backend-project/
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
pip &lt;span class="nb"&gt;install &lt;/span&gt;gunicorn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Setup your .env and modify accordingly
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
nano .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Test the server run using unicorn
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvicorn main:app &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;span class="nb"&gt;cat&lt;/span&gt; .env
nano configs/setting.py 
uvicorn main:app &lt;span class="nt"&gt;--host&lt;/span&gt; 0.0.0.0 &lt;span class="nt"&gt;--port&lt;/span&gt; 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Now Setup gunicorn service for running your application as a service
&lt;/h1&gt;

&lt;h1&gt;
  
  
  Get the Gunicorn installed path and project path by the below command
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;which gunicorn
&lt;span class="nb"&gt;pwd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Create below service file and put the below following content in there
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo nano /etc/systemd/system/devapi_gunicorn.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
&lt;span class="o"&gt;[&lt;/span&gt;Unit]
&lt;span class="nv"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Gunicorn instance to serve fast API application
&lt;span class="nv"&gt;After&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;network.target

&lt;span class="o"&gt;[&lt;/span&gt;Service]
&lt;span class="nv"&gt;User&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ubuntu
&lt;span class="nv"&gt;Group&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ubuntu
&lt;span class="nv"&gt;WorkingDirectory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/ubuntu/backend-project/
&lt;span class="nv"&gt;ExecStart&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/home/ubuntu/venv/bin/gunicorn &lt;span class="nt"&gt;-w&lt;/span&gt; 4 &lt;span class="nt"&gt;-k&lt;/span&gt; uvicorn.workers.UvicornWorker &lt;span class="nt"&gt;-b&lt;/span&gt; 0.0.0.0:8080 main:app

&lt;span class="o"&gt;[&lt;/span&gt;Install]
&lt;span class="nv"&gt;WantedBy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;multi-user.target

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

&lt;/div&gt;



&lt;h1&gt;
  
  
  Now enable Gunicorn service
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;devapi_gunicorn
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start devapi_gunicorn
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status devapi_gunicorn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Now create an nginx config file and change it according to your domain name
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /etc/nginx/sites-available/
&lt;span class="nb"&gt;sudo &lt;/span&gt;nano orangedev.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;
   server &lt;span class="o"&gt;{&lt;/span&gt;

    server_name orangedev.com&lt;span class="p"&gt;;&lt;/span&gt;

    location / &lt;span class="o"&gt;{&lt;/span&gt;
        proxy_pass http://localhost:8080&lt;span class="p"&gt;;&lt;/span&gt;
        proxy_http_version 1.1&lt;span class="p"&gt;;&lt;/span&gt;
        proxy_set_header Upgrade &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        proxy_set_header Connection &lt;span class="s1"&gt;'upgrade'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        proxy_set_header Host &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        proxy_cache_bypass &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

  &lt;span class="o"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;h1&gt;
  
  
  Move the nginx config file to the sites-enabled
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/orangedev.com /etc/nginx/sites-enabled/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Check the Nginx config and reload the Nginx
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Allow fire all for SSH, HTTP, and HTTPS only
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw status
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow ssh
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow http
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow https
&lt;span class="c"&gt;# For posgres from another server&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 5432/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable
sudo &lt;/span&gt;ufw status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  If everything goes well install certbot for SSL certificated
&lt;/h1&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;snap &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--classic&lt;/span&gt; certbot
&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /snap/bin/certbot /usr/bin/certbot
&lt;span class="o"&gt;[&lt;/span&gt;N:B]
&lt;span class="c"&gt;# Setup domain name and point to the correct IP and check from DNS checker then run the below command&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>fastapi</category>
      <category>aws</category>
      <category>ec2</category>
      <category>postgres</category>
    </item>
    <item>
      <title>How to detect your Python library path from docker in your VS Code</title>
      <dc:creator>Shaikh Al Amin</dc:creator>
      <pubDate>Tue, 05 Nov 2024 16:28:07 +0000</pubDate>
      <link>https://dev.to/shaikhalamin/how-to-detect-your-python-library-path-from-docker-in-your-vs-code-3i8i</link>
      <guid>https://dev.to/shaikhalamin/how-to-detect-your-python-library-path-from-docker-in-your-vs-code-3i8i</guid>
      <description>&lt;h2&gt;
  
  
  Vs Code plugins dependencies:
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;1. Install python plugin from Microsoft
2. Install pylance plugin from Microsoft
3. Install Black Formatter plugin from Microsoft

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create a virtual env inside your project root.
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3.10 -m venv venv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Activate the venv.
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Install the dependency from requirments.txt
&lt;/h2&gt;

&lt;p&gt;pip install -r requirments.txt&lt;/p&gt;

&lt;h2&gt;
  
  
  Select python env from venv for VS Code interpreter
&lt;/h2&gt;

&lt;p&gt;You can set the interpreter by pressing&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ctrl+Shift+P, searching for Python: Select Interpreter,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then choosing the virtual environment in your project root&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(.venv/bin/python or similar).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
    </item>
  </channel>
</rss>
