<?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: Mohammad Awwaad</title>
    <description>The latest articles on DEV Community by Mohammad Awwaad (@mohamadawwaad).</description>
    <link>https://dev.to/mohamadawwaad</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%2F520580%2F085e947b-e32e-4a46-94c3-24d33596fbfd.png</url>
      <title>DEV Community: Mohammad Awwaad</title>
      <link>https://dev.to/mohamadawwaad</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mohamadawwaad"/>
    <language>en</language>
    <item>
      <title>The Zero-Cost Cloud Engineer Part 5: Brownfield Terraforming and the 'Zero-Diff' Philosophy</title>
      <dc:creator>Mohammad Awwaad</dc:creator>
      <pubDate>Sat, 11 Apr 2026 16:14:54 +0000</pubDate>
      <link>https://dev.to/mohamadawwaad/the-zero-cost-cloud-engineer-part-5-brownfield-terraforming-and-the-zero-diff-philosophy-3ekk</link>
      <guid>https://dev.to/mohamadawwaad/the-zero-cost-cloud-engineer-part-5-brownfield-terraforming-and-the-zero-diff-philosophy-3ekk</guid>
      <description>&lt;h2&gt;
  
  
  The Zero-Cost Cloud Engineer
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Part 5: Infrastructure as Code &amp;amp; The Brownfield Migration
&lt;/h2&gt;

&lt;p&gt;In the first four parts of this series, we manually built a highly secure, zero-cost Google Cloud microservice ecosystem. We navigated "ClickOps" UI traps, established isolated networks, and deployed Spring Boot Java applications.&lt;/p&gt;

&lt;p&gt;But in enterprise engineering, manual UI clicking is a massive liability. If a disaster strikes, we cannot spend hours trying to remember which IAM roles or network tags we used. We need &lt;strong&gt;Infrastructure as Code (Terraform)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;However, migrating a currently running, live server into Terraform without accidentally destroying it is a high-risk operation known as a &lt;strong&gt;Brownfield Migration&lt;/strong&gt;. Here is the Architect's guide to doing it safely.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 1: The Ephemeral Security Token
&lt;/h3&gt;

&lt;p&gt;Terraform needs permission to manage your GCP resources. A common beginner mistake is generating a permanent Service Account JSON key and leaving it on a local laptop hard drive—a massive security risk.&lt;/p&gt;

&lt;p&gt;The Architect's solution is &lt;strong&gt;Application Default Credentials (ADC)&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;From your local machine, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud auth application-default login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates a deeply encrypted, temporary token linked strictly to your identity. When your Terraform session is finished for the day, you simply run &lt;code&gt;gcloud auth application-default revoke&lt;/code&gt; to obliterate the token and harden your machine against compromised scripts.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2: Defining the Provider
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;main.tf&lt;/code&gt; file. This acts as the architectural blueprint. First, declare the Google provider and link it to your specific sandbox project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;google&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/google"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 5.0"&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="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"google"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"your-actual-gcp-project-id"&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east1"&lt;/span&gt;
  &lt;span class="nx"&gt;zone&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east1-b"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Step 3: The Danger of "Apply" &amp;amp; Importing State
&lt;/h3&gt;

&lt;p&gt;Next, you write out the explicit definition of your previously built &lt;code&gt;e2-micro&lt;/code&gt; VM natively in HCL (HashiCorp Configuration Language). &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🚨 The Creation Trap:&lt;/strong&gt; If you run &lt;code&gt;terraform apply&lt;/code&gt; immediately after writing your code, Terraform will assume it manages 0 resources and will attempt to create a brand new VM. Google Cloud will heavily reject this attempt, throwing a &lt;code&gt;409 Conflict: Resource already exists&lt;/code&gt; error since the server actually lives in the cloud.&lt;/p&gt;

&lt;p&gt;Instead, we must download the exact specifications of the live server into Terraform's "brain" (the &lt;code&gt;.tfstate&lt;/code&gt; file) by running an import command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform import google_compute_instance.free_tier_vm projects/[YOUR_PROJECT]/zones/us-east1-b/instances/free-tier-vm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Step 4: Surviving "Forces Replacement"
&lt;/h3&gt;

&lt;p&gt;Once imported, you run &lt;code&gt;terraform plan&lt;/code&gt; to see what changes Terraform wants to make. You might be horrified to see: &lt;code&gt;forces replacement&lt;/code&gt; displayed in red text. &lt;/p&gt;

&lt;p&gt;Terraform is ruthlessly exact. If your code declares &lt;code&gt;image = "debian-cloud/debian-13"&lt;/code&gt;, but the real cloud server was built using a hyper-specific internal ID string like &lt;code&gt;debian-13-trixie-v20250101&lt;/code&gt;, Terraform decides the only way to resolve the mismatch is to wipe your hard drive and delete the server!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Architect's Fix (&lt;code&gt;ignore_changes&lt;/code&gt;):&lt;/strong&gt;&lt;br&gt;
To survive a Brownfield import, use the lifecycle &lt;code&gt;ignore_changes&lt;/code&gt; block. This explicitly tells Terraform to ignore slight discrepancies in specific fields, preventing a catastrophic server wipe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;  &lt;span class="nx"&gt;lifecycle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ignore_changes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;boot_disk&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;initialize_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# Prevents Terraform from deleting dynamic SSH keys!&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;em&gt;Note: Ignoring &lt;code&gt;metadata&lt;/code&gt; is especially crucial in GCP, as Google dynamically injects your SSH keys into the metadata block when you connect natively via the &lt;code&gt;gcloud compute ssh&lt;/code&gt; IAP tunnel. Without this exclusion, Terraform will declare war on Google's automation and constantly try to delete your SSH access!&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 5: The "Zero-Diff" Goal vs. VM Restarts
&lt;/h3&gt;

&lt;p&gt;If you declared your VM's Service Account as &lt;code&gt;email = "default"&lt;/code&gt;, but Google imported it as &lt;code&gt;1234567-compute@developer.gserviceaccount.com&lt;/code&gt;, Terraform will flag this as a required update (&lt;code&gt;~&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;Because modifying core VM identities requires the server to be completely stopped, Terraform will fail the plan, refusing to take a production system down without explicit safety permissions. You can circumvent this by supplying &lt;code&gt;allow_stopping_for_update = true&lt;/code&gt; to gracefully cycle it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Architect's Pivot:&lt;/strong&gt;&lt;br&gt;
Do you actually need to restart the VM? &lt;br&gt;
In a perfect Brownfield migration, the Architect's goal is a &lt;strong&gt;"Zero-Diff Plan."&lt;/strong&gt; Instead of forcing a restart to make the server match your generic code snippets, &lt;em&gt;change your code to match the server.&lt;/em&gt; Replace &lt;code&gt;"default"&lt;/code&gt; with the exact email string retrieved from the cloud. &lt;/p&gt;

