1. מבוא
סקירה כללית
ב-Codelab הזה תגדירו את Cloud Run כך שייבנו ויפרסו באופן אוטומטי גרסאות חדשות של האפליקציה בכל פעם שתדחפו שינויים בקוד המקור למאגר GitHub.
אפליקציית ההדגמה הזו שומרת נתוני משתמשים ב-Firestore, אבל רק חלק מהנתונים נשמרים בצורה תקינה. תגדירו פריסות רציפות כך שכשתדחפו תיקון באג למאגר GitHub, התיקון יהיה זמין אוטומטית בגרסה חדשה.
מה תלמדו
- כתיבת אפליקציית אינטרנט של Express באמצעות Cloud Shell Editor
- קישור החשבון ב-GitHub ל-Google Cloud לפריסות רציפות
- פריסה אוטומטית של האפליקציה ב-Cloud Run
- איך משתמשים ב-HTMX וב-TailwindCSS
2. הגדרה ודרישות
דרישות מוקדמות
- יש לכם חשבון GitHub ואתם יודעים איך ליצור קוד ולדחוף אותו למאגרים.
- אתם מחוברים ל-Cloud Console.
- כבר פרסתם שירות Cloud Run. לדוגמה, אפשר לפעול לפי ההוראות שבמדריך למתחילים לפריסת שירות אינטרנט מקוד מקור.
הפעלת Cloud Shell
- ב-Cloud Console, לוחצים על Activate Cloud Shell
.

אם זו הפעם הראשונה שאתם מפעילים את Cloud Shell, יוצג לכם מסך ביניים עם תיאור של השירות. אם הוצג לכם מסך ביניים, לוחצים על המשך.

הקצאת המשאבים והחיבור ל-Cloud Shell נמשכים רק כמה רגעים.

