GCP for DevOps — Project: Serverless REST API
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