DEV Community

Nicholas
Nicholas

Posted on

Automated Earth Observation: Deploying a Gemma 4 26B Satellite Image Analysis Engine on Cloud Run

As a Google Developer Expert for Earth Engine and community lead in Nairobi, my daily workflow revolves around turning satellite imagery into actionable environmental intelligence. Traditional computer vision architectures—like MobileNetV2 or ResNet—are adequate for basic land-cover classification. However, when monitoring climate risk, deforestation trends, or agricultural health, simple labels like "forest" or "crop" fall short. We need complex spatial reasoning, context-aware analysis, and detailed narrative reporting.

In this article, I will walk you through building and deploying a cloud-native, geospatial image analysis application. We will transition from standard classification models to Gemma 4 26B (A4B), an open Mixture-of-Experts (MoE) multimodal model, paired with Google AI Studio, Python/Flask, GitHub Actions, and Google Cloud Run.

*1. System Architecture: *

Below is the end-to-end architecture powering our continuous integration and deployment pipeline, from prompt ideation in AI Studio to serverless deployment on Google Cloud Run.

2. Prerequisites & GCP Environment Setup

Before building the application, ensure your developer environment and Google Cloud project are configured.

Prerequisites

  1. Google Cloud Platform (GCP) Account with an active billing project.
  2. Google AI Studio API Key: Issued via Google AI Studio.
  3. Google Cloud CLI (gcloud) installed and authenticated locally.
  4. Docker installed locally for container testing.

GCP Project Setup
Run the following commands in your terminal to initialize your environment:

# 1. Set your GCP project ID
export PROJECT_ID="i-dropped-my-gcp-project-id"
gcloud config set project $PROJECT_ID

# 2. Enable required GCP Service APIs
gcloud services enable \
    run.googleapis.com \
    artifactregistry.googleapis.com \
    secretmanager.googleapis.com \
    iamcredentials.googleapis.com

# 3. Create Artifact Registry repository for Docker images
gcloud artifacts repositories create geo-apps \
    --repository-format=docker \
    --location=us-central1 \
    --description="Docker repository for satellite image analyzer"

# 4. Store your Gemini API Key in Secret Manager
echo -n "YOUR_GEMINI_API_KEY" | gcloud secrets create GEMINI_API_KEY \
    --data-file=- \
    --replication-policy="automatic"
Enter fullscreen mode Exit fullscreen mode

3. Ideation in Google AI Studio
Google AI Studio provides a fast prototyping sandbox for prompt engineering and model evaluation. Rather than manually tuning vision prompts in code, I used Gemini as an architectural co-pilot within AI Studio to test multimodal analysis on European Space Agency Sentinel-2 and Landsat optical imagery.

