GCP for DevOps — Project: Serverless REST API

3 minute read

Project 3: Serverless REST API

Build a serverless REST API using Cloud Run, API Gateway, Firestore, and Secret Manager. This project demonstrates how to build production-grade serverless applications with GCP managed services.

Architecture

Serverless REST API Architecture:
┌──────────┐    ┌──────────────┐    ┌──────────────┐
│  Client   │───▶│  API         │───▶│  Cloud Run   │
│           │    │  Gateway     │    │  Service     │
└──────────┘    └──────────────┘    └──────┬───────┘
                                           │
                          ┌────────────────┼────────────────┐
                          ▼                ▼                ▼
                   ┌──────────┐    ┌──────────┐    ┌──────────┐
                   │Firestore │    │  Secret  │    │  Cloud   │
                   │(database)│    │  Manager │    │  Logging │
                   └──────────┘    └──────────┘    └──────────┘

Step 1: Enable APIs

$ gcloud services enable \
    run.googleapis.com \
    apigateway.googleapis.com \
    servicecontrol.googleapis.com \
    firestore.googleapis.com \
    secretmanager.googleapis.com \
    --project=my-project
Operation finished successfully.

Step 2: Set Up Firestore

# Create Firestore database in Native mode
$ gcloud firestore databases create \
    --location=us-central1 \
    --type=firestore-native
Waiting for operation to finish...done.

Step 3: Store API Key in Secret Manager

# Create a secret for the API key
$ echo -n "my-super-secret-api-key-12345" | \
    gcloud secrets create api-key \
    --data-file=- \
    --replication-policy=automatic
Created secret [api-key].

# Grant Cloud Run service account access
$ gcloud secrets add-iam-policy-binding api-key \
    --member="serviceAccount:PROJECT_NUMBER-compute@developer.gserviceaccount.com" \
    --role="roles/secretmanager.secretAccessor"
Updated IAM policy for secret [api-key].

Step 4: Application Code

# app.py
import os
import json
from flask import Flask, request, jsonify
from google.cloud import firestore
from google.cloud import secretmanager

app = Flask(__name__)
db = firestore.Client()

def get_secret(secret_id):
    """Retrieve a secret from Secret Manager."""
    client = secretmanager.SecretManagerServiceClient()
    project_id = os.environ.get('GOOGLE_CLOUD_PROJECT')
    name = f"projects/{project_id}/secrets/{secret_id}/versions/latest"
    response = client.access_secret_version(request={"name": name})
    return response.payload.data.decode("UTF-8")

@app.route('/api/items', methods=['GET'])
def list_items():
    """List all items from Firestore."""
    items = []
    docs = db.collection('items').stream()
    for doc in docs:
        item = doc.to_dict()
        item['id'] = doc.id
        items.append(item)
    return jsonify(items)

@app.route('/api/items', methods=['POST'])
def create_item():
    """Create a new item in Firestore."""
    data = request.get_json()
    if not data or 'name' not in data:
        return jsonify({"error": "name is required"}), 400
    
    doc_ref = db.collection('items').document()
    doc_ref.set({
        'name': data['name'],
        'description': data.get('description', ''),
        'created_at': firestore.SERVER_TIMESTAMP
    })
    return jsonify({"id": doc_ref.id, "status": "created"}), 201

@app.route('/api/items/<item_id>', methods=['GET'])
def get_item(item_id):
    """Get a single item by ID."""
    doc = db.collection('items').document(item_id).get()
    if not doc.exists:
        return jsonify({"error": "not found"}), 404
    item = doc.to_dict()
    item['id'] = doc.id
    return jsonify(item)

@app.route('/api/items/<item_id>', methods=['DELETE'])
def delete_item(item_id):
    """Delete an item by ID."""
    db.collection('items').document(item_id).delete()
    return jsonify({"status": "deleted"}), 200

@app.route('/health', methods=['GET'])
def health():
    return jsonify({"status": "healthy"}), 200

if __name__ == '__main__':
    port = int(os.environ.get('PORT', 8080))
    app.run(host='0.0.0.0', port=port)
