Bu codelab hakkında
1. Genel Bakış
Cloud Run, kodunuzu doğrudan Google'ın ölçeklenebilir altyapısında çalıştırmanıza olanak tanıyan, tümüyle yönetilen bir platformdur. Bu Codelab'de, Node.js Admin SDK'sı kullanılarak Cloud Run'daki bir Angular uygulamasının Firestore veritabanına nasıl bağlanacağı gösterilmektedir.
Bu laboratuvarda şunları öğreneceksiniz:
- Firestore veritabanı oluşturma
- Cloud Run'a, Firestore veritabanınıza bağlanan bir uygulama dağıtma
2. Ön koşullar
- Google Hesabınız yoksa Google Hesabı oluşturmanız gerekir.
- İş veya okul hesabı yerine kişisel hesap kullanıyorsanız. İş ve okul hesaplarında, bu laboratuvar için gereken API'leri etkinleştirmenizi engelleyen kısıtlamalar olabilir.
3. Proje ayarlama
- Google Cloud Console'da oturum açın.
- Cloud Console'da faturalandırmayı etkinleştirin.
- Bu laboratuvarı tamamlamak, bulut kaynaklarında 1 ABD dolarından az maliyete mal olur.
- Daha fazla ödeme alınmaması için bu laboratuvarın sonundaki adımları uygulayarak kaynakları silebilirsiniz.
- Yeni kullanıcılar 300 ABD doları tutarındaki ücretsiz deneme sürümünden yararlanabilir.
- Yeni bir proje oluşturun veya mevcut bir projeyi yeniden kullanmayı seçin.
4. Cloud Shell Düzenleyici'yi açma
- Cloud Shell Düzenleyici'ye gidin
- Terminal ekranın alt kısmında görünmüyorsa açın:
- Hamburger menüsünü
tıklayın.
- Terminal'i tıklayın.
- Yeni Terminal'i tıklayın
- Hamburger menüsünü
- Terminalde projenizi şu komutla ayarlayın:
- Biçim:
gcloud config set project [PROJECT_ID]
- Örnek:
gcloud config set project lab-project-id-example
- Proje kimliğinizi hatırlamıyorsanız:
- Tüm proje kimliklerinizi şu komutla listeleyebilirsiniz:
gcloud projects list | awk '/PROJECT_ID/{print $2}'
- Tüm proje kimliklerinizi şu komutla listeleyebilirsiniz:
- Biçim:
- Yetkilendirme isteğinde bulunulursa devam etmek için Yetkilendir'i tıklayın.
- Aşağıdaki mesajı görürsünüz:
Updated property [core/project].
WARNING
görüyorsanız veDo you want to continue (Y/N)?
soruyorsanız proje kimliğini yanlış girdiğiniz muhtemeldir.N
,Enter
tuşlarına basın vegcloud config set project
komutunu tekrar çalıştırmayı deneyin.
5. API'leri etkinleştir
Terminalde API'leri etkinleştirin:
gcloud services enable \
firestore.googleapis.com \
run.googleapis.com \
artifactregistry.googleapis.com \
cloudbuild.googleapis.com
Yetkilendirme isteğinde bulunulursa devam etmek için Yetkilendir'i tıklayın.
Bu komutun tamamlanması birkaç dakika sürebilir ancak sonunda şuna benzer bir başarılı mesaj gösterilir:
Operation "operations/acf.p2-73d90d00-47ee-447a-b600" finished successfully.
6. Firestore veritabanı oluşturma
- Firestore veritabanı oluşturmak için
gcloud firestore databases create
komutunu çalıştırıngcloud firestore databases create --location=nam5
7. Başvuruyu hazırlama
HTTP isteklerine yanıt veren bir Next.js uygulaması hazırlayın.
task-app
adlı yeni bir Next.js projesi oluşturmak için şu komutu kullanın:npx --yes @angular/cli@19.2.5 new task-app \
--minimal \
--inline-template \
--inline-style \
--ssr \
--server-routing \
--defaults- Dizin
task-app
olarak değiştirilir:cd task-app
- Firestore veritabanı ile etkileşimde bulunmak için
firebase-admin
'ü yükleyin.npm install firebase-admin
server.ts
dosyasını Cloud Shell Düzenleyici'de açın: Artık ekranın üst kısmında bir dosya görünecektir. Bucloudshell edit src/server.ts
server.ts
dosyasını buradan düzenleyebilirsiniz.server.ts
dosyasının mevcut içeriğini silin.- Aşağıdaki kodu kopyalayıp açılan
server.ts
dosyasına yapıştırın:import {
AngularNodeAppEngine,
createNodeRequestHandler,
isMainModule,
writeResponseToNodeResponse,
} from '@angular/ssr/node';
import express from 'express';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { initializeApp, applicationDefault, getApps } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
type Task = {
id: string;
title: string;
status: 'IN_PROGRESS' | 'COMPLETE';
createdAt: number;
};
const credential = applicationDefault();
// Only initialize app if it does not already exist
if (getApps().length === 0) {
initializeApp({ credential });
}
const db = getFirestore();
const tasksRef = db.collection('tasks');
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
const app = express();
const angularApp = new AngularNodeAppEngine();
app.use(express.json());
app.get('/api/tasks', async (req, res) => {
const snapshot = await tasksRef.orderBy('createdAt', 'desc').limit(100).get();
const tasks: Task[] = snapshot.docs.map(doc => ({
id: doc.id,
title: doc.data()['title'],
status: doc.data()['status'],
createdAt: doc.data()['createdAt'],
}));
res.send(tasks);
});
app.post('/api/tasks', async (req, res) => {
const newTaskTitle = req.body.title;
if(!newTaskTitle){
res.status(400).send("Title is required");
return;
}
await tasksRef.doc().create({
title: newTaskTitle,
status: 'IN_PROGRESS',
createdAt: Date.now(),
});
res.sendStatus(200);
});
app.put('/api/tasks', async (req, res) => {
const task: Task = req.body;
if (!task || !task.id || !task.title || !task.status) {
res.status(400).send("Invalid task data");
return;
}
await tasksRef.doc(task.id).set(task);
res.sendStatus(200);
});
app.delete('/api/tasks', async (req, res) => {
const task: Task = req.body;
if(!task || !task.id){
res.status(400).send("Task ID is required");
return;
}
await tasksRef.doc(task.id).delete();
res.sendStatus(200);
});
/**
* Serve static files from /browser
*/
app.use(
express.static(browserDistFolder, {
maxAge: '1y',
index: false,
redirect: false,
}),
);
/**
* Handle all other requests by rendering the Angular application.
*/
app.use('/**', (req, res, next) => {
angularApp
.handle(req)
.then((response) =>
response ? writeResponseToNodeResponse(response, res) : next(),
)
.catch(next);
});
/**
* Start the server if this module is the main entry point.
* The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
*/
if (isMainModule(import.meta.url)) {
const port = process.env['PORT'] || 4000;
app.listen(port, () => {
console.log(`Node Express server listening on http://localhost:${port}`);
});
}
/**
* Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions.
*/
export const reqHandler = createNodeRequestHandler(app); angular.json
dosyasını Cloud Shell Düzenleyici'de açın: Şimdicloudshell edit angular.json
"externalDependencies": ["firebase-admin"]
satırınıangular.json
dosyasına ekleyeceğiz.angular.json
dosyasının mevcut içeriğini silin.- Aşağıdaki kodu kopyalayıp açılan
angular.json
dosyasına yapıştırın:{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"task-app": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"inlineTemplate": true,
"inlineStyle": true,
"skipTests": true
},
"@schematics/angular:class": {
"skipTests": true
},
"@schematics/angular:directive": {
"skipTests": true
},
"@schematics/angular:guard": {
"skipTests": true
},
"@schematics/angular:interceptor": {
"skipTests": true
},
"@schematics/angular:pipe": {
"skipTests": true
},
"@schematics/angular:resolver": {
"skipTests": true
},
"@schematics/angular:service": {
"skipTests": true
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/task-app",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
],
"scripts": [],
"server": "src/main.server.ts",
"outputMode": "server",
"ssr": {
"entry": "src/server.ts"
},
"externalDependencies": ["firebase-admin"]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "task-app:build:production"
},
"development": {
"buildTarget": "task-app:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n"
}
}
}
}
}
"externalDependencies": ["firebase-admin"]
app.component.ts
dosyasını Cloud Shell Düzenleyici'de açın: Ekranın üst kısmında mevcut bir dosya görünür. Bucloudshell edit src/app/app.component.ts
app.component.ts
dosyasını buradan düzenleyebilirsiniz.app.component.ts
dosyasının mevcut içeriğini silin.- Aşağıdaki kodu kopyalayıp açılan
app.component.ts
dosyasına yapıştırın:import { afterNextRender, Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
type Task = {
id: string;
title: string;
status: 'IN_PROGRESS' | 'COMPLETE';
createdAt: number;
};
@Component({
selector: 'app-root',
standalone: true,
imports: [FormsModule],
template: `
<section>
<input
type="text"
placeholder="New Task Title"
[(ngModel)]="newTaskTitle"
class="text-black border-2 p-2 m-2 rounded"
/>
<button (click)="addTask()">Add new task</button>
<table>
<tbody>
@for (task of tasks(); track task) {
@let isComplete = task.status === 'COMPLETE';
<tr>
<td>
<input
(click)="updateTask(task, { status: isComplete ? 'IN_PROGRESS' : 'COMPLETE' })"
type="checkbox"
[checked]="isComplete"
/>
</td>
<td>{{ task.title }}</td>
<td>{{ task.status }}</td>
<td>
<button (click)="deleteTask(task)">Delete</button>
</td>
</tr>
}
</tbody>
</table>
</section>
`,
styles: '',
})
export class AppComponent {
newTaskTitle = '';
tasks = signal<Task[]>([]);
constructor() {
afterNextRender({
earlyRead: () => this.getTasks()
});
}
async getTasks() {
const response = await fetch(`/api/tasks`);
const tasks = await response.json();
this.tasks.set(tasks);
}
async addTask() {
await fetch(`/api/tasks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: this.newTaskTitle,
status: 'IN_PROGRESS',
createdAt: Date.now(),
}),
});
this.newTaskTitle = '';
await this.getTasks();
}
async updateTask(task: Task, newTaskValues: Partial<Task>) {
await fetch(`/api/tasks`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...task, ...newTaskValues }),
});
await this.getTasks();
}
async deleteTask(task: any) {
await fetch('/api/tasks', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(task),
});
await this.getTasks();
}
}
Uygulama artık dağıtılmaya hazırdır.
8. Uygulamayı Cloud Run'a dağıtma
- Uygulamanızı Cloud Run'a dağıtmak için aşağıdaki komutu çalıştırın:
gcloud run deploy helloworld \
--region=us-central1 \
--source=. - İstenirse devam etmek istediğinizi onaylamak için
Y
veEnter
'ye basın:Do you want to continue (Y/n)? Y
Birkaç dakika sonra uygulama, ziyaret edebileceğiniz bir URL sağlar.
Uygulamanızı çalışırken görmek için URL'ye gidin. URL'yi her ziyaret ettiğinizde veya sayfayı her yenilediğinizde görev uygulamasını görürsünüz.
9. Tebrikler
Bu laboratuvarda şunları nasıl yapacağınızı öğrendiniz:
- PostgreSQL için Cloud SQL örneği oluşturma
- Cloud Run'a, Cloud SQL veritabanınıza bağlanan bir uygulama dağıtma
Temizleme
Cloud SQL'in ücretsiz bir katmanı yoktur ve kullanmaya devam ederseniz sizden ücret alınır. Ek ücret ödememek için Cloud projenizi silebilirsiniz.
Cloud Run, hizmet kullanılmadığında ücret almaz ancak container görüntüsünü Artifact Registry'de depolamak için sizden ücret alınabilir. Cloud projenizi sildiğinizde, söz konusu projede kullanılan tüm kaynakların faturalandırması durdurulur.
İsterseniz projeyi silin:
gcloud projects delete $GOOGLE_CLOUD_PROJECT
Gereksiz kaynakları CloudShell diskinizden de silebilirsiniz. Şunları yapabilirsiniz:
- Codelab proje dizinini silin:
rm -rf ~/task-app
- Uyarı! Sonraki işlem geri alınamaz. Yer açmak için Cloud Shell'inizdeki her şeyi silmek istiyorsanız ana dizininizi tamamen silebilirsiniz. Saklamak istediğiniz her şeyin başka bir yere kaydedildiğinden emin olun.
sudo rm -rf $HOME