&lt;p&gt;By perfectly mirroring reality, &lt;code&gt;terraform plan&lt;/code&gt; will output &lt;strong&gt;"0 to add, 0 to change, 0 to destroy,"&lt;/strong&gt; allowing you to safely wrap your live application in massive automation without incurring a single second of downtime.&lt;/p&gt;




&lt;h3&gt;
  
  
  Series Conclusion: The Zero-Cost Architect
&lt;/h3&gt;

&lt;p&gt;We started this journey by acknowledging the reality of the expired $300 GCP credit. Instead of giving up or relying on expensive defaults, we purposefully built an entire production-grade ecosystem within the fierce constraints of the Always Free tier.&lt;/p&gt;

&lt;p&gt;We provisioned an &lt;code&gt;e2-micro&lt;/code&gt; secure island without a public IP. We piped Spring Boot logs natively to Google's Ops Agent. We decoupled our communications safely using Standard Pub/Sub instead of the FinOps trap of "Lite." We managed secrets in a global vault, streamed files securely to Object Storage, and finally brought everything under the strict governance of Immutable Infrastructure as Code.&lt;/p&gt;

&lt;p&gt;Building inside constraints is what separates developers from Cloud Architects. You now have a complete, zero-cost Google Cloud laboratory. Keep iterating, keep building, and never accept the console defaults!&lt;/p&gt;

</description>
      <category>gcp</category>
      <category>terraform</category>
      <category>architecture</category>
      <category>devops</category>
    </item>
    <item>
      <title>The Zero-Cost Cloud Engineer Part 4: Cloud Storage, Secret Manager, and the Legacy Access Trap</title>
      <dc:creator>Mohammad Awwaad</dc:creator>
      <pubDate>Fri, 03 Apr 2026 19:06:33 +0000</pubDate>
      <link>https://dev.to/mohamadawwaad/the-zero-cost-cloud-engineer-part-4-cloud-storage-secret-manager-and-the-legacy-access-trap-32hi</link>
      <guid>https://dev.to/mohamadawwaad/the-zero-cost-cloud-engineer-part-4-cloud-storage-secret-manager-and-the-legacy-access-trap-32hi</guid>
      <description>&lt;h2&gt;
  
  
  The Zero-Cost Cloud Engineer
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Part 4: Hybrid Storage, Secrets, and the Legacy VM Trap
&lt;/h2&gt;

&lt;p&gt;In our previous tutorials, we secured an internet-less Compute Engine VM, established centralized logging, and decoupled our architecture with Pub/Sub. Now, we hit the next major architectural bottleneck: &lt;strong&gt;Our 30GB Hard Drive limit.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you allow users to upload files directly to your VM's block storage, you will quickly max out your Free Tier limits, crashing your OS. Resilient architectures offload files to Object Storage (Google Cloud Storage) and never hardcode connection properties. &lt;/p&gt;

&lt;p&gt;This tutorial covers integrating Google Cloud Storage (GCS) and Secret Manager into a Spring Boot application, entirely zero-cost.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 1: Provisioning Hybrid Storage
&lt;/h3&gt;

&lt;p&gt;In Google Cloud Storage (GCS), you don't have "folders"; you have "Buckets" filled with "Objects." For the Always Free tier, GCP gives you &lt;strong&gt;5 GB-months&lt;/strong&gt; of Standard Storage per month. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🚨 The FinOps Trap (Soft Delete):&lt;/strong&gt; A "GB-month" calculates storage sequentially. If you upload a 5 GB file and delete it 24 hours later, you consume a fraction of a GB-month. However, Google recently enabled &lt;strong&gt;Soft Delete&lt;/strong&gt; by default with a 7-day retention period. If you delete files to stay under the 5GB limit, Soft Delete secretly retains them, charging your quota and triggering a sudden billing alert.&lt;/p&gt;

