Skip to content

Commit 60bc4fa

Browse files
committed
docs: Add sample cloudrun application
1 parent 2e1ed21 commit 60bc4fa

10 files changed

Lines changed: 450 additions & 0 deletions

File tree

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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+
## IAM Authentication Prerequisites
31+
32+
33+
For IAM authentication to work, you must ensure two things:
34+
35+
1. **The Cloud Run service's service account has the `Cloud SQL Client` role.** You can grant this role with the following command:
36+
```bash
37+
gcloud projects add-iam-policy-binding PROJECT_ID \
38+
--member="serviceAccount:SERVICE_ACCOUNT_EMAIL" \
39+
--role="roles/cloudsql.client"
40+
```
41+
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.
42+
43+
2. **The service account is added as a database user to your Cloud SQL instance.** You can do this with the following command:
44+
```bash
45+
gcloud sql users create SERVICE_ACCOUNT_EMAIL \
46+
--instance=INSTANCE_NAME \
47+
--type=cloud_iam_user
48+
```
49+
Replace `SERVICE_ACCOUNT_EMAIL` with the same service account email and `INSTANCE_NAME` with your Cloud SQL instance name.
50+
51+
## Deploy the Application to Cloud Run
52+
53+
Follow these steps to deploy the application to Cloud Run.
54+
55+
### Build and Push the Docker Image
56+
57+
1. **Enable the Artifact Registry API:**
58+
59+
```bash
60+
gcloud services enable artifactregistry.googleapis.com
61+
```
62+
63+
2. **Create an Artifact Registry repository:**
64+
65+
```bash
66+
gcloud artifacts repositories create REPO_NAME \
67+
--repository-format=docker \
68+
--location=REGION
69+
```
70+
71+
3. **Configure Docker to authenticate with Artifact Registry:**
72+
73+
```bash
74+
gcloud auth configure-docker REGION-docker.pkg.dev
75+
```
76+
77+
4. **Build the Docker image (replace `mysql` with `postgres` or `sqlserver` as needed):**
78+
79+
```bash
80+
docker build -t REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_NAME mysql
81+
```
82+
83+
5. **Push the Docker image to Artifact Registry:**
84+
85+
```bash
86+
docker push REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_NAME
87+
```
88+
89+
### Deploy to Cloud Run
90+
91+
Deploy the container image to Cloud Run using the `gcloud run deploy` command.
92+
93+
94+
**Sample Values:**
95+
* `SERVICE_NAME`: `my-cloud-run-service`
96+
* `REGION`: `us-central1`
97+
* `PROJECT_ID`: `my-gcp-project-id`
98+
* `REPO_NAME`: `my-artifact-repo`
99+
* `IMAGE_NAME`: `my-app-image`
100+
* `INSTANCE_CONNECTION_NAME`: `my-gcp-project-id:us-central1:my-instance-name`
101+
* `DB_USER`: `my-db-user` (for password-based authentication)
102+
* `DB_IAM_USER`: `my-service-account@my-gcp-project-id.iam.gserviceaccount.com` (for IAM-based authentication)
103+
* `DB_NAME`: `my-db-name`
104+
* `DB_SECRET_NAME`: `projects/my-gcp-project-id/secrets/my-db-secret/versions/latest`
105+
* `VPC_NETWORK`: `my-vpc-network`
106+
* `SUBNET_NAME`: `my-vpc-subnet`
107+
108+
109+
**For MySQL and PostgreSQL (Public IP):**
110+
111+
```bash
112+
gcloud run deploy SERVICE_NAME \
113+
--image=REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_NAME \
114+
--add-cloudsql-instances=INSTANCE_CONNECTION_NAME \
115+
--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 \
116+
--region=REGION
117+
```
118+
119+
**For MySQL and PostgreSQL (Private IP):**
120+
121+
```bash
122+
gcloud run deploy SERVICE_NAME \
123+
--image=REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_NAME \
124+
--add-cloudsql-instances=INSTANCE_CONNECTION_NAME \
125+
--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 \
126+
--network=VPC_NETWORK \
127+
--subnet=SUBNET_NAME \
128+
--vpc-egress=private-ranges-only \
129+
--region=REGION
130+
```
131+
132+
**For SQL Server (Public IP):**
133+
134+
```bash
135+
gcloud run deploy SERVICE_NAME \
136+
--image=REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_NAME \
137+
--add-cloudsql-instances=INSTANCE_CONNECTION_NAME \
138+
--set-env-vars=DB_USER=DB_USER,DB_NAME=DB_NAME,DB_SECRET_NAME=DB_SECRET_NAME,INSTANCE_CONNECTION_NAME=INSTANCE_CONNECTION_NAME \
139+
--region=REGION
140+
```
141+
142+
**For SQL Server (Private IP):**
143+
144+
```bash
145+
gcloud run deploy SERVICE_NAME \
146+
--image=REGION-docker.pkg.dev/PROJECT_ID/REPO_NAME/IMAGE_name \
147+
--add-cloudsql-instances=INSTANCE_CONNECTION_NAME \
148+
--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 \
149+
--network=VPC_NETWORK \
150+
--subnet=SUBNET_NAME \
151+
--vpc-egress=private-ranges-only \
152+
--region=REGION
153+
```
154+
155+
> [!NOTE]
156+
> **`For PSC connections`**
157+
>
158+
> 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: 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
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import os
2+
import sqlalchemy
3+
from flask import Flask
4+
from google.cloud.sql.connector import Connector, IPTypes
5+
from google.cloud import secretmanager
6+
7+
# Initialize Flask app
8+
app = Flask(__name__)
9+
10+
# Initialize the Connector object with lazy refresh
11+
connector = Connector(refresh_strategy="lazy")
12+
secret_client = secretmanager.SecretManagerServiceClient()
13+
14+
# Function to create a database connection using IAM authentication
15+
def get_iam_connection() -> sqlalchemy.engine.base.Connection:
16+
instance_connection_name = os.environ["INSTANCE_CONNECTION_NAME"]
17+
db_user = os.environ["DB_IAM_USER"] # IAM service account email
18+
db_name = os.environ["DB_NAME"]
19+
ip_type_str = os.environ.get("IP_TYPE", "PUBLIC")
20+
ip_type = IPTypes[ip_type_str]
21+
22+
conn = connector.connect(
23+
instance_connection_name,
24+
"pymysql",
25+
user=db_user,
26+
db=db_name,
27+
ip_type=ip_type,
28+
enable_iam_auth=True
29+
)
30+
return conn
31+
32+
# Function to create a database connection using password-based authentication
33+
def get_password_connection() -> sqlalchemy.engine.base.Connection:
34+
instance_connection_name = os.environ["INSTANCE_CONNECTION_NAME"]
35+
db_user = os.environ["DB_USER"] # Database username
36+
db_name = os.environ["DB_NAME"]
37+
db_secret_name = os.environ["DB_SECRET_NAME"]
38+
ip_type_str = os.environ.get("IP_TYPE", "PUBLIC")
39+
ip_type = IPTypes[ip_type_str]
40+
41+
secret_response = secret_client.access_secret_version(name=db_secret_name)
42+
db_password = secret_response.payload.data.decode("UTF-8")
43+
44+
conn = connector.connect(
45+
instance_connection_name,
46+
"pymysql",
47+
user=db_user,
48+
password=db_password,
49+
db=db_name,
50+
ip_type=ip_type,
51+
)
52+
return conn
53+
54+
# Create the SQLAlchemy engines
55+
iam_engine = sqlalchemy.create_engine(
56+
"mysql+pymysql://",
57+
creator=get_iam_connection,
58+
)
59+
password_engine = sqlalchemy.create_engine(
60+
"mysql+pymysql://",
61+
creator=get_password_connection,
62+
)
63+
64+
@app.route("/")
65+
def password_auth_index():
66+
try:
67+
with password_engine.connect() as conn:
68+
result = conn.execute(sqlalchemy.text("SELECT 1")).fetchall()
69+
return f"Database connection successful (password authentication), result: {result}"
70+
except Exception as e:
71+
return f"Error connecting to the database (password authentication): {e}", 500
72+
73+
@app.route("/iam")
74+
def iam_auth_index():
75+
try:
76+
with iam_engine.connect() as conn:
77+
result = conn.execute(sqlalchemy.text("SELECT 1")).fetchall()
78+
return f"Database connection successful (IAM authentication), result: {result}"
79+
except Exception as e:
80+
return f"Error connecting to the database (IAM authentication): {e}", 500
81+
82+
if __name__ == "__main__":
83+
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
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import os
2+
import sqlalchemy
3+
from flask import Flask
4+
from google.cloud.sql.connector import Connector, IPTypes
5+
from google.cloud import secretmanager
6+
7+
# Initialize Flask app
8+
app = Flask(__name__)
9+
10+
# Initialize the Connector object with lazy refresh
11+
connector = Connector(refresh_strategy="lazy")
12+
secret_client = secretmanager.SecretManagerServiceClient()
13+
14+
# Function to create a database connection using IAM authentication
15+
def get_iam_connection() -> sqlalchemy.engine.base.Connection:
16+
instance_connection_name = os.environ["INSTANCE_CONNECTION_NAME"]
17+
db_user = os.environ["DB_IAM_USER"] # IAM service account email
18+
db_name = os.environ["DB_NAME"]
19+
ip_type_str = os.environ.get("IP_TYPE", "PUBLIC")
20+
ip_type = IPTypes[ip_type_str]
21+
22+
conn = connector.connect(
23+
instance_connection_name,
24+
"pg8000",
25+
user=db_user,
26+
db=db_name,
27+
ip_type=ip_type,
28+
enable_iam_auth=True
29+
)
30+
return conn
31+
32+
# Function to create a database connection using password-based authentication
33+
def get_password_connection() -> sqlalchemy.engine.base.Connection:
34+
instance_connection_name = os.environ["INSTANCE_CONNECTION_NAME"]
35+
db_user = os.environ["DB_USER"] # Database username
36+
db_name = os.environ["DB_NAME"]
37+
db_secret_name = os.environ["DB_SECRET_NAME"]
38+
ip_type_str = os.environ.get("IP_TYPE", "PUBLIC")
39+
ip_type = IPTypes[ip_type_str]
40+
41+
secret_response = secret_client.access_secret_version(name=db_secret_name)
42+
db_password = secret_response.payload.data.decode("UTF-8")
43+
44+
conn = connector.connect(
45+
instance_connection_name,
46+
"pg8000",
47+
user=db_user,
48+
password=db_password,
49+
db=db_name,
50+
ip_type=ip_type,
51+
)
52+
return conn
53+
54+
# Create the SQLAlchemy engines
55+
iam_engine = sqlalchemy.create_engine(
56+
"postgresql+pg8000://",
57+
creator=get_iam_connection,
58+
)
59+
password_engine = sqlalchemy.create_engine(
60+
"postgresql+pg8000://",
61+
creator=get_password_connection,
62+
)
63+
64+
@app.route("/")
65+
def password_auth_index():
66+
try:
67+
with password_engine.connect() as conn:
68+
result = conn.execute(sqlalchemy.text("SELECT 1")).fetchall()
69+
return f"Database connection successful (password authentication), result: {result}"
70+
except Exception as e:
71+
return f"Error connecting to the database (password authentication): {e}", 500
72+
73+
@app.route("/iam")
74+
def iam_auth_index():
75+
try:
76+
with iam_engine.connect() as conn:
77+
result = conn.execute(sqlalchemy.text("SELECT 1")).fetchall()
78+
return f"Database connection successful (IAM authentication), result: {result}"
79+
except Exception as e:
80+
return f"Error connecting to the database (IAM authentication): {e}", 500
81+
82+
if __name__ == "__main__":
83+
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[pg8000]
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)