# requirements.txt
Flask==3.0.0
google-cloud-firestore==2.14.0
google-cloud-secret-manager==2.18.0
gunicorn==21.2.0
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 8080
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:app"]

Step 5: Deploy to Cloud Run

# Build and deploy in one command
$ gcloud run deploy items-api \
    --source=. \
    --region=us-central1 \
    --allow-unauthenticated \
    --set-env-vars=GOOGLE_CLOUD_PROJECT=my-project \
    --memory=256Mi \
    --cpu=1 \
    --min-instances=0 \
    --max-instances=10
Deploying...done.
Service URL: https://items-api-abc123-uc.a.run.app

Step 6: Configure API Gateway

# openapi-spec.yaml
swagger: '2.0'
info:
  title: Items API
  version: '1.0'
schemes:
  - https
produces:
  - application/json
paths:
  /api/items:
    get:
      summary: List all items
      operationId: listItems
      x-google-backend:
        address: https://items-api-abc123-uc.a.run.app
      responses:
        '200':
          description: A list of items
    post:
      summary: Create an item
      operationId: createItem
      x-google-backend:
        address: https://items-api-abc123-uc.a.run.app
      responses:
        '201':
          description: Item created
  /api/items/{itemId}:
    get:
      summary: Get an item
      operationId: getItem
      parameters:
        - name: itemId
          in: path
          required: true
          type: string
      x-google-backend:
        address: https://items-api-abc123-uc.a.run.app
      responses:
        '200':
          description: An item
    delete:
      summary: Delete an item
      operationId: deleteItem
      parameters:
        - name: itemId
          in: path
          required: true
          type: string
      x-google-backend:
        address: https://items-api-abc123-uc.a.run.app
      responses:
        '200':
          description: Item deleted
# Create API config
$ gcloud api-gateway api-configs create items-config-v1 \
    --api=items-api \
    --openapi-spec=openapi-spec.yaml \
    --project=my-project
Waiting for API Config to be created...done.

# Create the gateway
$ gcloud api-gateway gateways create items-gateway \
    --api=items-api \
    --api-config=items-config-v1 \
    --location=us-central1 \
    --project=my-project
Waiting for Gateway to be created...done.

Step 7: Test the API

# Get the gateway URL
$ gcloud api-gateway gateways describe items-gateway \
    --location=us-central1 \
    --format="value(defaultHostname)"
items-gateway-abc123.uc.gateway.dev

# Create an item
$ curl -X POST https://items-gateway-abc123.uc.gateway.dev/api/items \
    -H "Content-Type: application/json" \
    -d '{"name": "Widget", "description": "A useful widget"}'
{"id": "abc123def456", "status": "created"}

# List items
$ curl https://items-gateway-abc123.uc.gateway.dev/api/items
[{"id": "abc123def456", "name": "Widget", "description": "A useful widget"}]

# Get single item
$ curl https://items-gateway-abc123.uc.gateway.dev/api/items/abc123def456
{"id": "abc123def456", "name": "Widget", "description": "A useful widget"}

# Delete item
$ curl -X DELETE https://items-gateway-abc123.uc.gateway.dev/api/items/abc123def456
{"status": "deleted"}

Clean Up

# Delete API Gateway
$ gcloud api-gateway gateways delete items-gateway \
    --location=us-central1 --quiet

# Delete API config
$ gcloud api-gateway api-configs delete items-config-v1 \
    --api=items-api --quiet

# Delete Cloud Run service
$ gcloud run services delete items-api --region=us-central1 --quiet

# Delete Firestore data
$ gcloud firestore databases delete --database="(default)" --quiet

# Delete secret
$ gcloud secrets delete api-key --quiet

Key Learnings

  • Cloud Run for autoscaling serverless containers
  • API Gateway for managed API exposure
  • Firestore for serverless NoSQL data storage
  • Secret Manager for secure credential management
  • Zero infrastructure management with fully managed services

Next → Terraform 3-Tier Architecture

Updated: