GCP for DevOps — Serverless Practice Exercises

3 minute read

Serverless Practice Exercises

Exercise 1 — Deploy a Cloud Function

Deploy an HTTP Cloud Function that returns a JSON greeting.

Steps:

  1. Create a function directory with main.py and requirements.txt
  2. Write a function that accepts a name query parameter
  3. Deploy as a gen2 HTTP function
  4. Test with curl
Show Answer
# main.py
import functions_framework
from flask import jsonify

@functions_framework.http
def greet(request):
    name = request.args.get("name", "World")
    return jsonify({"message": f"Hello, {name}!", "service": "cloud-functions"})
# requirements.txt
functions-framework==3.*
flask==3.*
$ gcloud functions deploy greet \
    --gen2 \
    --runtime=python312 \
    --region=us-central1 \
    --source=. \
    --entry-point=greet \
    --trigger-http \
    --allow-unauthenticated

$ curl "https://us-central1-PROJECT_ID.cloudfunctions.net/greet?name=DevOps"
# {"message": "Hello, DevOps!", "service": "cloud-functions"}

Exercise 2 — Cloud Run with Traffic Splitting

Deploy two revisions of a Cloud Run service and split traffic between them.

Steps:

  1. Deploy a Cloud Run service returning “v1”
  2. Update the service to return “v2”
  3. Split traffic 80/20 between v2 and v1
  4. Verify with multiple requests
Show Answer
# Deploy v1
$ gcloud run deploy traffic-demo \
    --image=us-docker.pkg.dev/cloudrun/container/hello \
    --region=us-central1 \
    --tag=v1 \
    --allow-unauthenticated

# Note the revision name (e.g., traffic-demo-00001-abc)

# Deploy v2 with an update
$ gcloud run deploy traffic-demo \
    --image=us-docker.pkg.dev/cloudrun/container/hello \
    --region=us-central1 \
    --tag=v2 \
    --set-env-vars=VERSION=v2

# Split traffic 80/20
$ gcloud run services update-traffic traffic-demo \
    --region=us-central1 \
    --to-revisions=traffic-demo-00002-xyz=80,traffic-demo-00001-abc=20

# Verify traffic split
$ gcloud run services describe traffic-demo \
    --region=us-central1 \
    --format="yaml(status.traffic)"

Exercise 3 — API Gateway with Rate Limiting

Create an API Gateway that proxies requests to a Cloud Function with API key authentication.

Steps:

  1. Deploy a backend Cloud Function
  2. Create an OpenAPI spec with API key security
  3. Create an API Gateway
  4. Test with and without an API key
Show Answer
# openapi-spec.yaml
swagger: "2.0"
info:
  title: My Serverless API
  version: "1.0"
schemes:
  - https
produces:
  - application/json
security:
  - api_key: []
securityDefinitions:
  api_key:
    type: apiKey
    name: x-api-key
    in: header
paths:
  /greet:
    get:
      operationId: greet
      x-google-backend:
        address: https://us-central1-PROJECT_ID.cloudfunctions.net/greet
      responses:
        "200":
          description: OK
$ gcloud api-gateway apis create my-api --project=PROJECT_ID

$ gcloud api-gateway api-configs create my-config \
    --api=my-api \
    --openapi-spec=openapi-spec.yaml \
    --project=PROJECT_ID

$ gcloud api-gateway gateways create my-gateway \
    --api=my-api \
    --api-config=my-config \
    --location=us-central1 \
    --project=PROJECT_ID

# Test without key (should fail)
$ curl "https://my-gateway-abc123.uc.gateway.dev/greet"
# 403 Forbidden

# Test with key
$ curl -H "x-api-key: YOUR_API_KEY" \
    "https://my-gateway-abc123.uc.gateway.dev/greet?name=DevOps"
# {"message": "Hello, DevOps!"}

Exercise 4 — Firestore CRUD with Cloud Function

Create a Cloud Function that performs CRUD operations on Firestore.

Steps:

  1. Create a Firestore database
  2. Deploy a Cloud Function that handles GET/POST/DELETE
  3. Test all operations
Show Answer
# main.py
import functions_framework
from google.cloud import firestore
from flask import jsonify, request

