In this codelab, you'll learn how to deploy a full-stack application with a database to Google Cloud 👉 Cloud Run. Cloud Run is a fully managed platform that enables you to run your code directly on Google's scalable infrastructure. You'll use the Cloud SQL Node.js connector to connect a Node.js backend to a Cloud SQL for PostgreSQL database, and an Angular frontend to interact with the backend.

What you'll

Architecture Overview

Architecture Diagram The architecture of the application consists of the following components:

What you'll need

Before you begin, ensure you have a Google Account.

  1. Sign-in to the Google Cloud Console.Google Cloud Console screen
  2. Enable billing in the Cloud Console.Billing screen
    • Completing this lab should cost less than $1 USD in Cloud resources.
    • You can follow the steps at the end of this lab to delete resources to avoid further charges.
    • New users are eligible for the ($300 USD Free Trial).
    Billing screen
  3. Create a new project or choose to reuse an existing project.Create a new project screenCreate a new project screen 👆Reuse a project screenReuse an existing project 👆
  1. In your Project Welcome Screen or in your Dashboard click the Cloud Shell Icon to open the Cloud Shell Terminal.Cloud Shell Icon
  2. When prompted to authorize, click Authorize to continue.Authorize
  3. In the terminal, we are going to set your project Id:
    • List all your project ids with
      gcloud projects list | awk '/PROJECT_ID/{print $2}'
      
    • Set your project id with
      gcloud config set project PROJECT_ID
      
      Replace PROJECT_ID with your project id. For example:
      gcloud config set project my-project-id
      
  4. You should see this message:
    Updated property [core/project].
    
    If you see a WARNING and are asked Do you want to continue (Y/N)?, then you have likely entered the project ID incorrectly. Press N, press Enter, double check your project ID and try to run the gcloud config set project command again.
    • You can verify your project id with this command:
      gcloud config get-value project
      
      This should return your project id.

You need to enable the following Google Cloud APIs:

Run the following command in the Cloud Shell terminal to enable these APIs:

gcloud services enable \
  sqladmin.googleapis.com \
  run.googleapis.com \
  artifactregistry.googleapis.com \
  cloudbuild.googleapis.com

After a few moments, you should see a message indicating that each service has been successfully enabled.

Similar to this:

Operation "operations/acf.p2-73d90d00-47ee-447a-b600" finished successfully.

A service account in Google Cloud is a special type of Google account that an application or a virtual machine (VM) can use to make authorized API calls. Unlike user accounts, which represent a human user, service accounts represent an application or a service. The provided commands set up a service account with specific permissions to allow a Cloud Run application to securely connect to and interact with a Cloud SQL database.

Create and configure a Google Cloud service account to be used by Cloud Run so that it has the correct permissions to connect to Cloud SQL.

  1. Run the gcloud iam service-accounts create command as follows to create a new service account named quickstart-service-account:
    gcloud iam service-accounts create quickstart-service-account \
        --display-name="Quickstart Service Account"
    
  2. Assign the Cloud SQL Client role to the service account. This role allows the service account to connect to Cloud SQL instances.
    gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \
        --member="serviceAccount:quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
        --role="roles/cloudsql.client"
    
  3. Assign the Cloud SQL Instance User role to the service account. This role allows the service account to perform operations on Cloud SQL instances.
    gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \
        --member="serviceAccount:quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
        --role="roles/cloudsql.instanceUser"
    
  4. Assign the Log Writer role to the service account. This role allows the service account to write logs to Google Cloud's logging service.
    gcloud projects add-iam-policy-binding ${GOOGLE_CLOUD_PROJECT} \
        --member="serviceAccount:quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
        --role="roles/logging.logWriter"
    

Why These Roles Are Needed

These commands ensure that the service account has the necessary permissions to interact with Cloud SQL and log activities, which is essential for applications running on Cloud Run that need to access a Cloud SQL database.

