Deploying Spring Boot 3 on Cloud Run with Cloud SQL Private Service Connect Using Terraform
Introduction:
In this post, we'll walk through the steps required to create Infrastructure as Code (IaC) with Terraform for provisioning Cloud SQL Private Service Connect instances. For a detailed guide, check out this post on LinkedIn. Once the infrastructure is in place, we will build and deploy a Spring Boot 3 application that connects to these Cloud SQL instances using the appropriate methods.
Spring Boot Application
In the application.yaml file, we configure the data sources for Private Service connect Cloud SQL instances:
spring:
jpa:
defer-datasource-initialization: true
sql:
init:
mode: always
datasource:
psc:
url: jdbc:postgresql:///
database: my-database3
cloudSqlInstance: <PROJECT-ID>:<REGION>:psc-instance
username: <user>
password: <passoword>
ipTypes: PSC
socketFactory: com.google.cloud.sql.postgres.SocketFactory
driverClassName: org.postgresql.Driver
package com.henry.democloudsql.configuration;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import javax.naming.NamingException;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@EnableJpaRepositories(
basePackages = "com.henry.democloudsql.repository",
entityManagerFactoryRef = "pscEntityManager",
transactionManagerRef = "pscTransactionManager"
)
public class PSCpostgresConfig {
@Value("${spring.datasource.psc.url}")
private String url;
@Value("${spring.datasource.psc.database}")
private String database;
@Value("${spring.datasource.psc.cloudSqlInstance}")
private String cloudSqlInstance;
@Value("${spring.datasource.psc.username}")
private String username;
@Value("${spring.datasource.psc.password}")
private String password;
@Value("${spring.datasource.psc.ipTypes}")
private String ipTypes;
@Value("${spring.datasource.psc.socketFactory}")
private String socketFactory;
@Value("${spring.datasource.psc.driverClassName}")
private String driverClassName;
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean pscEntityManager()
throws NamingException {
LocalContainerEntityManagerFactoryBean em
= new LocalContainerEntityManagerFactoryBean();
em.setDataSource(pscDataSource());
em.setPackagesToScan("com.henry.democloudsql.model");
JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
em.setJpaProperties(pscHibernateProperties());
return em;
}
@Bean
@Primary
public DataSource pscDataSource() throws IllegalArgumentException {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(String.format(url + "%s", database));
config.setUsername(username);
config.setPassword(password);
config.addDataSourceProperty("socketFactory", socketFactory);
config.addDataSourceProperty("cloudSqlInstance", cloudSqlInstance);
config.addDataSourceProperty("ipTypes", ipTypes);
config.setMaximumPoolSize(5);
config.setMinimumIdle(5);
config.setConnectionTimeout(10000);
config.setIdleTimeout(600000);
config.setMaxLifetime(1800000);
return new HikariDataSource(config);
}
private Properties pscHibernateProperties() {
Properties properties = new Properties();
return properties;
}
@Bean
@Primary
public PlatformTransactionManager pscTransactionManager() throws NamingException {
final JpaTransactionManager transactionManager = new JpaTransactionManager();
transactionManager.setEntityManagerFactory(pscEntityManager().getObject());
return transactionManager;
}
}
Entity Models
package com.henry.democloudsql.model;
import jakarta.persistence.*;
import lombok.*;
import java.math.BigDecimal;
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "table3")
public class Table3 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "product")
private String product;
@Column(name = "price")
private BigDecimal price;
}
Repositories
package com.henry.democloudsql.repository;
import com.henry.democloudsql.model.Table3;
import org.springframework.data.repository.CrudRepository;
public interface Table3Repository extends CrudRepository<Table3, Long> {
}
Services
package com.henry.democloudsql.service;
public sealed interface DefaultService<T, G> permits Table3ServiceImpl {
T save(T obj);
Iterable<T> findAll();
T findById(G id);
}
package com.henry.democloudsql.service;
import com.henry.democloudsql.repository.Table3Repository;
import org.springframework.stereotype.Service;
@Service
public final class Table3ServiceImpl implements DefaultService {
private final Table3Repository table3Repository;
public Table3ServiceImpl(Table3Repository table3Repository) {
this.table3Repository = table3Repository;
}
@Override
public Object save(Object obj) {
return null;
}
@Override
public Iterable findAll() {
return table3Repository.findAll();
}
@Override
public Object findById(Object id) {
return null;
}
}
Controllers
package com.henry.democloudsql.controller;
import com.henry.democloudsql.model.Table3;
import com.henry.democloudsql.service.DefaultService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v3")
public class Table3Controller {
private final DefaultService<Table3, Long> defaultService;
public Table3Controller(DefaultService<Table3, Long> defaultService) {
this.defaultService = defaultService;
}
@GetMapping
public Iterable<Table3> findAll(){
return defaultService.findAll();
}
}
The Dockerfile is used to build the Docker image for the Spring Boot application:
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY target/demo-cloudsql.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
This Dockerfile uses the openjdk:17-jdk-slim base image, sets the working directory to /app, copies the built Spring Boot JAR file (demo-cloudsql.jar) into the container, and specifies the entrypoint to run the JAR file.
After creating the Dockerfile, you can build the Docker image locally using the following command:
Additional Details
Before deploying the Spring Boot application, run the following Terraform commands to provision the infrastructure:
terraform init
terraform validate
terraform apply -auto-approve
After modifying MY_PROJECT_ID in application.yml on Spring Boot App, run:
mvn clean install
After clean install, you can build the Docker image locally using the following command:
docker build -t quickstart-springboot:1.0.1 .
This command builds the Docker image with the tag quickstart-springboot:1.0.1 using the Dockerfile in the current directory.
Deployment and Integration
resource "google_artifact_registry_repository" "my-repo" {
location = var.region
repository_id = "my-repo"
description = "example docker repository"
format = "DOCKER"
}
Push Docker Image to Artifact Registry
To push the Docker image to the Artifact Registry, you first need to tag it with the appropriate URL:
docker tag quickstart-springboot:1.0.1 us-central1-docker.pkg.dev/MY_PROJECT_ID/my-repo/quickstart-springboot:1.0.1
Replace MY_PROJECT_ID with your actual GCP project ID.
Then, push the tagged image to the Artifact Registry:
docker push us-central1-docker.pkg.dev/MY_PROJECT_ID/my-repo/quickstart-springboot:1.0.1
Deploy to Cloud Run
Deploy the Spring Boot application to Google Cloud Run using the gcloud command:
With VPC Connector:?
gcloud run deploy springboot-run-psc-vpc-connector \
--image us-central1-docker.pkg.dev/MY_PROJECT_ID/my-repo/quickstart-springboot:1.0.1 \
--region=us-central1 \
--allow-unauthenticated \
--service-account=cloudsql-service-account-id@terraform-workspace-413615.iam.gserviceaccount.com \
--vpc-connector private-cloud-sql
With Direct VPC egress:?
gcloud beta run deploy springboot-run-psc-direct-vpc-egress \
--image=us-central1-docker.pkg.dev/MY_PROJECT_ID/my-repo/quickstart-springboot:1.0.1 \
--allow-unauthenticated \
--service-account=cloudsql-service-account-id@terraform-workspace-413615.iam.gserviceaccount.com \
--network=nw1-vpc \
--subnet=nw1-vpc-sub1-us-central1 \
--vpc-egress=all-traffic \
--region=us-central1 \
--project=MY_PROJECT_ID
This command deploys the Spring Boot application to Cloud Run, using the Docker image from the Artifact Registry. It also specifies the service account created by Terraform (cloudsql-service-account-id@terraform-workspace-413615.iam.gserviceaccount.com)
TEST
Source Code
Here on?GitHub.