Deploying Spring Boot 3 on Cloud Run with Cloud SQL Private Service Connect Using Terraform

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

  • Data Sources Configuration

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

        

  • Database Configuration Classes

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();
    }
}

        

  1. Dockerization

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

  • Executing Terraform Commands:?

Before deploying the Spring Boot application, run the following Terraform commands to provision the infrastructure:

terraform init

terraform validate

terraform apply -auto-approve        


  • Building and Deploying Spring Boot Application:?


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

  • Artifact Repository was created by tf project

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.

要查看或添加评论,请登录

Henry Xiloj Herrera的更多文章

社区洞察

其他会员也浏览了