Skip to content

Separate dev, test, and prod Spring Boot profiles for O365 integration #22

@axymthr

Description

@axymthr

This project should use MS Graph APIs to:

  • Send emails via O365 Outlook
  • Sync list of employees from O365 to local cache

In dev and test profile we don't want to use the actual O365 endpoints or use account credentials.
Instead we want to use O365 auth credentials only in prod profile.
In dev and test profile we should use Wiremock or setup a MockEmailService for local development and testing.

See #20 and #21 for the starting code changes.

1. Project Structure:

Here's a typical Mavenproject structure:

my-spring-app/
├── pom.xml                   
├── compose.yml        # For local Postgres dev using Docker
├── .gitignore                # To exclude target/, secrets, etc.
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/myapp/
│   │   │       └── MySpringBootApplication.java # Main application class
│   │   │       └── config/
│   │   ├── resources/
│   │   │   ├── application.properties         # Default profile (implicitly 'dev')
│   │   │   ├── application-prod.properties    # Production profile specifics
│   └── test/
│       ├── java/
│       │   └── com/example/myapp/
│       │       └── MyServiceIntegrationTest.java # Integration test using Testcontainers
│       └── resources/
│           └── application-test.properties        # Optional: Test-specific properties
│                                             # (often empty if main props are suitable)
└── external_config/             # <<-- IMPORTANT: This directory lives OUTSIDE your Git repo on the VM
    └── application-prod.properties # External prod properties file with secrets
    └── certs/
        └── keystore.p12           # External PKCS12 keystore file

2. Dependencies (pom.xml ):

Ensure you have these key dependencies:

  • Spring Boot Starter Web (spring-boot-starter-web)
  • Spring Boot Starter Data JPA (spring-boot-starter-data-jpa)
  • Spring Boot Starter Actuator (spring-boot-starter-actuator) (Recommended for health checks etc.)
  • PostgreSQL Driver (org.postgresql:postgresql)
  • Testcontainers (org.testcontainers:postgresql, org.testcontainers:junit-jupiter - for tests)

3. Configuration Files:

a) src/main/resources/application.properties (Default/Dev Profile)

Properties

# --- Default Application Settings (implicitly 'dev' profile) ---

# Server port (optional, default is 8080)
server.port=8080

# --- Database Configuration (Defaults for Docker Compose) ---
# These values should match the environment variables set in compose.yml
# Spring Boot automatically picks up matching ENV VARS (e.g., SPRING_DATASOURCE_URL)
# Providing defaults here can be helpful for IDEs but isn't strictly necessary if docker-compose provides them.

# JPA/Hibernate Settings (Example)
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true


# --- Application Specific Properties (Example) ---
myapp.feature.x.enabled=true
myapp.some.api.url=http://localhost:9090/api

b) src/main/resources/application-prod.properties (Production Profile Placeholders)

Properties

# --- Production Overrides & Additions ---

# Define placeholders for sensitive/environment-specific values.
# These MUST be provided by an external configuration source when 'prod' profile is active.
# Startup will fail if Spring cannot resolve these placeholders.

# --- Production Database Configuration ---
spring.datasource.url=${MYAPP_PROD_DB_URL}
spring.datasource.username=${MYAPP_PROD_DB_USER}
spring.datasource.password=${MYAPP_PROD_DB_PASSWORD}

# Optional: Tune production connection pool (e.g., HikariCP)
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=300000 # 5 minutes

# JPA/Hibernate Production Settings
spring.jpa.show-sql=false # Usually disabled in prod
spring.jpa.properties.hibernate.format_sql=false


# --- TLS/SSL Configuration ---
server.ssl.enabled=true
server.ssl.key-store-type=PKCS12
# Reference the keystore location via a placeholder - provided externally
server.ssl.key-store=${MYAPP_KEYSTORE_PATH}
# Reference the keystore password via a placeholder - provided externally
server.ssl.key-store-password=${MYAPP_KEYSTORE_PASSWORD}
# Specify the alias of your key within the keystore
server.ssl.key-alias=${MYAPP_KEYSTORE_ALIAS:myalias} # Provide default if needed, but better to require external prop
# Optional: Specify supported protocols and cipher suites for better security
# server.ssl.protocol=TLSv1.3
# server.ssl.enabled-protocols=TLSv1.3,TLSv1.2
# server.ssl.ciphers=...

# --- Application Specific Properties (Prod Overrides) ---
myapp.feature.x.enabled=false
myapp.some.api.url=${MYAPP_PROD_API_URL}

# --- Actuator Security (Example - secure actuator endpoints in prod) ---
# management.endpoints.web.exposure.include=health,info # Expose only specific endpoints
# management.endpoint.health.show-details=when-authorized # Or 'never'
# Consider Spring Security for proper protection

# --- Ensuring Required Properties Exist ---
# By using placeholders like ${MYAPP_PROD_DB_URL} without defaults (or ':'),
# Spring Boot will fail on startup if the property isn't found in any source
# (like environment variables or the external application-prod.properties).
# This inherently satisfies the requirement to fail if prod config is missing.

# Optional: You can add explicit checks using @ConfigurationProperties validation or Initializers if needed
# for more complex validation logic beyond simple existence.

c) external_config/application-prod.properties (External File on VM - NOT IN GIT)