Setup a PostgreSQL Database

  1. Create the Cloud SQL instance
    • Run the following command to create a new Cloud SQL instance:
      gcloud sql instances create quickstart-instance \
          --database-version=POSTGRES_14 \
          --cpu=4 \
          --memory=16GB \
          --region=us-central1 \
          --database-flags=cloudsql.iam_authentication=on
      
      This command may take a few minutes to complete.
      • This command creates a managed PostgreSQL database instance in the us-central1 region with 4 CPUs and 16GB of memory.
      • The cloudsql.iam_authentication=on flag enables IAM-based authentication for secure access.
      Once the instance is created, you should see a message indicating that the operation was successful, similar to:
      Creating Cloud SQL instance for POSTGRES_14...done.
      Created [https://sqladmin.googleapis.com/sql/v1beta4/projects/principal-fact-471601-n1/instances/quickstart-instance].
      NAME: quickstart-instance
      DATABASE_VERSION: POSTGRES_14
      LOCATION: us-central1-c
      TIER: db-custom-4-16384
        PRIMARY_ADDRESS: 34.63.128.0
        PRIVATE_ADDRESS: -
        STATE: RUNNABLE
      
  2. Create a Cloud SQL database
    • Run the following command to create a new database within the instance:
      gcloud sql databases create quickstart_db \
          --instance=quickstart-instance
      
      • This command creates a database named quickstart_db in the quickstart-instance.
  3. Create a PostgreSQL database user
    • Run the following command to create a new user for the service account you created earlier to access the database:
      gcloud sql users create quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam \
          --instance=quickstart-instance \
          --type=cloud_iam_service_account
      

Create an Angular application

  1. Prepare an Angular application that responds to HTTP requests.To create a new Angular project named task-app, use the command:
    npx --yes @angular/cli@20 new task-app \
        --minimal \
        --inline-template \
        --inline-style \
        --ssr \
        --defaults
    
    • The npx command runs the Angular CLI without needing to install it.
    • Type n when prompted with the following message:
        Would you like to share pseudonymous usage data about this project with the Angular Team ...
      
      This command creates a minimal Angular project with server-side rendering (SSR) and server-side routing enabled. Server-side rendering (SSR) is a technique where the server generates a fully rendered HTML page for each user request and sends it to the browser. This provides faster initial load times and better search engine optimization (SEO) compared to client-side rendering (where JavaScript builds the page in the browser). Server-side routing ensures that navigation between pages is handled on the server, improving performance and enabling deep linking.
  2. Navigate to the project directory by changing the directory to task-app:
    cd task-app
    
  3. Install node-postgres and the Cloud SQL Node.js connector libraries to interact with the PostgreSQL database, along with the TypeScript types for PostgreSQL as a development dependency:
    npm install pg \
    @google-cloud/cloud-sql-connector \
    google-auth-library
    
    • The pg library is used to interact with your PostgreSQL database.
    • The @google-cloud/cloud-sql-connector library provides a way to connect to Cloud SQL instances securely.
    • The google-auth-library is used for authenticating requests to Google Cloud services.
  4. To enable TypeScript support for PostgreSQL in your Angular application, install the @types/pg package as a development dependency:
    npm install --save-dev @types/pg
    
  5. In the Cloud Shell screen click Open Editor to open the Cloud Shell Editor.Cloud Shell Editor This is where you will modify the backend server and frontend application in the next steps.
  1. Set up the backend server. Open the server.ts file in the Cloud Shell Editor. Navigate to the src folder and locate the server.ts file.Open server.tsor open it using the following command:
    cloudshell edit src/server.ts
    
  2. Delete the existing contents of the server.ts file.
  3. Copy the following code and paste it into the opened server.ts file:
     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 pg from 'pg';
     import { AuthTypes, Connector } from '@google-cloud/cloud-sql-connector';
     import { GoogleAuth } from 'google-auth-library';
    
     const auth = new GoogleAuth();
    
     const { Pool } = pg;
    
     type Task = {
       id: string;
       title: string;
       status: 'IN_PROGRESS' | 'COMPLETE';
       createdAt: number;
     };
    
     const projectId = await auth.getProjectId();
    
     const connector = new Connector();
     const clientOpts = await connector.getOptions({
       instanceConnectionName: `${projectId}:us-central1:quickstart-instance`,
       authType: AuthTypes.IAM,
     });
    
     const pool = new Pool({
       ...clientOpts,
       user: `quickstart-service-account@${projectId}.iam`,
       database: 'quickstart_db',
     });
    
     const tableCreationIfDoesNotExist = async () => {
       await pool.query(`CREATE TABLE IF NOT EXISTS tasks (
           id SERIAL NOT NULL,
           created_at timestamp NOT NULL,
           status VARCHAR(255) NOT NULL default 'IN_PROGRESS',
           title VARCHAR(1024) NOT NULL,
           PRIMARY KEY (id)
         );`);
     }
    
     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) => {
       await tableCreationIfDoesNotExist();
       const { rows } = await pool.query(`SELECT id, created_at, status, title FROM tasks ORDER BY created_at DESC LIMIT 100`);
       res.send(rows);
     });
    
     app.post('/api/tasks', async (req, res) => {
       const newTaskTitle = req.body.title;
       if (!newTaskTitle) {
         res.status(400).send("Title is required");
         return;
       }
       await tableCreationIfDoesNotExist();
       await pool.query(`INSERT INTO tasks(created_at, status, title) VALUES(NOW(), 'IN_PROGRESS', $1)`, [newTaskTitle]);
       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 tableCreationIfDoesNotExist();
       await pool.query(
         `UPDATE tasks SET status = $1, title = $2 WHERE id = $3`,
         [task.status, task.title, task.id]
       );
       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 tableCreationIfDoesNotExist();
       await pool.query(`DELETE FROM tasks WHERE id = $1`, [task.id]);
       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);
    
    
    • The file sets up the Express server to handle API requests and to connect to the PostgreSQL database.
    • The tableCreationIfDoesNotExist function ensures that the tasks table is created if it doesn't already exist.
  4. Save the file.
  1. Navigate to the src folder and open the app.ts fileOpen app.tsor open it using the following command:
    cloudshell edit src/app.ts
    
  2. Delete the existing contents of the app.ts file.
  3. Add the following code to the app.ts file:
     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',
       imports: [FormsModule],
       template: `
         <main class="container">
           <h1>{{ title() }}</h1>
           <section class="task-input">
           <input
             type="text"
             placeholder="New Task Title"
             [(ngModel)]="newTaskTitle"
             (keyup.enter)="addTask()"
           />
           <button (click)="addTask()">Add new task</button>
           </section>
           <table class="task-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 class="task-title" [class.complete]="isComplete">{{ task.title }}</td>
                   <td class="task-status">{{ task.status }}</td>
                   <td>
                     <button (click)="deleteTask(task)">Delete</button>
                   </td>
                 </tr>
               }
             </tbody>
           </table>
         </main>
       `,
       styles: [
         `
           :host {
             display: block;
             font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
               Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
               "Segoe UI Symbol";
             background-color: #f0f2f5;
             color: #333;
             min-height: 100vh;
           }
           .container {
             max-width: 800px;
             margin: 0 auto;
             padding: 2rem;
           }
           h1 {
             font-size: 3rem;
             font-weight: bold;
             color: #1a202c;
             text-align: center;
             margin-bottom: 2rem;
           }
           .task-input {
             display: flex;
             gap: 0.5rem;
             margin-bottom: 2rem;
           }
           .task-input input {
             flex-grow: 1;
             padding: 0.75rem;
             border: 1px solid #cbd5e0;
             border-radius: 0.375rem;
             font-size: 1rem;
           }
           .task-input button, .task-table button {
             padding: 0.75rem 1.5rem;
             background-color: #4299e1;
             color: white;
             border: none;
             border-radius: 0.375rem;
             cursor: pointer;
             font-weight: bold;
             transition: background-color 0.2s;
           }
           .task-input button:hover, .task-table button:hover {
             background-color: #3182ce;
           }
           .task-table {
             width: 100%;
             border-collapse: collapse;
             background: white;
             border-radius: 0.5rem;
             box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
           }
           .task-table td {
             padding: 1rem;
             border-bottom: 1px solid #e2e8f0;
           }
           .task-title.complete {
             text-decoration: line-through;
             color: #a0aec0;
           }
         `,
       ],
     })
     export class App {
       newTaskTitle = '';
       tasks = signal<Task[]>([]);
       readonly title = signal('to-do-tracker');
    
       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();
       }
     }
    
    
  4. Save the file.

