Event-Driven Order Processing on GCP
Build a Python microservice on Cloud Run triggered by Pub/Sub via Eventarc.
Introduction
30 Second Summary
Every time you place an online order, a chain of services kicks into action behind the scenes. Your payment gets verified, inventory gets updated, and a confirmation email lands in your inbox. Each of these steps is triggered by an event flowing through a messaging system.
In this project, you will build an event-driven order processing service on Google Cloud. You'll deploy a Python microservice to Cloud Run that listens for order events from Pub/Sub, validates the data, and writes records to Firestore.
What You'll Build
An order processing microservice that receives events via Pub/Sub, validates and enriches the data, and persists order records to Firestore with full observability through structured logging and trace correlation.
By the end of this project, you'll have:
- A Python Flask service deployed to Cloud Run from source code, with no Dockerfile needed.
- An Eventarc trigger routing Pub/Sub messages to your service, secured by a dedicated least-privilege service account.
- Structured JSON logs correlated with request traces, visible in Cloud Logging and Cloud Trace.
- Secret Mission: Add a dead-letter topic for failed messages and create a Cloud Monitoring alerting policy.
Are there any prerequisites?
You'll need a Google Cloud account (the free trial includes $300 in credits for 90 days) and Python installed on your machine. Basic familiarity with the command line is helpful but not required.
Install the Google Cloud CLI and Configure Your Project
Your order processing service needs a Google Cloud project with several services wired together. Cloud Run hosts the service, Pub/Sub delivers the messages, Eventarc routes them, and Firestore stores the results.
In this step, you'll install the Google Cloud CLI, create a project, and enable all the APIs your service depends on.
In this step, get ready to:
- Install and configure the Google Cloud CLI.
- Enable the required Cloud APIs and set a default region.
- Create a Firestore database in Native mode.
Install and initialize the Google Cloud CLI
The Google Cloud CLI (gcloud) is the command-line tool you'll use to deploy services, manage resources, and configure your project from the terminal.
Windows
- Download the Google Cloud CLI installer:
- https://dl.google.com/dl/cloudsdk/channels/rapid/GoogleCloudSDKInstaller.exe
- Run the installer and accept all defaults. The installer bundles its own Python, so no extra setup is needed.
- After the installer finishes, open a new terminal. Press the Windows key to open your search bar.
- Type PowerShell and press Enter to open it.
Why a new terminal?
The installer adds gcloud to your system PATH. Any terminal that was already open before the install won't pick up that change. Opening a fresh PowerShell window ensures the gcloud command is recognized.
macOS
- Open Terminal. Press Cmd+Space to open Spotlight, type Terminal, and press Enter.
- Install the Google Cloud CLI using Homebrew by running:
brew update && brew install --cask gcloud-cli
Don't have Homebrew?
If the brew command is not found, install Homebrew first by following the instructions at https://brew.sh. Then re-run the command above.
- After installation, open a new terminal window so gcloud is available on your PATH.
- Initialize the CLI by running this command:
gcloud init
What will gcloud init ask me?
This command launches an interactive setup wizard. It will walk you through several prompts:
- Sign in: your browser opens automatically for Google account authentication. Click Allow to grant access.
- Pick or create a project: select Create a new project and enter a name like gcp-order-service. Project IDs must be globally unique, so you may need to add a short suffix (e.g. gcp-order-service-123).
- Default Compute Engine zone: you can skip this prompt by answering N. You'll set your preferred region separately in the next substep.
- Verify your configuration by running:
gcloud config list
You should see output listing your account email and project name under the [core] section.
✔️ I see my account and project listed
Your CLI is configured and ready. Move on to the next substep.
ⓧ I see an error or gcloud is not recognized
- Make sure you opened a new terminal window after the installer completed. Terminals opened before the install will not have gcloud on their PATH.
- Re-run the installer if needed and ensure you accepted defaults.
- Try running gcloud --version to confirm the CLI is installed. If it prints a version number, re-run gcloud init.
Enable the required APIs and set the default region
Google Cloud APIs are disabled by default. You need to switch on each service your project will use before you can interact with it.
- Enable all six APIs needed for this project by running:
gcloud services enable run.googleapis.com cloudbuild.googleapis.com eventarc.googleapis.com pubsub.googleapis.com firestore.googleapis.com logging.googleapis.com
What does this command do?
This single command activates six Google Cloud services on your project:
- Cloud Run (run.googleapis.com) hosts your containerized service.
- Cloud Build (cloudbuild.googleapis.com) builds container images from source code.
- Eventarc (eventarc.googleapis.com) routes events to your service.
- Pub/Sub (pubsub.googleapis.com) delivers messages between services.
- Firestore (firestore.googleapis.com) provides a document database.
- Cloud Logging (logging.googleapis.com) collects structured logs from your service.
- Set your default region for Cloud Run and Eventarc by running these commands:
gcloud config set run/region us-central1
gcloud config set eventarc/location us-central1
Setting a default region means you won't have to pass --region us-central1 to every command later in the project.
Seeing a permission or billing error?
If the gcloud services enable command fails with a billing-related message, your project may not have billing linked. Open the Google Cloud Console, navigate to Billing, and link your free trial account to the project.
Help me troubleshoot this error
Create the Firestore database
Firestore is the document database where your service will write processed order records. You need to create the database before deploying the service.
- Create a Firestore database in Native mode by running:
gcloud firestore databases create --location=us-central1
What does this command do?
This creates a Firestore database in Native mode (the default type) with the database ID (default). Native mode provides strong consistency and real-time document queries.
The us-central1 location keeps the database in the same region as your Cloud Run service, minimizing latency.
- Verify the database was created by running:
gcloud firestore databases list
You should see one entry with the database name ending in (default) and type FIRESTORE_NATIVE.
Database creation failed?
If you see an error saying the database already exists, a previous project may have created one. Run gcloud firestore databases list to confirm it's there. If the location is wrong, you'll need to delete and recreate it.
If you see a permission error, make sure firestore.googleapis.com was enabled in the previous substep.
Help me fix my Firestore setup
Your Google Cloud project is fully configured. Next up, you'll write the Flask service that processes order events.
Build the Order Processing Service
Your Google Cloud project is configured with all the services you need. Firestore, Pub/Sub, Eventarc, and Cloud Run are all enabled and ready.
Now it's time to write the microservice that processes order events. This Python application receives messages from Pub/Sub, validates the data, and writes order records to Firestore.
In this step, get ready to:
- Create the project directory and write the Flask application.
- Define the project dependencies in requirements.txt.
- Review the key code patterns for Pub/Sub, trace correlation, and Firestore.
Create the project directory and Flask application
Your service needs a local directory to hold the source code. Cloud Run will deploy directly from this source folder in the next step.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir order-processor
cd order-processor
- Confirm the folder exists by running:
ls
You should see the order-processor folder listed on your Desktop. The folder is empty for now.
- Create a new file called main.py inside the order-processor folder. Open it in your preferred text editor.
- Add the imports and application setup by pasting the following code into main.py:
import base64
import json
import os
from datetime import datetime, timezone
from flask import Flask, request
from google.cloud import firestore
# Initialize the Flask app and Firestore client
app = Flask(__name__)
db = firestore.Client()
# Cloud Run sets this automatically at runtime
PROJECT = os.environ.get("GOOGLE_CLOUD_PROJECT", "")
What does this code do?
These lines set up the foundation of your service. Flask handles incoming HTTP requests, and the Firestore client connects to your database.
The GOOGLE_CLOUD_PROJECT environment variable is automatically provided by Cloud Run at runtime. You do not need to set it manually. Your code uses it to build trace correlation links for structured logging.
- Save main.py.
- Confirm main.py exists by running ls in your terminal. You should see main.py listed in the output.
Don't see main.py?
Make sure your terminal is inside the order-processor folder. Run pwd to check your current directory.
If you created the file with a GUI editor, confirm it saved to the order-processor folder on your Desktop, not your home directory.
Help me find my main.py file
- Below the existing code in main.py, add the structured logging function:
def structured_log(severity, message, **kwargs):
"""Emit a structured JSON log entry with trace correlation."""
log_fields = {}
# Extract the trace ID from the incoming request header
trace_header = request.headers.get("X-Cloud-Trace-Context")
if trace_header and PROJECT:
trace_id = trace_header.split("/")[0]
log_fields["logging.googleapis.com/trace"] = (
f"projects/{PROJECT}/traces/{trace_id}"
)
entry = dict(
severity=severity,
message=message,
**log_fields,
**kwargs,
)
print(json.dumps(entry))
What does this function do?
Cloud Run sends an X-Cloud-Trace-Context header with every request. This function extracts the trace ID from that header and includes it in the log output as the logging.googleapis.com/trace field.
When Cloud Logging sees this field, it groups your application logs under the parent request log. This makes debugging much easier because you can see all logs for a single request in one place.
- Save main.py. You will not see output yet. The next chunk adds the route handler that uses this function.
- Below the structured_log function in main.py, add the main route handler and the startup block:
@app.route("/", methods=["POST"])
def process_order():
"""Receive a Pub/Sub message via Eventarc and write order to Firestore."""
# Parse the incoming CloudEvents envelope
envelope = request.get_json(silent=True)
if not envelope:
structured_log("ERROR", "No JSON payload received")
return "Bad Request: no JSON payload", 400
# Extract the Pub/Sub message data
message = envelope.get("message", {})
data = message.get("data")
if not data:
structured_log("ERROR", "No message data found in envelope")
return "Bad Request: no message data", 400
# Decode the base64-encoded order payload
try:
order_json = base64.b64decode(data).decode("utf-8")
order = json.loads(order_json)
except (ValueError, json.JSONDecodeError) as e:
structured_log("ERROR", f"Failed to decode message: {e}")
return f"Bad Request: invalid message data", 400
# Validate the order has an ID
order_id = order.get("order_id")
if not order_id:
structured_log("ERROR", "Missing order_id in order data")
return "Bad Request: missing order_id", 400
# Enrich the order with processing metadata
order["processed_at"] = datetime.now(timezone.utc).isoformat()
order["status"] = "received"
# Write the order document to Firestore
doc_ref = db.collection("orders").document(order_id)
doc_ref.set(order)
structured_log(
"INFO",
f"Order {order_id} processed successfully",
order_id=order_id,
customer=order.get("customer", "unknown"),
)
return f"Order {order_id} processed", 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
What does this route do?
This is the heart of the service. When Eventarc delivers a Pub/Sub message, it arrives as an HTTP POST with a JSON envelope. The data field inside the envelope contains your original message encoded in base64.
The function decodes the message, validates the order has an order_id, adds a processed_at timestamp and status field, then writes the document to the orders collection in Firestore. Firestore creates the collection automatically on first write.
The if __name__ == "__main__" block at the bottom lets Cloud Run start the server on the correct port. Cloud Run sets the PORT environment variable automatically.
- Save main.py.
- Run ls in your terminal. You should see main.py listed.
Seeing a syntax error when you review the file?
Check that the @app.route decorator is directly above def process_order(): with no blank lines between them.
Make sure you have a blank line separating the end of structured_log from the @app.route decorator.
Help me fix a syntax error in my main.py
✔️ Awesome, I've got everything!
Great. Double check you have saved main.py before moving on.
ⓧ I'd like to double check the full code
Here is the complete main.py file. Compare it with yours to make sure everything matches:
import base64
import json
import os
from datetime import datetime, timezone
from flask import Flask, request
from google.cloud import firestore
app = Flask(__name__)
db = firestore.Client()
PROJECT = os.environ.get("GOOGLE_CLOUD_PROJECT", "")
def structured_log(severity, message, **kwargs):
"""Emit a structured JSON log entry with trace correlation."""
log_fields = {}
trace_header = request.headers.get("X-Cloud-Trace-Context")
if trace_header and PROJECT:
trace_id = trace_header.split("/")[0]
log_fields["logging.googleapis.com/trace"] = (
f"projects/{PROJECT}/traces/{trace_id}"
)
entry = dict(
severity=severity,
message=message,
**log_fields,
**kwargs,
)
print(json.dumps(entry))
@app.route("/", methods=["POST"])
def process_order():
"""Receive a Pub/Sub message via Eventarc and write order to Firestore."""
envelope = request.get_json(silent=True)
if not envelope:
structured_log("ERROR", "No JSON payload received")
return "Bad Request: no JSON payload", 400
message = envelope.get("message", {})
data = message.get("data")
if not data:
structured_log("ERROR", "No message data found in envelope")
return "Bad Request: no message data", 400
try:
order_json = base64.b64decode(data).decode("utf-8")
order = json.loads(order_json)
except (ValueError, json.JSONDecodeError) as e:
structured_log("ERROR", f"Failed to decode message: {e}")
return f"Bad Request: invalid message data", 400
order_id = order.get("order_id")
if not order_id:
structured_log("ERROR", "Missing order_id in order data")
return "Bad Request: missing order_id", 400
order["processed_at"] = datetime.now(timezone.utc).isoformat()
order["status"] = "received"
doc_ref = db.collection("orders").document(order_id)
doc_ref.set(order)
structured_log(
"INFO",
f"Order {order_id} processed successfully",
order_id=order_id,
customer=order.get("customer", "unknown"),
)
return f"Order {order_id} processed", 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
Create the requirements file
Cloud Run needs to know which Python packages your service depends on. The requirements.txt file lists these dependencies with pinned versions so builds are reproducible.
- Create a new file called requirements.txt in the order-processor folder and add the following content:
Flask==3.1.3
gunicorn==23.0.0
google-cloud-firestore==2.28.0
What are these packages?
- Flask is the web framework that handles incoming HTTP requests from Eventarc.
- gunicorn is a production-grade WSGI server that Cloud Run uses to run your Flask app. Cloud Build detects it automatically.
- google-cloud-firestore is the official Python client library for reading and writing Firestore documents.
- Save requirements.txt.
- Run ls in your terminal. You should see both main.py and requirements.txt listed.
Only see one file?
Make sure both files are saved inside the order-processor folder, not in a parent or nested directory.
Check that requirements.txt has no file extension like .txt.txt. Some editors add an extra extension by default on Windows.
Help me find my missing requirements.txt file
Review the key patterns in your code
Before deploying, it helps to understand three patterns that make this service work with the Google Cloud ecosystem.
How does Pub/Sub deliver messages?
Pub/Sub wraps your original message in a CloudEvents envelope. The actual order data lives inside envelope["message"]["data"] as a base64-encoded string. Your code decodes it back to JSON before processing.
This encoding is standard for all Pub/Sub push deliveries. Every service that receives Pub/Sub messages through Eventarc follows this same decode pattern.
How does trace correlation work?
Cloud Run automatically generates a trace for each incoming request and sends the X-Cloud-Trace-Context header to your service. By extracting the trace ID and including it as logging.googleapis.com/trace in your log entries, Cloud Logging nests your logs under the parent request log.
This means you can click a single request in Cloud Logging and expand it to see all your application logs for that specific invocation.
How does Firestore handle collections?
Firestore creates collections implicitly on first write. You do not need to create the orders collection ahead of time. The first time your service calls db.collection("orders").document(order_id).set(order), Firestore creates both the collection and the document automatically.
Your service code is ready. Next up, you will deploy it to Cloud Run directly from source without needing a Dockerfile.
Deploy to Cloud Run from Source
Your Flask application and dependencies are ready locally. Now it's time to put them into production by deploying to Cloud Run.
Cloud Run can deploy directly from your source code. No Dockerfile needed. Cloud Build automatically detects your Python app, creates a container image using Google Cloud Buildpacks, and stores it in Artifact Registry.
In this step, get ready to:
- Deploy the order-processor service to Cloud Run from source.
- Verify the deployment and retrieve the service URL.
- Grant the service's runtime account permission to write to Firestore.
Deploy the service from source
When you deploy from source, Cloud Build uploads your code, builds a container image using Buildpacks, and deploys it to Cloud Run in one command. The --no-allow-unauthenticated flag ensures only authenticated callers (like Eventarc) can invoke the service.
- Make sure your terminal is inside your project directory (the folder containing main.py and requirements.txt).
- Deploy the service to Cloud Run by running this command:
gcloud run deploy order-processor --source . --region us-central1 --no-allow-unauthenticated
What will this command ask you?
The command triggers a few interactive prompts. Here is how to respond:
- If prompted to enable the Artifact Registry API, respond y.
- If prompted to create a repository in the specified region, respond y.
- The build and deployment takes a few minutes. Wait for it to complete.
On success, the terminal displays a service URL. This confirms your service is live.
What does this command do?
- Cloud Build uploads your source code and uses Google Cloud Buildpacks to detect your Python app and package it into a container image.
- The container image is stored in an Artifact Registry repository named cloud-run-source-deploy (created automatically).
- Cloud Run deploys the container and routes traffic to it. The --no-allow-unauthenticated flag restricts access to authenticated callers only.
Deployment failed or timed out?
- Make sure you are running the command from the directory that contains main.py and requirements.txt. Cloud Build looks for these files in the current directory.
- If you see a permissions error, confirm you authenticated with gcloud init and have Owner or Editor access on the project.
- If the build fails with a dependency error, double-check your requirements.txt has the exact package names and versions listed earlier.
Help me troubleshoot my Cloud Run deploy-from-source failure
Verify the deployment
- Confirm the service is running by retrieving its URL with this command:
gcloud run services describe order-processor --region us-central1 --format="value(status.url)"
You should see a URL like https://order-processor-XXXXX-uc.a.run.app printed in your terminal. This is your service's public endpoint.
No URL returned?
- Run gcloud run services list --region us-central1 to check if the service exists. If it does not appear, the deploy did not succeed.
- Verify your default region is set to us-central1 by running gcloud config list.
Help me figure out why my Cloud Run service URL is not showing up
Grant Firestore write permissions
Your Cloud Run service runs under a runtime service account. By default, this is the Compute Engine default service account. It needs explicit permission to write to Firestore.
- Retrieve the service account email that your Cloud Run service uses by running:
gcloud run services describe order-processor --region us-central1 --format="value(spec.template.spec.serviceAccountName)"
What does this return?
This outputs the email address of the IAM service account your service runs as. Copy this value. You will use it in the next command.
- Grant the roles/datastore.user role to the service account so it can read and write Firestore documents. Run this command, replacing the placeholders with your values:
gcloud projects add-iam-policy-binding [[PROJECT_ID="your-gcp-project-id"]] --member="serviceAccount:[[SA_EMAIL="service account email from previous command"]]" --role="roles/datastore.user"
Why is this step necessary?
Google Cloud follows the principle of least privilege. Even though the runtime service account exists, it does not automatically have access to every resource in the project.
The roles/datastore.user role grants permission to create, read, update, and delete Firestore documents. Without it, the service would return a permission-denied error when trying to write orders.
You should see output listing the updated IAM policy bindings for the project. This confirms the role was granted successfully.
Seeing a permission denied error?
- Make sure you replaced the project ID with your actual GCP project ID (visible in gcloud config list under core/project).
- Confirm the service account email is correct by re-running the describe command above and copying the full email address.
- Ensure your own account has the Owner or IAM Admin role on the project.
Help me fix a permission denied error when granting IAM roles
Your service is deployed and authorized to write to Firestore. Next up, you will configure Eventarc to automatically route Pub/Sub messages to this service.
Configure Eventarc with a Least-Privilege Service Account
Your order processing service is deployed on Cloud Run and ready to receive requests. Now you need something to actually send it those requests when new orders arrive.
Eventarc connects event sources (like Pub/Sub topics) directly to Cloud Run services. Instead of using the default Compute Engine service account (which has broad permissions), you'll create a dedicated service account with only the minimum permissions it needs to invoke your service.
In this step, get ready to:
- Create a dedicated IAM service account for the Eventarc trigger.
- Grant the service account permission to invoke the Cloud Run service.
- Create the Eventarc trigger to route Pub/Sub messages to your service.
Create a dedicated service account
Every GCP resource that acts on behalf of another needs an identity. The Eventarc trigger will invoke your Cloud Run service, so it needs a service account that has permission to do so.
- Create the dedicated service account by running this command:
gcloud iam service-accounts create eventarc-trigger-sa --display-name="Eventarc Trigger SA"
What does this command do?
This creates a new service account named eventarc-trigger-sa in your project. The --display-name flag gives it a human-readable label in the Google Cloud Console.
The service account email will be eventarc-trigger-sa@YOUR_PROJECT.iam.gserviceaccount.com. You'll use this email in the next commands.
- Grant the service account the Cloud Run Invoker role by running this command:
gcloud run services add-iam-policy-binding order-processor --region us-central1 --member="serviceAccount:eventarc-trigger-sa@[[PROJECT_ID="your-gcp-project-id"]].iam.gserviceaccount.com" --role="roles/run.invoker"
What does this command do?
This grants the eventarc-trigger-sa service account the roles/run.invoker role, but only on the order-processor service. It cannot invoke any other Cloud Run service in your project.
This is the principle of least privilege in action. The trigger account can do exactly one thing: call your order processing service.
- You should see a policy binding output listing the new member and role. Confirm roles/run.invoker appears in the output.
Why a dedicated service account?
Google Cloud projects come with a default Compute Engine service account that has the Editor role. Using it for triggers gives far more permissions than needed.
A dedicated service account with a single, scoped role means that even if something goes wrong, the blast radius is limited to invoking one service. The Professional Cloud Developer exam specifically tests this pattern under Section 1.2: running services with least-privileged access.
Seeing a permission denied error?
Make sure you're authenticated as the project owner or have the roles/iam.serviceAccountAdmin role. You can check your active account with gcloud auth list.
Also confirm your project ID is correct. Run gcloud config get-value project to check.
Help me troubleshoot a permission denied error when creating a service account in Google Cloud.
Create the Eventarc trigger
Now that the service account exists and has the right permission, you can create the Eventarc trigger. This is the glue that connects Pub/Sub messages to your Cloud Run service.
- Create the Eventarc trigger by running this command:
gcloud eventarc triggers create order-trigger --destination-run-service=order-processor --destination-run-region=us-central1 --location=us-central1 --event-filters="type=google.cloud.pubsub.topic.v1.messagePublished" --service-account=eventarc-trigger-sa@[[PROJECT_ID="your-gcp-project-id"]].iam.gserviceaccount.com
What does this command do?
- order-trigger is the name of your trigger.
- --destination-run-service=order-processor tells Eventarc where to send events.
- --event-filters="type=google.cloud.pubsub.topic.v1.messagePublished" specifies it listens for Pub/Sub publish events.
- --service-account attaches your least-privilege service account as the identity used to invoke Cloud Run.
- Because you didn't specify an existing topic, Eventarc automatically creates a new Pub/Sub topic for you.
First-time provisioning delay
The first time you create an Eventarc trigger in a project, Google may need to provision the Eventarc service agent. If you see a transient error, wait a moment and retry the command.
- Confirm the trigger was created by running:
gcloud eventarc triggers list --location=us-central1
- You should see order-trigger listed with its destination as order-processor and status as ACTIVE.
Trigger not showing as ACTIVE?
New triggers can take up to two minutes to become fully functional. Run the list command again after a short wait.
If the trigger failed to create, check that you granted roles/run.invoker to the correct service account email. A typo in the project ID portion of the email is a common cause.
Help me troubleshoot my Eventarc trigger not activating.
Retrieve the auto-created Pub/Sub topic name
Eventarc created a Pub/Sub topic automatically when you made the trigger. You need this topic name to publish test messages in the next step.
- Get the topic name by running this command:
gcloud eventarc triggers describe order-trigger --location=us-central1 --format="value(transport.pubsub.topic)"
- You should see a topic name in the output (something like projects/your-project/topics/eventarc-...). Copy this value and save it somewhere. You'll need it in the next step.
What does this command do?
The --format="value(transport.pubsub.topic)" flag extracts just the topic path from the trigger's full description. This is the topic you'll publish messages to when testing the end-to-end pipeline.
Seeing an empty output?
If the command returns nothing, your trigger may still be provisioning. Wait two minutes and try again.
Also confirm the trigger name is exactly order-trigger and the location is us-central1.
Help me retrieve my Eventarc trigger's Pub/Sub topic.
Your event pipeline is fully wired. The Eventarc trigger is listening for Pub/Sub messages and will route them to your Cloud Run service using the least-privilege service account. Next up, you'll publish test messages and verify the entire pipeline works end to end.
Test the Pipeline and Verify Observability
Your event-driven pipeline is fully wired. Pub/Sub delivers messages, Eventarc routes them, and Cloud Run processes them into Firestore.
In this step, you will send real order messages through the pipeline and verify that every layer works. You will confirm data lands in Firestore, check that structured logs emit correctly, and view trace correlation in Cloud Logging and Cloud Trace.
In this step, get ready to:
- Publish test order messages to your Pub/Sub topic.
- Verify order documents appear in Firestore.
- Confirm structured JSON logs with trace correlation in Cloud Logging.
Publish a test order message to Pub/Sub
To verify the end-to-end pipeline, you need to publish a message to the Pub/Sub topic that Eventarc created. The message simulates an order event that your Cloud Run service will process.
- Publish a test order to your Pub/Sub topic by running this command:
gcloud pubsub topics publish [[TOPIC_NAME="your-auto-created-topic-name"]] --message="{\"order_id\": \"ORD-001\", \"customer\": \"Alice\", \"item\": \"Widget Pro\", \"quantity\": 3, \"price\": 29.99}"
What does this command do?
The gcloud pubsub topics publish command sends a JSON-encoded order to the Pub/Sub topic. Eventarc picks up this message, wraps it in a CloudEvents envelope, and delivers it as an HTTP POST to your Cloud Run service.
Your service then base64-decodes the message, validates the fields, and writes the order to Firestore.
First message may take longer
Eventarc triggers can take up to two minutes to become fully active after creation. If your first message does not appear in Firestore immediately, wait two minutes and try again.
Verify the order in Firestore
Now you will confirm that the message made it all the way through the pipeline and landed as a document in Firestore.
- Open the Google Cloud Console in your browser.
- Navigate to the Databases page (search for "Firestore" in the console search bar).
- Select your database from the list.
- Click Firestore Studio in the left navigation menu.
- Look for an orders collection. Click it to expand it.
- Click the ORD-001 document.
You should see the order fields (customer, item, quantity, price) plus a processed_at timestamp and status set to received.
Why processed_at and status?
Your main.py code enriches every order with these two fields before writing to Firestore. The processed_at field records the exact UTC time the service handled the message. The status field tracks the order lifecycle.
- Publish a second test order to confirm the pipeline is consistent:
gcloud pubsub topics publish [[TOPIC_NAME="your-auto-created-topic-name"]] --message="{\"order_id\": \"ORD-002\", \"customer\": \"Bob\", \"item\": \"Gadget X\", \"quantity\": 1, \"price\": 49.99}"
- Return to Firestore Studio and refresh the page.
- Confirm that an ORD-002 document now appears in the orders collection alongside ORD-001.
Don't see the orders collection?
If the orders collection does not appear after two minutes, check the following:
- Make sure you used the correct topic name from the previous step. Run gcloud eventarc triggers describe order-trigger --location=us-central1 --format="value(transport.pubsub.topic)" to confirm.
- Verify that the runtime service account has the roles/datastore.user role.
- Check the Cloud Run logs for errors (you will learn how in the next substep).
Help me troubleshoot why my order is not appearing in Firestore after publishing to Pub/Sub.
Verify structured logging with trace correlation
Your service emits structured JSON logs with trace correlation using the X-Cloud-Trace-Context header. Cloud Logging automatically parses these entries and groups them under request traces.
- In the Google Cloud Console, search for Logs Explorer in the top search bar and open it.
- In the Query pane, enter the following filter to scope to your Cloud Run service:
resource.type="cloud_run_revision"
resource.labels.service_name="order-processor"
- Click Run query.
You should see log entries for the order processing requests. Each structured log entry contains severity, message, and logging.googleapis.com/trace fields.
What is trace correlation?
When Cloud Run receives a request, it assigns a trace ID via the X-Cloud-Trace-Context header. Your structured_log function extracts this ID and includes it in the logging.googleapis.com/trace field of each log entry.
Cloud Logging uses this field to group your container logs under the parent request log. This means you can see all logs from a single request in one place, instead of hunting through interleaved entries.
- Find a request log entry (it shows the HTTP POST to /).
- Click the triangle icon at the left of the request log entry.
Your container logs (the structured JSON entries from your structured_log function) appear nested beneath the request log. This is the parent-child correlation in action.
- Navigate to Cloud Trace by searching for it in the console search bar.
- Look for recent traces from the order-processor service.
- Click a trace to view the request waterfall showing the end-to-end timing of the request.
Cloud Trace sampling
Cloud Run samples traces automatically at a rate of 0.1 requests per second per instance. If you only published two messages, you might see one or both traces. For a production service handling many requests, the sampling provides representative coverage without excessive overhead.
Not seeing any log entries?
- Double-check the query filter uses resource.type="cloud_run_revision" exactly as written.
- Make sure you selected the correct project at the top of the Logs Explorer page.
- Adjust the time range in Logs Explorer to cover the last hour.
Help me troubleshoot why I cannot see Cloud Run logs in Logs Explorer.
Secret mission
Add a Dead-Letter Topic and Monitoring Alert
In production, messages sometimes fail processing. Without a safety net, those messages vanish silently. Your mission: configure a dead-letter queue that catches failed messages after 5 retries, then wire up a Cloud Monitoring alert that fires the instant a message lands in the DLQ. This is exactly the kind of production-grade error handling the Professional Cloud Developer exam tests.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs within Google Cloud's free tier for low-traffic usage, so there are no ongoing costs unless you exceed the free limits.
Resources you used:
- Cloud Run service (order-processor) in us-central1.
- Firestore database ((default)) in us-central1.
- Pub/Sub topics (auto-created by Eventarc trigger, plus order-dlq).
- Eventarc trigger (order-trigger).
- IAM service account (eventarc-trigger-sa).
- Artifact Registry repository (auto-created by Cloud Build).
- Cloud Monitoring alerting policy (Secret Mission).
- Local project directory with main.py and requirements.txt.
Keep everything running
No action needed. Choose this if you're still actively building or want to keep the pipeline available for testing.
- Everything stays within Google Cloud's free tier for low-traffic usage.
- Cloud Run: 2 million requests/month free.
- Firestore: 1 GB storage free.
- Pub/Sub: 10 GB messages/month free.
- Cloud Build: 120 build-minutes/day free.
- Artifact Registry: first 500 MB free.
- No cost unless you exceed these limits.
Pause - I'll come back to this later
Since Cloud Run scales to zero when not receiving traffic, there is effectively no cost to leaving it deployed. The free tier covers idle resources. If you want to reduce clutter, you can delete the Eventarc trigger and recreate it later.
- Delete the Eventarc trigger to stop message routing (you can recreate it later):
- Leave the Cloud Run service deployed. It scales to zero and incurs no cost when idle.
- Leave the Firestore database. Storage within 1 GB is free.
gcloud eventarc triggers delete order-trigger --location=us-central1
When you return, recreate the trigger with the same command from Step 4.
Delete - I don't want to use this again
Remove all project resources and start fresh. You have two options: delete resources individually, or delete the entire GCP project.
Option 1: Delete individual resources
- Delete the Cloud Run service, Eventarc trigger, dead-letter topic, and service account by running these commands:
gcloud run services delete order-processor --region us-central1
gcloud eventarc triggers delete order-trigger --location=us-central1
gcloud pubsub topics delete order-dlq
gcloud iam service-accounts delete eventarc-trigger-sa@[[PROJECT_ID="your-gcp-project-id"]].iam.gserviceaccount.com
- Delete the Firestore database:
gcloud firestore databases delete --database="(default)"
- Delete the Artifact Registry repository that Cloud Build auto-created. Find the repository name first:
gcloud artifacts repositories list --location=us-central1
- Then delete it using the name from the output:
gcloud artifacts repositories delete cloud-run-source-deploy --location=us-central1
- If you completed the Secret Mission, delete the alerting policy. Find the policy ID first:
gcloud monitoring policies list --format="table(name, displayName)"
- Then delete it using the policy name from the output:
gcloud monitoring policies delete [[POLICY_NAME="projects/your-project/alertPolicies/your-policy-id"]]
- Delete your local project files:
rm -rf main.py requirements.txt
Option 2: Delete the entire project
This removes all resources, APIs, and data associated with the project in one command.
- Delete the GCP project entirely by running:
gcloud projects delete [[PROJECT_ID="your-gcp-project-id"]]
Project deletion is permanent
Deleting the project removes all resources, billing configurations, and data within it. Google Cloud gives you a 30-day recovery window, but after that the project and all its contents are gone permanently.
Nice Work!
Nice Work!
You just built a production-pattern event-driven microservice on Google Cloud. From a Python Flask app to a fully deployed, observable, and secure order processing pipeline, you've demonstrated the core skills tested across all four Professional Cloud Developer exam domains.
Here's what you accomplished:
- Built and deployed a Flask microservice to Cloud Run from source using gcloud run deploy --source ., with Cloud Build handling containerization automatically.
- Configured Eventarc to route Pub/Sub messages to your Cloud Run service with a dedicated least-privilege service account, writing order documents to Firestore.
- Implemented structured JSON logging with trace correlation via the X-Cloud-Trace-Context header, and verified end-to-end observability in Cloud Logging and Cloud Trace.
- Secret Mission: Added a dead-letter topic with retry configuration and a Cloud Monitoring alerting policy for proactive error handling.
Ready to quiz yourself?