google-site-verification: google61fe8ba583a51912.html
Google Cloud Run with Java: Building Serverless Containers

Google Cloud Run is a fully managed serverless platform that automatically scales your stateless containers. This guide covers building, deploying, and optimizing Java applications for Cloud Run.


Core Concepts

Why Cloud Run for Java?

  • Serverless: No infrastructure management
  • Auto-scaling: Scales to zero when not in use
  • Pay-per-use: Only pay for request processing time
  • Container-based: Use any Java framework or library
  • Fast deployment: Quick iterations and deployments

Key Considerations for Java on Cloud Run:

  • Cold Start Optimization: Minimize startup time
  • Memory Management: Configure appropriate memory limits
  • Stateless Design: No local storage or session state
  • Health Checks: Implement proper readiness/liveness probes
  • Concurrency: Handle multiple requests per container

Dependencies and Setup

1. Maven Dependencies
<properties> <spring-boot.version>3.1.0</spring-boot.version> <jib-maven-plugin.version>3.4.0</jib-maven-plugin.version> <google-cloud.version>2.20.0</google-cloud.version> </properties> <dependencies> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Boot Actuator for Health Checks --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Spring Boot Configuration Processor --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <version>${spring-boot.version}</version> <optional>true</optional> </dependency> <!-- Google Cloud Libraries --> <dependency> <groupId>com.google.cloud</groupId> <artifactId>google-cloud-storage</artifactId> <version>${google-cloud.version}</version> </dependency> <dependency> <groupId>com.google.cloud</groupId> <artifactId>google-cloud-secretmanager</artifactId> <version>${google-cloud.version}</version> </dependency> <dependency> <groupId>com.google.cloud</groupId> <artifactId>google-cloud-logging</artifactId> <version>${google-cloud.version}</version> </dependency> <!-- Database (PostgreSQL) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>${spring-boot.version}</version> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.6.0</version> </dependency> <!-- Redis for Caching --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>${spring-boot.version}</version> </dependency> <!-- Testing --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <version>${spring-boot.version}</version> <scope>test</scope> </dependency> <!-- Testcontainers for Integration Tests --> <dependency> <groupId>org.testcontainers</groupId> <artifactId>junit-jupiter</artifactId> <version>1.18.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.testcontainers</groupId> <artifactId>postgresql</artifactId> <version>1.18.3</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <!-- Spring Boot Maven Plugin --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> <!-- Jib for Container Building --> <plugin> <groupId>com.google.cloud.tools</groupId> <artifactId>jib-maven-plugin</artifactId> <version>${jib-maven-plugin.version}</version> <configuration> <from> <image>eclipse-temurin:21-jre</image> </from> <to> <image>gcr.io/${google.cloud.project}/${project.artifactId}:${project.version}</image> </to> <container> <jvmFlags> <jvmFlag>-Xmx512m</jvmFlag> <jvmFlag>-Xms256m</jvmFlag> <jvmFlag>-XX:+UseG1GC</jvmFlag> <jvmFlag>-XX:MaxRAMPercentage=75.0</jvmFlag> </jvmFlags> <environment> <SPRING_PROFILES_ACTIVE>cloudrun</SPRING_PROFILES_ACTIVE> </environment> <ports> <port>8080</port> </ports> <creationTime>USE_CURRENT_TIMESTAMP</creationTime> </container> </configuration> </plugin> </plugins> </build>
2. Google Cloud SDK Setup
# Install Google Cloud SDK curl https://sdk.cloud.google.com | bash exec -l $SHELL # Initialize and authenticate gcloud init gcloud auth configure-docker # Set project gcloud config set project YOUR_PROJECT_ID

Application Implementation

