14 min read

    26 - Hands-on Lab - Full-Stack App with DB & GCS

    gcpcloudlabcapstonecloud-runfirestoregcsfullstack

    Welcome to Day 26 of Learn GCP in 30 Days! Today is your Week 4 Capstone Hands-on Lab.

    Over the last five days, you mastered Google Cloud's core storage and database pillars: Cloud Storage (GCS) for files, Cloud SQL & Spanner for relational data, Firestore for realtime documents, and Memorystore for caching. Today, you will connect all the pieces into a live, working Cloud Photo Journal Web Application!

    🎯

    Today's Goal Today, you will build and deploy a real-world, production-ready Full-Stack application from scratch. You will deploy a containerized Python web frontend on Cloud Run, store uploaded photo files in Google Cloud Storage (GCS), save post captions and metadata in Firestore, and view your live app running globally over HTTPSβ€”all backed by our strict $0.00 credit safety guarantee!


    πŸ—οΈ The Problem: The Fragile "All-in-One" Server

    Before cloud architectures existed, how did developers build file-upload web apps?

    mermaid

    What Existed Previously:

    In traditional web hosting, developers ran their web server, database, and file storage all on the same virtual machine:

    • Files were saved to a local folder like /var/uploads/.
    • The database ran locally on localhost:3306.

    Problems Faced:

    • πŸ’₯ Single Point of Total Failure: If the VM crashed or the hard disk corrupted, your website, your database, and your customers' uploaded photos were destroyed simultaneously.
    • 🚫 Impossible to Scale Horizontally: If you added a second web server to handle traffic, Server B could not see files uploaded to Server A's local hard drive!
    • πŸ’Έ Wasted Storage & High Costs: You had to provision massive, expensive virtual disks in advance just in case users uploaded large video or image files.

    How Modern Cloud Architecture Solves It (The 3-Tier Decoupled App):

    mermaid
    1. Stateless Compute on Cloud Run: The web app container holds zero local state. It can instantly scale from 0 to 1,000 instances and back to 0 ($0.00 idle cost).
    2. Infinite File Storage on GCS: Photos are streamed directly to a Google Cloud Storage bucket with 99.999999999% (11 9's) durability.
    3. Realtime Document Metadata on Firestore: Post captions, upload timestamps, and image URLs are stored as lightning-fast JSON documents in Firestore.

    πŸ• Real-World Analogy: The Restaurant, The Cash Register & The Cold Vault

    mermaid
    • 🀡 The Waiter (Cloud Run): Greets guests, takes orders, and serves food. The waiter does not carry 50 pounds of raw potatoes in their apron.
    • πŸ“– The Guest Ledger (Firestore): A neat book at the front desk recording customer names, table numbers, and bill totals (small, fast structured metadata).
    • ❄️ The Industrial Cold Vault (Cloud Storage): A massive, dedicated storage warehouse in the back where hundreds of pounds of supplies are safely preserved.

    πŸ› οΈ Step-by-Step Hands-On Lab

    Follow these steps to build, containerize, and deploy your complete Cloud Photo Journal app.

    mermaid

    Step 1: Initialize Project Variables & Enable APIs

    Open your Google Cloud Shell terminal and set up your project environment:

    bash
    # 1. Set environment variables
    export PROJECT_ID=$(gcloud config get-value project)
    export REGION="us-central1"
    export BUCKET_NAME="photo-journal-${PROJECT_ID}"
    
    # 2. Enable necessary GCP APIs
    gcloud services enable \
        run.googleapis.com \
        firestore.googleapis.com \
        storage.googleapis.com \
        cloudbuild.googleapis.com
    

    Step 2: Create the Cloud Storage (GCS) Bucket

    Create a dedicated bucket to hold user-uploaded photos and allow public read access for displaying images in the browser:

    bash
    # 1. Create a Standard Storage bucket in your region
    gcloud storage buckets create gs://${BUCKET_NAME} \
        --location=${REGION} \
        --uniform-bucket-level-access
    
    # 2. Grant public read permissions to view uploaded images
    gcloud storage buckets add-iam-policy-binding gs://${BUCKET_NAME} \
        --member="allUsers" \
        --role="roles/storage.objectViewer"
    

    Step 3: Initialize Firestore in Native Mode

    If you already created a Firestore database in Day 23, you can skip database creation. If not, initialize it now:

    bash
    gcloud firestore databases create \
        --location=${REGION} \
        --type=firestore-native
    

    (If it reports that the database already exists, you are ready to proceed!)


    Step 4: Write the Application Code

    Let's create a clean project folder and author our Python Flask application.

    bash
    mkdir -p ~/photo-journal-app
    cd ~/photo-journal-app
    

    1. Author the Backend (main.py)

    πŸ“

    Terminal File Creation Options Choose either the 1-Click command or the manual editor:

    ⚑ Option A: Fast 1-Click Way (Copy & Paste):

    bash
    cat << 'EOF' > main.py
    import os
    import datetime
    from flask import Flask, render_template, request, redirect, url_for
    from google.cloud import firestore
    from google.cloud import storage
    
    app = Flask(__name__)
    
    # Initialize GCP Clients
    db = firestore.Client()
    storage_client = storage.Client()
    BUCKET_NAME = os.environ.get("BUCKET_NAME")
    
    @app.route("/", methods=["GET"])
    def index():
        # Fetch all photos from Firestore ordered by creation date descending
        photos_ref = db.collection("photos").order_by("created_at", direction=firestore.Query.DESCENDING)
        photos = [doc.to_dict() for doc in photos_ref.stream()]
        return render_template("index.html", photos=photos, bucket_name=BUCKET_NAME)
    
    @app.route("/upload", methods=["POST"])
    def upload():
        title = request.form.get("title", "Untitled")
        caption = request.form.get("caption", "")
        image_file = request.files.get("photo")
    
        if image_file and image_file.filename != "":
            # Generate unique filename with timestamp
            timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d_%H%M%S")
            filename = f"{timestamp}_{image_file.filename}"
    
            # 1. Upload the raw image file to GCS
            bucket = storage_client.bucket(BUCKET_NAME)
            blob = bucket.blob(filename)
            blob.upload_from_file(image_file, content_type=image_file.content_type)
            
            # Public URL of the uploaded image
            image_url = f"https://storage.googleapis.com/{BUCKET_NAME}/{filename}"
    
            # 2. Save metadata to Firestore
            db.collection("photos").add({
                "title": title,
                "caption": caption,
                "image_url": image_url,
                "filename": filename,
                "created_at": datetime.datetime.now(datetime.timezone.utc)
            })
    
        return redirect(url_for("index"))
    
    if __name__ == "__main__":
        app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
    EOF
    

    πŸ“ Option B: Manual Way with Nano:

    bash
    nano main.py
    

    (Paste the code above, press Ctrl+O β†’\rightarrow Enter to save, then Ctrl+X to exit).


    2. Author the Frontend HTML Template (templates/index.html)

    bash
    mkdir -p templates
    

    ⚑ Fast 1-Click Way (Copy & Paste):

    bash
    cat << 'EOF' > templates/index.html
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>πŸ“Έ Cloud Photo Journal - GCP Week 4 Capstone</title>
      <style>
        * { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
        body { background: #f0f2f5; color: #1c1e21; padding: 2rem; }
        .container { max-width: 900px; margin: 0 auto; }
        header { text-align: center; margin-bottom: 2rem; }
        header h1 { font-size: 2.2rem; color: #1a73e8; margin-bottom: 0.5rem; }
        header p { color: #5f6368; font-size: 1rem; }
        .badge { display: inline-block; background: #e8f0fe; color: #1967d2; padding: 0.3rem 0.8rem; border-radius: 20px; font-weight: bold; margin-top: 0.5rem; }
        
        .card { background: white; border-radius: 12px; padding: 1.5rem; margin-bottom: 2rem; box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
        .card h2 { font-size: 1.3rem; margin-bottom: 1rem; color: #202124; }
        .form-group { margin-bottom: 1rem; }
        label { display: block; font-weight: 600; margin-bottom: 0.4rem; font-size: 0.9rem; }
        input[type="text"], textarea { width: 100%; padding: 0.75rem; border: 1px solid #dadce0; border-radius: 8px; font-size: 1rem; }
        input[type="file"] { margin-top: 0.3rem; }
        button { background: #1a73e8; color: white; border: none; padding: 0.8rem 1.5rem; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; transition: 0.2s; }
        button:hover { background: #1557b0; }
    
        .gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 1.5rem; }
        .photo-card { background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 10px rgba(0,0,0,0.06); transition: transform 0.2s; }
        .photo-card:hover { transform: translateY(-4px); }
        .photo-card img { width: 100%; height: 200px; object-fit: cover; }
        .photo-card .info { padding: 1rem; }
        .photo-card h3 { font-size: 1.1rem; color: #202124; margin-bottom: 0.4rem; }
        .photo-card p { color: #5f6368; font-size: 0.9rem; line-height: 1.4; }
        .empty-state { text-align: center; padding: 3rem; color: #80868b; }
      </style>
    </head>
    <body>
      <div class="container">
        <header>
          <h1>πŸ“Έ Cloud Photo Journal</h1>
          <p>Built with <strong>Cloud Run</strong> (Frontend) + <strong>Firestore</strong> (Metadata) + <strong>Cloud Storage</strong> (Photos)</p>
          <div class="badge">☁️ Week 4 Capstone Lab</div>
        </header>
    
        <div class="card">
          <h2>✨ Add New Photo Memory</h2>
          <form action="/upload" method="POST" enctype="multipart/form-data">
            <div class="form-group">
              <label for="title">Title</label>
              <input type="text" id="title" name="title" placeholder="e.g., Mount Rainier Hiking Trip" required>
            </div>
            <div class="form-group">
              <label for="caption">Caption / Story</label>
              <textarea id="caption" name="caption" rows="2" placeholder="What made this moment special?"></textarea>
            </div>
            <div class="form-group">
              <label for="photo">Select Image</label>
              <input type="file" id="photo" name="photo" accept="image/*" required>
            </div>
            <button type="submit">πŸš€ Upload to Google Cloud</button>
          </form>
        </div>
    
        <h2 style="margin-bottom: 1rem; color: #202124;">πŸ–ΌοΈ Recent Memories (Live from Firestore & GCS)</h2>
        
        {% if photos %}
          <div class="gallery">
            {% for photo in photos %}
              <div class="photo-card">
                <img src="{{ photo.image_url }}" alt="{{ photo.title }}" loading="lazy">
                <div class="info">
                  <h3>{{ photo.title }}</h3>
                  <p>{{ photo.caption }}</p>
                </div>
              </div>
            {% endfor %}
          </div>
        {% else %}
          <div class="card empty-state">
            <p>No photos uploaded yet! Be the first to upload a memory above. πŸš€</p>
          </div>
        {% endif %}
      </div>
    </body>
    </html>
    EOF
    

    3. Define Dependencies (requirements.txt)

    bash
    cat << 'EOF' > requirements.txt
    Flask==3.0.3
    google-cloud-firestore==2.16.0
    google-cloud-storage==2.16.0
    gunicorn==22.0.0
    EOF
    

    4. Define Container Specification (Dockerfile)

    bash
    cat << 'EOF' > Dockerfile
    FROM python:3.11-slim
    WORKDIR /app
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    COPY . .
    ENV PORT=8080
    CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app
    EOF
    

    Step 5: Deploy the Full-Stack App to Cloud Run

    Deploy directly from source code using Google Cloud Buildpack on Cloud Run:

    bash
    gcloud run deploy photo-journal-app \
        --source . \
        --region=${REGION} \
        --allow-unauthenticated \
        --set-env-vars BUCKET_NAME=${BUCKET_NAME} \
        --quiet
    

    What happens behind the scenes:

    1. Cloud Build packages your Python code and Dockerfile into a container image.
    2. Cloud Run provisions a serverless container instance and assigns it an automatic global HTTPS endpoint.
    3. The Cloud Run service automatically connects to Firestore and Cloud Storage using its built-in Compute Engine default Service Account!

    Step 6: Test Your Live Full-Stack Application!

    1. Retrieve the Live Web URL

    bash
    gcloud run services describe photo-journal-app \
        --region=${REGION} \
        --format='value(status.url)'
    

    2. Open and Interact with the Web App

    1. Open the printed HTTPS URL in your web browser.
    2. Enter a Title (e.g., My First Cloud Photo).
    3. Enter a Caption (e.g., Testing GCS and Firestore integration on Cloud Run!).
    4. Choose an image file from your computer and click Upload to Google Cloud.
    5. Watch the page instantly refresh and display your image fetched from Cloud Storage alongside metadata pulled from Firestore!
    mermaid

    πŸ” Verification & Inspection: Look Inside the Cloud

    Let's verify how each service stored your data under the hood:

    1. Inspect the Stored Image in Cloud Storage

    bash
    gcloud storage ls gs://${BUCKET_NAME}
    

    (You will see your timestamped image file securely stored in your GCS bucket!)

    2. Inspect the Document in Firestore (Web Console)

    1. In the GCP Web Console, search for Firestore.
    2. Click Databases β†’\rightarrow (default) β†’\rightarrow Data.
    3. Click on the photos collection β†’\rightarrow View your uploaded post title, caption, and public GCS URL!

    πŸ›‘οΈ Step 7: Credit Safety & Resource Teardown ($0.00 Guarantee)

    To make sure your 300FreeTrialcreditispreservedwithβˆ—βˆ—300 Free Trial credit is preserved with **0.00 ongoing charges**, clean up the demo services:

    mermaid

    1. Delete the Cloud Run Service

    bash
    gcloud run services delete photo-journal-app --region=${REGION} --quiet
    

    2. Delete the Cloud Storage Bucket (and all photos)

    bash
    gcloud storage rm --recursive gs://${BUCKET_NAME}
    

    3. (Optional) Clean up Firestore Collection

    If you want to clear your demo posts in Firestore:

    1. Go to Firestore in the GCP Console.
    2. Click the three dots next to the photos collection β†’\rightarrow Click Delete collection.

    🧠 Daily Practice Drill & Knowledge Check

    Test your understanding of full-stack cloud architectures:

    text
    // Try answering these:
    1. Why is it a best practice to store user uploaded images in Cloud Storage (GCS) rather than on the Cloud Run container's local disk?A) Cloud Run does not have a CPUB) Cloud Run containers are stateless and ephemeral; files saved locally are lost when instances scale downC) Cloud Storage only accepts images smaller than 1 KBD) Python cannot write files to local disk
    2. In our 3-tier decoupled architecture, what is the role of Firestore?A) Converting JPEG images to PNG formatB) Storing structured document metadata (captions, titles, image URLs, timestamps)C) Routing domain names with DNSD) Providing virtual server RAM
    3. How does the Cloud Run container authenticate with GCS and Firestore without hardcoding API keys or passwords in the code?A) It uses Google's Application Default Credentials (ADC) via the default Service AccountB) It sends passwords in plain text headersC) It connects through public Wi-FiD) Google Cloud doesn't require security permissions
    4. What does setting --allow-unauthenticated on Cloud Run accomplish?A) It turns off data encryptionB) It allows anyone on the public internet to visit your web app over HTTPS without a Google loginC) It deletes the Firestore databaseD) It disables billing alerts
    
    πŸ’‘ Click for Solutions
    1. B (Cloud Run containers are stateless and ephemeral) β€” Cloud Run instances scale dynamically and restart frequently. Decoupling storage to GCS ensures files are permanent and accessible across all container instances.
    2. B (Storing structured document metadata) β€” Firestore serves as the fast, flexible NoSQL database storing JSON-like records that point to the actual files in GCS.
    3. A (Application Default Credentials via Service Account) β€” Cloud Run automatically assumes its assigned IAM Service Account identity, allowing secure, keyless access to GCP services.
    4. B (Public Internet Access over HTTPS) β€” It configures the Cloud Run ingress IAM policy so standard web visitors can access the public web frontend.

    πŸ† Week 4 Mastery Recap & What's Next!

    πŸŽ‰ Congratulations on completing Week 4: Storage & Databases!

    mermaid

    You now possess the skills to architect and deploy any persistent data layer on Google Cloud!

    πŸš€ Entering Week 5: Analytics, DevOps & GenAI Capstone

    Tomorrow, we begin the final stretch of our 30-day journey:

    • Day 27: BigQuery 101 β€” Querying terabytes of Big Data in seconds with serverless SQL data warehousing!
    • Day 28: Secret Manager & Cloud Build CI/CD β€” Automating deployments securely.
    • Day 29: Vertex AI Studio & Gemini API β€” Building Generative AI applications.
    • Day 30: Final Capstone Project & Course Graduation! πŸŽ“

    ← 25 - Enterprise DBs - Spanner and Bigtable Overview | Next Topic β†’ 27 - BigQuery 101 - SQL Data Warehouse