The application is now ready to be deployed.

Click Open Terminal to open the Cloud Shell terminal and ensure you are in the task-app directory. If you are not, navigate to it using:

cd ~/task-app
  1. Run the command below to deploy your application to Cloud Run.
    gcloud run deploy to-do-tracker \
       --region=us-central1 \
       --source=. \
       --service-account="quickstart-service-account@${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com" \
       --allow-unauthenticated
    
    Cloud Run uses Cloud Native Buildpacks to build a Docker image from your source code. This process requires no Dockerfile and sets permissions via the service account, allowing unauthenticated access.
    • --region=us-central1: Specifies the region where the service will be deployed.
    • --source=.: Deploys the application from the current directory.
    • --service-account: Specifies the service account to use for the deployment. Use the quickstart-service-account created earlier.
    • --allow-unauthenticated: Allows public access to the application.
  2. When prompted, type y and Enter to confirm that you would like to continue:
    Do you want to continue (Y/n)? Y
    
    After a few minutes, the application should provide a URL for you to visit.

Navigate to the Service URL to see your application in action. Every time you visit the URL or refresh the page, you will see the task app. 👇

to-do-tracker app

Test the application

Use the application to add, update, and delete tasks. Verify that the backend and database are working as expected.

You can use tools like Postman or curl to test the API endpoints directly.