במכונה הווירטואלית הזו טעונים כל הכלים הדרושים למפתחים. יש בה ספריית בית בנפח מתמיד של 5GB והיא פועלת ב-Google Cloud, מה שמשפר מאוד את הביצועים והאימות של הרשת. אפשר לבצע את רוב העבודה ב-codelab הזה, אם לא את כולה, באמצעות דפדפן.
אחרי שמתחברים ל-Cloud Shell, אמור להופיע אימות ושהפרויקט מוגדר לפי מזהה הפרויקט.
- מריצים את הפקודה הבאה ב-Cloud Shell כדי לוודא שעברתם אימות:
gcloud auth list
פלט הפקודה
Credentialed Accounts
ACTIVE ACCOUNT
* <my_account>@<my_domain.com>
To set the active account, run:
$ gcloud config set account `ACCOUNT`
- מריצים את הפקודה הבאה ב-Cloud Shell כדי לוודא שפקודת gcloud מכירה את הפרויקט:
gcloud config list project
פלט הפקודה
[core] project = <PROJECT_ID>
אם הוא לא מוגדר, אפשר להגדיר אותו באמצעות הפקודה הבאה:
gcloud config set project <PROJECT_ID>
פלט הפקודה
Updated property [core/project].
3. הפעלת ממשקי API והגדרת משתני סביבה
הפעלת ממשקי ה-API
ב-Codelab הזה נדרש שימוש בממשקי ה-API הבאים. כדי להפעיל את ממשקי ה-API האלה, מריצים את הפקודה הבאה:
gcloud services enable run.googleapis.com \
cloudbuild.googleapis.com \
firestore.googleapis.com \
iamcredentials.googleapis.com
הגדרה של משתני סביבה
אתם יכולים להגדיר משתני סביבה שישמשו אתכם לאורך כל ה-codelab הזה.
REGION=<YOUR-REGION> PROJECT_ID=<YOUR-PROJECT-ID> PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)') SERVICE_ACCOUNT="firestore-accessor" SERVICE_ACCOUNT_ADDRESS=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com
4. יצירה של חשבון שירות
חשבון השירות הזה ישמש את Cloud Run כדי לבצע קריאה ל-Vertex AI Gemini API. לחשבון השירות הזה יהיו גם הרשאות קריאה וכתיבה ל-Firestore וקריאת סודות מ-Secret Manager.
קודם יוצרים את חשבון השירות באמצעות הפקודה הבאה:
gcloud iam service-accounts create $SERVICE_ACCOUNT \ --display-name="Cloud Run access to Firestore"
עכשיו נותנים לחשבון השירות הרשאות קריאה וכתיבה ב-Firestore.
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \ --role=roles/datastore.user
5. יצירה והגדרה של פרויקט Firebase
- במסוף Firebase, לוחצים על הוספת פרויקט.
- מזינים את הערך <YOUR_PROJECT_ID> כדי להוסיף את Firebase לאחד מהפרויקטים הקיימים ב-Google Cloud
- אם תופיע בקשה, תצטרכו לקרוא את התנאים של Firebase ולאשר אותם.
- לוחצים על המשך.
- לוחצים על אישור תוכנית התשלומים כדי לאשר את תוכנית התשלומים של Firebase.
- הפעלת Google Analytics ב-codelab הזה היא אופציונלית.
- לוחצים על הוספת Firebase.
- אחרי שהפרויקט נוצר, לוחצים על המשך.
- בתפריט Build (פיתוח), לוחצים על Firestore database (מסד נתונים של Firestore).
- לוחצים על יצירת מסד נתונים.
- בוחרים את האזור מהתפריט הנפתח מיקום ולוחצים על הבא.
- משתמשים באפשרות ברירת המחדל התחלה במצב ייצור ולוחצים על יצירה.
6. כתיבת הבקשה
קודם יוצרים ספרייה לקוד המקור ועוברים לספרייה הזו.
mkdir cloud-run-github-cd-demo && cd $_
לאחר מכן, יוצרים קובץ package.json עם התוכן הבא:
{
"name": "cloud-run-github-cd-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node app.js",
"nodemon": "nodemon app.js",
"tailwind-dev": "npx tailwindcss -i ./input.css -o ./public/output.css --watch",
"tailwind": "npx tailwindcss -i ./input.css -o ./public/output.css",
"dev": "npm run tailwind && npm run nodemon"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@google-cloud/firestore": "^7.3.1",
"axios": "^1.6.7",
"express": "^4.18.2",
"htmx.org": "^1.9.10"
},
"devDependencies": {
"nodemon": "^3.1.0",
"tailwindcss": "^3.4.1"
}
}
קודם יוצרים קובץ מקור app.js עם התוכן שבהמשך. הקובץ הזה מכיל את נקודת הכניסה לשירות ואת הלוגיקה העיקרית של האפליקציה.
const express = require("express");
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
const path = require("path");
const { get } = require("axios");
const { Firestore } = require("@google-cloud/firestore");
const firestoreDb = new Firestore();
const fs = require("fs");
const util = require("util");
const { spinnerSvg } = require("./spinnerSvg.js");
const service = process.env.K_SERVICE;
const revision = process.env.K_REVISION;
app.use(express.static("public"));
app.get("/edit", async (req, res) => {
res.send(`<form hx-post="/update" hx-target="this" hx-swap="outerHTML">
<div>
<p>
<label>Name</label>
<input class="border-2" type="text" name="name" value="Cloud">
</p><p>
<label>Town</label>
<input class="border-2" type="text" name="town" value="Nibelheim">
</p>
</div>
<div class="flex items-center mr-[10px] mt-[10px]">
<button class="btn bg-blue-500 text-white px-4 py-2 rounded-lg text-center text-sm font-medium mr-[10px]">Submit</button>
<button class="btn bg-gray-200 text-gray-800 px-4 py-2 rounded-lg text-center text-sm font-medium mr-[10px]" hx-get="cancel">Cancel</button>
${spinnerSvg}
</div>
</form>`);
});
app.post("/update", async function (req, res) {
let name = req.body.name;
let town = req.body.town;
const doc = firestoreDb.doc(`demo/${name}`);
//TODO: fix this bug
await doc.set({
name: name
/* town: town */
});
res.send(`<div hx-target="this" hx-swap="outerHTML" hx-indicator="spinner">
<p>
<div><label>Name</label>: ${name}</div>
</p><p>
<div><label>Town</label>: ${town}</div>
</p>
<button
hx-get="/edit"
class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium mt-[10px]"
>
Click to update
</button>
</div>`);
});
app.get("/cancel", (req, res) => {
res.send(`<div hx-target="this" hx-swap="outerHTML">
<p>
<div><label>Name</label>: Cloud</div>
</p><p>
<div><label>Town</label>: Nibelheim</div>
</p>
<div>
<button
hx-get="/edit"
class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium mt-[10px]"
>
Click to update
</button>
</div>
</div>`);
});
const port = parseInt(process.env.PORT) || 8080;
app.listen(port, async () => {
console.log(`booth demo: listening on port ${port}`);
//serviceMetadata = helper();
});
app.get("/helper", async (req, res) => {
let region = "";
let projectId = "";
let div = "";
try {
// Fetch the token to make a GCF to GCF call
const response1 = await get(
"http://metadata.google.internal/computeMetadata/v1/project/project-id",
{
headers: {
"Metadata-Flavor": "Google"
}
}
);
// Fetch the token to make a GCF to GCF call
const response2 = await get(
"http://metadata.google.internal/computeMetadata/v1/instance/region",
{
headers: {
"Metadata-Flavor": "Google"
}
}
);
projectId = response1.data;
let regionFull = response2.data;
const index = regionFull.lastIndexOf("/");
region = regionFull.substring(index + 1);
div = `
<div>
This created the revision <code>${revision}</code> of the
Cloud Run service <code>${service}</code> in <code>${region}</code>
for project <code>${projectId}</code>.
</div>`;
} catch (ex) {
// running locally
div = `<div> This is running locally.</div>`;
}
res.send(div);
});
יוצרים קובץ בשם spinnerSvg.js
module.exports.spinnerSvg = `<svg id="spinner" alt="Loading..."
class="htmx-indicator animate-spin -ml-1 mr-3 h-5 w-5 text-blue-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>`;
יצירת קובץ input.css ל-tailwindCSS
@tailwind base; @tailwind components; @tailwind utilities;
יצירה של קובץ tailwind.config.js עבור tailwindCSS
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./**/*.{html,js}"],
theme: {
extend: {}
},
plugins: []
};
המערכת תיצור קובץ .gitignore.
node_modules/ npm-debug.log coverage/ package-lock.json .DS_Store
עכשיו יוצרים ספרייה חדשה בשם public.
mkdir public cd public
בתוך הספרייה הציבורית הזו, יוצרים את הקובץ index.html עבור ממשק הקצה, שבו ישמש htmx.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<script
src="https://unpkg.com/htmx.org@1.9.10"
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
crossorigin="anonymous"
></script>
<link href="./output.css" rel="stylesheet" />
<title>Demo 1</title>
</head>
<body
class="font-sans bg-body-image bg-cover bg-center leading-relaxed"
>
<div class="container max-w-[700px] mt-[50px] ml-auto mr-auto">
<div class="hero flex items-center">
<div class="message text-base text-center mb-[24px]">
<h1 class="text-2xl font-bold mb-[10px]">
It's running!
</h1>
<div class="congrats text-base font-normal">
Congratulations, you successfully deployed your
service to Cloud Run.
</div>
</div>
</div>
<div class="details mb-[20px]">
<p>
<div hx-trigger="load" hx-get="/helper" hx-swap="innerHTML" hx-target="this">Hello</div>
</p>
</div>
<p
class="callout text-sm text-blue-700 font-bold pt-4 pr-6 pb-4 pl-10 leading-tight"
>
You can deploy any container to Cloud Run that listens for
HTTP requests on the port defined by the
<code>PORT</code> environment variable. Cloud Run will
scale automatically based on requests and you never have to
worry about infrastructure.
</p>
<h1 class="text-2xl font-bold mt-[40px] mb-[20px]">
Persistent Storage Example using Firestore
</h1>
<div hx-target="this" hx-swap="outerHTML">
<p>
<div><label>Name</label>: Cloud</div>
</p><p>
<div><label>Town</label>: Nibelheim</div>
</p>
<div>
<button
hx-get="/edit"
class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium mt-[10px]"
>
Click to update
</button>
</div>
</div>
<h1 class="text-2xl font-bold mt-[40px] mb-[20px]">
What's next
</h1>
<p class="next text-base mt-4 mb-[20px]">
You can build this demo yourself!
</p>
<p class="cta">
<button
class="bg-blue-500 text-white px-4 py-2 rounded-lg text-center text-sm font-medium"
>
VIEW CODELAB
</button>
</p>
</div>
</body>
</html>
7. הרצת האפליקציה באופן מקומי
בקטע הזה, תריצו את האפליקציה באופן מקומי כדי לוודא שיש באפליקציה באג שמופיע כשהמשתמש מנסה לשמור נתונים.
קודם כל, תצטרכו לקבל את התפקיד Datastore User כדי לגשת ל-Firestore (אם אתם משתמשים בזהות שלכם לאימות, למשל אם אתם מריצים את הפקודה ב-Cloud Shell), או שתוכלו להתחזות לחשבון המשתמש שיצרתם קודם.
שימוש ב-ADC בזמן הרצה מקומית
אם אתם מריצים את הפקודה ב-Cloud Shell, אתם כבר מריצים אותה במכונה וירטואלית של Google Compute Engine. פרטי הכניסה שמשויכים למכונה הווירטואלית הזו (כפי שמוצג בהרצת הפקודה gcloud auth list) ישמשו אוטומטית את Application Default Credentials (ADC), כך שלא צריך להשתמש בפקודה gcloud auth application-default login. עם זאת, עדיין צריך להקצות לזהות שלכם את התפקיד 'משתמש Datastore'. אפשר לדלג לקטע הפעלת האפליקציה באופן מקומי.
אבל אם אתם מריצים את הפקודה במסוף המקומי (כלומר לא ב-Cloud Shell), תצטרכו להשתמש ב-Application Default Credentials כדי לבצע אימות ל-Google APIs. יש שתי אפשרויות: 1) להתחבר באמצעות פרטי הכניסה (אם יש לכם את התפקיד Datastore User) או 2) להתחבר באמצעות התחזות לחשבון השירות שבו נעשה שימוש ב-codelab הזה.
אפשרות 1) שימוש בפרטי הכניסה שלכם ל-ADC
אם רוצים להשתמש באמצעי האימות, אפשר קודם להריץ את הפקודה gcloud auth list כדי לבדוק איך מתבצע האימות ב-gcloud. לאחר מכן, יכול להיות שתצטרכו להקצות לזהות שלכם את התפקיד 'משתמש ב-Vertex AI'. אם לישות שלכם יש את התפקיד Owner, כבר יש לכם את תפקיד המשתמש Datastore User. אם לא, אפשר להריץ את הפקודה הזו כדי להעניק לזהות שלכם את תפקיד המשתמש ב-Vertex AI ואת תפקיד המשתמש במאגר הנתונים.
USER=<YOUR_PRINCIPAL_EMAIL> gcloud projects add-iam-policy-binding $PROJECT_ID \ --member user:$USER \ --role=roles/datastore.user
לאחר מכן מריצים את הפקודה הבאה
gcloud auth application-default login
אפשרות 2) התחזות לחשבון שירות ל-ADC
אם רוצים להשתמש בחשבון השירות שנוצר ב-codelab הזה, לחשבון המשתמש צריך להיות מוקצה התפקיד 'יצירת אסימונים בחשבון שירות'. כדי לקבל את התפקיד הזה, מריצים את הפקודה הבאה:
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member user:$USER \ --role=roles/iam.serviceAccountTokenCreator
בשלב הבא, מריצים את הפקודה הבאה כדי להשתמש ב-ADC עם חשבון השירות
gcloud auth application-default login --impersonate-service-account=$SERVICE_ACCOUNT_ADDRESS
הפעלת האפליקציה באופן מקומי
בשלב הבא, מוודאים שאתם בספריית הבסיס cloud-run-github-cd-demo של ה-codelab.
cd .. && pwd
עכשיו מתקינים יחסי תלות.
npm install
לבסוף, מריצים את הסקריפט הבא כדי להפעיל את האפליקציה. הסקריפט הזה ייצור גם את הקובץ output.css מ-tailwindCSS.
npm run dev
עכשיו פותחים את דפדפן האינטרנט בכתובת http://localhost:8080. אם אתם ב-Cloud Shell, אתם יכולים לפתוח את האתר באמצעות הכפתור 'תצוגה מקדימה של אתר' ולבחור באפשרות 'תצוגה מקדימה של יציאה 8080'.