This file contains the actual secrets and production values. Place it in a known location on your deployment VM (e.g., /etc/myapp/config/ or alongside the JAR).

Properties

# --- Actual Production Values ---
# Stored securely on the VM, outside the application JAR/WAR

MYAPP_PROD_DB_URL=jdbc:postgresql://your-prod-db-hostname:5432/prod_database
MYAPP_PROD_DB_USER=prod_db_user
MYAPP_PROD_DB_PASSWORD=p@$$wOrd_f0r_pr0d_Db!

MYAPP_KEYSTORE_PATH=file:/etc/myapp/certs/keystore.p12 # Absolute path on the VM
MYAPP_KEYSTORE_PASSWORD=k3ySt0r3_S3cr3t!
MYAPP_KEYSTORE_ALIAS=your_prod_key_alias # The alias used when creating the keystore

MYAPP_PROD_API_URL=https://prod.otherservice.com/api

# Any other properties needing externalization

5. Integration Test (MyServiceIntegrationTest.java)

Java

package com.example.myapp;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;

import javax.sql.DataSource;
import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) // Adjust if you need a web server for tests
@Testcontainers // Enable Testcontainers support for JUnit 5
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // Disable default H2 replacement
// @ActiveProfiles("test") // Optional: activate a specific test profile if needed
public class MyServiceIntegrationTest {

    // Define the PostgreSQL container
    @Container
    private static final PostgreSQLContainer<?> postgresContainer =
            new PostgreSQLContainer<>(DockerImageName.parse("postgres:15"))
                    .withDatabaseName("testdb")
                    .withUsername("testuser")
                    .withPassword("testsecret");

    // Dynamically set Spring properties based on the running container
    @DynamicPropertySource
    static void postgresqlProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgresContainer::getJdbcUrl);
        registry.add("spring.datasource.username", postgresContainer::getUsername);
        registry.add("spring.datasource.password", postgresContainer::getPassword);
    }

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired
    private DataSource dataSource; // Inject DataSource to verify connection

    // Example Test
    @Test
    void contextLoads() {
        assertThat(applicationContext).isNotNull();
        System.out.println("Test Datasource URL: " + dataSource.toString()); // Check it's the Testcontainers URL
    }

    @Test
    void databaseIsRunningAndAccessible() throws Exception {
         assertThat(postgresContainer.isRunning()).isTrue();
         // You can perform DB operations here using JPA repositories or JdbcTemplate
    }

    // Add your actual integration tests here...
}

7. Running Production:

  1. Build: mvn clean package or gradle clean build (ensure the build skips tests if they need a running docker daemon).

  2. Deploy: Copy the generated JAR (my-spring-app-0.0.1-SNAPSHOT.jar) to your VM.

  3. Place External Config: Ensure external_config/application-prod.properties and external_config/certs/keystore.p12 are placed in the configured locations on the VM (e.g., /etc/myapp/config/ and /etc/myapp/certs/). Secure their file permissions appropriately.

  4. Run:

    Bash

    java -jar my-spring-app-0.0.1-SNAPSHOT.jar \
         --spring.profiles.active=prod \
         --spring.config.import=optional:file:/etc/myapp/config/application-prod.properties
         # Add any other required JVM args (-Xmx etc)
    
    • --spring.profiles.active=prod: Activates the prod profile, loading application-prod.properties from the classpath and looking for external sources.
    • --spring.config.import=optional:file:/etc/myapp/config/application-prod.properties: Tells Spring Boot to explicitly load properties from this external file. Using optional: prevents failure if the file doesn't exist, but relying on placeholder resolution failure is the primary mechanism here. Alternatively, Spring Boot automatically searches locations like ./config/, ./ relative to the JAR. Placing the file next to the JAR named application-prod.properties often works without the explicit import statement. Check Spring Boot's externalized configuration documentation for exact loading order.
    • The application will now load application.properties, then the packaged application-prod.properties, and finally the external /etc/myapp/config/application-prod.properties. Properties from the external file will override those in the packaged files.
    • If the external file is missing or doesn't define MYAPP_PROD_DB_URL, MYAPP_PROD_DB_USER, MYAPP_PROD_DB_PASSWORD, MYAPP_KEYSTORE_PATH, MYAPP_KEYSTORE_PASSWORD etc., Spring Boot will fail during startup because it cannot resolve the ${...} placeholders in the packaged application-prod.properties.

8. Keystore Safety:

  • Is it safe to commit the PKCS12 keystore to Github? NO. Absolutely not. Keystores, especially PKCS12, contain your private key, which is highly sensitive. Committing it to source control is a major security risk.
  • Passing it from the VM disk: This is the correct approach. As shown in the application-prod.properties and the external file example, you configure server.ssl.key-store to point to the file path on the VM (e.g., file:/etc/myapp/certs/keystore.p12) and provide the password externally as well (MYAPP_KEYSTORE_PASSWORD). Ensure the file permissions on the keystore file on the VM are restrictive (only readable by the user running the Spring Boot application).

This setup provides a clear separation between development and production, leverages standard Spring Boot features for configuration management and profiles, ensures database schemas are managed consistently with Flyway, and handles sensitive production configuration and secrets securely.

Sub-issues

Metadata

Metadata

Labels

enhancementNew feature or request

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions