Arcade Voyage: App Modernization | GSP650
Solution for Arcade Voyage: App Modernization | GSP650. 1 lab: GSP650. Fast copy-paste commands for Google Cloud.
GSP650 — Build a Resilient, Asynchronous System with Cloud Run and Pub/Sub
Estimated time: 20 minutes
# 🚀 Build a Resilient, Asynchronous System with Cloud Run and Pub/Sub > ⚠️ **Disclaimer:** This is an independent, community-made walkthrough created for educational purposes, hands-on practice, and Google Cloud certification preparation. This guide is designed to help learners understand Cloud Run, Pub/Sub, asynchronous messaging, service-to-service invocation, and resilient cloud application workflows through hands-on practice. Always attempt the lab yourself first and follow Google Cloud Sk
clear
CYAN='\e[1;36m'
BLUE='\e[1;34m'
RESET='\e[0m'
BOLD='\e[1m'
GREEN='\e[1;32m'
YELLOW='\e[1;33m'
MAGENTA='\e[1;35m'
echo -e "${CYAN}${BOLD}"
cat << "EOF"
____ _ _ _ __ ___
/ __ \ | | (_) | / _| / _ \
| | | |_ __| |__ _| |_ ___ | |_ | | | |_ __ ___
| | | | '__| '_ \| | __| / _ \ | _|| | | | '_ \/ __|
| |__| | | | |_) | | |_ | (_) | | | | |_| | |_) \__ \
\____/|_| |_.__/|_|\__| \___/ |_| \___/| .__/|___/
| |
|_|
EOF
echo -e "${RESET}"
echo -e "${BLUE}${BOLD}╔════════════════════════════════════════════════════════════╗${RESET}"
echo -e "${BLUE}${BOLD}║ 🚀 BROUGHT TO YOU BY ORBIT OF OPS ║${RESET}"
echo -e "${BLUE}${BOLD}╚════════════════════════════════════════════════════════════╝${RESET}\n"
echo -e "${MAGENTA}${BOLD}Please check your lab instructions panel for your assigned Region.${RESET}"
read -p "Enter your Region (e.g., us-central1): " REGION
echo ""
export PROJECT_ID=$(gcloud config get-value core/project 2>/dev/null)
gcloud config set compute/region "${REGION}" --quiet >/dev/null 2>&1
echo -e "${CYAN}Task 1: Ensuring APIs & Pub/Sub Topic exist...${RESET}"
gcloud services enable run.googleapis.com pubsub.googleapis.com cloudbuild.googleapis.com --quiet
gcloud pubsub topics create new-lab-report --quiet 2>/dev/null || true
echo -e "${YELLOW}Setting up fresh directories...${RESET}"
cd ~
rm -rf pet-theory
mkdir -p ~/pet-theory/lab05/lab-service
mkdir -p ~/pet-theory/lab05/email-service
mkdir -p ~/pet-theory/lab05/sms-service
echo -e "${CYAN}Task 2: Building & Deploying Lab Report Service (Takes ~2 minutes)...${RESET}"
cd ~/pet-theory/lab05/lab-service
cat << 'EOF' > package.json
{
"name": "lab-report-service",
"version": "1.0.0",
"main": "index.js",
"scripts": { "start": "node index.js" },
"dependencies": { "@google-cloud/pubsub": "^3.0.0", "body-parser": "^1.20.0", "express": "^4.18.0" }
}
EOF
cat << 'EOF' > index.js
const {PubSub} = require('@google-cloud/pubsub');
const pubsub = new PubSub();
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.json());
const port = process.env.PORT || 8080;
app.listen(port, () => { console.log('Listening on port', port); });
app.post('/', async (req, res) => {
try {
const labReport = req.body;
await publishPubSubMessage(labReport);
res.status(204).send();
} catch (ex) {
console.log(ex);
res.status(500).send(ex);
}
})
async function publishPubSubMessage(labReport) {
const buffer = Buffer.from(JSON.stringify(labReport));
await pubsub.topic('new-lab-report').publish(buffer);
}
EOF
cat << 'EOF' > Dockerfile
FROM node:18
WORKDIR /usr/src/app
COPY package.json package*.json ./
RUN npm install --only=production
COPY . .
CMD [ "npm", "start" ]
EOF
gcloud builds submit --tag "gcr.io/${PROJECT_ID}/lab-report-service" --quiet
gcloud run deploy lab-report-service --image "gcr.io/${PROJECT_ID}/lab-report-service" --platform managed --region "${REGION}" --allow-unauthenticated --max-instances=1 --quiet
echo -e "${CYAN}Task 3: Building & Deploying Email Service (Takes ~2 minutes)...${RESET}"
cd ~/pet-theory/lab05/email-service
cat << 'EOF' > package.json
{
"name": "email-service",
"version": "1.0.0",
"main": "index.js",
"scripts": { "start": "node index.js" },
"dependencies": { "body-parser": "^1.20.0", "express": "^4.18.0" }
}
EOF
cat << 'EOF' > index.js
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.json());
const port = process.env.PORT || 8080;
app.listen(port, () => { console.log('Listening on port', port); });
app.post('/', async (req, res) => {
const labReport = decodeBase64Json(req.body.message.data);
try {
console.log(`Email Service: Report ${labReport.id} trying...`);
sendEmail();
console.log(`Email Service: Report ${labReport.id} success :-)`);
res.status(204).send();
} catch (ex) {
console.log(`Email Service: Report ${labReport.id} failure: ${ex}`);
res.status(500).send();
}
})
function decodeBase64Json(data) { return JSON.parse(Buffer.from(data, 'base64').toString()); }
function sendEmail() { console.log('Sending email'); }
EOF
cat << 'EOF' > Dockerfile
FROM node:18
WORKDIR /usr/src/app
COPY package.json package*.json ./
RUN npm install --only=production
COPY . .
CMD [ "npm", "start" ]
EOF
gcloud builds submit --tag "gcr.io/${PROJECT_ID}/email-service" --quiet
gcloud run deploy email-service --image "gcr.io/${PROJECT_ID}/email-service" --platform managed --region "${REGION}" --no-allow-unauthenticated --max-instances=1 --quiet
echo -e "\n${GREEN}${BOLD}✅ Command 1 Complete! You may click 'Check my progress' on the first 3 tasks, then proceed to Command 2.${RESET}"CYAN='\e[1;36m'
BLUE='\e[1;34m'
RESET='\e[0m'
BOLD='\e[1m'
GREEN='\e[1;32m'
YELLOW='\e[1;33m'
echo -e "${BLUE}${BOLD}╔════════════════════════════════════════════════════════════╗${RESET}"
echo -e "${BLUE}${BOLD}║ 🚀 COMMAND 2 OF 2: SMS SERVICE & EVENT TRIGGERS ║${RESET}"
echo -e "${BLUE}${BOLD}╚════════════════════════════════════════════════════════════╝${RESET}\n"
export PROJECT_ID=$(gcloud config get-value core/project 2>/dev/null)
export REGION=$(gcloud config get-value compute/region 2>/dev/null)
export PROJECT_NUMBER=$(gcloud projects describe "${PROJECT_ID}" --format='value(projectNumber)' 2>/dev/null)
echo -e "${CYAN}Task 4: Building & Deploying SMS Service (Takes ~2 minutes)...${RESET}"
cd ~/pet-theory/lab05/sms-service
cat << 'EOF' > package.json
{
"name": "sms-service",
"version": "1.0.0",
"main": "index.js",
"scripts": { "start": "node index.js" },
"dependencies": { "body-parser": "^1.20.0", "express": "^4.18.0" }
}
EOF
cat << 'EOF' > index.js
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
app.use(bodyParser.json());
const port = process.env.PORT || 8080;
app.listen(port, () => { console.log('Listening on port', port); });
app.post('/', async (req, res) => {
const labReport = decodeBase64Json(req.body.message.data);
try {
console.log(`SMS Service: Report ${labReport.id} trying...`);
sendSms();
console.log(`SMS Service: Report ${labReport.id} success :-)`);
res.status(204).send();
} catch (ex) {
console.log(`SMS Service: Report ${labReport.id} failure: ${ex}`);
res.status(500).send();
}
})
function decodeBase64Json(data) { return JSON.parse(Buffer.from(data, 'base64').toString()); }
function sendSms() { console.log('Sending SMS'); }
EOF
cat << 'EOF' > Dockerfile
FROM node:18
WORKDIR /usr/src/app
COPY package.json package*.json ./
RUN npm install --only=production
COPY . .
CMD [ "npm", "start" ]
EOF
gcloud builds submit --tag "gcr.io/${PROJECT_ID}/sms-service" --quiet
gcloud run deploy sms-service --image "gcr.io/${PROJECT_ID}/sms-service" --platform managed --region "${REGION}" --no-allow-unauthenticated --max-instances=1 --quiet
echo -e "${CYAN}Finalizing IAM Policies and Event Triggers...${RESET}"
gcloud iam service-accounts create pubsub-cloud-run-invoker --display-name "PubSub Cloud Run Invoker" --quiet 2>/dev/null || true
gcloud run services add-iam-policy-binding email-service --member="serviceAccount:pubsub-cloud-run-invoker@${PROJECT_ID}.iam.gserviceaccount.com" --role="roles/run.invoker" --region="${REGION}" --quiet
gcloud run services add-iam-policy-binding sms-service --member="serviceAccount:pubsub-cloud-run-invoker@${PROJECT_ID}.iam.gserviceaccount.com" --role="roles/run.invoker" --region="${REGION}" --quiet
gcloud projects add-iam-policy-binding "${PROJECT_ID}" --member="serviceAccount:service-${PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com" --role="roles/iam.serviceAccountTokenCreator" --quiet >/dev/null
export EMAIL_SERVICE_URL=$(gcloud run services describe email-service --region "${REGION}" --format="value(status.address.url)")
export SMS_SERVICE_URL=$(gcloud run services describe sms-service --region "${REGION}" --format="value(status.address.url)")
export LAB_REPORT_SERVICE_URL=$(gcloud run services describe lab-report-service --region "${REGION}" --format="value(status.address.url)")
gcloud pubsub subscriptions delete email-service-sub --quiet 2>/dev/null || true
gcloud pubsub subscriptions create email-service-sub --topic new-lab-report --push-endpoint="${EMAIL_SERVICE_URL}" --push-auth-service-account="pubsub-cloud-run-invoker@${PROJECT_ID}.iam.gserviceaccount.com" --quiet
gcloud pubsub subscriptions delete sms-service-sub --quiet 2>/dev/null || true
gcloud pubsub subscriptions create sms-service-sub --topic new-lab-report --push-endpoint="${SMS_SERVICE_URL}" --push-auth-service-account="pubsub-cloud-run-invoker@${PROJECT_ID}.iam.gserviceaccount.com" --quiet
echo -e "${CYAN}Testing Lab Report Application Pipeline...${RESET}"
curl -s -X POST -H "Content-Type: application/json" -d "{\"id\": 12}" "${LAB_REPORT_SERVICE_URL}" >/dev/null &
curl -s -X POST -H "Content-Type: application/json" -d "{\"id\": 34}" "${LAB_REPORT_SERVICE_URL}" >/dev/null &
curl -s -X POST -H "Content-Type: application/json" -d "{\"id\": 56}" "${LAB_REPORT_SERVICE_URL}" >/dev/null &
echo -e "\n${GREEN}${BOLD}🎉 AUTOMATION COMPLETE! Click 'Check my progress' on ALL remaining tasks to collect your 100% score!${RESET}"
echo -e "${GREEN}${BOLD}# ─────────────────────────────────────────────────────────────${RESET}"
echo -e "${GREEN}${BOLD}# Lab cleared? Like the video and subscribe to Orbit of Ops 🚀${RESET}"
echo -e "${GREEN}${BOLD}# youtube.com/@orbitofops${RESET}"
echo -e "${GREEN}${BOLD}# ─────────────────────────────────────────────────────────────${RESET}"