You have successfully deployed a full-stack Angular application to Google Cloud Run with a Cloud SQL backend. This application allows you to manage tasks and will scale automatically based on demand.

In this lab, you have learned how to do the following:

Cleanup Instructions

Cloud SQL does not have a free tier and will charge you if you continue to use it. To avoid incurring unnecessary costs, delete the Cloud project after completing the codelab.

While Cloud Run does not charge when the service is not in use, you might still be charged for storing the container image in Artifact Registry. Deleting your Cloud project stops billing for all the resources used within that project.

Delete the Cloud Project

To delete the entire project and stop all billing:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT}

Optional: Delete Specific Resources

If you prefer not to delete the entire project, you can delete individual resources:

  1. Delete the Cloud SQL instance:
    gcloud sql instances delete quickstart-instance --quiet
    
  2. Delete the Cloud Run service:
    gcloud run services delete to-do-tracker --region=us-central1 --quiet
    
  3. Delete the codelab project directory:
    rm -rf ~/task-app
    
  4. Warning! This next action is can't be undone! If you would like to delete everything on your Cloud Shell to free up space, you can delete your whole home directory. Be careful that everything you want to keep is saved somewhere else.
    sudo rm -rf $HOME
    
    In the Cloud Shell menu, click More > Restart. Confirm the restart to provision a new VM and reset the home directory to its default state.

Resources 📚

Thank you 🙏

I hope you found this codelab helpful in understanding how to deploy a full-stack Angular application to Google Cloud Run with a Cloud SQL backend.

Feedback

I value your feedback! If you have any suggestions or encounter issues, please let us know by submitting feedback at GitHub Issues.