מזינים טקסט בשדות הקלט של השם והעיר ולוחצים על שמירה. לאחר מכן מרעננים את הדף. תשימו לב שהשדה 'עיר' לא נשמר. בקטע הבא נסביר איך לפתור את הבאג הזה.
מפסיקים את ההפעלה של אפליקציית Express באופן מקומי (למשל, Ctrl^c ב-MacOS).
8. יצירת מאגר ב-GitHub
בספרייה המקומית, יוצרים מאגר חדש עם main כשם ברירת המחדל של הענף.
git init git branch -M main
מבצעים קומיט של בסיס הקוד הנוכחי שמכיל את הבאג. תתקנו את הבאג אחרי שתגדירו פריסה רציפה.
git add . git commit -m "first commit for express application"
עוברים אל GitHub ויוצרים מאגר ריק שהוא פרטי או ציבורי. ב-codelab הזה מומלץ לתת למאגר השם cloud-run-auto-deploy-codelab כדי ליצור מאגר ריק, משאירים את כל הגדרות ברירת המחדל לא מסומנות או מגדירים אותן כ'ללא', כך שלא יהיה תוכן במאגר כברירת מחדל כשהוא נוצר, למשל:

אם השלמתם את השלב הזה בצורה נכונה, ההוראות הבאות יופיעו בדף המאגר הריק:

מריצים את הפקודות הבאות לפי ההוראות שבמאמר שליחת מאגר קיים משורת הפקודה:
קודם מוסיפים את המאגר המרוחק על ידי הפעלת הפקודה
git remote add origin <YOUR-REPO-URL-PER-GITHUB-INSTRUCTIONS>
ואז מעלים את הענף הראשי למאגר המרוחק.
git push -u origin main
9. הגדרת פריסה רציפה
עכשיו, אחרי שיש לכם קוד ב-GitHub, אתם יכולים להגדיר פריסה רציפה. נכנסים אל Cloud Console for Cloud Run.
- לוחצים על 'יצירת שירות'.
- לוחצים על פריסה רציפה ממאגר.
- לוחצים על הגדרת Cloud Build.
- בקטע Source repository (מאגר מקור)
- בוחרים ב-GitHub כספק המאגר
- לוחצים על Manage connected repositories (ניהול מאגרי נתונים מקושרים) כדי להגדיר את הגישה של Cloud Build למאגר.
- בוחרים את המאגר ולוחצים על הבא.
- בקטע Build Configuration (הגדרת בנייה)
- השארת הענף כ-^main$
- בקטע Build Type (סוג בנייה), בוחרים באפשרות Go, Node.js, Python, Java, .NET Core, Ruby or PHP via Google Cloud's buildpacks (Go, Node.js, Python, Java, .NET Core, Ruby או PHP באמצעות buildpacks של Google Cloud).
- השארת ספריית ההקשר של ה-Build כ-
/ - לוחצים על שמירה.
- בקטע 'אימות'
- לוחצים על אפשר הפעלות לא מאומתות.
- בקטעים Container(s), Volumes, Networking, Security
- בכרטיסייה Security (אבטחה), בוחרים את חשבון השירות שיצרתם בשלב קודם, למשל
Cloud Run access to Firestore
- בכרטיסייה Security (אבטחה), בוחרים את חשבון השירות שיצרתם בשלב קודם, למשל
- לוחצים על יצירה.
הפעולה הזו תפרוס את שירות Cloud Run שמכיל את הבאג שתתקנו בקטע הבא.
10. תיקון הבאג
תיקון הבאג בקוד
ב-Cloud Shell Editor, פותחים את הקובץ app.js ועוברים לתגובה //TODO: fix this bug
משנים את השורה הבאה מ-
//TODO: fix this bug
await doc.set({
name: name
});
עד
//fixed town bug
await doc.set({
name: name,
town: town
});
כדי לאמת את התיקון, מריצים את הפקודה
npm run start
ופותחים את דפדפן האינטרנט. שומרים שוב את הנתונים של העיר ומרעננים. אחרי הרענון, תראו שנתוני העיר שהזנתם נשמרו בצורה תקינה.
אחרי שאימתתם את התיקון, אפשר לפרוס אותו. קודם, מבצעים קומיט לתיקון.
git add . git commit -m "fixed town bug"
ואז מעלים אותו למאגר המקור ב-GitHub.
git push origin main
השינויים ייפרסו אוטומטית באמצעות Cloud Build. אפשר להיכנס ל-Cloud Console בשביל שירות Cloud Run כדי לעקוב אחרי שינויים בפריסה.
אימות התיקון בסביבת הייצור
אחרי שבמסוף Cloud בשירות Cloud Run מוצגת גרסה שנייה שמשרתת 100% מתעבורת הנתונים, למשל https://console.cloud.google.com/run/detail/<YOUR_REGION>/<YOUR_SERVICE_NAME>/revisions, אפשר לפתוח את כתובת ה-URL של שירות Cloud Run בדפדפן ולוודא שנתוני העיר שהוזנו לאחרונה נשמרים אחרי רענון הדף.
11. מזל טוב!
כל הכבוד, סיימתם את ה-Codelab!
מומלץ לעיין במסמכים Cloud Run ופריסה רציפה מ-git.
מה נכלל
- כתיבת אפליקציית אינטרנט של Express באמצעות Cloud Shell Editor
- קישור החשבון ב-GitHub ל-Google Cloud לפריסות רציפות
- פריסה אוטומטית של האפליקציה ב-Cloud Run
- איך משתמשים ב-HTMX וב-TailwindCSS
12. הסרת המשאבים
כדי להימנע מחיובים לא מכוונים (לדוגמה, אם שירותי Cloud Run מופעלים בטעות יותר פעמים מההקצאה החודשית של הפעלות Cloud Run בתוכנית בחינם), אפשר למחוק את Cloud Run או את הפרויקט שיצרתם בשלב 2.
כדי למחוק את שירות Cloud Run, עוברים אל Cloud Run Cloud Console בכתובת https://console.cloud.google.com/run ומוחקים את שירות Cloud Run שיצרתם ב-codelab הזה, למשל, מוחקים את השירות cloud-run-auto-deploy-codelab.
אם אתם רוצים למחוק את הפרויקט כולו, אתם יכולים להיכנס לכתובת https://console.cloud.google.com/cloud-resource-manager, לבחור את הפרויקט שיצרתם בשלב 2 וללחוץ על 'מחיקה'. אם תמחקו את הפרויקט, תצטרכו לשנות את הפרויקטים ב-Cloud SDK. כדי לראות את רשימת כל הפרויקטים הזמינים, מריצים את הפקודה gcloud projects list.