Skip to content

Commit f4246bd

Browse files
authored
docs: add documentation and sample for GKE sidecar proxy (#718)
Adds documentation and sample for running PGAdapter as a sidecar proxy on GKE. Fixes #701
1 parent 1f19263 commit f4246bd

File tree

6 files changed

+354
-1
lines changed

6 files changed

+354
-1
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ in-process server (the latter is only supported for Java applications).
3535

3636
### Docker
3737

38-
See [running PGAdapter using Docker](docs/docker.md) for more examples for running PGAdapter in Docker.
38+
* See [running PGAdapter using Docker](docs/docker.md) for more examples for running PGAdapter in Docker.
39+
* See [running PGAdapter as a sidecar proxy](docs/sidecar-proxy.md) for how to run PGAdapter as a
40+
sidecar proxy in a Kubernetes cluster.
3941

4042
Replace the project, instance and database names and the credentials file in the example below to
4143
run PGAdapter from a pre-built Docker image.

docs/sidecar-proxy.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Google Cloud Spanner PGAdapter - Sidecar Proxy
2+
3+
PGAdapter can be used as a sidecar proxy in for example a Kubernetes cluster. Kubernetes sidecar
4+
containers are those containers that run parallel with the main container in the pod.
5+
6+
Running PGAdapter in a "sidecar" pattern is recommend over running as a separate service for several reasons:
7+
8+
* Prevents a single point of failure - each application's access to your database is independent
9+
of the others, making it more resilient.
10+
* Allows you to scope resource requests more accurately - because PGAdapter consumes resources
11+
linearly to usage, this pattern allows you to more accurately scope and request resources to match
12+
your applications as it scales.
13+
14+
## Configuration
15+
16+
Add PGAdapter to the pod configuration under `containers`:
17+
18+
```yaml
19+
containers:
20+
- name: pgadapter
21+
image: gcr.io/cloud-spanner-pg-adapter/pgadapter
22+
ports:
23+
- containerPort: 5432
24+
args:
25+
- "-p my-project"
26+
- "-i my-instance"
27+
- "-d my-database"
28+
- "-x"
29+
resources:
30+
requests:
31+
# PGAdapter's memory use scales linearly with the number of active
32+
# connections. Fewer open connections will use less memory. Adjust
33+
# this value based on your application's requirements.
34+
# A minimum of 384MiB + 2MiB per open connection is recommended.
35+
memory: "512Mi"
36+
# PGAdapter's CPU use scales linearly with the amount of IO between
37+
# the database and the application. Adjust this value based on your
38+
# application's requirements.
39+
cpu: "1"
40+
```
41+
42+
[This sample application](../samples/sidecar-proxy) contains a step-by-step guide for how to set up
43+
PGAdapter as a sidecar proxy in a Google Kubernetes cluster. This example also includes setting up
44+
a Workload Identity with a Kubernetes Service Account.

samples/sidecar-proxy/Dockerfile

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
# http://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+
FROM golang:1.19.2 as builder
16+
WORKDIR /app
17+
RUN go mod init example-app
18+
RUN go get github.com/jackc/pgx/v4
19+
COPY *.go ./
20+
RUN CGO_ENABLED=0 GOOS=linux go build -o /example-app
21+
22+
FROM gcr.io/distroless/base-debian11
23+
WORKDIR /
24+
COPY --from=builder /example-app /example-app
25+
ENV PORT 8080
26+
USER nonroot:nonroot
27+
CMD ["/example-app"]

samples/sidecar-proxy/README.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Using PGAdapter on Kubernetes
2+
3+
PGAdapter can be used as a sidecar proxy on Kubernetes. This directory contains a small sample
4+
application that shows how to set this up.
5+
6+
## Creating a GKE cluster
7+
8+
These steps create a GKE cluster where the sample app can be deployed. You can skip these
9+
steps if you already have a GKE cluster, and instead modify your existing deployment to
10+
include PGAdapter.
11+
12+
See https://cloud.google.com/kubernetes-engine/docs/deploy-app-cluster for more details on
13+
how to set up a GKE cluster.
14+
15+
1. Create an Autopilot cluster named example-cluster and update it to use Workload Identity.
16+
17+
```shell
18+
export PROJECT_ID=<YOUR_PROJECT_ID>
19+
export REGION=<YOUR_COMPUTE_REGION>
20+
21+
gcloud config set project $PROJECT_ID
22+
gcloud container clusters create-auto example-cluster \
23+
--region=$REGION
24+
gcloud container clusters update example-cluster \
25+
--region=$REGION \
26+
--workload-pool=$PROJECT_ID.svc.id.goog
27+
```
28+
29+
2. Get authentication credentials for the cluster.
30+
31+
```shell
32+
gcloud container clusters get-credentials example-cluster \
33+
--region $REGION
34+
```
35+
36+
This command configures `kubectl` to use the cluster you created.
37+
38+
3. Create a repository and build the sample application
39+
40+
```shell
41+
gcloud artifacts repositories create example-repo \
42+
--repository-format=docker \
43+
--location=$REGION \
44+
--description="Example Docker repository"
45+
docker build --platform linux/amd64 \
46+
-t ${REGION}-docker.pkg.dev/${PROJECT_ID}/example-repo/example-app .
47+
```
48+
49+
4. Authenticate and push the Docker image to Artifact Registry
50+
51+
```shell
52+
gcloud auth configure-docker ${REGION}-docker.pkg.dev
53+
docker push ${REGION}-docker.pkg.dev/${PROJECT_ID}/example-repo/example-app
54+
```
55+
56+
## Setting up a service account
57+
58+
The first step to running PGAdapter in Kubernetes is creating a Google Cloud IAM
59+
service account (GSA) to represent your application. It is recommended that you create
60+
a service account unique to each application, instead of using the same service
61+
account everywhere. This model is more secure since it allows you to limit
62+
permissions on a per-application basis.
63+
64+
The service account must be granted access to the Cloud Spanner database that the
65+
application will be connecting to. It is enough to assign the `Cloud Spanner Database User`
66+
role for this example to the service account.
67+
68+
```shell
69+
export GSA_NAME=pgadapter-example-gsa
70+
gcloud iam service-accounts create $GSA_NAME \
71+
--description="PGAdapter Example Service Account" \
72+
--display-name="PGAdapter Example Service Account"
73+
gcloud projects add-iam-policy-binding $PROJECT_ID \
74+
--member="serviceAccount:${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \
75+
--role="roles/spanner.databaseUser"
76+
```
77+
78+
## Providing the service account to PGAdapter
79+
80+
Next, you need to configure Kubernetes to provide the service account to PGAdapter.
81+
This example uses GKE's [Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity)
82+
for this. This method allows you to bind a [Kubernetes Service Account (KSA)](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)
83+
to a Google Service Account (GSA). The GSA will then be accessible to applications
84+
using the matching KSA.
85+
86+
1. Create a Kubernetes Service Account (KSA)
87+
88+
```shell
89+
export KSA_NAME=pgadapter-example-ksa
90+
kubectl create serviceaccount $KSA_NAME --namespace default
91+
```
92+
93+
2. Enable the IAM binding between your `$GSA_NAME` and `$KSA_NAME`:
94+
95+
```sh
96+
gcloud iam service-accounts add-iam-policy-binding \
97+
--role roles/iam.workloadIdentityUser \
98+
--member "serviceAccount:${PROJECT_ID}.svc.id.goog[default/$KSA_NAME]" \
99+
${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com
100+
```
101+
102+
3. Add an annotation to `$KSA_NAME` to complete the binding:
103+
104+
```sh
105+
kubectl annotate serviceaccount \
106+
$KSA_NAME \
107+
iam.gke.io/gcp-service-account=${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com
108+
```
109+
110+
## Deploying the application
111+
112+
1. Deploy the example application and PGAdapter to Kubernetes. The script below replaces the
113+
placeholders in the deployment manifest with your values.
114+
115+
```shell
116+
export SPANNER_PROJECT_ID=$PROJECT_ID
117+
export SPANNER_INSTANCE_ID=<YOUR_SPANNER_INSTANCE_ID>
118+
export SPANNER_DATABASE_ID=<YOUR_SPANNER_DATABASE_ID>
119+
120+
sed -e 's|<YOUR_KSA_NAME>|'"${KSA_NAME}"'|g' \
121+
-e 's|<YOUR_REGION>|'"${REGION}"'|g' \
122+
-e 's|<YOUR_PROJECT_ID>|'"${PROJECT_ID}"'|g' \
123+
-e 's|<YOUR_SPANNER_PROJECT_ID>|'"${SPANNER_PROJECT_ID}"'|g' \
124+
-e 's|<YOUR_SPANNER_INSTANCE_ID>|'"${SPANNER_INSTANCE_ID}"'|g' \
125+
-e 's|<YOUR_SPANNER_DATABASE_ID>|'"${SPANNER_DATABASE_ID}"'|g' \
126+
manifests/example-app-deployment.yaml > manifests/my-app-deployment.yaml
127+
128+
kubectl apply -f manifests/my-app-deployment.yaml
129+
```
130+
131+
2. Expose the deployment to the Internet
132+
133+
```shell
134+
kubectl expose deployment pgadapter-gke-example \
135+
--type LoadBalancer --port 80 --target-port 8080
136+
```
137+
138+
## Inspect and view the application
139+
140+
1. Inspect the running Pods by using `kubectl get pods`
141+
142+
```shell
143+
kubectl get pods
144+
```
145+
146+
2. Inspect the pgadapter-gke-example Service by using `kubectl get service`
147+
148+
```shell
149+
kubectl get service pgadapter-gke-example
150+
```
151+
152+
From this command's output, copy the Service's external IP address from the EXTERNAL-IP column.
153+
154+
3. View the output of the application
155+
156+
Use the EXTERNAL-IP address from the previous command.
157+
158+
```shell
159+
curl http://<EXTERNAL-IP>
160+
```

samples/sidecar-proxy/main.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/**
2+
* Copyright 2023 Google Inc.
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+
* http://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+
package main
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"log"
23+
"net/http"
24+
"os"
25+
26+
"github.com/jackc/pgx/v4"
27+
)
28+
29+
func main() {
30+
// register greet function to handle all requests
31+
mux := http.NewServeMux()
32+
mux.HandleFunc("/", greet)
33+
34+
// use PORT environment variable, or default to 8080
35+
port := os.Getenv("PORT")
36+
if port == "" {
37+
port = "8080"
38+
}
39+
40+
// start the web server on port and accept requests
41+
log.Printf("Server listening on port %s", port)
42+
log.Fatal(http.ListenAndServe(":"+port, mux))
43+
}
44+
45+
// greet connects to PGAdapter and responds to the request with a greeting from Cloud Spanner.
46+
func greet(w http.ResponseWriter, r *http.Request) {
47+
log.Printf("Serving request: %s", r.URL.Path)
48+
host, _ := os.Hostname()
49+
fmt.Fprintf(w, "Hostname: %s\n", host)
50+
51+
conn, err := pgx.Connect(context.Background(), "postgres://uid:pwd@127.0.0.1:5432/?sslmode=disable")
52+
if err != nil {
53+
fmt.Fprintf(w, "Unable to connect to database: %v\n", err)
54+
return
55+
}
56+
defer conn.Close(context.Background())
57+
58+
var greeting string
59+
err = conn.QueryRow(context.Background(), "select 'Greeting from PGAdapter!'").Scan(&greeting)
60+
if err != nil {
61+
fmt.Fprintf(w, "QueryRow failed: %v\n", err)
62+
return
63+
}
64+
fmt.Fprintln(w, greeting)
65+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
2+
# Copyright 2023 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+
# http://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+
apiVersion: apps/v1
17+
kind: Deployment
18+
metadata:
19+
name: pgadapter-gke-example
20+
spec:
21+
selector:
22+
matchLabels:
23+
app: pgadapter-example
24+
template:
25+
metadata:
26+
labels:
27+
app: pgadapter-example
28+
spec:
29+
# TODO(developer): replace this value with the actual Kubernetes Service Account name
30+
serviceAccountName: <YOUR_KSA_NAME>
31+
containers:
32+
- name: example-app
33+
# TODO(developer): replace these values
34+
image: <YOUR_REGION>-docker.pkg.dev/<YOUR_PROJECT_ID>/example-repo/example-app
35+
imagePullPolicy: Always
36+
ports:
37+
- containerPort: 8080
38+
resources:
39+
requests:
40+
cpu: 200m
41+
- name: pgadapter
42+
image: gcr.io/cloud-spanner-pg-adapter/pgadapter
43+
ports:
44+
- containerPort: 5432
45+
# TODO(developer): Replace with actual project, instance and database name
46+
args:
47+
- "-p <YOUR_SPANNER_PROJECT_ID>"
48+
- "-i <YOUR_SPANNER_INSTANCE_ID>"
49+
- "-d <YOUR_SPANNER_DATABASE_ID>"
50+
- "-x"
51+
resources:
52+
requests:
53+
memory: "512Mi"
54+
cpu: 200m
55+
---

0 commit comments

Comments
 (0)