By tweaking system instructions, temperature (0.2 for structured outputs), and output formats inside AI Studio, I obtained the exact structured JSON schema required before exporting the prompt logic directly to Python using the officialGoogle Gen AI SDK (google-genai`).

4. Upgrading the Machine Learning Engine to Gemma 4 26B A4B
Standard computer vision models (like MobileNetV2) perform rigid 1,000-class ImageNet lookups. By upgrading our inference engine to Gemma 4 26B A4B—a Mixture-of-Experts model that activates ~3.8B parameters per token—we enable complex geospatial reasoning over satellite imagery.

Project Directory Structure


image_analyzer_app/
├── .github/
│ └── workflows/
│ └── deploy.yml # CI/CD GitHub Actions Pipeline
├── app.py # Flask server & Gemma 4 vision logic
├── requirements.txt # Python production dependencies
├── Dockerfile # Optimized container configuration
└── templates/
└── index.html # Web UI for image analysis

  1. Updated Dependencies (requirements.txt) We replace heavy local machine learning dependencies like tensorflow with lightweight API wrappers, keeping our Cloud Run container image under 150MB.


Flask==3.0.2
google-genai==1.0.0
Pillow==10.2.0
gunicorn==21.2.0
python-dotenv==1.0.1

  1. Upgraded Python Backend (app.py) This script initializes the google-genai client, reads uploaded satellite imagery directly as bytes, and requests structured vision reasoning from gemma-4-26b-a4b-it.

`
import os
import io
from flask import Flask, request, render_template, jsonify
from google import genai
from google.genai import types
from PIL import Image

app = Flask(name)

Initialize the Google Gen AI Client

Fetches API key automatically from GOOGLE_API_KEY environment variable

api_key = os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
client = genai.Client(api_key=api_key)

SYSTEM_INSTRUCTION = """
You are an expert Remote Sensing Specialist and Geospatial Data Scientist.
Analyze the uploaded image (satellite, aerial, or environmental capture) and provide:

  1. Primary Land Cover / Object Identification
  2. Environmental & Geospatial Context (e.g., vegetation health, urban density, water body status)
  3. Anomalies or Climate/Disaster Risk Factors (e.g., flood risk, deforestation, drought signs) Keep responses structured, analytical, concise, and actionable. """

@app.route('/', methods=['GET'])
def index():
return render_template('index.html')

@app.route('/analyze', methods=['POST'])
def analyze():
if 'file' not in request.files:
return jsonify({'error': 'No file uploaded'}), 400

file = request.files['file']
if file.filename == '':
    return jsonify({'error': 'No file selected'}), 400

try:
    # Read and validate image
    img_bytes = file.read()
    pil_image = Image.open(io.BytesIO(img_bytes)).convert("RGB")

    # Execute vision inference using Gemma 4 26B A4B
    response = client.models.generate_content(
        model="gemma-4-26b-a4b-it",
        contents=[
            pil_image,
            "Perform detailed geospatial and environmental analysis on this image."
        ],
        config=types.GenerateContentConfig(
            system_instruction=SYSTEM_INSTRUCTION,
            temperature=0.2,
            max_output_tokens=1024
        )
    )

    return jsonify({'analysis': response.text})

except Exception as e:
    return jsonify({'error': str(e)}), 500
Enter fullscreen mode Exit fullscreen mode

if name == 'main':
port = int(os.environ.get('PORT', 8080))
app.run(host='0.0.0.0', port=port)
`

  1. Updated Frontend Interface (templates/index.html) The modern Tailwind UI renders Markdown results returned by the model:

`
<!DOCTYPE html>




Geospatial Satellite Analyzer | Gemma 4







Earth Observation Intelligence


Powered by Gemma 4 26B A4B & Google Cloud Run


    <div class="bg-slate-800 p-8 rounded-2xl shadow-2xl border border-slate-700">
        <div class="mb-6">
            <label class="block mb-2 text-sm font-medium text-slate-300">Upload Satellite / Aerial Image</label>
            <input type="file" id="imageInput" accept="image/*" 
                class="block w-full text-sm text-slate-400 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-emerald-500 file:text-white hover:file:bg-emerald-600 cursor-pointer">
        </div>

        <button onclick="uploadImage()" id="btn" class="w-full bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-3 rounded-xl transition duration-200">
            Run Geospatial Analysis
        </button>

        <div id="loading" class="hidden mt-6 text-center text-emerald-400 animate-pulse">
            Analyzing imagery with Gemma 4 26B...
        </div>

        <div id="results" class="mt-8 hidden bg-slate-900/80 p-6 rounded-xl border border-slate-700">
            <h2 class="text-xl font-semibold mb-4 text-emerald-400 border-b border-slate-700 pb-2">Analysis Report</h2>
            <div id="analysisContent" class="prose prose-invert max-w-none text-slate-200 leading-relaxed space-y-2"></div>
        </div>
    </div>
</div>

<script>
    async function uploadImage() {
        const input = document.getElementById('imageInput');
        const btn = document.getElementById('btn');
        const loading = document.getElementById('loading');
        const resultsDiv = document.getElementById('results');
        const content = document.getElementById('analysisContent');

        if (!input.files[0]) return alert("Please select an image file first!");

        btn.disabled = true;
        loading.classList.remove('hidden');
        resultsDiv.classList.add('hidden');
        content.innerHTML = '';

        const formData = new FormData();
        formData.append('file', input.files[0]);

        try {
            const response = await fetch('/analyze', { method: 'POST', body: formData });
            const data = await response.json();

            if (data.error) {
                alert("Error: " + data.error);
            } else {
                content.innerHTML = marked.parse(data.analysis);
                resultsDiv.classList.remove('hidden');
            }
        } catch (err) {
            alert("Failed to process request.");
        } finally {
            btn.disabled = false;
            loading.classList.add('hidden');
        }
    }
</script>



`

  1. Production Containerization (Dockerfile) We utilize an optimized multi-stage build running gunicorn for web delivery:

`
FROM python:3.11-slim

Prevent Python from writing pyc files to disc and enable unbuffered logging

ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PORT=8080

WORKDIR /app

Install system dependencies required for image processing

RUN apt-get update && apt-get install -y --no-install-recommends \
libgl1-mesa-glx \
libglib2.0-0 \
&& rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

Run application using production WSGI server

CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 app:app
`

5. Continuous Integration & Deployment (CI/CD Pipeline)
To maintain a production-grade workflow, manual command-line deployments should be replaced with keyless, automated deployments triggered via GitHub Actions using Workload Identity Federation.

1. Configure Workload Identity Federation
Execute these commands locally to authorize GitHub Actions without storing long-lived GCP service account keys in your repository secrets:

`

Create Service Account for GitHub Actions

gcloud iam service-accounts create github-runner \
--display-name="GitHub Actions Deployer"

Grant roles to the Service Account

gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:github-runner@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/run.developer"

gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:github-runner@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/artifactregistry.writer"

gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:github-runner@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/iam.serviceAccountUser"

Allow Secret Manager accessor access to Cloud Run service agent

gcloud secrets add-iam-policy-binding GEMINI_API_KEY \
--member="serviceAccount:github-runner@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
`

2. GitHub Actions Workflow (.github/workflows/deploy.yml)
Create this file in your project repository to automate builds on every push to main:

`
name: Build and Deploy to Cloud Run

on:
push:
branches:
- main

env:
PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
REGION: us-central1
GAR_REPO: geo-apps
SERVICE_NAME: satellite-analyzer

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4

  - name: Authenticate to Google Cloud
    uses: google-github-actions/auth@v2
    with:
      workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }}
      service_account: ${{ secrets.GCP_WIF_SA }}

  - name: Set up Cloud SDK
    uses: google-github-actions/setup-gcloud@v2

  - name: Authorize Docker for Artifact Registry
    run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev --quiet

  - name: Build and Push Docker Image
    run: |
      IMAGE_URI="${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.GAR_REPO }}/${{ env.SERVICE_NAME }}:${{ github.sha }}"
      docker build -t $IMAGE_URI .
      docker push $IMAGE_URI
      echo "IMAGE_URI=$IMAGE_URI" >> $GITHUB_ENV

  - name: Deploy to Cloud Run
    run: |
      gcloud run deploy ${{ env.SERVICE_NAME }} \
        --image=${{ env.IMAGE_URI }} \
        --region=${{ env.REGION }} \
        --platform=managed \
        --allow-unauthenticated \
        --memory=1Gi \
        --cpu=1 \
        --set-secrets=GEMINI_API_KEY=GEMINI_API_KEY:latest

`
6. Manual Cloud Run Deployment Option
If you prefer to deploy directly from your local terminal before enabling GitHub Actions, run:


gcloud run deploy satellite-analyzer \
--source . \
--region us-central1 \
--allow-unauthenticated \
--memory 1Gi \
--cpu 1 \
--set-secrets GEMINI_API_KEY=GEMINI_API_KEY:latest

7. Final Results
Transitioning from local ML inference engines to Gemma 4 26B A4B hosted serverlessly on Cloud Run provides several structural advantages:

Lightweight Deployment Footprint: Container image sizes are reduced significantly by offloading heavy parameter processing to serverless APIs.

Context-Aware Spatial Intelligence: Rather than outputting simple classification labels, the app returns structured analysis covering environmental health, canopy stress, and disaster risk indicators.

Automated Serverless Architecture: Cloud Run scales automatically from zero requests to handle traffic spikes smoothly, keeping costs directly tied to active usage.

Top comments (0)