Skip to content

Commit 576a56a

Browse files
authored
docs: add Cloud Run sample for Python (#884)
* docs: add Cloud Run sample for Python * chore: remove commented code * chore: add missing copyright header
1 parent ddbba41 commit 576a56a

File tree

6 files changed

+191
-0
lines changed

6 files changed

+191
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Dockerfile
2+
README.md
3+
*.pyc
4+
*.pyo
5+
*.pyd
6+
__pycache__
7+
.pytest_cache
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright 2023 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+
# Start with a Docker image that includes a JRE. This is needed for PGAdapter.
16+
FROM eclipse-temurin:17-jre AS jre
17+
18+
# Use the official lightweight Python image.
19+
# https://hub.docker.com/_/python
20+
FROM python:3.11-slim
21+
22+
# Copy the JRE into the Pyton image.
23+
ENV JAVA_HOME=/opt/java/openjdk
24+
COPY --from=jre $JAVA_HOME $JAVA_HOME
25+
ENV PATH="${JAVA_HOME}/bin:${PATH}"
26+
27+
# Allow statements and log messages to immediately appear in the logs
28+
ENV PYTHONUNBUFFERED True
29+
30+
# Copy local code to the container image.
31+
ENV APP_HOME /app
32+
WORKDIR $APP_HOME
33+
COPY . ./
34+
35+
# Install production dependencies.
36+
RUN pip install --no-cache-dir -r requirements.txt
37+
38+
# Add the latest version of PGAdapter to the container.
39+
ADD https://storage.googleapis.com/pgadapter-jar-releases/pgadapter.tar.gz /pgadapter.tar.gz
40+
RUN mkdir /pgadapter
41+
RUN tar -xzf /pgadapter.tar.gz -C /pgadapter
42+
43+
# Copy the startup script that will start both PGAdapter and the web service.
44+
COPY ./startup.sh /startup.sh
45+
RUN chmod +x /startup.sh
46+
47+
# Set an ENTRYPOINT for the container.
48+
# The `startup.sh` file will start both PGAdapter and the web service.
49+
ENTRYPOINT ["/bin/bash", "/startup.sh"]

samples/python/cloud-run/README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# PGAdapter Cloud Run Sample for Python
2+
3+
This sample application shows how to build and deploy a Python application with PGAdapter to Google Cloud Run.
4+
5+
## Build
6+
7+
First build the Docker image locally and tag it. Replace the `IMAGE_URL` in the script with your own repository.
8+
Make sure that the repository that you are referencing exists.
9+
See https://cloud.google.com/artifact-registry/docs/repositories/create-repos for more information on how to set up
10+
Google Cloud artifact repositories.
11+
12+
```shell
13+
export IMAGE_URL=my-location-docker.pkg.dev/my-project/my-repository/my-image:latest
14+
docker build . --tag $IMAGE_URL
15+
```
16+
17+
Example: `export IMAGE_URL=europe-north1-docker.pkg.dev/my-project/sample-test/pgadapter-sample:latest`
18+
19+
Test the Docker image locally:
20+
21+
```shell
22+
docker run \
23+
-p 8080:8080 \
24+
-v /path/to/local_credentials.json:/credentials.json:ro \
25+
--env GOOGLE_APPLICATION_CREDENTIALS=/credentials.json \
26+
--env SPANNER_PROJECT=my-project \
27+
--env SPANNER_INSTANCE=my-instance \
28+
--env SPANNER_DATABASE=my-database \
29+
$IMAGE_URL
30+
```
31+
32+
Verify that the connection to Cloud Spanner works correctly by opening a new shell and executing:
33+
34+
```shell
35+
curl http://localhost:8080
36+
```
37+
38+
## Deploying to Cloud Run
39+
40+
Push the Docker image to Artifact Registry:
41+
42+
```shell
43+
gcloud auth configure-docker
44+
docker push $IMAGE_URL
45+
```
46+
47+
Then deploy the image as a service on Cloud Run:
48+
49+
```shell
50+
export SERVICE=my-service
51+
gcloud run deploy $SERVICE \
52+
--image $IMAGE_URL \
53+
--update-env-vars SPANNER_PROJECT=my-project,SPANNER_INSTANCE=my-instance,SPANNER_DATABASE=my-database
54+
```
55+
56+
__NOTE__: This example does not specify any credentials for PGAdapter when it is run on Cloud Run. This means that
57+
PGAdapter will use the default credentials that is used by Cloud Run. This is by default the default compute engine
58+
service account. See https://cloud.google.com/run/docs/securing/service-identity for more information on how service
59+
accounts work on Google Cloud Run.
60+
61+
Test the service (replace URL with your actual service URL):
62+
63+
```shell
64+
curl https://my-service-xyz.run.app
65+
```
66+
67+
### Authenticated Cloud Run Service
68+
69+
If your Cloud Run service requires authentication, then first add an IAM binding for your own account and include
70+
an authentication header with the request:
71+
72+
```shell
73+
gcloud run services add-iam-policy-binding $SERVICE \
74+
--member='user:your-email@gmail.com' \
75+
--role='roles/run.invoker'
76+
curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" https://my-service-xyz.run.app
77+
```
78+

samples/python/cloud-run/main.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import os
2+
import psycopg
3+
from flask import Flask
4+
5+
project = os.environ['SPANNER_PROJECT']
6+
instance = os.environ['SPANNER_INSTANCE']
7+
database = os.environ['SPANNER_DATABASE']
8+
app = Flask(__name__)
9+
10+
11+
@app.route("/")
12+
def hello_world():
13+
# Connect to Cloud Spanner using psycopg3.
14+
# Note that we use the fully qualified database name to connect to the
15+
# database, as the Dockerfile startup script starts PGAdapter without a
16+
# default project or instance.
17+
with psycopg.connect("host=localhost port={port} "
18+
"dbname=projects/{project}/instances/{instance}/databases/{database} "
19+
"sslmode=disable"
20+
.format(port=5432,
21+
project=project,
22+
instance=instance,
23+
database=database)) as conn:
24+
conn.autocommit = True
25+
with conn.cursor() as cur:
26+
cur.execute("select 'Hello world!' as hello")
27+
return "Greeting from Cloud Spanner PostgreSQL: {greeting}\n"\
28+
.format(greeting=cur.fetchone()[0])
29+
30+
31+
if __name__ == "__main__":
32+
app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
psycopg[binary]~=3.1.9
2+
Flask==2.1.0
3+
gunicorn==20.1.0
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/bin/bash
2+
3+
# Run both PGAdapter and the web server when the Docker container is started.
4+
# Start PGAdapter in plain TCP only mode, that is:
5+
# 1. Do not specify any credentials. This will make sure PGAdapter picks the credentials from the current environment.
6+
# You must ensure that the service account that is used by Cloud Run is allowed to access the Cloud Spanner database
7+
# that your application is using.
8+
# 2. Do not specify any specific project, instance or database. This will require the application to specify a fully
9+
# qualified database name when connecting to PGAdapter.
10+
# 3. Set the Unix domain socket directory to the empty string to disable domain sockets. Domain sockets are not
11+
# supported on Cloud Run.
12+
13+
# Start PGAdapter in the background.
14+
java -jar /pgadapter/pgadapter.jar -dir= &
15+
16+
# Run the web service on container startup. Here we use the gunicorn
17+
# webserver, with one worker process and 8 threads.
18+
# For environments with multiple CPU cores, increase the number of workers
19+
# to be equal to the cores available.
20+
# Timeout is set to 0 to disable the timeouts of the workers to allow Cloud Run to handle instance scaling.
21+
port="${PORT:-8080}"
22+
exec gunicorn --bind ":$port" --workers 1 --threads 8 --timeout 0 main:app

0 commit comments

Comments
 (0)