&lt;p&gt;To provision a strictly free bucket:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Navigate to &lt;strong&gt;Cloud Storage Buckets&lt;/strong&gt; in the GCP Console and click &lt;strong&gt;Create&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Give it a globally unique name (e.g., &lt;code&gt;zero-cost-bucket-yourname&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Location:&lt;/strong&gt; You &lt;em&gt;must&lt;/em&gt; choose a Region that matches your VM (e.g., &lt;code&gt;us-east1&lt;/code&gt;) to avoid data egress charges.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Storage Class:&lt;/strong&gt; Choose &lt;strong&gt;Standard&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Protection:&lt;/strong&gt; Expand "Choose how to protect object data" and &lt;strong&gt;Disable Soft Delete&lt;/strong&gt; (or set retention to 0 days) to prevent hidden quota consumption. &lt;/li&gt;
&lt;li&gt; Click &lt;strong&gt;Create&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Step 2: The Security Vault (Secret Manager)
&lt;/h3&gt;

&lt;p&gt;We don't want to hardcode our new bucket name into our source code. We want to fetch it securely on boot. GCP offers 6 completely free secret versions per month.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Navigate to &lt;strong&gt;Secret Manager&lt;/strong&gt; and click &lt;strong&gt;CREATE SECRET&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Name:&lt;/strong&gt; &lt;code&gt;GCS_BUCKET_NAME&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Secret Value:&lt;/strong&gt; &lt;code&gt;your-bucket-name&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; Leave replication as &lt;strong&gt;Automatic&lt;/strong&gt; (Global) and click Create.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Step 3: Conquering the Legacy "Access Scopes" Trap
&lt;/h3&gt;

&lt;p&gt;Your Compute Engine VM holds an IAM Identity. You use the GCP Console (IAM &amp;amp; Admin) to attach the &lt;code&gt;Storage Object Admin&lt;/code&gt; and &lt;code&gt;Secret Manager Secret Accessor&lt;/code&gt; roles to it. You assume you're finished. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🚨 The Legacy Architecture Trap:&lt;/strong&gt; When you attempt to upload a file in Java, you instantly receive a &lt;code&gt;PERMISSION_DENIED&lt;/code&gt; exception. Why? GCE VMs are handcuffed by a legacy system called "Access Scopes." &lt;/p&gt;

&lt;p&gt;When you create a VM with GUI defaults, it applies the "Default access" scope, which throttles storage to &lt;code&gt;devstorage.read_only&lt;/code&gt; and explicitly blocks Secret Manager—&lt;strong&gt;entirely overriding your new IAM admin privileges!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Architect's Fix:&lt;/strong&gt; We must transition the VM to modern IAM validation by granting it the &lt;code&gt;cloud-platform&lt;/code&gt; scope.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run these commands from your local laptop to strip the legacy handcuffs:
&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;# 1. Stop the instance&lt;/span&gt;
gcloud compute instances stop free-tier-vm &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east1-b

&lt;span class="c"&gt;# 2. Grant full API access (Delegating authority completely to IAM)&lt;/span&gt;
gcloud compute instances set-service-account free-tier-vm &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;us-east1-b &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--scopes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://www.googleapis.com/auth/cloud-platform

&lt;span class="c"&gt;# 3. Restart the instance&lt;/span&gt;
gcloud compute instances start free-tier-vm &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-east1-b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Step 4: The Spring Boot Integration
&lt;/h3&gt;

&lt;p&gt;With modern Spring Boot 3.4+ / 4.x, the config data loader evaluates cloud imports incredibly early. To prevent local tests running on your laptop from blindly seeking GCP credentials, use a profile-guarded &lt;code&gt;application.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hello-gcp&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;activate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;on-profile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;!test"&lt;/span&gt;
    &lt;span class="na"&gt;import&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sm://&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, create a controller that demands its configuration strictly from the Secret Manager vault during instantiation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.google.cloud.storage.BlobId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.google.cloud.storage.BlobInfo&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.google.cloud.storage.Storage&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.beans.factory.annotation.Value&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.web.bind.annotation.*&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.web.multipart.MultipartFile&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.io.IOException&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="nd"&gt;@RequestMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api/files"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GcsUploadController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Storage&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;bucketName&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// The sm:// prefix forces a fetch from GCP Secret Manager!&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;GcsUploadController&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Storage&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;@Value&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"${sm://GCS_BUCKET_NAME}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;bucketName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bucketName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bucketName&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@PostMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/upload"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;uploadFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;MultipartFile&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;BlobInfo&lt;/span&gt; &lt;span class="n"&gt;blobInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BlobInfo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newBuilder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;BlobId&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucketName&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getOriginalFilename&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setContentType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContentType&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blobInfo&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBytes&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"Uploaded "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getOriginalFilename&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" directly to secure bucket!"&lt;/span&gt;&lt;span class="o"&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;p&gt;&lt;em&gt;Note: For local Maven builds (&lt;code&gt;mvn clean package&lt;/code&gt;), remember to create a test profile that uses &lt;code&gt;@MockitoBean&lt;/code&gt; and &lt;code&gt;spring.cloud.gcp.core.enabled=false&lt;/code&gt; to bypass these enterprise connections.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 5: End-to-End Verification
&lt;/h3&gt;

&lt;p&gt;Deploy your updated &lt;code&gt;.jar&lt;/code&gt; to your internet-less VM via Identity-Aware Proxy (IAP) as we outlined in past tutorials.&lt;/p&gt;

&lt;p&gt;To verify the file upload, we bridge our local laptop to the private server using port forwarding:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Establish a Local Tunnel:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute ssh free-tier-vm &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; 8080:localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Trigger the Upload via cURL:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s2"&gt;"file=@/path/to/any/local_test.jpg"&lt;/span&gt; http://localhost:8080/api/files/upload
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validate:&lt;/strong&gt; The terminal will return &lt;code&gt;"Uploaded local_test.jpg directly to secure bucket!"&lt;/code&gt;. If you refresh the GCP Console Storage Browser, you will see your file sitting securely in the cloud, completely detached from your VM's tiny hard drive.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;By identifying the "Soft Delete" billing trap, locking credentials inside Secret Manager, tearing down the legacy Compute Engine "Access Scopes," and streaming files natively to GCS, you have built a production-grade file repository without spending a dime.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Part 5&lt;/strong&gt;, we will bring the entire ecosystem under control natively using Infrastructure as Code (Terraform).&lt;/p&gt;

</description>
      <category>gcp</category>
      <category>devops</category>
      <category>architecture</category>
      <category>springboot</category>
    </item>
    <item>
      <title>The Zero-Cost Cloud Engineer Part 3: Decoupled Messaging with Pub/Sub on the Always Free Tier</title>
      <dc:creator>Mohammad Awwaad</dc:creator>
      <pubDate>Sat, 21 Mar 2026 19:48:07 +0000</pubDate>
      <link>https://dev.to/mohamadawwaad/the-zero-cost-cloud-engineer-part-3-decoupled-messaging-with-pubsub-on-the-always-free-tier-18o8</link>
      <guid>https://dev.to/mohamadawwaad/the-zero-cost-cloud-engineer-part-3-decoupled-messaging-with-pubsub-on-the-always-free-tier-18o8</guid>
      <description>&lt;h2&gt;
  
  
  The Zero-Cost Cloud Engineer
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Part 3: Asynchronous Messaging and the "Lite" Trap
&lt;/h2&gt;

&lt;p&gt;In [Part 2], we established our "Secure Island" foundation—a private Compute Engine VM with no external IP and centralized logging. However, a single VM is a monolith. To build a resilient, cloud-native ecosystem, we need decoupled communication. &lt;/p&gt;

&lt;p&gt;This tutorial covers the implementation of an asynchronous messaging loop using Google Cloud Pub/Sub and Spring Boot, strictly within the &lt;strong&gt;10 GB/month Always Free allotment&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 1: Provisioning the Pub/Sub Infrastructure
&lt;/h3&gt;

&lt;p&gt;When configuring messaging in Google Cloud, beginners often face a critical choice in the console: &lt;strong&gt;Pub/Sub&lt;/strong&gt; vs. &lt;strong&gt;Pub/Sub Lite&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🚨 The FinOps Trap:&lt;/strong&gt; "Lite" sounds perfect for small-scale zero-cost projects, but it requires provisioned throughput capacity. Google bills you for that reserved capacity immediately, even if you send zero messages. &lt;strong&gt;Standard Pub/Sub&lt;/strong&gt; is serverless, scales to zero, and includes a generous free tier of 10 GB/month.&lt;/p&gt;

&lt;p&gt;To set up the zero-cost infrastructure in the &lt;a href="https://console.cloud.google.com/" rel="noopener noreferrer"&gt;GCP Console&lt;/a&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Create a Topic:&lt;/strong&gt; Navigate to &lt;strong&gt;Pub/Sub &amp;gt; Topics&lt;/strong&gt; and click &lt;strong&gt;Create Topic&lt;/strong&gt;. Name it &lt;code&gt;demo-topic&lt;/code&gt;. Uncheck the default subscription option.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Create a Subscription:&lt;/strong&gt; Under &lt;strong&gt;Pub/Sub &amp;gt; Subscriptions&lt;/strong&gt;, click &lt;strong&gt;Create Subscription&lt;/strong&gt;. Name it &lt;code&gt;demo-subscription&lt;/code&gt; and link it to your &lt;code&gt;demo-topic&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Configure Delivery:&lt;/strong&gt; Select &lt;strong&gt;Pull&lt;/strong&gt; delivery. Ensure "Exactly-Once Delivery" remains &lt;strong&gt;unchecked&lt;/strong&gt;, as guarantees outside of "at-least-once" delivery can fall outside the Always Free tier. We handle message deduplication (idempotency) within our application logic instead to maintain a zero-cost profile.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Step 2: Granting Identity-Aware Permissions (IAM)
&lt;/h3&gt;

&lt;p&gt;Our VM identity (the Compute Engine default service account) has no inherent permission to interact with Pub/Sub. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if you skip this?&lt;/strong&gt; If you deploy your Spring Boot app now, it will crash immediately with a &lt;code&gt;PermissionDeniedException&lt;/code&gt;. We must explicitly "stamp its passport" for messaging.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Navigate to &lt;strong&gt;IAM &amp;amp; Admin &amp;gt; IAM&lt;/strong&gt; in the GCP Console.&lt;/li&gt;
&lt;li&gt; Locate the principal named: &lt;code&gt;[PROJECT_NUMBER]-compute@developer.gserviceaccount.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Click the pencil icon (&lt;strong&gt;Edit Principal&lt;/strong&gt;) next to it.&lt;/li&gt;
&lt;li&gt; Click &lt;strong&gt;Add Another Role&lt;/strong&gt; and attach these two specific roles:

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;Pub/Sub Publisher&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;Pub/Sub Subscriber&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Step 3: Integrating Spring Boot
&lt;/h3&gt;

&lt;p&gt;To enable Pub/Sub in your Spring Boot application, include the &lt;strong&gt;Spring Cloud GCP Pub/Sub Starter&lt;/strong&gt; in your &lt;code&gt;pom.xml&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Architect's Note: If you are building on a modern stack like **Spring Boot 4.x&lt;/em&gt;* and &lt;strong&gt;Java 25&lt;/strong&gt;, you must align with &lt;strong&gt;Spring Cloud GCP 8.x&lt;/strong&gt; or newer. Earlier versions use legacy package structures incompatible with modern auto-configuration).*&lt;/p&gt;

&lt;h4&gt;
  
  
  The Publisher
&lt;/h4&gt;

&lt;p&gt;Implement a simple REST endpoint to broadcast messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.web.bind.annotation.*&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.google.cloud.spring.pubsub.core.PubSubTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PubSubController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;PubSubTemplate&lt;/span&gt; &lt;span class="n"&gt;pubSubTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;PubSubController&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PubSubTemplate&lt;/span&gt; &lt;span class="n"&gt;pubSubTemplate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pubSubTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pubSubTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/publish"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;publish&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;pubSubTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo-topic"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"Message published: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&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;h4&gt;
  
  
  The Subscriber
&lt;/h4&gt;

&lt;p&gt;Implement a background service to pull and acknowledge messages:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.springframework.stereotype.Service&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.google.cloud.spring.pubsub.core.PubSubTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;jakarta.annotation.PostConstruct&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PubSubSubscriber&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;PubSubTemplate&lt;/span&gt; &lt;span class="n"&gt;pubSubTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;PubSubSubscriber&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PubSubTemplate&lt;/span&gt; &lt;span class="n"&gt;pubSubTemplate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pubSubTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pubSubTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@PostConstruct&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;pubSubTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subscribe&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"demo-subscription"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPubsubMessage&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getData&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toStringUtf8&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Received message: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="c1"&gt;// Process message and acknowledge&lt;/span&gt;
            &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ack&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&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;h3&gt;
  
  
  Step 4: The Secure Island Deployment
&lt;/h3&gt;

&lt;p&gt;Since our VM is isolated from the public internet, we deploy using the &lt;strong&gt;Identity-Aware Proxy (IAP)&lt;/strong&gt; tunnel. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deployment Workflow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cleanup:&lt;/strong&gt; Before uploading, connect to your VM via IAP SSH and ensure a clean environment by stopping existing processes and removing old artifacts (like the jar from Part 2):&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pkill &lt;span class="nt"&gt;-9&lt;/span&gt; java
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/&lt;span class="k"&gt;*&lt;/span&gt;.jar
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Transfer the Artifact:&lt;/strong&gt; Build your application locally (&lt;code&gt;mvn clean package&lt;/code&gt;) and transfer the artifact from your laptop's local terminal:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute scp target/hello-gcp.jar free-tier-vm:~/ &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Run with Observability:&lt;/strong&gt; Execute the application on the VM, piping output to the &lt;code&gt;syslog&lt;/code&gt; file (which we configured the Ops Agent to read in Part 2) to ensure visibility in Cloud Logging:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-jar&lt;/span&gt; ~/hello-gcp.jar 2&amp;gt;&amp;amp;1 | logger &lt;span class="nt"&gt;-t&lt;/span&gt; spring-boot &amp;amp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Step 5: End-to-End Verification
&lt;/h3&gt;

&lt;p&gt;To verify the messaging loop, we need to hit the &lt;code&gt;/publish&lt;/code&gt; REST endpoint to trigger the process. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🚨 The Network Trap:&lt;/strong&gt; Because the VM has no public IP, you cannot simply &lt;code&gt;curl http://&amp;lt;VM_IP&amp;gt;:8080/publish&lt;/code&gt;. The connection will be refused.&lt;/p&gt;

&lt;p&gt;To bridge our local machine to the private VM without exposing it to the internet, we establish a Local Port Forwarding Tunnel:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Establish a Local Tunnel&lt;/strong&gt; from your laptop terminal:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud compute ssh free-tier-vm &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;-L&lt;/span&gt; 8080:localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trigger the Publisher:&lt;/strong&gt; Use your laptop's web browser or local &lt;code&gt;curl&lt;/code&gt; to hit the mapped port:&lt;br&gt;
&lt;code&gt;http://localhost:8080/publish?message=HelloZeroCost&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Validate in Logs Explorer:&lt;/strong&gt; Navigate to the &lt;strong&gt;GCP Logs Explorer&lt;/strong&gt; in the console and query for your specific message:&lt;br&gt;
&lt;code&gt;jsonPayload.message:"Received message: HelloZeroCost"&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the log appears, your decoupled architecture is successfully operating entirely within the "Secure Island" constraints!&lt;/p&gt;




&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;We have successfully moved from a standalone VM to a decoupled messaging architecture without incurring a single cent in costs. By choosing the serverless Standard Pub/Sub model over Lite, managing permissions through IAM, and implementing a robust local-tunnel verification workflow, we maintain a production-grade environment within the Always Free tier.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Part 4&lt;/strong&gt;, we will solve the storage bottleneck of our 30GB persistent disk by offloading files securely to Google Cloud Storage (GCS).&lt;/p&gt;

</description>
      <category>gcp</category>
      <category>microservices</category>
      <category>architecture</category>
      <category>cloud</category>
    </item>
    <item>
      <title>The Zero-Cost Cloud Engineer: Observability and the 'Where are my logs?' Problem</title>
      <dc:creator>Mohammad Awwaad</dc:creator>
      <pubDate>Wed, 11 Mar 2026 03:16:12 +0000</pubDate>
      <link>https://dev.to/mohamadawwaad/the-zero-cost-cloud-engineer-observability-and-the-where-are-my-logs-problem-eb5</link>
      <guid>https://dev.to/mohamadawwaad/the-zero-cost-cloud-engineer-observability-and-the-where-are-my-logs-problem-eb5</guid>
      <description>&lt;h2&gt;
  
  
  The Zero-Cost Cloud Engineer: Mastering GCP on the Always Free Tier
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Part 2: Observability and the "Where are my logs?" Problem
&lt;/h2&gt;

&lt;p&gt;In [Part 1], we successfully spun up our "Always Free" &lt;code&gt;e2-micro&lt;/code&gt; VM, secured it by removing the public IP address, and learned how to access it via the GCP Console's Identity-Aware Proxy (IAP) SSH button.&lt;/p&gt;

&lt;p&gt;But a secure VM is useless if we can't see what's happening inside it. If our application crashes or our tiny 1GB of memory maxes out, we need to know instantly. We need &lt;strong&gt;Observability&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Dream: Centralized Logging
&lt;/h3&gt;

&lt;p&gt;Why not just read logs directly in the SSH terminal? While easier for a single VM, "The Cloud Way" treats servers as disposable cattle, not beloved pets:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;VMs die unexpectedly:&lt;/strong&gt; If your VM crashes and its hard drive is wiped, your terminal logs vanish. If logs are streamed to a central dashboard, you can investigate &lt;em&gt;why&lt;/em&gt; it crashed post-mortem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal Scaling:&lt;/strong&gt; When your app scales to 10 VMs, you can't manually scan 10 SSH terminals. You need one aggregated window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Alerting:&lt;/strong&gt; A central dashboard can automatically email you if the word "Exception" appears. A raw terminal cannot. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is exactly how Netflix and Uber manage their fleets. Google Cloud's centralized dashboard is called &lt;strong&gt;Cloud Logging&lt;/strong&gt; (generously free up to 50 GiB/month) and &lt;strong&gt;Cloud Monitoring&lt;/strong&gt; (free for massive metric allotments). &lt;/p&gt;

&lt;p&gt;We just need to install the &lt;strong&gt;Google Cloud Ops Agent&lt;/strong&gt; on our VM to ship the data there.&lt;/p&gt;

&lt;h3&gt;
  
  
  The GUI Trap: OS Policies on a Tiny VM
&lt;/h3&gt;

&lt;p&gt;Following our rule of prioritizing the "ease of the UI," you naturally navigate to the VM details page in the GCP Console, click the &lt;strong&gt;Observability&lt;/strong&gt; tab, and click the cheerful &lt;strong&gt;Install Agent&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;Instantly, an error pops up: &lt;em&gt;"The following instance is not eligible for installing or updating Ops Agent via OS policy assignment."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Happened?&lt;/strong&gt;&lt;br&gt;
The GUI installer is actually a massive enterprise feature under the hood called "OS Config Policies," designed for forcing software updates across fleets of thousands of VMs simultaneously. An &lt;code&gt;e2-micro&lt;/code&gt; VM is simply too small to run these heavy background synchronization services reliably, so the GUI bulk installer rejects it.&lt;/p&gt;

&lt;p&gt;We have to fall back to the terminal.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Terminal Trap: A VM without Internet
&lt;/h3&gt;

&lt;p&gt;You click the &lt;strong&gt;SSH&lt;/strong&gt; button in the Console to open the terminal and paste the official manual installation command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sSO&lt;/span&gt; https://dl.google.com/cloudagents/add-google-cloud-ops-agent-repo.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;bash add-google-cloud-ops-agent-repo.sh &lt;span class="nt"&gt;--also-install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The terminal hangs. Ten seconds pass. Thirty seconds pass. Nothing prints to the screen. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Happened?&lt;/strong&gt;&lt;br&gt;
Remember our security posture from Part 1? We explicitly removed the External IP address to avoid getting billed. Because of that, the VM has &lt;strong&gt;zero outbound internet access&lt;/strong&gt;. The &lt;code&gt;curl&lt;/code&gt; command is desperately trying to reach the internet to download the script, and the VPC network is silently dropping it.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Solution: Private Google Access
&lt;/h3&gt;

&lt;p&gt;How do private, disconnected servers download critical updates? &lt;/p&gt;

&lt;p&gt;We use a 100% free Google networking feature called &lt;strong&gt;Private Google Access&lt;/strong&gt;. This incredibly powerful feature acts as a secure internal tunnel, allowing isolated VMs to reach Google's internal services (like OS package repositories and Cloud Storage) without needing the public internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let's fix it using the UI:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the GCP Console search bar, type &lt;strong&gt;VPC networks&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click on the subnet matching your VM's region (e.g., &lt;code&gt;default&lt;/code&gt; in &lt;code&gt;us-east1&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;EDIT&lt;/strong&gt; at the top.&lt;/li&gt;
&lt;li&gt;Locate the &lt;strong&gt;Private Google Access&lt;/strong&gt; toggle, turn it &lt;strong&gt;ON&lt;/strong&gt;, and hit Save. &lt;em&gt;(Ignore any promotional popups or overlapping IP warnings).&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Go back to your VM's SSH terminal, cancel the hanging command with &lt;code&gt;Ctrl+C&lt;/code&gt;, and run it again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It immediately succeeds! Google's internal traffic routing kicks in. If you wait a minute and refresh the &lt;strong&gt;Observability&lt;/strong&gt; tab on the VM details page, beautiful CPU and Memory graphs will begin populating. &lt;/p&gt;
&lt;h3&gt;
  
  
  Deploying Spring Boot
&lt;/h3&gt;

&lt;p&gt;Let's test our setup.&lt;br&gt;
Instead of dealing with Maven commands on a tiny VM—which wouldn't work anyway because our secure VM has no internet access to download dependencies from Maven Central—use the incredibly easy &lt;a href="https://start.spring.io" rel="noopener noreferrer"&gt;Spring Initializr UI (start.spring.io)&lt;/a&gt; or your local IDE (IntelliJ/Eclipse) to generate a basic Spring Boot project on your laptop. Add a simple &lt;code&gt;@Scheduled&lt;/code&gt; task that prints a log every 5 seconds.&lt;/p&gt;

&lt;p&gt;Build the &lt;code&gt;.jar&lt;/code&gt; locally on your laptop, and transfer it over the secure IAP tunnel directly from your local terminal:&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 scp target/demo-0.0.1-SNAPSHOT.jar &amp;lt;YOUR_VM_NAME&amp;gt;:~/ &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;YOUR_VM_ZONE&amp;gt; &lt;span class="nt"&gt;--tunnel-through-iap&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Back in the VM's SSH window, run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-jar&lt;/span&gt; ~/demo-0.0.1-SNAPSHOT.jar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Final Boss: "Where are my logs?"
&lt;/h3&gt;

&lt;p&gt;The app prints logs securely to your SSH terminal. Excellent.&lt;br&gt;
Now, open the &lt;strong&gt;Logs Explorer&lt;/strong&gt; in the GCP Console to view them centrally.&lt;/p&gt;

&lt;p&gt;The page is entirely blank. Your Spring Boot logs are nowhere to be found. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Happened?&lt;/strong&gt;&lt;br&gt;
The Ops Agent doesn't monitor foreground terminal processes. Furthermore, in newer versions of the agent, Google &lt;strong&gt;turned off text logging by default&lt;/strong&gt; to protect massive clients from accidentally sending petabytes of logs and racking up huge bills. It only collects metrics out of the box.&lt;/p&gt;

&lt;p&gt;We must forcefully configure it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First, stop your terminal process and change how you execute the app. Pipe the standard output to the central Linux diary (&lt;code&gt;/var/log/syslog&lt;/code&gt;) using the &lt;code&gt;logger&lt;/code&gt; tool:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   java &lt;span class="nt"&gt;-jar&lt;/span&gt; ~/demo-0.0.1-SNAPSHOT.jar 2&amp;gt;&amp;amp;1 | logger &lt;span class="nt"&gt;-t&lt;/span&gt; spring-boot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Second, tell the Ops Agent to listen to the syslog. In the SSH terminal, edit &lt;code&gt;/etc/google-cloud-ops-agent/config.yaml&lt;/code&gt; to include exactly this:
&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;logging&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
     &lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;syslog&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
         &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;files&lt;/span&gt;
         &lt;span class="na"&gt;include_paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
         &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/log/syslog&lt;/span&gt;
     &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
       &lt;span class="na"&gt;pipelines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
         &lt;span class="na"&gt;default_pipeline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
           &lt;span class="na"&gt;receivers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;syslog&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Restart the agent: &lt;code&gt;sudo systemctl restart google-cloud-ops-agent&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, go back to the Logs Explorer and query for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;logName=~".*syslog"
jsonPayload.message=~"spring-boot"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Boom!&lt;/strong&gt; Your Java application is now reporting enterprise-grade, centralized logs entirely from a secure, internet-less server—and you haven't been charged a single cent. &lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Part 3&lt;/strong&gt;, we'll explore Pub/Sub and decouple our architecture.&lt;/p&gt;

</description>
      <category>gcp</category>
      <category>java</category>
      <category>observability</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The Zero-Cost Cloud Engineer: The $300 Dilemma and Provisioning the Free VM</title>
      <dc:creator>Mohammad Awwaad</dc:creator>
      <pubDate>Sun, 08 Mar 2026 19:17:19 +0000</pubDate>
      <link>https://dev.to/mohamadawwaad/the-zero-cost-cloud-engineer-the-300-dilemma-and-provisioning-the-free-vm-1llm</link>
      <guid>https://dev.to/mohamadawwaad/the-zero-cost-cloud-engineer-the-300-dilemma-and-provisioning-the-free-vm-1llm</guid>
      <description>&lt;h2&gt;
  
  
  The Zero-Cost Cloud Engineer: Mastering GCP on the Always Free Tier
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Part 1: The $300 Dilemma and Provisioning the Free VM
&lt;/h2&gt;

&lt;p&gt;It’s a tale as old as time for developers: You sign up for Google Cloud Platform (GCP). You see that glorious "$300 Free Trial Credit" banner. You think of all the massive, distributed microservice architectures you are going to build over the weekend to learn the cloud.&lt;/p&gt;

&lt;p&gt;Then, life happens. Work gets busy. You blink, and 90 days have passed. You log back in, and your $300 credit has silently expired. You didn't even have time to spin up a single database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't panic.&lt;/strong&gt; This is actually a blessing in disguise.&lt;/p&gt;

&lt;p&gt;When you have $300 of "funny money," you don't learn how to architect systems efficiently. You over-provision RAM. You leave databases running overnight. You accidentally pay for public IP addresses you don't need. &lt;/p&gt;

&lt;p&gt;By losing the credit, you are now forced to explore the &lt;strong&gt;"Always Free" Tier&lt;/strong&gt;. This tier never expires, but it imposes strict limits on disk space, memory, and network usage. Building a functioning ecosystem inside these constraints makes you a significantly better Cloud Engineer because you are forced to understand exactly what every resource costs and how to optimize it.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 1: Designing the Zero-Cost VM
&lt;/h3&gt;

&lt;p&gt;Our entry point to the cloud is a core compute instance (a VM). However, if you simply click "Create VM" and accept the defaults, Google will aggressively charge you.&lt;/p&gt;

&lt;p&gt;To stay 100% free, your VM must strictly adhere to these rules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Machine Type:&lt;/strong&gt; &lt;code&gt;e2-micro&lt;/code&gt; (2 vCPUs, 1 GB memory)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Region:&lt;/strong&gt; Must be in an eligible US region like &lt;code&gt;us-east1&lt;/code&gt;, &lt;code&gt;us-west1&lt;/code&gt;, or &lt;code&gt;us-central1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Disk:&lt;/strong&gt; 30 GB standard persistent disk (&lt;strong&gt;CRITICAL:&lt;/strong&gt; Ensure this is &lt;em&gt;not&lt;/em&gt; an SSD balanced disk, which is the default in the UI and will immediately start charging you hourly!).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Warding Off the Hidden Costs
&lt;/h3&gt;

&lt;p&gt;There are two major traps that catch beginners and start generating bills on the very first day:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Default SSD Trap:&lt;/strong&gt; When you create a VM, GCP defaults the boot disk to a "Balanced persistent disk" (SSD). The Always Free tier &lt;em&gt;only&lt;/em&gt; covers a basic "Standard persistent disk." If you don't manually change this dropdown, you will be billed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The External IP Trap:&lt;/strong&gt; By default, GCP assigns your new VM an &lt;strong&gt;External IP Address&lt;/strong&gt; (a public IPv4 address). As of 2024, Google charges for all public IPv4 addresses, regardless of whether you are actually serving web traffic. Furthermore, having a public IP means bots will immediately start probing your tiny 1GB VM, eating up your precious CPU cycles.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The Golden Rule:&lt;/strong&gt; We prefer the GCP graphical UI over command-line interfaces for provisioning infrastructure as beginners. CLI defaults can easily sneak in public IPs if you aren't paying close attention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Preparing for Java 25 (The Startup Script Catch-22)
&lt;/h3&gt;

&lt;p&gt;To run a Spring Boot application, our VM needs Java. The most efficient way to install software on a new VM is using a &lt;strong&gt;Startup Script&lt;/strong&gt;. We want to use Debian 13 (Trixie) and install the latest Java 25 (LTS).&lt;/p&gt;

&lt;p&gt;Here is the exact startup script we need:&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;# 1. Set up 2GB of swap space (crucial for 1GB VMs)&lt;/span&gt;
fallocate &lt;span class="nt"&gt;-l&lt;/span&gt; 2G /swapfile
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /swapfile
mkswap /swapfile
swapon /swapfile
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'/swapfile none swap sw 0 0'&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/fstab

&lt;span class="c"&gt;# 2. Install dependencies &amp;amp; Add Adoptium repository&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; wget apt-transport-https gnupg
wget &lt;span class="nt"&gt;-qO&lt;/span&gt; - https://packages.adoptium.net/artifactory/api/gpg/key/public | gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/trusted.gpg.d/adoptium.gpg &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"deb https://packages.adoptium.net/artifactory/deb &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'/^VERSION_CODENAME/{print$2}'&lt;/span&gt; /etc/os-release&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; main"&lt;/span&gt; | &lt;span class="nb"&gt;tee&lt;/span&gt; /etc/apt/sources.list.d/adoptium.list

&lt;span class="c"&gt;# 3. Install Java 25 (LTS)&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; temurin-25-jdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;🚨 The Catch-22:&lt;/strong&gt; If we provision the VM with &lt;em&gt;No External IP&lt;/em&gt; from the start, this script will immediately fail. The VM won't be able to reach &lt;code&gt;deb.debian.org&lt;/code&gt; or &lt;code&gt;packages.adoptium.net&lt;/code&gt; to download Java.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Workaround:&lt;/strong&gt; We will provision the VM with a temporary External IP, let the script run, and then instantly delete the IP so we don't get billed for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Provisioning via the GCP Console
&lt;/h3&gt;

&lt;p&gt;Here is how to safely provision your VM and apply the workaround:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the &lt;a href="https://console.cloud.google.com" rel="noopener noreferrer"&gt;GCP Console&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Compute Engine &amp;gt; VM instances&lt;/strong&gt; and click &lt;strong&gt;Create Instance&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Name your instance (e.g., &lt;code&gt;free-tier-vm&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Select &lt;code&gt;us-east1&lt;/code&gt; as your region.&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Machine Configuration&lt;/strong&gt;, change the series to &lt;code&gt;E2&lt;/code&gt; and the machine type to &lt;code&gt;e2-micro&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Scroll down to the &lt;strong&gt;Boot Disk&lt;/strong&gt; section. Click Change. Select the &lt;strong&gt;Debian 13&lt;/strong&gt; image, ensure the disk type is "Standard Persistent Disk", and set the size to 30 GB.&lt;/li&gt;
&lt;li&gt;Expand the &lt;strong&gt;Advanced Options&lt;/strong&gt;, then expand &lt;strong&gt;Management&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Automation&lt;/strong&gt; &amp;gt; &lt;strong&gt;Startup script&lt;/strong&gt; text box, paste the Bash script from Step 3.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Temporarily&lt;/em&gt; leave the networking settings alone (let it assign a public IPv4 address).&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create&lt;/strong&gt; at the bottom.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Wait roughly &lt;strong&gt;2 minutes&lt;/strong&gt; for the VM to fully boot and the backend script to finish installing Java.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Securing the Island (Stripping the IP)
&lt;/h3&gt;

&lt;p&gt;Now, we must immediately lock down the VM to prevent getting billed for the public IP.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go back to your VM's details page.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;EDIT&lt;/strong&gt; at the top top.&lt;/li&gt;
&lt;li&gt;Scroll down to &lt;strong&gt;Network interfaces&lt;/strong&gt; and click the default network.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;External IPv4 address&lt;/strong&gt;, change it from "Ephemeral" to &lt;strong&gt;None&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your VM is now a perfectly secure, completely free, Java 25-equipped island.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Accessing the Disconnected Island
&lt;/h3&gt;

&lt;p&gt;How do you, the developer, access a VM with no public IP?&lt;/p&gt;

&lt;p&gt;If you try to use standard terminal SSH on your laptop (&lt;code&gt;ssh user@&amp;lt;ip&amp;gt;&lt;/code&gt;), it will fail because there is no public IP to hit.&lt;/p&gt;

&lt;p&gt;Instead, go back to your &lt;strong&gt;VM instances&lt;/strong&gt; page in the GCP Console. Next to your new VM, click the &lt;strong&gt;SSH&lt;/strong&gt; button.&lt;/p&gt;

&lt;p&gt;A new browser window will open, and a terminal will appear. You are in! &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connecting via your Local Terminal&lt;/strong&gt;&lt;br&gt;
If you prefer to work from your local laptop terminal instead of a browser pop-up, Google provides a seamless command that leverages this exact same secure tunnel via the gcloud CLI:&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 free-tier-vm &lt;span class="nt"&gt;--zone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;YOUR_REGION&amp;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;em&gt;(Replace &lt;code&gt;&amp;lt;YOUR_REGION&amp;gt;&lt;/code&gt; with the zone your VM is in, e.g., &lt;code&gt;us-east1-b&lt;/code&gt;)&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How does this magic work?&lt;/strong&gt;&lt;br&gt;
Whether using the browser button or the &lt;code&gt;gcloud ssh&lt;/code&gt; command, Google is using a free service under the hood called &lt;strong&gt;Identity-Aware Proxy (IAP)&lt;/strong&gt;. It securely tunnels your traffic through Google's internal backbone directly into your private VM, all without exposing the VM to the public internet.&lt;/p&gt;

&lt;p&gt;You now have a perfectly secure, absolutely free cloud server.&lt;/p&gt;

&lt;p&gt;But having a running VM is just the "entry ticket." To truly learn Google Cloud, you should use this isolated VM as a secure &lt;strong&gt;hub&lt;/strong&gt; to explore the other "Always Free" services natively, building out a production-like ecosystem without ever generating a monthly bill.&lt;/p&gt;

&lt;p&gt;In &lt;strong&gt;Part 2&lt;/strong&gt;, we will use our new hub to solve absolute necessity: Observability, and we'll learn exactly why your Java applications seem to magically hide their logs when deployed to a secure cloud VM.&lt;/p&gt;

</description>
      <category>gcp</category>
      <category>devops</category>
      <category>architecture</category>
      <category>springboot</category>
    </item>
    <item>
      <title>I Built a Production-Ready Spring Boot Architecture (So You Don't Have To)</title>
      <dc:creator>Mohammad Awwaad</dc:creator>
      <pubDate>Fri, 06 Mar 2026 10:38:07 +0000</pubDate>
      <link>https://dev.to/mohamadawwaad/i-built-a-production-ready-spring-boot-architecture-so-you-dont-have-to-2mba</link>
      <guid>https://dev.to/mohamadawwaad/i-built-a-production-ready-spring-boot-architecture-so-you-dont-have-to-2mba</guid>
      <description>&lt;h2&gt;
  
  
  I Built a Production-Ready Spring Boot Microservices Architecture (So You Don't Have To)
&lt;/h2&gt;

&lt;h2&gt;
  
  
  The Problem: The "Hello World" Trap
&lt;/h2&gt;

&lt;p&gt;Every time we start a new microservices project, we fall into the same trap. Setting up a basic Spring Boot app is fast—but taking it to &lt;strong&gt;production readiness&lt;/strong&gt; is exhausting. &lt;/p&gt;

&lt;p&gt;You need to configure an API Gateway, set up an Identity Provider (like Keycloak), figure out how to securely serve both a web app and mobile clients, configure distributed tracing, establish a robust PostgreSQL + Redis baseline, and wire it all together in Docker. Suddenly, you've burned two weeks of dev time before writing a single line of business logic.&lt;/p&gt;

&lt;p&gt;I got tired of repeating this process, so I built an &lt;strong&gt;Enterprise Spring Microservices Template&lt;/strong&gt; that solves these infrastructural headaches out of the box.&lt;/p&gt;

&lt;p&gt;In this article, I want to walk through the architectural decisions behind this template, specifically focusing on the &lt;strong&gt;"Two-Lane" Security Architecture&lt;/strong&gt; and &lt;strong&gt;Defense in Depth&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ The "Two-Lane" Architecture: BFF for Web, JWT for Mobile
&lt;/h2&gt;

&lt;p&gt;One of the biggest security debates in modern web development is where to store your authentication tokens. Storing JWTs in &lt;code&gt;localStorage&lt;/code&gt; in the browser makes your app vulnerable to Cross-Site Scripting (XSS). &lt;/p&gt;

&lt;p&gt;To solve this, this architecture implements the &lt;strong&gt;Backend-For-Frontend (BFF)&lt;/strong&gt; pattern for browser clients, while keeping standard Token-based access for mobile clients.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lane 1: The Web Application (Angular 18)
&lt;/h3&gt;

&lt;p&gt;Web applications should never touch the actual JWT. Instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Angular HTTP interceptor prefixes all API calls with &lt;code&gt;/bff/api/{service}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The BFF handles the OAuth2 Login flow with Keycloak and receives the tokens.&lt;/li&gt;
&lt;li&gt;The BFF stores the tokens securely and issues an HttpOnly, Secure Session Cookie to the browser.&lt;/li&gt;
&lt;li&gt;When Angular makes a request, the BFF attaches the saved JWT and forwards the request to the Spring Cloud Gateway.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Lane 2: The Mobile Application
&lt;/h3&gt;

&lt;p&gt;Mobile apps don't suffer from the same browser XSS vulnerabilities. iOS and Android have secure enclaves (like Keychain) to store tokens. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The mobile app authenticates directly with Keycloak and receives the JWT.&lt;/li&gt;
&lt;li&gt;The mobile app makes requests directly to the API Gateway using the Authorization Header (&lt;code&gt;Bearer &amp;lt;token&amp;gt;&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🛡️ Defense in Depth: Zero-Trust Security
&lt;/h2&gt;

&lt;p&gt;Just securing the outer perimeter isn't enough anymore. If an attacker breaches the API Gateway, your internal microservices shouldn't blindly trust the incoming request. &lt;/p&gt;

&lt;p&gt;This template enforces a strict &lt;strong&gt;Defense in Depth&lt;/strong&gt; strategy using JWKS (JSON Web Key Sets).&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Gateway Validation
&lt;/h3&gt;

&lt;p&gt;The Spring Cloud Gateway acts as the first line of defense. It completely strips the BFF prefix and restructures the path.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;UI Request:&lt;/code&gt; -&amp;gt; &lt;code&gt;/bff/api/profile-service/users&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BFF Forwards:&lt;/code&gt; -&amp;gt; &lt;code&gt;/profile-service/users&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Gateway Rewrites:&lt;/code&gt; -&amp;gt; &lt;code&gt;/api/users&lt;/code&gt; (Routes to &lt;code&gt;profile-service&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the Gateway, the JWT signature and basic claims are validated before the request ever touches an internal network.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Service-Level Validation (Microsegmentation)
&lt;/h3&gt;

&lt;p&gt;Even though the Gateway validated the token, every individual microservice validates it &lt;em&gt;again&lt;/em&gt;. &lt;br&gt;
Each Spring Boot service fetches the JWKS from Keycloak to cryptographically verify the token. Furthermore, fine-grained &lt;strong&gt;Role-Based Access Control (RBAC)&lt;/strong&gt; is enforced at the method or endpoint level within the specific service. &lt;/p&gt;




&lt;h2&gt;
  
  
  ⏱️ Solving the Token Expiry Edge Case (Proactive Token Refresh)
&lt;/h2&gt;

&lt;p&gt;Have you ever experienced a bug where a user clicks a button, the request goes through, but the request fails mid-flight because the token expired a millisecond too soon?&lt;/p&gt;

&lt;p&gt;This template addresses token lifecycle issues with a &lt;strong&gt;60-Second Proactive Refresh Buffer&lt;/strong&gt;.&lt;br&gt;
When the client (BFF or Mobile app) prepares to make a request, it checks the token's expiration. If the token is set to expire in the next 60 seconds, it actively requests a refresh &lt;em&gt;before&lt;/em&gt; dispatching the API call. &lt;/p&gt;

&lt;p&gt;This eliminates mid-flight 401 Unauthorized errors and provides a perfectly smooth user experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 Observability First
&lt;/h2&gt;

&lt;p&gt;When a request travels through an Angular app -&amp;gt; BFF -&amp;gt; Gateway -&amp;gt; Microservice, finding out where it failed is a nightmare without traceability.&lt;/p&gt;

&lt;p&gt;This architecture has &lt;strong&gt;Observability included by default&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Micrometer Tracing&lt;/strong&gt;: Generates and propagates standard &lt;code&gt;traceId&lt;/code&gt; and &lt;code&gt;spanId&lt;/code&gt; across all services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loki / Grafana&lt;/strong&gt;: Centralized structured logging. You can query logs by &lt;code&gt;traceId&lt;/code&gt; to instantly see the entire lifecycle of a request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zipkin&lt;/strong&gt;: Visualizes request latency and bottlenecks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need to manually inject UUIDs into your logs; the context is already handled by the framework.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Get Started Immediately
&lt;/h2&gt;

&lt;p&gt;If you want to skip the boilerplate phase of your next project and start directly with a scalable, secure, and observable baseline, you can use this template.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Stack:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Java 25 (LTS), Spring Boot 4.x, Spring Cloud Gateway&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Angular 18&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure:&lt;/strong&gt; Keycloak, PostgreSQL, Redis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DevOps:&lt;/strong&gt; Fully containerized with Docker / Maven&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔗 &lt;strong&gt;&lt;a href="https://github.com/mohamad-awwaad/Enterprise-Spring-Microservices-Template" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
🔗 &lt;strong&gt;&lt;a href="https://gitlab.com/innovaxons/enterprise-spring-microservices-template" rel="noopener noreferrer"&gt;GitLab Repository&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you found this architecture breakdown helpful, consider giving the repository a ⭐️ on GitHub! I'd love to hear your thoughts on the Two-Lane approach in the comments.&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>java</category>
      <category>microservices</category>
      <category>security</category>
    </item>
  </channel>
</rss>
