Skip to content

Commit adc25f6

Browse files
authored
docs: Add sample Cloud Run example application (#1335)
1 parent 2e1ed21 commit adc25f6

File tree

10 files changed

+623
-0
lines changed

10 files changed

+623
-0
lines changed

samples/cloudrun/README.md

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# Connecting Cloud Run to Cloud SQL with the Python 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 Python 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 Python Application
6+
7+
The following Python applications demonstrate how to connect to a Cloud SQL instance using the Cloud SQL Python Connector.
8+
9+
### `mysql/main.py` and `postgres/main.py`
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+
16+
### `sqlserver/main.py`
17+
18+
This file contains the core application logic for connecting to a Cloud SQL for SQL Server instance. It uses the `cloud-sql-python-connector` to create a SQLAlchemy connection pool with password-based authentication at the `/` route.
19+
20+
> [!NOTE]
21+
>
22+
> Cloud SQL for SQL Server does not support IAM database authentication.
23+
24+
25+
> [!NOTE]
26+
> **Lazy Refresh**
27+
>
28+
> The sample code in all three `main.py` files initializes the `Connector` with `refresh_strategy=lazy`. This is a recommended approach to avoid connection errors and optimize cost by preventing background processes from running when the CPU is throttled.
29+
30+
## Global Variables and Lazy Instantiation
31+
32+
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.
33+
34+
The `Connector` and SQLAlchemy `Engine` objects are defined as global variables (initially set to `None`) and are lazily instantiated (created only when needed) inside the request handlers.
35+
36+
This approach offers several benefits:
37+
38+
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.
39+
2. **Resource Efficiency:** Expensive operations, like establishing background connections or fetching secrets, are only performed when actually required.
40+
3. **Connection Reuse:** Once initialized, the global `Connector` and `Engine` 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.
41+
42+
## IAM Authentication Prerequisites
43+
44+
45+
For IAM authentication to work, you must ensure two things:
46+
47+
1. **The Cloud Run service's service account has the `Cloud SQL Client` role.** You can grant this role with the following command:
48+
```bash
49+
gcloud projects add-iam-policy-binding PROJECT_ID \
50+
--member="serviceAccount:SERVICE_ACCOUNT_EMAIL" \
51+
--role="roles/cloudsql.client"
52+
```
53+
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.
54+
55+
2. **The service account is added as a database user to your Cloud SQL instance.** You can do this with the following command:
56+
```bash
57+
gcloud sql users create SERVICE_ACCOUNT_EMAIL \
58+
--instance=INSTANCE_NAME \
59+
--type=cloud_iam_user
60+
```
61+
Replace `SERVICE_ACCOUNT_EMAIL` with the same service account email and `INSTANCE_NAME` with your Cloud SQL instance name.
62+
63+
For Password-based authentication to work:
64+
65+
1. **The Cloud Run service's service account has the `Secret Accessor` role.** You can grant this role with the following command:
66+
```bash
67+
gcloud projects add-iam-policy-binding PROJECT_ID \
68+
--member="serviceAccount:SERVICE_ACCOUNT_EMAIL" \
69+
--role="roles/secretmanager.secretAccessor"
70+
```
71+
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.
72+
73+
## Deploy the Application to Cloud Run
74+
75+
Follow these steps to deploy the application to Cloud Run.
76+
77+
### Build and Push the Docker Image
78+
79+
1. **Enable the Artifact Registry API:**
80+
81+
```bash
82+
gcloud services enable artifactregistry.googleapis.com
83+
```
84+
85+
2. **Create an Artifact Registry repository:**
86+
87+
```bash
88+
gcloud artifacts repositories create REPO_NAME \
89+
--repository-format=docker \
90+
--location=REGION
91+
```
92+
93+
3. **Configure Docker to authenticate with Artifact Registry:**
94+
95+
```bash
96+
gcloud auth configure-docker REGION-docker.pkg.dev
97+
```
98+
99+
4. **Build the Docker image (replace `mysql` with `postgres` or `sqlserver` as needed):**
100+
101+
```bash
102+
docker build -t REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_NAME mysql
103+
```
104+
105+
5. **Push the Docker image to Artifact Registry:**
106+
107+
```bash
108+
docker push REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_NAME
109+
```
110+
111+
### Deploy to Cloud Run
112+
113+
Deploy the container image to Cloud Run using the `gcloud run deploy` command.
114+
115+
116+
**Sample Values:**
117+
* `SERVICE_NAME`: `my-cloud-run-service`
118+
* `REGION`: `us-central1`
119+
* `PROJECT_ID`: `my-gcp-project-id`
120+
* `REPO_NAME`: `my-artifact-repo`
121+
* `IMAGE_NAME`: `my-app-image`
122+
* `INSTANCE_CONNECTION_NAME`: `my-gcp-project-id:us-central1:my-instance-name`
123+
* `DB_USER`: `my-db-user` (for password-based authentication)
124+
* `DB_IAM_USER`: `my-service-account@my-gcp-project-id.iam.gserviceaccount.com` (for IAM-based authentication)
125+
* `DB_NAME`: `my-db-name`
126+
* `DB_PASSWORD`: `my-user-pass-secret-name`
127+
* `VPC_NETWORK`: `my-vpc-network`
128+
* `SUBNET_NAME`: `my-vpc-subnet`
129+
130+
131+
**For MySQL and PostgreSQL (Public IP):**
132+
133+
```bash
134+
gcloud run deploy SERVICE_NAME \
135+
--image=REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_NAME \
136+
--set-env-vars=DB_USER=DB_USER,DB_IAM_USER=DB_IAM_USER,DB_NAME=DB_NAME,DB_SECRET_NAME=DB_SECRET_NAME,INSTANCE_CONNECTION_NAME=INSTANCE_CONNECTION_NAME \
137+
--region=REGION \
138+
--update-secrets=DB_PASSWORD=DB_PASSWORD:latest
139+
```
140+
141+
**For MySQL and PostgreSQL (Private IP):**
142+
143+
```bash
144+
gcloud run deploy SERVICE_NAME \
145+
--image=REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_NAME \
146+
--set-env-vars=DB_USER=DB_USER,DB_IAM_USER=DB_IAM_USER,DB_NAME=DB_NAME,DB_SECRET_NAME=DB_SECRET_NAME,INSTANCE_CONNECTION_NAME=INSTANCE_CONNECTION_NAME,IP_TYPE=PRIVATE \
147+
--network=VPC_NETWORK \
148+
--subnet=SUBNET_NAME \
149+
--vpc-egress=private-ranges-only \
150+
--region=REGION \
151+
--update-secrets=DB_PASSWORD=DB_PASSWORD:latest
152+
```
153+
154+
**For SQL Server (Public IP):**
155+
156+
```bash
157+
gcloud run deploy SERVICE_NAME \
158+
--image=REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_NAME \
159+
--set-env-vars=DB_USER=DB_USER,DB_NAME=DB_NAME,DB_SECRET_NAME=DB_SECRET_NAME,INSTANCE_CONNECTION_NAME=INSTANCE_CONNECTION_NAME \
160+
--region=REGION \
161+
--update-secrets=DB_PASSWORD=DB_PASSWORD:latest
162+
```
163+
164+
**For SQL Server (Private IP):**
165+
166+
```bash
167+
gcloud run deploy SERVICE_NAME \
168+
--image=REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_name \
169+
--set-env-vars=DB_USER=DB_USER,DB_NAME=DB_NAME,DB_SECRET_NAME=DB_SECRET_NAME,INSTANCE_CONNECTION_NAME=INSTANCE_CONNECTION_NAME,IP_TYPE=PRIVATE \
170+
--network=VPC_NETWORK \
171+
--subnet=SUBNET_NAME \
172+
--vpc-egress=private-ranges-only \
173+
--region=REGION \
174+
--update-secrets=DB_PASSWORD=DB_PASSWORD:latest
175+
```
176+
177+
> [!NOTE]
178+
> **`For PSC connections`**
179+
>
180+
> 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

samples/cloudrun/mysql/Dockerfile

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Use the official lightweight Python image.
2+
# https://hub.docker.com/_/python
3+
FROM python:3.12-slim
4+
5+
# Allow statements and log messages to immediately appear in the Knative logs
6+
ENV PYTHONUNBUFFERED True
7+
8+
# Copy local code to the container image.
9+
ENV APP_HOME /app
10+
WORKDIR $APP_HOME
11+
COPY . .
12+
13+
# Install production dependencies.
14+
RUN pip install --no-cache-dir -r requirements.txt
15+
16+
# Run the web service on container startup.
17+
# Use gunicorn for production deployments.
18+
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app

samples/cloudrun/mysql/main.py

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""
2+
Copyright 2025 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
"""
16+
17+
import os
18+
import sqlalchemy
19+
from flask import Flask
20+
from google.cloud.sql.connector import Connector, IPTypes
21+
22+
# Initialize Flask app
23+
app = Flask(__name__)
24+
25+
# Connector and SQLAlchemy engines are initialized as None to allow for lazy instantiation.
26+
#
27+
# The Connector object is a global variable to ensure that the same connector
28+
# instance is used across all requests. This prevents the unnecessary creation
29+
# of new Connector instances, which is inefficient and can lead to connection
30+
# limits being reached.
31+
#
32+
# Lazy instantiation (initializing the Connector and Engine only when needed)
33+
# allows the Cloud Run service to start up faster, as it avoids performing
34+
# initialization tasks (like fetching secrets or metadata) during startup.
35+
connector = None
36+
iam_engine = None
37+
password_engine = None
38+
39+
40+
# Function to create a database connection using IAM authentication
41+
def get_iam_connection() -> sqlalchemy.engine.base.Connection:
42+
"""Creates a database connection using IAM authentication."""
43+
instance_connection_name = os.environ["INSTANCE_CONNECTION_NAME"]
44+
db_user = os.environ["DB_IAM_USER"] # IAM service account email
45+
db_name = os.environ["DB_NAME"]
46+
ip_type_str = os.environ.get("IP_TYPE", "PUBLIC")
47+
ip_type = IPTypes[ip_type_str]
48+
49+
conn = connector.connect(
50+
instance_connection_name,
51+
"pymysql",
52+
user=db_user,
53+
db=db_name,
54+
ip_type=ip_type,
55+
enable_iam_auth=True,
56+
)
57+
return conn
58+
59+
60+
# Function to create a database connection using password-based authentication
61+
def get_password_connection() -> sqlalchemy.engine.base.Connection:
62+
"""Creates a database connection using password authentication."""
63+
instance_connection_name = os.environ["INSTANCE_CONNECTION_NAME"]
64+
db_user = os.environ["DB_USER"] # Database username
65+
db_name = os.environ["DB_NAME"]
66+
db_password = os.environ["DB_PASSWORD"]
67+
ip_type_str = os.environ.get("IP_TYPE", "PUBLIC")
68+
ip_type = IPTypes[ip_type_str]
69+
70+
conn = connector.connect(
71+
instance_connection_name,
72+
"pymysql",
73+
user=db_user,
74+
password=db_password,
75+
db=db_name,
76+
ip_type=ip_type,
77+
)
78+
return conn
79+
80+
81+
# This example uses two distinct SQLAlchemy engines to demonstrate two different
82+
# authentication methods (IAM and password-based) in the same application.
83+
#
84+
# In a typical production application, you would generally only need one
85+
# SQLAlchemy engine, configured for your preferred authentication method.
86+
# Both engines are defined globally to allow for connection pooling and
87+
# reuse across requests.
88+
89+
90+
def connect_with_password() -> sqlalchemy.engine.base.Connection:
91+
"""Initializes the connector and password engine if necessary, then returns a connection."""
92+
global connector, password_engine
93+
94+
if connector is None:
95+
connector = Connector(refresh_strategy="lazy")
96+
97+
if password_engine is None:
98+
password_engine = sqlalchemy.create_engine(
99+
"mysql+pymysql://",
100+
creator=get_password_connection,
101+
)
102+
103+
return password_engine.connect()
104+
105+
106+
def connect_with_iam() -> sqlalchemy.engine.base.Connection:
107+
"""Initializes the connector and IAM engine if necessary, then returns a connection."""
108+
global connector, iam_engine
109+
110+
if connector is None:
111+
connector = Connector(refresh_strategy="lazy")
112+
113+
if iam_engine is None:
114+
iam_engine = sqlalchemy.create_engine(
115+
"mysql+pymysql://",
116+
creator=get_iam_connection,
117+
)
118+
119+
return iam_engine.connect()
120+
121+
122+
@app.route("/")
123+
def password_auth_index():
124+
try:
125+
with connect_with_password() as conn:
126+
result = conn.execute(sqlalchemy.text("SELECT 1")).fetchall()
127+
return f"Database connection successful (password authentication), result: {result}"
128+
except Exception as e:
129+
return f"Error connecting to the database (password authentication)", 500
130+
131+
132+
@app.route("/iam")
133+
def iam_auth_index():
134+
try:
135+
with connect_with_iam() as conn:
136+
result = conn.execute(sqlalchemy.text("SELECT 1")).fetchall()
137+
return f"Database connection successful (IAM authentication), result: {result}"
138+
except Exception as e:
139+
return f"Error connecting to the database (IAM authentication)", 500
140+
141+
if __name__ == "__main__":
142+
app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
cloud-sql-python-connector[pymysql]
2+
sqlalchemy
3+
Flask
4+
gunicorn
5+
google-cloud-secret-manager
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Use the official lightweight Python image.
2+
# https://hub.docker.com/_/python
3+
FROM python:3.12-slim
4+
5+
# Allow statements and log messages to immediately appear in the Knative logs
6+
ENV PYTHONUNBUFFERED True
7+
8+
# Copy local code to the container image.
9+
ENV APP_HOME /app
10+
WORKDIR $APP_HOME
11+
COPY . .
12+
13+
# Install production dependencies.
14+
RUN pip install --no-cache-dir -r requirements.txt
15+
16+
# Run the web service on container startup.
17+
# Use gunicorn for production deployments.
18+
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 main:app

0 commit comments

Comments
 (0)