db = firestore.Client()
COLLECTION = "tasks"

@functions_framework.http
def tasks_api(req):
    if req.method == "GET":
        docs = db.collection(COLLECTION).stream()
        tasks = [{**doc.to_dict(), "id": doc.id} for doc in docs]
        return jsonify(tasks)

    elif req.method == "POST":
        data = req.get_json()
        doc_ref = db.collection(COLLECTION).add({
            "title": data["title"],
            "done": False,
            "created_at": firestore.SERVER_TIMESTAMP,
        })
        return jsonify({"id": doc_ref[1].id, "status": "created"}), 201

    elif req.method == "DELETE":
        task_id = req.args.get("id")
        db.collection(COLLECTION).document(task_id).delete()
        return jsonify({"status": "deleted"})

    return jsonify({"error": "Method not allowed"}), 405
$ gcloud firestore databases create --location=us-central1 --type=firestore-native

$ gcloud functions deploy tasks-api \
    --gen2 \
    --runtime=python312 \
    --region=us-central1 \
    --entry-point=tasks_api \
    --trigger-http \
    --allow-unauthenticated

# Create a task
$ curl -X POST -H "Content-Type: application/json" \
    -d '{"title":"Learn Firestore"}' \
    "https://us-central1-PROJECT_ID.cloudfunctions.net/tasks-api"

# List tasks
$ curl "https://us-central1-PROJECT_ID.cloudfunctions.net/tasks-api"

# Delete a task
$ curl -X DELETE "https://us-central1-PROJECT_ID.cloudfunctions.net/tasks-api?id=TASK_ID"

Exercise 5 — Eventarc to Workflow Pipeline

Build an event-driven pipeline: Cloud Storage upload triggers Eventarc, which starts a Workflow that calls a Cloud Run service.

Steps:

  1. Deploy a Cloud Run processing service
  2. Create a Workflow that calls the service
  3. Create an Eventarc trigger for GCS uploads
  4. Upload a file and verify the pipeline
Show Answer
# workflow.yaml
main:
  params: [event]
  steps:
    - extract_info:
        assign:
          - bucket: ${event.data.bucket}
          - filename: ${event.data.name}
          - size: ${event.data.size}

    - log_event:
        call: sys.log
        args:
          text: ${"Processing file " + filename + " from " + bucket}
          severity: "INFO"

    - process_file:
        call: http.post
        args:
          url: https://my-processor-abc123.a.run.app/process
          body:
            bucket: ${bucket}
            filename: ${filename}
          auth:
            type: OIDC
        result: process_result

    - return_result:
        return:
          file: ${filename}
          status: ${process_result.body.status}
# Deploy workflow
$ gcloud workflows deploy file-pipeline \
    --location=us-central1 \
    --source=workflow.yaml \
    --service-account=workflow-sa@PROJECT_ID.iam.gserviceaccount.com

# Create Eventarc trigger
$ gcloud eventarc triggers create gcs-to-workflow \
    --location=us-central1 \
    --destination-workflow=file-pipeline \
    --destination-workflow-location=us-central1 \
    --event-filters="type=google.cloud.storage.object.v1.finalized" \
    --event-filters="bucket=my-upload-bucket" \
    --service-account=eventarc-sa@PROJECT_ID.iam.gserviceaccount.com

# Test — upload a file
$ gsutil cp test-file.csv gs://my-upload-bucket/

# Verify workflow execution
$ gcloud workflows executions list file-pipeline --location=us-central1

Cleanup

$ gcloud functions delete greet --region=us-central1 --quiet
$ gcloud run services delete traffic-demo --region=us-central1 --quiet
$ gcloud api-gateway gateways delete my-gateway --location=us-central1 --quiet
$ gcloud api-gateway api-configs delete my-config --api=my-api --quiet
$ gcloud api-gateway apis delete my-api --quiet
$ gcloud functions delete tasks-api --region=us-central1 --quiet
$ gcloud firestore databases delete --database="(default)" --quiet
$ gcloud workflows delete file-pipeline --location=us-central1 --quiet
$ gcloud eventarc triggers delete gcs-to-workflow --location=us-central1 --quiet

Next Chapter → Monitoring & Logging

Updated: