-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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:
-
Build:
mvn clean packageorgradle clean build(ensure the build skips tests if they need a running docker daemon). -
Deploy: Copy the generated JAR (
my-spring-app-0.0.1-SNAPSHOT.jar) to your VM. -
Place External Config: Ensure
external_config/application-prod.propertiesandexternal_config/certs/keystore.p12are placed in the configured locations on the VM (e.g.,/etc/myapp/config/and/etc/myapp/certs/). Secure their file permissions appropriately. -
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 theprodprofile, loadingapplication-prod.propertiesfrom 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. Usingoptional: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 namedapplication-prod.propertiesoften 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 packagedapplication-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_PASSWORDetc., Spring Boot will fail during startup because it cannot resolve the${...}placeholders in the packagedapplication-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.propertiesand the external file example, you configureserver.ssl.key-storeto 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.