1. Main Application Class
package com.example.cloudrun; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; @SpringBootApplication @EnableConfigurationProperties(CloudRunProperties.class) public class CloudRunApplication { public static void main(String[] args) { SpringApplication.run(CloudRunApplication.class, args); } }
2. Cloud Run Configuration
package com.example.cloudrun.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @Configuration @ConfigurationProperties(prefix = "cloudrun") public class CloudRunProperties { private String serviceName; private String revision; private String configuration; private int maxInstances = 100; private int concurrency = 80; private int timeoutSeconds = 300; private String region = "us-central1"; // Getters and Setters public String getServiceName() { return serviceName; } public void setServiceName(String serviceName) { this.serviceName = serviceName; } public String getRevision() { return revision; } public void setRevision(String revision) { this.revision = revision; } public String getConfiguration() { return configuration; } public void setConfiguration(String configuration) { this.configuration = configuration; } public int getMaxInstances() { return maxInstances; } public void setMaxInstances(int maxInstances) { this.maxInstances = maxInstances; } public int getConcurrency() { return concurrency; } public void setConcurrency(int concurrency) { this.concurrency = concurrency; } public int getTimeoutSeconds() { return timeoutSeconds; } public void setTimeoutSeconds(int timeoutSeconds) { this.timeoutSeconds = timeoutSeconds; } public String getRegion() { return region; } public void setRegion(String region) { this.region = region; } }
3. Application Configuration
# application-cloudrun.yml server: port: 8080 servlet: context-path: / compression: enabled: true mime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json min-response-size: 1024 spring: application: name: cloudrun-demo datasource: url: ${DATABASE_URL:jdbc:postgresql://localhost:5432/cloudrun} username: ${DATABASE_USERNAME:postgres} password: ${DATABASE_PASSWORD:password} hikari: maximum-pool-size: 5 minimum-idle: 1 connection-timeout: 30000 max-lifetime: 1800000 jpa: hibernate: ddl-auto: validate properties: hibernate: dialect: org.hibernate.dialect.PostgreSQLDialect jdbc: lob: non_contextual_creation: true show_sql: false format_sql: false redis: host: ${REDIS_HOST:localhost} port: ${REDIS_PORT:6379} password: ${REDIS_PASSWORD:} lettuce: pool: max-active: 8 max-idle: 8 min-idle: 1 max-wait: -1ms cloud: gcp: project-id: ${GOOGLE_CLOUD_PROJECT} credentials: location: ${GOOGLE_APPLICATION_CREDENTIALS:} management: endpoints: web: exposure: include: health,info,metrics,env base-path: /actuator endpoint: health: show-details: always show-components: always probes: enabled: true metrics: enabled: true cloudrun: service-name: ${K_SERVICE:local} revision: ${K_REVISION:local} configuration: ${K_CONFIGURATION:local} max-instances: ${MAX_INSTANCES:100} concurrency: ${CONCURRENCY:80} timeout-seconds: ${TIMEOUT_SECONDS:300} region: ${REGION:us-central1} logging: level: com.example.cloudrun: INFO org.hibernate.SQL: WARN org.springframework.cloud.gcp: INFO pattern: level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]" # Optimize for Cloud Run spring: lifecycle: timeout-per-shutdown-phase: 30s threads: virtual: enabled: true
4. Health Check Configuration
package com.example.cloudrun.health; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.boot.actuate.health.Status; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.sql.Connection; import java.sql.Statement; import java.util.Map; @Component public class CloudRunHealthIndicator implements HealthIndicator { private final DataSource dataSource; private final CloudRunMetadata cloudRunMetadata; public CloudRunHealthIndicator(DataSource dataSource, CloudRunMetadata cloudRunMetadata) { this.dataSource = dataSource; this.cloudRunMetadata = cloudRunMetadata; } @Override public Health health() { try { // Check database connectivity boolean dbHealthy = checkDatabaseHealth(); // Build health details Health.Builder healthBuilder = dbHealthy ? Health.up() : Health.down(); healthBuilder .withDetail("service", cloudRunMetadata.getServiceName()) .withDetail("revision", cloudRunMetadata.getRevision()) .withDetail("region", cloudRunMetadata.getRegion()) .withDetail("timestamp", System.currentTimeMillis()) .withDetails(buildHealthDetails(dbHealthy)); return healthBuilder.build(); } catch (Exception e) { return Health.down(e) .withDetail("service", cloudRunMetadata.getServiceName()) .withDetail("error", e.getMessage()) .build(); } } private boolean checkDatabaseHealth() { try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) { return statement.execute("SELECT 1"); } catch (Exception e) { return false; } } private Map<String, Object> buildHealthDetails(boolean dbHealthy) { return Map.of( "database", dbHealthy ? "CONNECTED" : "DISCONNECTED", "memory_usage", Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(), "available_processors", Runtime.getRuntime().availableProcessors(), "uptime", ManagementFactory.getRuntimeMXBean().getUptime() ); } } @Component class CloudRunMetadata { @Value("${K_SERVICE:local}") private String serviceName; @Value("${K_REVISION:local}") private String revision; @Value("${REGION:us-central1}") private String region; public String getServiceName() { return serviceName; } public String getRevision() { return revision; } public String getRegion() { return region; } }
5. Domain Models
package com.example.cloudrun.model; import jakarta.persistence.*; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; import java.time.LocalDateTime; import java.util.UUID; @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.UUID) private UUID id; @NotBlank @Size(max = 100) @Column(nullable = false) private String firstName; @NotBlank @Size(max = 100) @Column(nullable = false) private String lastName; @NotBlank @Email @Size(max = 255) @Column(nullable = false, unique = true) private String email; @Enumerated(EnumType.STRING) @Column(nullable = false) private UserStatus status = UserStatus.ACTIVE; @Column(name = "created_at", nullable = false, updatable = false) private LocalDateTime createdAt; @Column(name = "updated_at", nullable = false) private LocalDateTime updatedAt; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); updatedAt = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updatedAt = LocalDateTime.now(); } // Constructors public User() {} public User(String firstName, String lastName, String email) { this.firstName = firstName; this.lastName = lastName; this.email = email; } // Getters and Setters public UUID getId() { return id; } public void setId(UUID id) { this.id = id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public UserStatus getStatus() { return status; } public void setStatus(UserStatus status) { this.status = status; } public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public LocalDateTime getUpdatedAt() { return updatedAt; } public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } } enum UserStatus { ACTIVE, INACTIVE, SUSPENDED } // Data Transfer Objects public class UserResponse { private final UUID id; private final String firstName; private final String lastName; private final String email; private final UserStatus status; private final LocalDateTime createdAt; public UserResponse(User user) { this.id = user.getId(); this.firstName = user.getFirstName(); this.lastName = user.getLastName(); this.email = user.getEmail(); this.status = user.getStatus(); this.createdAt = user.getCreatedAt(); } // Getters public UUID getId() { return id; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } public String getEmail() { return email; } public UserStatus getStatus() { return status; } public LocalDateTime getCreatedAt() { return createdAt; } } public class CreateUserRequest { @NotBlank @Size(max = 100) private String firstName; @NotBlank @Size(max = 100) private String lastName; @NotBlank @Email @Size(max = 255) private String email; // Getters and Setters public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } }
6. Repository Layer
package com.example.cloudrun.repository; import com.example.cloudrun.model.User; import com.example.cloudrun.model.UserStatus; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; import java.util.UUID; @Repository public interface UserRepository extends JpaRepository<User, UUID> { Optional<User> findByEmail(String email); List<User> findByStatus(UserStatus status); Page<User> findByStatus(UserStatus status, Pageable pageable); @Query("SELECT u FROM User u WHERE LOWER(u.firstName) LIKE LOWER(CONCAT('%', :name, '%')) OR LOWER(u.lastName) LIKE LOWER(CONCAT('%', :name, '%'))") Page<User> findByNameContainingIgnoreCase(@Param("name") String name, Pageable pageable); boolean existsByEmail(String email); long countByStatus(UserStatus status); }
7. Service Layer
package com.example.cloudrun.service; import com.example.cloudrun.model.User; import com.example.cloudrun.model.UserStatus; import com.example.cloudrun.repository.UserRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; import java.util.UUID; @Service @Transactional public class UserService { private static final Logger LOG = LoggerFactory.getLogger(UserService.class); private final UserRepository userRepository; private final CloudRunMetrics metrics; public UserService(UserRepository userRepository, CloudRunMetrics metrics) { this.userRepository = userRepository; this.metrics = metrics; } @Transactional(readOnly = true) public Page<User> getAllUsers(Pageable pageable) { LOG.debug("Fetching users page: {}", pageable); metrics.recordUserQuery(); return userRepository.findAll(pageable); } @Transactional(readOnly = true) @Cacheable(value = "users", key = "#id") public Optional<User> getUserById(UUID id) { LOG.debug("Fetching user by ID: {}", id); metrics.recordUserQuery(); return userRepository.findById(id); } @Transactional(readOnly = true) public Optional<User> getUserByEmail(String email) { LOG.debug("Fetching user by email: {}", email); metrics.recordUserQuery(); return userRepository.findByEmail(email); } public User createUser(User user) { LOG.info("Creating new user with email: {}", user.getEmail()); if (userRepository.existsByEmail(user.getEmail())) { throw new UserServiceException("User with email " + user.getEmail() + " already exists"); } User savedUser = userRepository.save(user); metrics.recordUserCreated(); LOG.info("Successfully created user: {}", savedUser.getId()); return savedUser; } public User updateUser(UUID id, User userUpdate) { LOG.info("Updating user: {}", id); User existingUser = userRepository.findById(id) .orElseThrow(() -> new UserServiceException("User not found: " + id)); // Update fields if provided if (userUpdate.getFirstName() != null) { existingUser.setFirstName(userUpdate.getFirstName()); } if (userUpdate.getLastName() != null) { existingUser.setLastName(userUpdate.getLastName()); } if (userUpdate.getStatus() != null) { existingUser.setStatus(userUpdate.getStatus()); } User updatedUser = userRepository.save(existingUser); metrics.recordUserUpdated(); LOG.info("Successfully updated user: {}", id); return updatedUser; } public void deleteUser(UUID id) { LOG.info("Deleting user: {}", id); User user = userRepository.findById(id) .orElseThrow(() -> new UserServiceException("User not found: " + id)); userRepository.delete(user); metrics.recordUserDeleted(); LOG.info("Successfully deleted user: {}", id); } @Transactional(readOnly = true) public long getActiveUsersCount() { long count = userRepository.countByStatus(UserStatus.ACTIVE); metrics.recordUserQuery(); return count; } public User deactivateUser(UUID id) { LOG.info("Deactivating user: {}", id); User user = userRepository.findById(id) .orElseThrow(() -> new UserServiceException("User not found: " + id)); user.setStatus(UserStatus.INACTIVE); User deactivatedUser = userRepository.save(user); metrics.recordUserDeactivated(); return deactivatedUser; } } class UserServiceException extends RuntimeException { public UserServiceException(String message) { super(message); } public UserServiceException(String message, Throwable cause) { super(message, cause); } }
8. REST Controllers
package com.example.cloudrun.controller; import com.example.cloudrun.model.CreateUserRequest; import com.example.cloudrun.model.User; import com.example.cloudrun.model.UserResponse; import com.example.cloudrun.service.UserService; import jakarta.validation.Valid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.UUID; @RestController @RequestMapping("/api/v1/users") public class UserController { private static final Logger LOG = LoggerFactory.getLogger(UserController.class); private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @GetMapping public ResponseEntity<Page<UserResponse>> getUsers( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(defaultValue = "createdAt") String sortBy, @RequestParam(defaultValue = "desc") String sortDirection) { LOG.info("Fetching users - page: {}, size: {}, sort: {}", page, size, sortBy); Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy); Pageable pageable = PageRequest.of(page, size, sort); Page<UserResponse> users = userService.getAllUsers(pageable) .map(UserResponse::new); return ResponseEntity.ok(users); } @GetMapping("/{id}") public ResponseEntity<UserResponse> getUserById(@PathVariable UUID id) { LOG.info("Fetching user by ID: {}", id); return userService.getUserById(id) .map(user -> ResponseEntity.ok(new UserResponse(user))) .orElse(ResponseEntity.notFound().build()); } @PostMapping public ResponseEntity<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) { LOG.info("Creating user with email: {}", request.getEmail()); User user = new User(request.getFirstName(), request.getLastName(), request.getEmail()); User createdUser = userService.createUser(user); return ResponseEntity.status(HttpStatus.CREATED) .body(new UserResponse(createdUser)); } @PutMapping("/{id}") public ResponseEntity<UserResponse> updateUser( @PathVariable UUID id, @Valid @RequestBody CreateUserRequest request) { LOG.info("Updating user: {}", id); User userUpdate = new User(request.getFirstName(), request.getLastName(), request.getEmail()); User updatedUser = userService.updateUser(id, userUpdate); return ResponseEntity.ok(new UserResponse(updatedUser)); } @DeleteMapping("/{id}") public ResponseEntity<Void> deleteUser(@PathVariable UUID id) { LOG.info("Deleting user: {}", id); userService.deleteUser(id); return ResponseEntity.noContent().build(); } @PostMapping("/{id}/deactivate") public ResponseEntity<UserResponse> deactivateUser(@PathVariable UUID id) { LOG.info("Deactivating user: {}", id); User deactivatedUser = userService.deactivateUser(id); return ResponseEntity.ok(new UserResponse(deactivatedUser)); } @GetMapping("/stats/active-count") public ResponseEntity<ActiveUsersCountResponse> getActiveUsersCount() { long count = userService.getActiveUsersCount(); return ResponseEntity.ok(new ActiveUsersCountResponse(count)); } } class ActiveUsersCountResponse { private final long activeUsersCount; public ActiveUsersCountResponse(long activeUsersCount) { this.activeUsersCount = activeUsersCount; } public long getActiveUsersCount() { return activeUsersCount; } }
9. Google Cloud Integration Services
package com.example.cloudrun.integration; import com.google.cloud.storage.BlobId; import com.google.cloud.storage.BlobInfo; import com.google.cloud.storage.Storage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.io.InputStream; import java.nio.channels.Channels; import java.util.UUID; @Service public class CloudStorageService { private static final Logger LOG = LoggerFactory.getLogger(CloudStorageService.class); private final Storage storage; @Value("${gcp.storage.bucket:default-bucket}") private String bucketName; public CloudStorageService(Storage storage) { this.storage = storage; } public String uploadFile(InputStream fileStream, String contentType, String originalFilename) { try { String fileName = generateFileName(originalFilename); BlobId blobId = BlobId.of(bucketName, fileName); BlobInfo blobInfo = BlobInfo.newBuilder(blobId) .setContentType(contentType) .build(); storage.createFrom(blobInfo, fileStream); String publicUrl = String.format("https://storage.googleapis.com/%s/%s", bucketName, fileName); LOG.info("File uploaded successfully: {}", publicUrl); return publicUrl; } catch (Exception e) { LOG.error("Failed to upload file to Cloud Storage: {}", e.getMessage(), e); throw new CloudStorageException("File upload failed", e); } } public void deleteFile(String fileName) { try { boolean deleted = storage.delete(bucketName, fileName); if (deleted) { LOG.info("File deleted successfully: {}", fileName); } else { LOG.warn("File not found for deletion: {}", fileName); } } catch (Exception e) { LOG.error("Failed to delete file from Cloud Storage: {}", e.getMessage(), e); throw new CloudStorageException("File deletion failed", e); } } private String generateFileName(String originalFilename) { String extension = ""; if (originalFilename != null && originalFilename.contains(".")) { extension = originalFilename.substring(originalFilename.lastIndexOf(".")); } return "uploads/" + UUID.randomUUID() + extension; } } class CloudStorageException extends RuntimeException { public CloudStorageException(String message) { super(message); } public CloudStorageException(String message, Throwable cause) { super(message, cause); } } package com.example.cloudrun.integration; import com.google.cloud.secretmanager.v1.AccessSecretVersionResponse; import com.google.cloud.secretmanager.v1.SecretManagerServiceClient; import com.google.cloud.secretmanager.v1.SecretVersionName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import java.io.IOException; @Service public class SecretManagerService { private static final Logger LOG = LoggerFactory.getLogger(SecretManagerService.class); public String getSecret(String secretId, String version) { try (SecretManagerServiceClient client = SecretManagerServiceClient.create()) { String projectId = System.getenv("GOOGLE_CLOUD_PROJECT"); SecretVersionName secretVersionName = SecretVersionName.of(projectId, secretId, version); AccessSecretVersionResponse response = client.accessSecretVersion(secretVersionName); String secretValue = response.getPayload().getData().toStringUtf8(); LOG.debug("Successfully retrieved secret: {}", secretId); return secretValue; } catch (IOException e) { LOG.error("Failed to retrieve secret {}: {}", secretId, e.getMessage()); throw new SecretManagerException("Failed to retrieve secret: " + secretId, e); } } public String getSecret(String secretId) { return getSecret(secretId, "latest"); } } class SecretManagerException extends RuntimeException { public SecretManagerException(String message) { super(message); } public SecretManagerException(String message, Throwable cause) { super(message, cause); } }
10. Metrics and Monitoring
package com.example.cloudrun.metrics; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; @Component public class CloudRunMetrics { private final Counter requestsCounter; private final Counter userCreatedCounter; private final Counter userUpdatedCounter; private final Counter userDeletedCounter; private final Counter userDeactivatedCounter; private final Counter userQueryCounter; private final Timer requestTimer; public CloudRunMetrics(MeterRegistry meterRegistry) { this.requestsCounter = Counter.builder("cloudrun.requests.total") .description("Total number of HTTP requests") .register(meterRegistry); this.userCreatedCounter = Counter.builder("cloudrun.users.created") .description("Number of users created") .register(meterRegistry); this.userUpdatedCounter = Counter.builder("cloudrun.users.updated") .description("Number of users updated") .register(meterRegistry); this.userDeletedCounter = Counter.builder("cloudrun.users.deleted") .description("Number of users deleted") .register(meterRegistry); this.userDeactivatedCounter = Counter.builder("cloudrun.users.deactivated") .description("Number of users deactivated") .register(meterRegistry); this.userQueryCounter = Counter.builder("cloudrun.users.queries") .description("Number of user queries") .register(meterRegistry); this.requestTimer = Timer.builder("cloudrun.request.duration") .description("HTTP request duration") .register(meterRegistry); } public void recordRequest() { requestsCounter.increment(); } public void recordUserCreated() { userCreatedCounter.increment(); } public void recordUserUpdated() { userUpdatedCounter.increment(); } public void recordUserDeleted() { userDeletedCounter.increment(); } public void recordUserDeactivated() { userDeactivatedCounter.increment(); } public void recordUserQuery() { userQueryCounter.increment(); } public void recordRequestTime(long duration, TimeUnit unit) { requestTimer.record(duration, unit); } }

Docker Configuration

1. Dockerfile for Cloud Run
# Use Eclipse Temurin JRE for smaller image size FROM eclipse-temurin:21-jre-jammy as runtime # Install curl for health checks RUN apt-get update && \ apt-get install -y curl && \ rm -rf /var/lib/apt/lists/* && \ apt-get clean # Create a non-root user RUN groupadd --system --gid 1000 appgroup && \ useradd --system --uid 1000 --gid appgroup appuser # Set working directory WORKDIR /app # Copy the JAR file COPY target/*.jar app.jar # Change ownership to non-root user RUN chown -R appuser:appgroup /app # Switch to non-root user USER appuser # Expose the port Cloud Run expects EXPOSE 8080 # Health check (Cloud Run uses startup and liveness probes) HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1 # Optimized JVM flags for Cloud Run ENV JAVA_OPTS="-Xmx512m -Xms256m -XX:+UseG1GC -XX:MaxRAMPercentage=75.0 -Djava.security.egd=file:/dev/./urandom" # Start the application ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar /app/app.jar"]
2. Jib Configuration for Maven
<!-- Enhanced Jib Configuration --> <plugin> <groupId>com.google.cloud.tools</groupId> <artifactId>jib-maven-plugin</artifactId> <version>3.4.0</version> <configuration> <from> <image>eclipse-temurin:21-jre-jammy</image> <platforms> <platform> <architecture>amd64</architecture> <os>linux</os> </platform> </platforms> </from> <to> <image>gcr.io/${google.cloud.project}/${project.artifactId}</image> <tags> <tag>${project.version}</tag> <tag>latest</tag> </tags> </to> <container> <user>1000:1000</user> <workingDirectory>/app</workingDirectory> <entrypoint> <arg>java</arg> <arg>-Xmx512m</arg> <arg>-Xms256m</arg> <arg>-XX:+UseG1GC</arg> <arg>-XX:MaxRAMPercentage=75.0</arg> <arg>-Djava.security.egd=file:/dev/./urandom</arg> <arg>-jar</arg> <arg>/app/app.jar</arg> </entrypoint> <ports> <port>8080</port> </ports> <environment> <SPRING_PROFILES_ACTIVE>cloudrun</SPRING_PROFILES_ACTIVE> <JAVA_TOOL_OPTIONS>-XX:+ExitOnOutOfMemoryError</JAVA_TOOL_OPTIONS> </environment> <creationTime>USE_CURRENT_TIMESTAMP</creationTime> <labels> <maintainer>[email protected]</maintainer> <version>${project.version}</version> <commit-sha>${git.commit.id.abbrev}</commit-sha> </labels> </container> </configuration> </plugin>

Deployment Configuration

1. Cloud Run Service Definition
# service.yaml apiVersion: serving.knative.dev/v1 kind: Service metadata: name: cloudrun-demo labels: app: cloudrun-demo environment: production spec: template: metadata: annotations: # Auto-scaling configuration autoscaling.knative.dev/maxScale: "100" autoscaling.knative.dev/minScale: "0" # Concurrency configuration run.googleapis.com/concurrency: "80" # CPU configuration run.googleapis.com/cpu-throttling: "true" # Startup configuration run.googleapis.com/startup-cpu-boost: "true" spec: containerConcurrency: 80 timeoutSeconds: 300 containers: - image: gcr.io/YOUR_PROJECT_ID/cloudrun-demo:latest ports: - containerPort: 8080 resources: limits: cpu: 1000m memory: 512Mi requests: cpu: 250m memory: 256Mi env: - name: SPRING_PROFILES_ACTIVE value: "cloudrun,production" - name: DATABASE_URL valueFrom: secretKeyRef: name: cloudrun-db-secret key: url - name: DATABASE_USERNAME valueFrom: secretKeyRef: name: cloudrun-db-secret key: username - name: DATABASE_PASSWORD valueFrom: secretKeyRef: name: cloudrun-db-secret key: password - name: REDIS_HOST value: "10.0.0.3" - name: MAX_INSTANCES value: "100" - name: CONCURRENCY value: "80" - name: TIMEOUT_SECONDS value: "300" startupProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 0 periodSeconds: 5 timeoutSeconds: 10 failureThreshold: 30 # 30 * 5 = 150 seconds max startup time livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 10 periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 traffic: - percent: 100 latestRevision: true
2. Deployment Scripts
#!/bin/bash # deploy.sh set -e # Configuration PROJECT_ID="your-project-id" SERVICE_NAME="cloudrun-demo" REGION="us-central1" IMAGE_NAME="gcr.io/$PROJECT_ID/$SERVICE_NAME" echo "Building and deploying $SERVICE_NAME to Cloud Run..." # Build the container image echo "Building container image..." mvn compile jib:build -DskipTests # Deploy to Cloud Run echo "Deploying to Cloud Run..." gcloud run deploy $SERVICE_NAME \ --image $IMAGE_NAME \ --platform managed \ --region $REGION \ --allow-unauthenticated \ --memory 512Mi \ --cpu 1 \ --concurrency 80 \ --max-instances 100 \ --timeout 300s \ --set-env-vars=SPRING_PROFILES_ACTIVE=cloudrun,production \ --update-secrets=DATABASE_URL=cloudrun-db-secret:latest \ --update-secrets=DATABASE_USERNAME=cloudrun-db-secret:latest \ --update-secrets=DATABASE_PASSWORD=cloudrun-db-secret:latest # Get the service URL SERVICE_URL=$(gcloud run services describe $SERVICE_NAME --region $REGION --format 'value(status.url)') echo "Service deployed successfully: $SERVICE_URL" # Run health check echo "Running health check..." curl -f "$SERVICE_URL/actuator/health" || exit 1 echo "Health check passed!"
3. Cloud Build Configuration
# cloudbuild.yaml steps: # Build the application - name: maven:3.9-eclipse-temurin-21 entrypoint: mvn args: ['clean', 'compile', '-DskipTests'] # Run tests - name: maven:3.9-eclipse-temurin-21 entrypoint: mvn args: ['test'] # Build container image - name: maven:3.9-eclipse-temurin-21 entrypoint: mvn args: ['compile', 'jib:build', '-DskipTests'] secretEnv: ['GOOGLE_APPLICATION_CREDENTIALS'] # Deploy to Cloud Run - name: 'gcr.io/cloud-builders/gcloud' args: - 'run' - 'deploy' - 'cloudrun-demo' - '--image=gcr.io/$PROJECT_ID/cloudrun-demo:latest' - '--region=us-central1' - '--platform=managed' - '--allow-unauthenticated' - '--memory=512Mi' - '--cpu=1' - '--concurrency=80' - '--max-instances=100' - '--timeout=300s' - '--set-env-vars=SPRING_PROFILES_ACTIVE=cloudrun,production' - '--update-secrets=DATABASE_URL=cloudrun-db-secret:latest' - '--update-secrets=DATABASE_USERNAME=cloudrun-db-secret:latest' - '--update-secrets=DATABASE_PASSWORD=cloudrun-db-secret:latest' # Secrets configuration availableSecrets: secretManager: - versionName: projects/$PROJECT_ID/secrets/cloudbuild-sa-key/versions/latest env: 'GOOGLE_APPLICATION_CREDENTIALS' # Images to store in Container Registry images: - 'gcr.io/$PROJECT_ID/cloudrun-demo:latest' # Timeout and options timeout: 1800s options: logging: CLOUD_LOGGING_ONLY

Testing

1. Unit Tests
package com.example.cloudrun.service; import com.example.cloudrun.model.User; import com.example.cloudrun.model.UserStatus; import com.example.cloudrun.repository.UserRepository; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.util.Optional; import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository; @Mock private CloudRunMetrics metrics; @InjectMocks private UserService userService; @Test void testCreateUser() { // Given User user = new User("John", "Doe", "[email protected]"); User savedUser = new User("John", "Doe", "[email protected]"); savedUser.setId(UUID.randomUUID()); when(userRepository.existsByEmail("[email protected]")).thenReturn(false); when(userRepository.save(any(User.class))).thenReturn(savedUser); // When User result = userService.createUser(user); // Then assertNotNull(result); assertNotNull(result.getId()); assertEquals("[email protected]", result.getEmail()); verify(userRepository).existsByEmail("[email protected]"); verify(userRepository).save(user); verify(metrics).recordUserCreated(); } @Test void testGetUserById() { // Given UUID userId = UUID.randomUUID(); User user = new User("John", "Doe", "[email protected]"); user.setId(userId); when(userRepository.findById(userId)).thenReturn(Optional.of(user)); // When Optional<User> result = userService.getUserById(userId); // Then assertTrue(result.isPresent()); assertEquals(userId, result.get().getId()); verify(userRepository).findById(userId); verify(metrics).recordUserQuery(); } }
2. Integration Tests
package com.example.cloudrun.controller; import com.example.cloudrun.model.CreateUserRequest; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; import org.testcontainers.shaded.com.fasterxml.jackson.databind.ObjectMapper; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc @ActiveProfiles("test") class UserControllerIntegrationTest { @Autowired private MockMvc mockMvc; private final ObjectMapper objectMapper = new ObjectMapper(); @Test void testCreateUser() throws Exception { CreateUserRequest request = new CreateUserRequest(); request.setFirstName("John"); request.setLastName("Doe"); request.setEmail("[email protected]"); mockMvc.perform(post("/api/v1/users") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))) .andExpect(status().isCreated()) .andExpect(jsonPath("$.firstName").value("John")) .andExpect(jsonPath("$.lastName").value("Doe")) .andExpect(jsonPath("$.email").value("[email protected]")); } @Test void testHealthCheck() throws Exception { mockMvc.perform(get("/actuator/health")) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value("UP")); } }
3. Test Configuration
# application-test.yml spring: datasource: url: jdbc:tc:postgresql:15:///testdb username: test password: test jpa: hibernate: ddl-auto: create-drop show-sql: true redis: host: localhost port: 6379 cloud: gcp: enabled: false logging: level: com.example.cloudrun: DEBUG

