Skip to content

Commit 53d3a44

Browse files
authored
docs: Add Cloud Run and Node JS connector examples (#506)
1 parent e5fd4e6 commit 53d3a44

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+3027
-0
lines changed

examples/cloudrun/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Node.js Samples for Cloud Run
2+
3+
This directory contains samples demonstrating how to connect to Cloud SQL from Cloud Run using various Node.js ORMs and the [Cloud SQL Node.js Connector](https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector).
4+
5+
## Available Samples
6+
7+
* [Knex.js](./knex)
8+
* [Prisma](./prisma)
9+
* [Sequelize](./sequelize)
10+
* [TypeORM](./typeorm)
11+
12+
Each ORM directory contains subdirectories for supported databases (MySQL, PostgreSQL, SQL Server), and each example includes implementations in:
13+
* CommonJS (`.cjs`)
14+
* ES Modules (`.mjs`)
15+
* TypeScript (`.ts`)
16+
17+
## Prerequisites
18+
19+
1. A Google Cloud Project with billing enabled.
20+
2. A Cloud SQL instance.
21+
3. A Cloud Run service account with the `Cloud SQL Client` IAM role.
22+
4. For IAM Authentication, the service account must be added as a database user.
23+
24+
## Deployment
25+
26+
Refer to the `README.md` in each ORM directory for specific deployment instructions.

examples/cloudrun/knex/README.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Connecting Cloud Run to Cloud SQL with the Node.js Connector
2+
3+
This guide provides a comprehensive walkthrough of how to connect a Cloud Run service to a Cloud SQL instance using the Cloud SQL Node.js Connector. It covers connecting to instances with both public and private IP addresses and demonstrates how to handle database credentials securely.
4+
5+
## Develop a Node.js Application
6+
7+
The following Node.js applications demonstrate how to connect to a Cloud SQL instance using the Cloud SQL Node.js Connector.
8+
9+
### `mysql2/index.cjs` and `pg/index.mjs`
10+
11+
These files contain the core application logic for connecting to a Cloud SQL for MySQL or PostgreSQL instance. They provide two separate authentication methods, each exposed at a different route:
12+
- `/`: Password-based authentication
13+
- `/iam`: IAM-based authentication
14+
15+
### `tedious/index.ts`
16+
17+
This file contains the core application logic for connecting to a Cloud SQL for SQL Server instance. It uses the `cloud-sql-nodejs-connector` to create a database connection pool with password-based authentication at the `/` route.
18+
19+
> [!NOTE]
20+
>
21+
> Cloud SQL for SQL Server does not support IAM database authentication.
22+
23+
## Lazy Instantiation
24+
25+
In a Cloud Run service, global variables are initialized when the container instance starts up. The application instance then handles subsequent requests until the container is spun down.
26+
27+
The `Connector` and `knex` objects are defined as global variables (initially set to `null`) and are lazily instantiated (created only when needed) inside the request handlers.
28+
29+
This approach offers several benefits:
30+
31+
1. **Faster Startup:** By deferring initialization until the first request, the Cloud Run service can start listening for requests almost immediately, reducing cold start latency.
32+
2. **Resource Efficiency:** Expensive operations, like establishing background connections or fetching secrets, are only performed when actually required.
33+
3. **Connection Reuse:** Once initialized, the global `Connector` and `knex` instances are reused for all subsequent requests to that container instance. This prevents the overhead of creating new connections for every request and avoids hitting connection limits.
34+
35+
## IAM Authentication Prerequisites
36+
37+
For IAM authentication to work, you must ensure two things:
38+
39+
1. **The Cloud Run service's service account has the `Cloud SQL Client` role.** You can grant this role with the following command:
40+
```bash
41+
gcloud projects add-iam-policy-binding PROJECT_ID \
42+
--member="serviceAccount:SERVICE_ACCOUNT_EMAIL" \
43+
--role="roles/cloudsql.client"
44+
```
45+
Replace `PROJECT_ID` with your Google Cloud project ID and `SERVICE_ACCOUNT_EMAIL` with the email of the service account your Cloud Run service is using.
46+
47+
2. **The service account is added as a database user to your Cloud SQL instance.** You can do this with the following command:
48+
```bash
49+
gcloud sql users create SERVICE_ACCOUNT_EMAIL \
50+
--instance=INSTANCE_NAME \
51+
--type=cloud_iam_user
52+
```
53+
Replace `SERVICE_ACCOUNT_EMAIL` with the same service account email and `INSTANCE_NAME` with your Cloud SQL instance name.
54+
55+
## Deploy the Application to Cloud Run
56+
57+
Follow these steps to deploy the application to Cloud Run.
58+
59+
### Build and Push the Docker Image
60+
61+
1. **Enable the Artifact Registry API:**
62+
63+
```bash
64+
gcloud services enable artifactregistry.googleapis.com
65+
```
66+
67+
2. **Create an Artifact Registry repository:**
68+
69+
```bash
70+
gcloud artifacts repositories create REPO_NAME \
71+
--repository-format=docker \
72+
--location=REGION
73+
```
74+
75+
3. **Configure Docker to authenticate with Artifact Registry:**
76+
77+
```bash
78+
gcloud auth configure-docker REGION-docker.pkg.dev
79+
```
80+
81+
4. **Build the Docker image (replace `mysql2` with `pg` or `tedious` as needed):**
82+
83+
```bash
84+
docker build -t REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_NAME mysql2
85+
```
86+
87+
5. **Push the Docker image to Artifact Registry:**
88+
89+
```bash
90+
docker push REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_NAME
91+
```
92+
93+
### Deploy to Cloud Run
94+
95+
Deploy the container image to Cloud Run using the `gcloud run deploy` command.
96+
97+
**Sample Values:**
98+
* `SERVICE_NAME`: `my-cloud-run-service`
99+
* `REGION`: `us-central1`
100+
* `PROJECT_ID`: `my-gcp-project-id`
101+
* `REPO_NAME`: `my-artifact-repo`
102+
* `IMAGE_NAME`: `my-app-image`
103+
* `INSTANCE_CONNECTION_NAME`: `my-gcp-project-id:us-central1:my-instance-name`
104+
* `DB_USER`: `my-db-user` (for password-based authentication)
105+
* `DB_IAM_USER`: `my-service-account@my-gcp-project-id.iam.gserviceaccount.com` (for IAM-based authentication)
106+
* `DB_NAME`: `my-db-name`
107+
* `DB_PASSWORD`: `my-user-pass-name`
108+
* `VPC_NETWORK`: `my-vpc-network`
109+
* `SUBNET_NAME`: `my-vpc-subnet`
110+
111+
**For MySQL and PostgreSQL (Public IP):**
112+
113+
```bash
114+
gcloud run deploy SERVICE_NAME \
115+
--image=REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_NAME \
116+
--set-env-vars=DB_USER=DB_USER,DB_IAM_USER=DB_IAM_USER,DB_NAME=DB_NAME,INSTANCE_CONNECTION_NAME=INSTANCE_CONNECTION_NAME \
117+
--region=REGION \
118+
--update-secrets=DB_PASSWORD=DB_PASSWORD:latest
119+
```
120+
121+
**For MySQL and PostgreSQL (Private IP):**
122+
123+
```bash
124+
gcloud run deploy SERVICE_NAME \
125+
--image=REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_NAME \
126+
--set-env-vars=DB_USER=DB_USER,DB_IAM_USER=DB_IAM_USER,DB_NAME=DB_NAME,INSTANCE_CONNECTION_NAME=INSTANCE_CONNECTION_NAME,IP_TYPE=PRIVATE \
127+
--network=VPC_NETWORK \
128+
--subnet=SUBNET_NAME \
129+
--vpc-egress=private-ranges-only \
130+
--region=REGION \
131+
--update-secrets=DB_PASSWORD=DB_PASSWORD:latest
132+
```
133+
134+
**For SQL Server (Public IP):**
135+
136+
```bash
137+
gcloud run deploy SERVICE_NAME \
138+
--image=REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_NAME \
139+
--set-env-vars=DB_USER=DB_USER,DB_NAME=DB_NAME,INSTANCE_CONNECTION_NAME=INSTANCE_CONNECTION_NAME \
140+
--region=REGION \
141+
--update-secrets=DB_PASSWORD=DB_PASSWORD:latest
142+
```
143+
144+
**For SQL Server (Private IP):**
145+
146+
```bash
147+
gcloud run deploy SERVICE_NAME \
148+
--image=REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_NAME \
149+
--set-env-vars=DB_USER=DB_USER,DB_NAME=DB_NAME,INSTANCE_CONNECTION_NAME=INSTANCE_CONNECTION_NAME,IP_TYPE=PRIVATE \
150+
--network=VPC_NETWORK \
151+
--subnet=SUBNET_NAME \
152+
--vpc-egress=private-ranges-only \
153+
--region=REGION \
154+
--update-secrets=DB_PASSWORD=DB_PASSWORD:latest
155+
```
156+
157+
> [!NOTE]
158+
> **`For PSC connections`**
159+
>
160+
> To connect to the Cloud SQL instance with PSC connection type, create a PSC endpoint, a DNS zone and DNS record for the instance in the same VPC network as the Cloud Run service and replace the `IP_TYPE` in the deploy command with `PSC`. To configure DNS records, refer to [Connect to an instance using Private Service Connect](https://docs.cloud.google.com/sql/docs/mysql/configure-private-service-connect) guide
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Use the official Node.js image.
2+
# https://hub.docker.com/_/node
3+
FROM node:25-slim
4+
5+
# Create and change to the app directory.
6+
WORKDIR /usr/src/app
7+
8+
# Copy application dependency manifests to the container image.
9+
# A wildcard is used to ensure both package.json AND package-lock.json are copied.
10+
# Copying this separately prevents re-running npm install on every code change.
11+
COPY package*.json ./
12+
13+
# Install production dependencies.
14+
# If you add a package-lock.json speed your build by switching to 'npm ci'.
15+
# RUN npm ci --only=production
16+
RUN npm install --omit=dev
17+
18+
# Copy local code to the container image.
19+
COPY . .
20+
21+
# Run the web service on container startup.
22+
CMD ["node", "index.cjs"]
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
const express = require('express');
16+
const {Connector, IpAddressTypes} = require('@google-cloud/cloud-sql-connector');
17+
const knex = require('knex');
18+
19+
const app = express();
20+
21+
// set up rate limiter: maximum of five requests per minute
22+
const RateLimit = require('express-rate-limit');
23+
const limiter = RateLimit({
24+
// 15 minutes
25+
windowMs: 15 * 60 * 1000,
26+
// max 100 requests per windowMs
27+
max: 100,
28+
});
29+
30+
// apply rate limiter to all requests
31+
app.use(limiter);
32+
33+
// Connector and connection pools are initialized as null to allow for lazy instantiation.
34+
// Lazy instantiation is a best practice for Cloud Run applications because it allows
35+
// the application to start faster and only initialize connections when they are needed.
36+
// This is especially important in serverless environments where applications may be
37+
// started and stopped frequently.
38+
let connector = null;
39+
let passwordPool = null;
40+
let iamPool = null;
41+
42+
// Helper to get the IP type enum from string
43+
function getIpType(ipTypeStr) {
44+
const ipType = ipTypeStr || 'PUBLIC';
45+
if (ipType === 'PRIVATE') {
46+
return IpAddressTypes.PRIVATE;
47+
} else if (ipType === 'PSC') {
48+
return IpAddressTypes.PSC;
49+
} else {
50+
return IpAddressTypes.PUBLIC;
51+
}
52+
}
53+
54+
// Function to create a database connection pool using IAM authentication
55+
async function createIamConnectionPool() {
56+
const instanceConnectionName = process.env.INSTANCE_CONNECTION_NAME;
57+
// IAM service account email
58+
const dbUser = process.env.DB_IAM_USER;
59+
const dbName = process.env.DB_NAME;
60+
const ipType = getIpType(process.env.IP_TYPE);
61+
62+
// Creates a new connector object.
63+
if (!connector) {
64+
connector = new Connector();
65+
}
66+
67+
// Get the connection options for the Cloud SQL instance.
68+
const clientOpts = await connector.getOptions({
69+
instanceConnectionName,
70+
ipType: ipType,
71+
authType: 'IAM',
72+
});
73+
74+
// Create a new knex connection pool.
75+
return knex({
76+
client: 'mysql2',
77+
connection: {
78+
...clientOpts,
79+
user: dbUser,
80+
database: dbName,
81+
},
82+
});
83+
}
84+
85+
// Function to create a database connection pool using password authentication
86+
async function createPasswordConnectionPool() {
87+
const instanceConnectionName = process.env.INSTANCE_CONNECTION_NAME;
88+
// Database username
89+
const dbUser = process.env.DB_USER;
90+
const dbName = process.env.DB_NAME;
91+
const dbPassword = process.env.DB_PASSWORD;
92+
const ipType = getIpType(process.env.IP_TYPE);
93+
94+
// Creates a new connector object.
95+
if (!connector) {
96+
connector = new Connector();
97+
}
98+
99+
// Get the connection options for the Cloud SQL instance.
100+
const clientOpts = await connector.getOptions({
101+
instanceConnectionName,
102+
ipType: ipType,
103+
});
104+
105+
// Create a new knex connection pool.
106+
return knex({
107+
client: 'mysql2',
108+
connection: {
109+
...clientOpts,
110+
user: dbUser,
111+
password: dbPassword,
112+
database: dbName,
113+
},
114+
});
115+
}
116+
117+
// Helper to get or create the password pool
118+
async function getPasswordConnectionPool() {
119+
if (!passwordPool) {
120+
passwordPool = await createPasswordConnectionPool();
121+
}
122+
return passwordPool;
123+
}
124+
125+
// Helper to get or create the IAM pool
126+
async function getIamConnectionPool() {
127+
if (!iamPool) {
128+
iamPool = await createIamConnectionPool();
129+
}
130+
return iamPool;
131+
}
132+
133+
app.get('/', async (req, res) => {
134+
try {
135+
const db = await getPasswordConnectionPool();
136+
// Use knex to run a simple query
137+
const result = await db.raw('SELECT 1');
138+
// Knex raw result for mysql2 is [rows, fields]
139+
res.send(`Database connection successful (password authentication), result: ${JSON.stringify(result[0])}`);
140+
} catch (err) {
141+
console.error(err);
142+
res.status(500).send(`Error connecting to the database (password authentication): ${err.message}`);
143+
}
144+
});
145+
146+
app.get('/iam', async (req, res) => {
147+
try {
148+
const db = await getIamConnectionPool();
149+
const result = await db.raw('SELECT 1');
150+
res.send(`Database connection successful (IAM authentication), result: ${JSON.stringify(result[0])}`);
151+
} catch (err) {
152+
console.error(err);
153+
res.status(500).send(`Error connecting to the database (IAM authentication): ${err.message}`);
154+
}
155+
});
156+
157+
const port = parseInt(process.env.PORT) || 8080;
158+
app.listen(port, () => {
159+
console.log(`Server running on port ${port}`);
160+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "knex-mysql-cloudrun",
3+
"version": "1.0.0",
4+
"description": "Knex MySQL example for Cloud Run (CommonJS)",
5+
"main": "index.cjs",
6+
"type": "commonjs",
7+
"scripts": {
8+
"start": "node index.cjs"
9+
},
10+
"dependencies": {
11+
"@google-cloud/cloud-sql-connector": "^1.8.4",
12+
"express": "^5.1.0",
13+
"knex": "^3.1.0",
14+
"mysql2": "^3.15.2",
15+
"express-rate-limit": "8.2.1"
16+
},
17+
"devDependencies": {
18+
}
19+
}

0 commit comments

Comments
 (0)