Tentang codelab ini
1. Ringkasan
Cloud Run adalah platform terkelola sepenuhnya yang memungkinkan Anda menjalankan kode langsung di atas infrastruktur Google yang skalabel. Codelab ini akan menunjukkan cara menghubungkan aplikasi Angular di Cloud Run ke database Firestore menggunakan Node.js Admin SDK.
Di lab ini, Anda akan mempelajari cara:
- Membuat database Firestore
- Men-deploy aplikasi ke Cloud Run yang terhubung ke database Firestore Anda
2. Prasyarat
- Jika belum memiliki Akun Google, Anda harus membuat Akun Google.
- Gunakan akun pribadi, bukan akun kantor atau sekolah. Akun kerja dan sekolah mungkin memiliki batasan yang mencegah Anda mengaktifkan API yang diperlukan untuk lab ini.
3. Penyiapan project
- Login ke Konsol Google Cloud.
- Aktifkan penagihan di Konsol Cloud.
- Menyelesaikan lab ini akan menghabiskan biaya kurang dari $1 USD untuk resource Cloud.
- Anda dapat mengikuti langkah-langkah di akhir lab ini untuk menghapus resource guna menghindari tagihan lebih lanjut.
- Pengguna baru memenuhi syarat untuk Uji Coba Gratis senilai$300 USD.
- Buat project baru atau pilih untuk menggunakan kembali project yang ada.
4. Membuka Cloud Shell Editor
- Buka Cloud Shell Editor
- Jika terminal tidak muncul di bagian bawah layar, buka:
- Klik menu tiga garis
- Klik Terminal
- Klik New Terminal
- Klik menu tiga garis
- Di terminal, tetapkan project Anda dengan perintah ini:
- Format:
gcloud config set project [PROJECT_ID]
- Contoh:
gcloud config set project lab-project-id-example
- Jika Anda tidak dapat mengingat project ID:
- Anda dapat mencantumkan semua project ID dengan:
gcloud projects list | awk '/PROJECT_ID/{print $2}'
- Anda dapat mencantumkan semua project ID dengan:
- Format:
- Jika diminta untuk memberikan otorisasi, klik Authorize untuk melanjutkan.
- Anda akan melihat pesan ini:
Jika Anda melihatUpdated property [core/project].
WARNING
dan ditanyaDo you want to continue (Y/N)?
, berarti Anda mungkin salah memasukkan project ID. TekanN
, tekanEnter
, lalu coba jalankan kembali perintahgcloud config set project
.
5. Mengaktifkan API
Di terminal, aktifkan API:
gcloud services enable \
firestore.googleapis.com \
run.googleapis.com \
artifactregistry.googleapis.com \
cloudbuild.googleapis.com
Jika diminta untuk memberikan otorisasi, klik Authorize untuk melanjutkan.
Pemrosesan perintah ini mungkin memerlukan waktu beberapa menit, tetapi pada akhirnya akan menampilkan pesan berhasil seperti ini:
Operation "operations/acf.p2-73d90d00-47ee-447a-b600" finished successfully.
6. Membuat Database Firestore
- Jalankan perintah
gcloud firestore databases create
untuk membuat database firestoregcloud firestore databases create --location=nam5
7. Menyiapkan Aplikasi
Siapkan aplikasi Next.js yang merespons permintaan HTTP.
- Untuk membuat project Next.js baru bernama
task-app
, gunakan perintah:npx --yes @angular/cli@19.2.5 new task-app \
--minimal \
--inline-template \
--inline-style \
--ssr \
--server-routing \
--defaults - Ubah direktori menjadi
task-app
:cd task-app
- Instal
firebase-admin
untuk berinteraksi dengan database Firestore.npm install firebase-admin
- Buka file
server.ts
di Cloud Shell Editor: File akan muncul di bagian atas layar. Di sinilah Anda dapat mengedit filecloudshell edit src/server.ts
server.ts
ini. - Hapus konten file
server.ts
yang ada. - Salin kode berikut dan tempelkan ke dalam file
server.ts
yang terbuka: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); - Buka file
angular.json
di Cloud Shell Editor: Sekarang kita akan menambahkan bariscloudshell edit angular.json
"externalDependencies": ["firebase-admin"]
ke fileangular.json
. - Hapus konten file
angular.json
yang ada. - Salin kode berikut dan tempelkan ke dalam file
angular.json
yang terbuka:{
"$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"]
- Buka file
app.component.ts
di Cloud Shell Editor: File yang ada sekarang akan muncul di bagian atas layar. Di sinilah Anda dapat mengedit filecloudshell edit src/app/app.component.ts
app.component.ts
ini. - Hapus konten file
app.component.ts
yang ada. - Salin kode berikut dan tempelkan ke dalam file
app.component.ts
yang terbuka: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();
}
}
Aplikasi kini siap di-deploy.
8. Men-deploy aplikasi ke Cloud Run
- Jalankan perintah di bawah untuk men-deploy aplikasi Anda ke Cloud Run:
gcloud run deploy helloworld \
--region=us-central1 \
--source=. - Jika diminta, tekan
Y
danEnter
untuk mengonfirmasi bahwa Anda ingin melanjutkan:Do you want to continue (Y/n)? Y
Setelah beberapa menit, aplikasi akan memberikan URL yang dapat Anda kunjungi.
Buka URL untuk melihat cara kerja aplikasi Anda. Setiap kali Anda mengunjungi URL atau memuat ulang halaman, Anda akan melihat aplikasi tugas.
9. Selamat
Di lab ini, Anda telah mempelajari cara melakukan hal-hal berikut:
- Membuat instance Cloud SQL for PostgreSQL
- Men-deploy aplikasi ke Cloud Run yang terhubung ke database Cloud SQL Anda
Pembersihan
Cloud SQL tidak memiliki paket gratis dan akan menagih Anda jika Anda terus menggunakannya. Anda dapat menghapus project Cloud untuk menghindari tagihan tambahan.
Meskipun Cloud Run tidak mengenakan biaya saat layanannya tidak digunakan, Anda mungkin tetap ditagih atas penyimpanan image container di Artifact Registry. Menghapus project Cloud akan menghentikan penagihan untuk semua resource yang digunakan dalam project tersebut.
Jika mau, hapus project:
gcloud projects delete $GOOGLE_CLOUD_PROJECT
Anda juga dapat menghapus resource yang tidak diperlukan dari disk cloudshell. Anda dapat:
- Hapus direktori project codelab:
rm -rf ~/task-app
- Peringatan! Tindakan berikutnya ini tidak dapat diurungkan. Jika ingin menghapus semua yang ada di Cloud Shell untuk mengosongkan ruang, Anda dapat menghapus seluruh direktori beranda. Pastikan semua yang ingin Anda simpan disimpan di tempat lain.
sudo rm -rf $HOME