Performance Optimization

1. Cold Start Optimization
package com.example.cloudrun.optimization; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Profile; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import javax.sql.DataSource; import java.sql.Connection; @Component @Profile("cloudrun") public class WarmupRunner implements CommandLineRunner { private static final Logger LOG = LoggerFactory.getLogger(WarmupRunner.class); private final DataSource dataSource; private final JdbcTemplate jdbcTemplate; private final RedisTemplate<String, String> redisTemplate; public WarmupRunner(DataSource dataSource, JdbcTemplate jdbcTemplate, RedisTemplate<String, String> redisTemplate) { this.dataSource = dataSource; this.jdbcTemplate = jdbcTemplate; this.redisTemplate = redisTemplate; } @Override public void run(String... args) throws Exception { LOG.info("Starting application warmup..."); // Warm up database connections warmupDatabase(); // Warm up Redis connection warmupRedis(); // Warm up any other critical components warmupComponents(); LOG.info("Application warmup completed"); } private void warmupDatabase() { try (Connection connection = dataSource.getConnection()) { jdbcTemplate.execute("SELECT 1"); LOG.info("Database connection warmed up successfully"); } catch (Exception e) { LOG.warn("Database warmup failed: {}", e.getMessage()); } } private void warmupRedis() { try { redisTemplate.opsForValue().get("warmup"); LOG.info("Redis connection warmed up successfully"); } catch (Exception e) { LOG.warn("Redis warmup failed: {}", e.getMessage()); } } private void warmupComponents() { // Warm up any other components that benefit from initialization // This could include HTTP clients, thread pools, etc. LOG.info("Component warmup completed"); } }
2. Memory Optimization Configuration
package com.example.cloudrun.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; @Configuration @EnableAsync public class AsyncConfiguration { @Bean(name = "taskExecutor") @ConditionalOnProperty(name = "spring.threads.virtual.enabled", havingValue = "false") public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // Conservative settings for Cloud Run executor.setCorePoolSize(2); executor.setMaxPoolSize(10); executor.setQueueCapacity(50); executor.setThreadNamePrefix("CloudRunAsync-"); executor.setWaitForTasksToCompleteOnShutdown(true); executor.setAwaitTerminationSeconds(30); executor.initialize(); return executor; } }

Monitoring and Logging

1. Structured Logging
package com.example.cloudrun.logging; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.UUID; @Component public class RequestLoggingFilter extends OncePerRequestFilter { private static final Logger LOG = LoggerFactory.getLogger(RequestLoggingFilter.class); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String requestId = UUID.randomUUID().toString(); MDC.put("requestId", requestId); MDC.put("service", "cloudrun-demo"); long startTime = System.currentTimeMillis(); try { LOG.info("Request started: {} {} from {}", request.getMethod(), request.getRequestURI(), request.getRemoteAddr()); filterChain.doFilter(request, response); } finally { long duration = System.currentTimeMillis() - startTime; LOG.info("Request completed: {} {} - Status: {} - Duration: {}ms", request.getMethod(), request.getRequestURI(), response.getStatus(), duration); MDC.clear(); } } }
2. Custom Metrics
package com.example.cloudrun.metrics; import io.micrometer.core.instrument.Gauge; import io.micrometer.core.instrument.MeterRegistry; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.util.concurrent.atomic.AtomicInteger; @Component public class RuntimeMetrics { private final AtomicInteger activeRequests = new AtomicInteger(0); private final MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); public RuntimeMetrics(MeterRegistry meterRegistry) { // Active requests gauge Gauge.builder("cloudrun.runtime.active.requests") .description("Number of active HTTP requests") .register(meterRegistry, activeRequests, AtomicInteger::get); // Memory usage gauge Gauge.builder("cloudrun.runtime.memory.used") .description("Used memory in bytes") .register(meterRegistry, this, RuntimeMetrics::getUsedMemory); // Available memory gauge Gauge.builder("cloudrun.runtime.memory.max") .description("Max memory in bytes") .register(meterRegistry, this, RuntimeMetrics::getMaxMemory); } public void incrementActiveRequests() { activeRequests.incrementAndGet(); } public void decrementActiveRequests() { activeRequests.decrementAndGet(); } private long getUsedMemory() { return memoryMXBean.getHeapMemoryUsage().getUsed() + memoryMXBean.getNonHeapMemoryUsage().getUsed(); } private long getMaxMemory() { return memoryMXBean.getHeapMemoryUsage().getMax() + memoryMXBean.getNonHeapMemoryUsage().getMax(); } @Scheduled(fixedRate = 30000) // Every 30 seconds public void logRuntimeStats() { Runtime runtime = Runtime.getRuntime(); long usedMemory = runtime.totalMemory() - runtime.freeMemory(); long maxMemory = runtime.maxMemory(); LOG.info("Runtime stats - Memory: {}/{} MB, Active requests: {}", usedMemory / 1024 / 1024, maxMemory / 1024 / 1024, activeRequests.get()); } }

Best Practices

1. Environment-specific Configuration
# application-production.yml spring: datasource: hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000 max-lifetime: 1800000 idle-timeout: 600000 redis: lettuce: pool: max-active: 16 max-idle: 8 min-idle: 2 jpa: properties: hibernate: connection: provider_disables_autocommit: true management: endpoints: web: exposure: include: health,info,metrics endpoint: health: show-details: never metrics: enabled: true logging: level: com.example.cloudrun: INFO org.hibernate: WARN org.springframework: INFO # Cloud Run specific optimizations spring: lifecycle: timeout-per-shutdown-phase: 30s threads: virtual: enabled: true
2. Security Configuration
package com.example.cloudrun.security; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http // Cloud Run handles SSL termination .requiresChannel(channel -> channel.anyRequest().requiresSecure()) // CORS configuration .cors(cors -> cors.configure(http)) // CSRF configuration (stateless API) .csrf(csrf -> csrf.disable()) // Authorization rules .authorizeHttpRequests(authz -> authz .requestMatchers("/actuator/health", "/actuator/info").permitAll() .requestMatchers("/api/v1/**").authenticated() .anyRequest().denyAll() ); return http.build(); } }

Conclusion

Building Java applications for Google Cloud Run provides:

  • Serverless operation with automatic scaling
  • Cost efficiency with pay-per-use pricing
  • Zero infrastructure management
  • Fast deployment and iteration cycles
  • Integration with Google Cloud services

Key optimization strategies covered:

  1. Container optimization with Jib and efficient base images
  2. Cold start reduction through warmup and JVM tuning
  3. Resource management with appropriate memory and CPU limits
  4. Health checks for reliable deployment and scaling
  5. Monitoring and logging with Cloud Operations
  6. Database and cache integration for performance
  7. Security best practices for production deployment

This architecture enables building highly scalable, cost-effective Java applications that automatically scale based on demand while maintaining excellent performance and reliability.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper