Skip to content

Commit

Permalink
docs: Cloud Run sidecar with Java app sample (#959)
Browse files Browse the repository at this point in the history
* docs: Cloud Run sidecar with Java app sample

Adds a sample for running PGAdapter as a sidecar on Cloud Run together with an
application written in Java. The main application connects to PGAdapter using
the PostgreSQL JDBC driver and uses Unix domain sockets for the underlying
connection. The Unix domain sockets uses an in-memory volume.

* chore: update copyright year

* chore: update sample to use environment variables

Modified the sample slightly to use the same setup as the Go sample.
This means using environment variables for the Cloud Spanner database
that should be used, and starting PGAdapter without a default project,
instance and database. This makes the PGAdapter instance usable for
connections to different databases, which means that applications can
connect to multiple databases using a single PGAdapter instance.

It also shows how a single setup can easily be used for both test and
production.

* fix: add service replace step

* chore: remove duplicated line
  • Loading branch information
olavloite committed Aug 9, 2023
1 parent 00ce9b5 commit e6603ef
Show file tree
Hide file tree
Showing 5 changed files with 382 additions and 0 deletions.
98 changes: 98 additions & 0 deletions samples/cloud-run/java/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# PGAdapter Cloud Run Sidecar Sample for Java

This sample application shows how to build and deploy a Java application with PGAdapter as a sidecar
to Google Cloud Run. The Java application connects to PGAdapter using a Unix domain socket using an
in-memory volume. This gives the lowest possible latency between your application and PGAdapter.

The sample is based on the [Cloud Run Quickstart Guide for Java](https://cloud.google.com/run/docs/quickstarts/build-and-deploy/deploy-java-service).
Refer to that guide for more in-depth information on how to work with Cloud Run.

## Configure

Modify the `service.yaml` file to match your Cloud Run project and region, and your Cloud Spanner database:

```shell
# TODO: Modify MY-REGION and MY-PROJECT to match your application container image.
image: MY-REGION.pkg.dev/MY-PROJECT/cloud-run-source-deploy/pgadapter-sidecar-example
...
# TODO: Modify these environment variables to match your Cloud Spanner database.
env:
- name: SPANNER_PROJECT
value: my-project
- name: SPANNER_INSTANCE
value: my-instance
- name: SPANNER_DATABASE
value: my-database
```

## Optional - Build and Run Locally

You can test the application locally to verify that the Cloud Spanner project, instance, and database
configuration is correct. For this, you first need to start PGAdapter on your local machine and then
run the application.

```shell
docker pull gcr.io/cloud-spanner-pg-adapter/pgadapter
docker run \
--name pgadapter-cloud-run-example \
--rm -d -p 5432:5432 \
-v /path/to/credentials.json:/credentials.json:ro \
gcr.io/cloud-spanner-pg-adapter/pgadapter \
-c /credentials.json -x
export SPANNER_PROJECT=my-project
export SPANNER_INSTANCE=my-instance
export SPANNER_DATABASE=my-database
mvn spring-boot:run
```

This will start a web server on port 8080. Run the following command to verify that it works:

```shell
curl localhost:8080
```

Stop the PGAdapter Docker container again with:

```shell
docker container stop pgadapter-cloud-run-example
```

## Deploying to Cloud Run

First make sure that you have authentication set up for pushing Docker images.

```shell
gcloud auth configure-docker
```

Build the application from source and deploy it to Cloud Run. Replace the generated service
file with the one from this directory. The latter will add PGAdapter as a sidecar container to the
service.

```shell
gcloud run deploy pgadapter-sidecar-example --source .
gcloud run services replace service.yaml
```

__NOTE__: This example does not specify any credentials for PGAdapter when it is run on Cloud Run. This means that
PGAdapter will use the default credentials that is used by Cloud Run. This is by default the default compute engine
service account. See https://cloud.google.com/run/docs/securing/service-identity for more information on how service
accounts work on Google Cloud Run.

Test the service (replace URL with your actual service URL):

```shell
curl https://my-service-xyz.run.app
```

### Authenticated Cloud Run Service

If your Cloud Run service requires authentication, then first add an IAM binding for your own account and include
an authentication header with the request:

```shell
gcloud run services add-iam-policy-binding my-service \
--member='user:your-email@gmail.com' \
--role='roles/run.invoker'
curl -H "Authorization: Bearer $(gcloud auth print-identity-token)" https://my-service-xyz.run.app
```
109 changes: 109 additions & 0 deletions samples/cloud-run/java/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<!--
Copyright 2023 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

<spring-boot.version>2.7.11</spring-boot.version>
<junixsocket.version>2.6.2</junixsocket.version>
</properties>

<modelVersion>4.0.0</modelVersion>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-spanner-pgadapter-samples-cloud-run-sidecar</artifactId>
<version>0.1.0-SNAPSHOT</version>
<name>Google Cloud Spanner PGAdapter Cloud Run Sidecar Sample for Java</name>
<packaging>jar</packaging>
<description>
Sample for deploying a Java application with PGAdapter as a sidecar to Cloud Run
</description>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.6.0</version>
<scope>compile</scope>
</dependency>
<!-- This dependency allows us to use Unix Domain Sockets to connect to PGAdapter/PostgreSQL. -->
<dependency>
<groupId>com.kohlschutter.junixsocket</groupId>
<artifactId>junixsocket-core</artifactId>
<version>${junixsocket.version}</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>com.kohlschutter.junixsocket</groupId>
<artifactId>junixsocket-common</artifactId>
<version>${junixsocket.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.3.2</version>
<configuration>
<to>
<image>gcr.io/PROJECT_ID/pgadapter-sidecar-example</image>
</to>
</configuration>
</plugin>
</plugins>
</build>
</project>
66 changes: 66 additions & 0 deletions samples/cloud-run/java/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
annotations:
run.googleapis.com/launch-stage: BETA
name: pgadapter-sidecar-example
spec:
template:
metadata:
annotations:
run.googleapis.com/execution-environment: gen1
# This registers 'pgadapter' as a dependency of 'app' and will ensure that pgadapter starts
# before the app container.
run.googleapis.com/container-dependencies: '{"app":["pgadapter"]}'
spec:
# Create an in-memory volume that can be used for Unix domain sockets.
volumes:
- name: sockets-dir
emptyDir:
sizeLimit: 50Mi
medium: Memory
containers:
# This is the main application container.
- name: app
# TODO: Modify MY-REGION and MY-PROJECT to match your application container image.
# Example: europe-north1-docker.pkg.dev/my-test-project/cloud-run-source-deploy/pgadapter-sidecar-example
image: MY-REGION.pkg.dev/MY-PROJECT/cloud-run-source-deploy/pgadapter-sidecar-example
# TODO: Modify these environment variables to match your Cloud Spanner database.
# The PGADAPTER_HOST variable is set to point to /sockets, which is the shared in-memory
# volume that is used for Unix domain sockets.
env:
- name: SPANNER_PROJECT
value: my-project
- name: SPANNER_INSTANCE
value: my-instance
- name: SPANNER_DATABASE
value: my-database
- name: PGADAPTER_HOST
value: /sockets
- name: PGADAPTER_PORT
value: "5432"
ports:
- containerPort: 8080
volumeMounts:
- mountPath: /sockets
name: sockets-dir
# This is the PGAdapter sidecar container.
- name: pgadapter
image: gcr.io/cloud-spanner-pg-adapter/pgadapter
volumeMounts:
- mountPath: /sockets
name: sockets-dir
args:
- -dir /sockets
- -x
# Add a startup probe that checks that PGAdapter is listening on port 5432.
# NOTE: This probe will cause PGAdapter to log an EOF warning. This error may be ignored.
# The warning is caused by the TCP probe, which will open a TCP connection to PGAdapter,
# but not send a PostgreSQL startup message, and instead just close the connection.
startupProbe:
initialDelaySeconds: 10
timeoutSeconds: 10
periodSeconds: 10
failureThreshold: 3
tcpSocket:
port: 5432
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.cloud.spanner.pgadapter.sample;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* Sample application for connecting to Cloud Spanner using the PostgreSQL JDBC driver on Cloud Run.
* This sample application runs PGAdapter as a sidecar container on Cloud Run.
*
* <p>Modify service.yaml to match your Cloud Run project and region, and your Cloud Spanner database.
*/
@SpringBootApplication
public class SampleApplication {

@Value("${NAME:World}")
String name;

final String project = getEnvOrDefault("SPANNER_PROJECT", "my-project");
final String instance = getEnvOrDefault("SPANNER_INSTANCE", "my-instance");
final String database = getEnvOrDefault("SPANNER_DATABASE", "my-database");
final String qualifiedDatabaseName =
String.format("projects/%s/instances/%s/databases/%s", project, instance, database);
final String urlEncodedDatabaseName;
final String pgadapterHost = getEnvOrDefault("PGADAPTER_HOST", "localhost");
final String pgadapterPort = getEnvOrDefault("PGADAPTER_PORT", "5432");

SampleApplication() {
try {
// We need to URL-encode the fully qualified database name before we can use it in a JDBC
// connection URL. Otherwise, the JDBC driver will complain about the connection string
// containing too many '/' characters.
urlEncodedDatabaseName = URLEncoder.encode(qualifiedDatabaseName, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException exception) {
throw new RuntimeException(exception);
}
}

static String getEnvOrDefault(String key, String defaultValue) {
return System.getenv(key) == null ? defaultValue : System.getenv(key);
}

@RestController
class HelloworldController {
@GetMapping("/")
String hello() {
String connectionUrl;
if (pgadapterHost.startsWith("/")) {
// Connect to PGAdapter using Unix Domain Sockets. This gives you the lowest possible
// latency. The PGAdapter sidecar container and the main container both share the /sockets
// directory, and PGAdapter is instructed to use this directory for Unix domain sockets.
connectionUrl = String.format("jdbc:postgresql://localhost/%s?"
+ "socketFactory=org.newsclub.net.unix.AFUNIXSocketFactory$FactoryArg"
+ "&socketFactoryArg=%s/.s.PGSQL.%s", urlEncodedDatabaseName, pgadapterHost, pgadapterPort);
} else {
// Use a TCP connection.
connectionUrl = String.format("jdbc:postgresql://%s:%s/%s",
pgadapterHost, pgadapterPort, urlEncodedDatabaseName);
}
// NOTE: You should use a JDBC connection pool for a production application.
try (Connection connection = DriverManager.getConnection(connectionUrl)) {
// Create a prepared statement that takes one query parameter that will be used as the
// name that will be greeted.
try (PreparedStatement statement =
connection.prepareStatement(
"select 'Hello ' || ? || ' from Cloud Spanner using JDBC!' as greeting")) {
statement.setString(1, name);
try (ResultSet resultSet = statement.executeQuery()) {
if (resultSet.next()) {
return resultSet.getString(1) + "\n";
} else {
return "No greeting was returned by Cloud Spanner!\n";
}
}
}
} catch (Throwable exception) {
return exception + "\n";
}
}
}

public static void main(String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
server.port=${PORT:8080}

0 comments on commit e6603ef

Please sign in to comment.