From 673e11a1111b9d3b7a15ca722540edecf292e1c5 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 31 Dec 2025 15:57:09 +1100 Subject: [PATCH 1/3] Refacotr the configs --- .env.example | 102 +++++++++++++-- auto/docker_start | 3 +- auto/docker_stop | 3 +- docker-compose-all.yml | 31 ++--- src/main/resources/application-dev.yml | 54 +++----- src/main/resources/application-local.yml | 33 ++--- src/main/resources/application-prod.yml | 53 +++----- src/main/resources/application.yml | 152 ++++++++++++++--------- src/test/resources/application-test.yml | 28 ++--- 9 files changed, 264 insertions(+), 195 deletions(-) diff --git a/.env.example b/.env.example index 47ce93c..6de7982 100644 --- a/.env.example +++ b/.env.example @@ -1,25 +1,105 @@ # ============================================================================= -# Environment Variables for docker-compose-all.yml +# Environment Variables Reference # ============================================================================= -# Copy this file to .env and customize values for your environment. -# -# Usage: -# cp .env.example .env -# # Edit .env with your values -# docker compose -f docker-compose-all.yml up -d +# Copy to .env and customize: cp .env.example .env # # SECURITY WARNING: # - Never commit .env to version control # - Use secrets management (Vault, AWS Secrets Manager) in production +# +# Profiles: +# - local: Docker Compose auto-configures DB/Kafka (minimal .env needed) +# - dev: Requires DB_*, JWT_*, KAFKA_* variables +# - prod: Requires all security-sensitive variables # ============================================================================= -# Database password (used by both app and postgres containers) +# ----------------------------------------------------------------------------- +# Profile Selection +# ----------------------------------------------------------------------------- +SPRING_PROFILES_ACTIVE=local + +# ----------------------------------------------------------------------------- +# Database Configuration +# ----------------------------------------------------------------------------- +# Required for: dev, prod profiles +# Local profile uses Docker Compose auto-configuration +DB_URL=jdbc:postgresql://localhost:5432/users +DB_USERNAME=app_user DB_PASSWORD=your_secure_password_here -# JWT secrets - MUST be at least 64 bytes for HS512 -# Generate with: openssl rand -base64 64 +# Connection Pool (optional, has defaults) +# DB_POOL_MAX=10 # Default: 10 (local/dev), 20 (prod) +# DB_POOL_MIN=2 # Default: 2 (local/dev), 5 (prod) + +# ----------------------------------------------------------------------------- +# Kafka Configuration +# ----------------------------------------------------------------------------- +# Required for: dev, prod profiles +# Local profile defaults to localhost:29092 (Docker Compose) +KAFKA_BOOTSTRAP_SERVERS=localhost:29092 + +# ----------------------------------------------------------------------------- +# JWT Configuration +# ----------------------------------------------------------------------------- +# SECURITY: Generate secrets with: openssl rand -base64 64 +# Required for: dev, prod profiles +# Local profile uses insecure defaults (DO NOT use in production) JWT_ACCESS_SECRET=your-64-byte-access-secret-generated-with-openssl-rand-base64-64-command JWT_REFRESH_SECRET=your-64-byte-refresh-secret-generated-with-openssl-rand-base64-64-command -# CORS allowed origins (comma-separated for multiple) +# Token expiration (optional, has defaults) +# JWT_ACCESS_EXPIRES_IN=15m +# JWT_REFRESH_EXPIRES_IN=7d +# JWT_ISSUER=user-service + +# ----------------------------------------------------------------------------- +# CORS Configuration +# ----------------------------------------------------------------------------- +# Required for: prod profile +# Dev/local default to http://localhost:3000 CORS_ALLOWED_ORIGINS=http://localhost:3000 + +# ----------------------------------------------------------------------------- +# Mail Configuration (Optional) +# ----------------------------------------------------------------------------- +# Required only if email functionality is needed +MAIL_HOST=smtp.gmail.com +MAIL_PORT=587 +MAIL_USERNAME= +MAIL_PASSWORD= +APP_MAIL_FROM=noreply@example.com +APP_MAIL_FROM_NAME=User Service + +# ----------------------------------------------------------------------------- +# OAuth2 Configuration (Optional) +# ----------------------------------------------------------------------------- +# Required only if social login is enabled + +# Google OAuth2 +OAUTH2_GOOGLE_CLIENT_ID= +OAUTH2_GOOGLE_CLIENT_SECRET= + +# GitHub OAuth2 +OAUTH2_GITHUB_CLIENT_ID= +OAUTH2_GITHUB_CLIENT_SECRET= + +# OAuth2 Redirect URLs (optional, defaults to static pages) +# APP_OAUTH2_SUCCESS_URL=http://localhost:3000/auth/callback +# APP_OAUTH2_FAILURE_URL=http://localhost:3000/auth/error + +# ----------------------------------------------------------------------------- +# Server Configuration (Optional) +# ----------------------------------------------------------------------------- +# SERVER_PORT=8080 +# GRPC_PORT=9090 +# GRPC_REFLECTION_ENABLED=true # Set to false in prod + +# ----------------------------------------------------------------------------- +# Logging (Optional) +# ----------------------------------------------------------------------------- +# LOG_LEVEL_APP=INFO # DEBUG for dev, INFO for prod + +# ----------------------------------------------------------------------------- +# Swagger (Optional) +# ----------------------------------------------------------------------------- +# SWAGGER_ENABLED=false # Overridden by profile configs diff --git a/auto/docker_start b/auto/docker_start index e52a462..8718993 100755 --- a/auto/docker_start +++ b/auto/docker_start @@ -1,3 +1,4 @@ #!/usr/bin/env sh -docker compose -f docker-compose-all.yml up -d --build \ No newline at end of file +docker compose -f docker-compose-all.yml up -d --build + diff --git a/auto/docker_stop b/auto/docker_stop index 7cb9395..922d9ac 100755 --- a/auto/docker_stop +++ b/auto/docker_stop @@ -1,3 +1,4 @@ #!/usr/bin/env sh -docker compose -f docker-compose-all.yml down \ No newline at end of file +docker compose -f docker-compose-all.yml down -v + diff --git a/docker-compose-all.yml b/docker-compose-all.yml index d4769c2..6e512ea 100644 --- a/docker-compose-all.yml +++ b/docker-compose-all.yml @@ -33,15 +33,15 @@ services: condition: service_healthy environment: # Profile: use 'dev' for development simulation, 'prod' for production - - SPRING_PROFILES_ACTIVE=dev + - SPRING_PROFILES_ACTIVE=local - # Database connection (matches dev/prod profile expectations) - - DATABASE_URL=jdbc:postgresql://postgres:5432/users - - DATABASE_USERNAME=app_user - - DATABASE_PASSWORD=${DB_PASSWORD:-changeme_in_production} + # Database connection + - DB_URL=jdbc:postgresql://postgres:5432/users + - DB_USERNAME=app_user + - DB_PASSWORD=${DB_PASSWORD:-changeme_in_production} # Kafka connection - - SPRING_KAFKA_BOOTSTRAP_SERVERS=kafka:9092 + - KAFKA_BOOTSTRAP_SERVERS=kafka:9092 # JWT secrets - MUST be overridden in production! # Generate with: openssl rand -base64 64 @@ -54,15 +54,18 @@ services: # JVM options for container environment - JAVA_OPTS=-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 - # Mail - - MAIL_USERNAME=${MAIL_USERNAME} - - MAIL_PASSWORD=${MAIL_PASSWORD} + # Mail (optional) + - MAIL_HOST=${MAIL_HOST:-smtp.gmail.com} + - MAIL_PORT=${MAIL_PORT:-587} + - MAIL_USERNAME=${MAIL_USERNAME:-} + - MAIL_PASSWORD=${MAIL_PASSWORD:-} + - APP_MAIL_FROM=${APP_MAIL_FROM:-noreply@example.com} - # OAuth 2 - - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} - - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} - - GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID} - - GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET} + # OAuth2 (optional) + - OAUTH2_GOOGLE_CLIENT_ID=${OAUTH2_GOOGLE_CLIENT_ID:-} + - OAUTH2_GOOGLE_CLIENT_SECRET=${OAUTH2_GOOGLE_CLIENT_SECRET:-} + - OAUTH2_GITHUB_CLIENT_ID=${OAUTH2_GITHUB_CLIENT_ID:-} + - OAUTH2_GITHUB_CLIENT_SECRET=${OAUTH2_GITHUB_CLIENT_SECRET:-} healthcheck: test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/actuator/health" ] interval: 30s diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index bc91662..e946fe4 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -1,60 +1,40 @@ # ============================================================================= # Development/Staging Profile # ============================================================================= -# Usage: Set SPRING_PROFILES_ACTIVE=dev +# Usage: SPRING_PROFILES_ACTIVE=dev ./gradlew bootRun # -# Features: -# - Connects to external PostgreSQL (via environment variables) -# - Swagger UI enabled for API testing -# - Info-level logging -# - JWT secrets from environment variables (required) +# Behavior: +# - Connects to external services (set via environment variables) +# - Enables Swagger UI for API testing +# - Enables gRPC reflection for debugging +# - Debug logging for app and SQL # # Required Environment Variables: -# - DATABASE_URL (e.g., jdbc:postgresql://dev-db.example.com:5432/users) -# - DATABASE_USERNAME -# - DATABASE_PASSWORD -# - JWT_ACCESS_SECRET (min 64 bytes for HS512) -# - JWT_REFRESH_SECRET (min 64 bytes for HS512) +# DB_URL - jdbc:postgresql://host:5432/database +# DB_USERNAME - Database username +# DB_PASSWORD - Database password +# JWT_ACCESS_SECRET - Min 64 bytes for HS512 +# JWT_REFRESH_SECRET - Min 64 bytes for HS512 +# KAFKA_BOOTSTRAP_SERVERS - Kafka broker address +# +# Optional: +# MAIL_HOST, MAIL_USERNAME, MAIL_PASSWORD - For email functionality +# OAUTH2_GOOGLE_CLIENT_ID, OAUTH2_GOOGLE_CLIENT_SECRET - Google OAuth +# OAUTH2_GITHUB_CLIENT_ID, OAUTH2_GITHUB_CLIENT_SECRET - GitHub OAuth # ============================================================================= spring: - # External database connection - datasource: - url: ${DATABASE_URL} - username: ${DATABASE_USERNAME} - password: ${DATABASE_PASSWORD} - driver-class-name: org.postgresql.Driver - hikari: - maximum-pool-size: 5 - minimum-idle: 2 - idle-timeout: 300000 - max-lifetime: 1800000 - connection-timeout: 30000 - - mail: - host: email-smtp.ap-southeast-2.amazonaws.com - port: 587 - username: ${MAIL_USERNAME} - password: ${MAIL_PASSWORD} - - # gRPC reflection enabled for dev debugging grpc: server: reflection: enabled: true -app: - mail: - from: noreply@daniel-guo.com - -# Enable Swagger UI for dev/staging springdoc: api-docs: enabled: true swagger-ui: enabled: true -# Development logging (more verbose than prod) logging: level: org.nkcoder: DEBUG diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index a3c964c..9092a9e 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -2,19 +2,18 @@ # Local Development Profile # ============================================================================= # Usage: ./gradlew bootRun --args='--spring.profiles.active=local' -# or set SPRING_PROFILES_ACTIVE=local # -# Features: -# - Docker Compose auto-starts PostgreSQL container -# - Swagger UI enabled for API exploration -# - Debug logging enabled -# - Default JWT secrets (DO NOT use in production) +# Behavior: +# - Auto-starts PostgreSQL & Kafka via Docker Compose +# - Enables Swagger UI for API exploration +# - Enables gRPC reflection for debugging tools (grpcurl, etc.) +# - Debug logging enabled +# +# Database & Kafka are auto-configured by Docker Compose. +# No .env file needed for basic local development. # ============================================================================= spring: - kafka: - bootstrap-servers: localhost:29092 - # Auto-start PostgreSQL via Docker Compose docker: compose: enabled: true @@ -22,34 +21,20 @@ spring: skip: in-tests: true - # gRPC reflection enabled for local debugging (grpcurl, etc.) grpc: server: reflection: enabled: true - mail: - host: email-smtp.ap-southeast-2.amazonaws.com - port: 587 - username: ${MAIL_USERNAME} - password: ${MAIL_PASSWORD} - -app: - mail: - from: noreply@daniel-guo.com - -# Enable Swagger UI for local development springdoc: api-docs: enabled: true swagger-ui: enabled: true -# Debug logging for local development logging: level: org.nkcoder: DEBUG org.springframework.security: DEBUG org.springframework.web: INFO - org.hibernate.SQL: INFO - org.hibernate.orm.jdbc.bind: INFO + org.hibernate.SQL: DEBUG diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index d073c9a..167c6be 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -1,61 +1,48 @@ # ============================================================================= # Production Profile # ============================================================================= -# Usage: Set SPRING_PROFILES_ACTIVE=prod +# Usage: SPRING_PROFILES_ACTIVE=prod # -# Features: -# - Connects to external PostgreSQL (via environment variables) -# - Swagger UI DISABLED for security -# - Minimal logging (WARN level) -# - Optimized connection pool settings -# - All secrets from environment variables (required) +# Behavior: +# - All config from environment variables (no defaults for secrets) +# - Swagger UI DISABLED for security +# - gRPC reflection DISABLED for security +# - Minimal logging (INFO for app, WARN for frameworks) +# - Optimized connection pool settings # # Required Environment Variables: -# - DATABASE_URL (e.g., jdbc:postgresql://prod-db.example.com:5432/users) -# - DATABASE_USERNAME -# - DATABASE_PASSWORD -# - JWT_ACCESS_SECRET (min 64 bytes for HS512) -# - JWT_REFRESH_SECRET (min 64 bytes for HS512) -# - CORS_ALLOWED_ORIGINS (e.g., https://myapp.com) +# DB_URL - jdbc:postgresql://host:5432/database +# DB_USERNAME - Database username +# DB_PASSWORD - Database password +# JWT_ACCESS_SECRET - Min 64 bytes for HS512 +# JWT_REFRESH_SECRET - Min 64 bytes for HS512 +# KAFKA_BOOTSTRAP_SERVERS - Kafka broker address +# CORS_ALLOWED_ORIGINS - e.g., https://myapp.com # -# Optional Environment Variables: -# - JWT_ACCESS_EXPIRES_IN (default: 15m) -# - JWT_REFRESH_EXPIRES_IN (default: 7d) -# - JWT_ISSUER (default: user-service) +# Recommended: +# DB_POOL_MAX=20 - Higher connection pool for production load +# DB_POOL_MIN=5 - Maintain minimum connections # ============================================================================= spring: - # External database connection with production-optimized pool datasource: - url: ${DATABASE_URL} - username: ${DATABASE_USERNAME} - password: ${DATABASE_PASSWORD} - driver-class-name: org.postgresql.Driver hikari: - maximum-pool-size: 20 - minimum-idle: 5 - idle-timeout: 300000 - max-lifetime: 1800000 - connection-timeout: 30000 - pool-name: UserServiceHikariPool - # Production optimizations + maximum-pool-size: ${DB_POOL_MAX:20} + minimum-idle: ${DB_POOL_MIN:5} leak-detection-threshold: 60000 validation-timeout: 5000 - # Disable gRPC reflection in production for security grpc: server: reflection: enabled: false -# Swagger UI DISABLED in production springdoc: api-docs: enabled: false swagger-ui: enabled: false -# Production logging - minimal, structured logging: level: root: WARN @@ -64,6 +51,6 @@ logging: org.springframework.web: WARN org.hibernate.SQL: WARN com.zaxxer.hikari: WARN - # Consider using JSON format for log aggregation in production + # JSON logging for production log aggregation (uncomment if needed) # pattern: # console: '{"timestamp":"%d{ISO8601}","level":"%level","logger":"%logger","message":"%msg"}%n' diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 32bf4a0..2da3578 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,12 +1,19 @@ # ============================================================================= -# Base Configuration - Shared across all profiles +# Base Configuration - Single Source of Truth # ============================================================================= -# Profile-specific configs override these values. -# Activate profiles via: --spring.profiles.active=local|dev|prod +# All settings use environment variables with sensible defaults. +# Profile configs (local, dev, prod) only override BEHAVIOR, not values. +# +# Activation: --spring.profiles.active=local|dev|prod +# +# Environment Variable Naming Convention: +# - Standard Spring: SPRING_*, SERVER_*, MANAGEMENT_* +# - Database: DB_* +# - Custom App: APP_* # ============================================================================= server: - port: 8080 + port: ${SERVER_PORT:8080} servlet: context-path: / shutdown: graceful @@ -15,12 +22,7 @@ spring: application: name: user-service - # Default: Docker Compose disabled (only enabled in 'local' profile) - docker: - compose: - enabled: false - - # Virtual threads (Project Loom) - enabled for all environments + # Virtual threads (Project Loom) threads: virtual: enabled: true @@ -31,8 +33,29 @@ spring: externalization: enabled: true + # ----------------------------------------------------------------------------- + # Database Configuration + # ----------------------------------------------------------------------------- + # Local: auto-configured by Docker Compose + # Dev/Prod: set via environment variables + datasource: + url: ${DB_URL:jdbc:postgresql://localhost:5432/users} + username: ${DB_USERNAME:postgres} + password: ${DB_PASSWORD:postgres} + driver-class-name: org.postgresql.Driver + hikari: + maximum-pool-size: ${DB_POOL_MAX:10} + minimum-idle: ${DB_POOL_MIN:2} + idle-timeout: 300000 + max-lifetime: 1800000 + connection-timeout: 30000 + pool-name: UserServiceHikariPool + + # ----------------------------------------------------------------------------- # Kafka Configuration + # ----------------------------------------------------------------------------- kafka: + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:29092} producer: key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: org.springframework.kafka.support.serializer.JsonSerializer @@ -42,7 +65,9 @@ spring: key-deserializer: org.apache.kafka.common.serialization.StringDeserializer value-deserializer: org.apache.kafka.common.serialization.StringDeserializer - # Flyway migrations + # ----------------------------------------------------------------------------- + # Database Migration (Flyway) + # ----------------------------------------------------------------------------- flyway: enabled: true locations: classpath:db/migration @@ -50,7 +75,9 @@ spring: validate-on-migrate: true validate-migration-naming: true - # JPA/Hibernate defaults + # ----------------------------------------------------------------------------- + # JPA/Hibernate + # ----------------------------------------------------------------------------- jpa: hibernate: ddl-auto: validate @@ -66,16 +93,20 @@ spring: fetch_size: 100 generate_statistics: false - # gRPC configuration + # ----------------------------------------------------------------------------- + # gRPC Configuration + # ----------------------------------------------------------------------------- grpc: server: - port: 9090 + port: ${GRPC_PORT:9090} max-inbound-message-size: 4MB max-inbound-metadata-size: 8KB reflection: - enabled: true + enabled: ${GRPC_REFLECTION_ENABLED:true} + # ----------------------------------------------------------------------------- # Mail Configuration + # ----------------------------------------------------------------------------- mail: host: ${MAIL_HOST:smtp.gmail.com} port: ${MAIL_PORT:587} @@ -92,11 +123,37 @@ spring: timeout: 5000 writetimeout: 5000 + # ----------------------------------------------------------------------------- + # OAuth2 Configuration + # ----------------------------------------------------------------------------- + security: + oauth2: + client: + registration: + google: + client-id: ${OAUTH2_GOOGLE_CLIENT_ID:} + client-secret: ${OAUTH2_GOOGLE_CLIENT_SECRET:} + scope: openid, profile, email + redirect-uri: "{baseUrl}/api/auth/oauth2/callback/google" + github: + client-id: ${OAUTH2_GITHUB_CLIENT_ID:} + client-secret: ${OAUTH2_GITHUB_CLIENT_SECRET:} + scope: read:user, user:email + redirect-uri: "{baseUrl}/api/auth/oauth2/callback/github" + provider: + github: + user-info-uri: https://api.github.com/user + user-name-attribute: id + +# ============================================================================= +# Application Custom Configuration +# ============================================================================= + # ----------------------------------------------------------------------------- # JWT Configuration # ----------------------------------------------------------------------------- -# IMPORTANT: Override secrets via environment variables in dev/prod! -# Default secrets are for local development only. +# SECURITY: Override secrets via environment variables in dev/prod! +# Generate secrets: openssl rand -base64 64 jwt: secret: access: ${JWT_ACCESS_SECRET:default-hmac512-access-secret-key-for-local-dev-only-not-for-production-64-bytes} @@ -106,14 +163,6 @@ jwt: refresh: ${JWT_REFRESH_EXPIRES_IN:7d} issuer: ${JWT_ISSUER:user-service} -# ----------------------------------------------------------------------------- -# Application Mail Settings -# ----------------------------------------------------------------------------- -app: - mail: - from: ${MAIL_FROM:noreply@example.com} - from-name: ${MAIL_FROM_NAME:User Service} - # ----------------------------------------------------------------------------- # CORS Configuration # ----------------------------------------------------------------------------- @@ -125,37 +174,20 @@ cors: max-age: 3600 # ----------------------------------------------------------------------------- -# OAuth2 Configuration +# App Mail Settings # ----------------------------------------------------------------------------- -# Configure OAuth2 providers for social login (Google, GitHub) -# Credentials must be set via environment variables -spring.security.oauth2.client: - registration: - google: - client-id: ${GOOGLE_CLIENT_ID:} - client-secret: ${GOOGLE_CLIENT_SECRET:} - scope: openid, profile, email - redirect-uri: "{baseUrl}/api/auth/oauth2/callback/google" - github: - client-id: ${GITHUB_CLIENT_ID:} - client-secret: ${GITHUB_CLIENT_SECRET:} - scope: read:user, user:email - redirect-uri: "{baseUrl}/api/auth/oauth2/callback/github" - provider: - github: - user-info-uri: https://api.github.com/user - user-name-attribute: id - -# OAuth2 redirect URLs for frontend -# Default to static pages bundled with this application -# Override via environment variables for external frontend (e.g., http://localhost:3000/auth/callback) -app.oauth2: - success-redirect-url: ${OAUTH2_SUCCESS_REDIRECT_URL:/callback.html} - failure-redirect-url: ${OAUTH2_FAILURE_REDIRECT_URL:/error.html} +app: + mail: + from: ${APP_MAIL_FROM:noreply@example.com} + from-name: ${APP_MAIL_FROM_NAME:User Service} + oauth2: + success-redirect-url: ${APP_OAUTH2_SUCCESS_URL:/callback.html} + failure-redirect-url: ${APP_OAUTH2_FAILURE_URL:/error.html} + +# ============================================================================= +# Observability Configuration +# ============================================================================= -# ----------------------------------------------------------------------------- -# Actuator Configuration -# ----------------------------------------------------------------------------- management: endpoints: web: @@ -176,26 +208,26 @@ management: enabled: true # ----------------------------------------------------------------------------- -# OpenAPI/Swagger Configuration (disabled by default, enabled in local/dev) +# OpenAPI/Swagger - disabled by default, enabled in local/dev profiles # ----------------------------------------------------------------------------- springdoc: api-docs: path: /api-docs - enabled: false + enabled: ${SWAGGER_ENABLED:false} swagger-ui: path: /swagger-ui.html - enabled: false + enabled: ${SWAGGER_ENABLED:false} tags-sorter: alpha operations-sorter: alpha show-actuator: false # ----------------------------------------------------------------------------- -# Logging Configuration (base level) +# Logging - defaults, overridden by profiles # ----------------------------------------------------------------------------- logging: level: root: INFO - org.nkcoder: INFO + org.nkcoder: ${LOG_LEVEL_APP:INFO} org.springframework.security: WARN org.springframework.web: WARN org.hibernate.SQL: WARN @@ -205,7 +237,7 @@ logging: console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" # ----------------------------------------------------------------------------- -# Application Information +# Application Info # ----------------------------------------------------------------------------- info: app: diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index ea64236..4f3b1bd 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -1,5 +1,10 @@ -# Test profile configuration -# This file is automatically loaded when running with @ActiveProfiles("test") or when Spring detects the test classpath +# ============================================================================= +# Test Profile Configuration +# ============================================================================= +# Auto-loaded with @ActiveProfiles("test") or @SpringBootTest +# Uses TestContainers for PostgreSQL (see TestContainersConfiguration.java) +# ============================================================================= + spring: # Disable OAuth2 autoconfiguration in tests autoconfigure: @@ -7,7 +12,7 @@ spring: - org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration - org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration - # Provide placeholder OAuth2 values to avoid validation errors + # Placeholder OAuth2 values to avoid validation errors security: oauth2: client: @@ -20,25 +25,23 @@ spring: client-secret: test-github-client-secret jpa: - # Show SQL in tests for debugging (disable in CI if too noisy) show-sql: true properties: hibernate: format_sql: true hibernate: - # Hibernate validates entity mappings against the actual schema created by Flyway, catching mapping errors early ddl-auto: validate - # faster tests open-in-view: false flyway: - # Run migrations in tests to match production schema enabled: true - # Clean database before migrating (test isolation) - clean-disabled: false + clean-disabled: false # Allow clean for test isolation + + grpc: + server: + port: 0 # Random available port # JWT configuration for tests -# Short secrets are fine for tests - not production jwt: secret: access: test-access-secret-key-minimum-64-bytes-for-hs512-algorithm-padding @@ -48,13 +51,10 @@ jwt: refresh: 7d issuer: test-issuer -# Logging - more verbose for debugging test failures +# Logging - verbose for debugging test failures logging: level: org.nkcoder: DEBUG org.springframework.security: DEBUG org.hibernate.sql: DEBUG org.hibernate.type.descriptor.sql: TRACE -grpc: - server: - port: 0 # Random available port From 026a1221068132a8331f1b0a86a20bb281e26777 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 31 Dec 2025 16:19:59 +1100 Subject: [PATCH 2/3] Use docker compose include and override --- auto/docker_logs | 2 +- auto/docker_start | 3 +- auto/docker_stop | 3 +- docker-compose-all.yml | 172 ------------------------------------ docker-compose-app.yml | 83 +++++++++++++++++ docker-compose-override.yml | 24 +++++ docker-compose.yml | 2 +- 7 files changed, 111 insertions(+), 178 deletions(-) delete mode 100644 docker-compose-all.yml create mode 100644 docker-compose-app.yml create mode 100644 docker-compose-override.yml diff --git a/auto/docker_logs b/auto/docker_logs index ca5cab2..1d6c9a9 100755 --- a/auto/docker_logs +++ b/auto/docker_logs @@ -1,3 +1,3 @@ #!/usr/bin/env sh -docker compose -f docker-compose-all.yml logs -f --tail 50 +docker compose -f docker-compose-app.yml logs -f --tail 50 diff --git a/auto/docker_start b/auto/docker_start index 8718993..49200a4 100755 --- a/auto/docker_start +++ b/auto/docker_start @@ -1,4 +1,3 @@ #!/usr/bin/env sh -docker compose -f docker-compose-all.yml up -d --build - +docker compose -f docker-compose-app.yml up -d --build diff --git a/auto/docker_stop b/auto/docker_stop index 922d9ac..92f030e 100755 --- a/auto/docker_stop +++ b/auto/docker_stop @@ -1,4 +1,3 @@ #!/usr/bin/env sh -docker compose -f docker-compose-all.yml down -v - +docker compose -f docker-compose-app.yml down -v diff --git a/docker-compose-all.yml b/docker-compose-all.yml deleted file mode 100644 index 6e512ea..0000000 --- a/docker-compose-all.yml +++ /dev/null @@ -1,172 +0,0 @@ -# ============================================================================= -# Docker Compose - Full Application Stack -# ============================================================================= -# Simulates dev/prod environment with all services running in containers. -# -# For production, use external secrets management (Vault, AWS Secrets Manager) -# instead of environment variables in this file. -# -# For container communications: App, kafka and PostgreSQL are all running inside Docker. -# Kafka has two listeners: 9092 (internal) and 29092 (external) -# Container app connects via `kafka:9092` (Docker network) -# Host debugging via `localhost:29092` -# KAFKA_LISTENERS: PLAINTEXT://:9092,EXTERNAL://:29092 -# KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,EXTERNAL://localhost:29092 -# ============================================================================= - -services: - # --------------------------------------------------------------------------- - # Application Service - # --------------------------------------------------------------------------- - user-service: - build: - context: . - dockerfile: Dockerfile - container_name: user-application - ports: - - "8080:8080" # REST API - - "9090:9090" # gRPC API - depends_on: - postgres: - condition: service_healthy - kafka: - condition: service_healthy - environment: - # Profile: use 'dev' for development simulation, 'prod' for production - - SPRING_PROFILES_ACTIVE=local - - # Database connection - - DB_URL=jdbc:postgresql://postgres:5432/users - - DB_USERNAME=app_user - - DB_PASSWORD=${DB_PASSWORD:-changeme_in_production} - - # Kafka connection - - KAFKA_BOOTSTRAP_SERVERS=kafka:9092 - - # JWT secrets - MUST be overridden in production! - # Generate with: openssl rand -base64 64 - - JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET:-dev-only-access-secret-key-must-be-at-least-64-bytes-long-for-hs512} - - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET:-dev-only-refresh-secret-key-must-be-at-least-64-bytes-long-for-hs512} - - # CORS - adjust for your frontend URL - - CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-http://localhost:3000} - - # JVM options for container environment - - JAVA_OPTS=-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 - - # Mail (optional) - - MAIL_HOST=${MAIL_HOST:-smtp.gmail.com} - - MAIL_PORT=${MAIL_PORT:-587} - - MAIL_USERNAME=${MAIL_USERNAME:-} - - MAIL_PASSWORD=${MAIL_PASSWORD:-} - - APP_MAIL_FROM=${APP_MAIL_FROM:-noreply@example.com} - - # OAuth2 (optional) - - OAUTH2_GOOGLE_CLIENT_ID=${OAUTH2_GOOGLE_CLIENT_ID:-} - - OAUTH2_GOOGLE_CLIENT_SECRET=${OAUTH2_GOOGLE_CLIENT_SECRET:-} - - OAUTH2_GITHUB_CLIENT_ID=${OAUTH2_GITHUB_CLIENT_ID:-} - - OAUTH2_GITHUB_CLIENT_SECRET=${OAUTH2_GITHUB_CLIENT_SECRET:-} - healthcheck: - test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/actuator/health" ] - interval: 30s - timeout: 10s - retries: 3 - start_period: 60s - restart: unless-stopped - deploy: - resources: - limits: - cpus: '2' - memory: 1G - reservations: - cpus: '0.5' - memory: 512M - networks: - - app-network - - # --------------------------------------------------------------------------- - # Apache Kafka - # --------------------------------------------------------------------------- - kafka: - image: apache/kafka:4.1.1 - container_name: user-application-kafka - hostname: kafka - ports: - - "29092:29092" # External port for debugging (remove in production) - environment: - KAFKA_NODE_ID: 1 - KAFKA_PROCESS_ROLES: broker,controller - KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:29092 - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,EXTERNAL://localhost:29092 - KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT - KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 - KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 - KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 - KAFKA_LOG_DIRS: /var/lib/kafka/data - volumes: - - kafka_data:/var/lib/kafka/data - healthcheck: - test: ["CMD-SHELL", "/opt/kafka/bin/kafka-broker-api-versions.sh --bootstrap-server localhost:9092 > /dev/null 2>&1"] - interval: 10s - timeout: 10s - retries: 5 - start_period: 30s - restart: unless-stopped - deploy: - resources: - limits: - cpus: '1' - memory: 1G - reservations: - cpus: '0.25' - memory: 512M - networks: - - app-network - - # --------------------------------------------------------------------------- - # PostgreSQL Database - # --------------------------------------------------------------------------- - postgres: - image: postgres:17-alpine - container_name: user-application-db - ports: - - "54321:5432" # External port for debugging (remove in production) - environment: - - POSTGRES_DB=users - - POSTGRES_USER=app_user - - POSTGRES_PASSWORD=${DB_PASSWORD:-changeme_in_production} - # Performance tuning for container - - POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: [ "CMD-SHELL", "pg_isready -U app_user -d users" ] - interval: 10s - timeout: 5s - retries: 5 - start_period: 30s - restart: unless-stopped - deploy: - resources: - limits: - cpus: '1' - memory: 512M - reservations: - cpus: '0.25' - memory: 256M - networks: - - app-network - -volumes: - postgres_data: - name: user-service-postgres-data - kafka_data: - name: user-service-kafka-data - -networks: - app-network: - name: user-service-network - driver: bridge diff --git a/docker-compose-app.yml b/docker-compose-app.yml new file mode 100644 index 0000000..3db8699 --- /dev/null +++ b/docker-compose-app.yml @@ -0,0 +1,83 @@ +# ============================================================================= +# Docker Compose - Full Application Stack +# ============================================================================= +# Usage: docker compose -f docker-compose-app.yml up -d +# +# Includes: +# - docker-compose.yml (base infrastructure: postgres, kafka) +# - docker-compose-override.yml (adds internal network for containers) +# - user-service application +# ============================================================================= + +include: + - path: + - docker-compose.yml + - docker-compose-override.yml + +services: + # --------------------------------------------------------------------------- + # Application Service + # --------------------------------------------------------------------------- + user-service: + build: + context: . + dockerfile: Dockerfile + container_name: user-application + ports: + - "8080:8080" # REST API + - "9090:9090" # gRPC API + depends_on: + postgres: + condition: service_healthy + kafka: + condition: service_healthy + environment: + - SPRING_PROFILES_ACTIVE=${SPRING_PROFILES_ACTIVE:-dev} + + # Database - connects to postgres service + - DB_URL=jdbc:postgresql://postgres:5432/users + - DB_USERNAME=test_user_rw + - DB_PASSWORD=test_user@pass01 + + # Kafka - internal listener + - KAFKA_BOOTSTRAP_SERVERS=kafka:9092 + + # JWT + - JWT_ACCESS_SECRET=${JWT_ACCESS_SECRET:-dev-only-access-secret-key-must-be-at-least-64-bytes-long-for-hs512} + - JWT_REFRESH_SECRET=${JWT_REFRESH_SECRET:-dev-only-refresh-secret-key-must-be-at-least-64-bytes-long-for-hs512} + + # CORS + - CORS_ALLOWED_ORIGINS=${CORS_ALLOWED_ORIGINS:-http://localhost:3000} + + # JVM options + - JAVA_OPTS=-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 + + # Mail (optional) + - MAIL_HOST=${MAIL_HOST:-smtp.gmail.com} + - MAIL_PORT=${MAIL_PORT:-587} + - MAIL_USERNAME=${MAIL_USERNAME:-} + - MAIL_PASSWORD=${MAIL_PASSWORD:-} + - APP_MAIL_FROM=${APP_MAIL_FROM:-noreply@example.com} + + # OAuth2 (optional) + - OAUTH2_GOOGLE_CLIENT_ID=${OAUTH2_GOOGLE_CLIENT_ID:-} + - OAUTH2_GOOGLE_CLIENT_SECRET=${OAUTH2_GOOGLE_CLIENT_SECRET:-} + - OAUTH2_GITHUB_CLIENT_ID=${OAUTH2_GITHUB_CLIENT_ID:-} + - OAUTH2_GITHUB_CLIENT_SECRET=${OAUTH2_GITHUB_CLIENT_SECRET:-} + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/actuator/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + restart: unless-stopped + deploy: + resources: + limits: + cpus: '2' + memory: 1G + reservations: + cpus: '0.5' + memory: 512M + networks: + - app-network diff --git a/docker-compose-override.yml b/docker-compose-override.yml new file mode 100644 index 0000000..d97189c --- /dev/null +++ b/docker-compose-override.yml @@ -0,0 +1,24 @@ +# ============================================================================= +# Infrastructure Override for Full Stack Deployment +# ============================================================================= +# Adds internal Kafka listener and shared network for container communication. +# Used with: docker-compose-app.yml (via include) +# ============================================================================= + +services: + kafka: + environment: + KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:29092 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,EXTERNAL://localhost:29092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,EXTERNAL:PLAINTEXT + networks: + - app-network + + postgres: + networks: + - app-network + +networks: + app-network: + name: user-service-network + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index 6f805e1..86da26f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ # ============================================================================= -# Docker Compose - Local Development (Database Only) +# Docker Compose - Local Development (Database and Kafka) # ============================================================================= # Used by Spring Boot's Docker Compose integration when: # spring.docker.compose.enabled=true (in application-local.yml) From f54b03a57df91c39fa387727d7a3cd68d567841b Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 31 Dec 2025 17:27:09 +1100 Subject: [PATCH 3/3] Add ecs and kubernetes deploy --- deploy/README.md | 172 ++++++++++++++++++++ deploy/ecs/task-def.dev.json | 99 +++++++++++ deploy/ecs/task-def.prod.json | 99 +++++++++++ deploy/k8s/base/configmap.yaml | 18 ++ deploy/k8s/base/deployment.yaml | 66 ++++++++ deploy/k8s/base/external-secret.yaml | 57 +++++++ deploy/k8s/base/kustomization.yaml | 16 ++ deploy/k8s/base/service.yaml | 19 +++ deploy/k8s/base/serviceaccount.yaml | 7 + deploy/k8s/overlays/dev/kustomization.yaml | 105 ++++++++++++ deploy/k8s/overlays/prod/hpa.yaml | 41 +++++ deploy/k8s/overlays/prod/kustomization.yaml | 108 ++++++++++++ deploy/k8s/overlays/prod/pdb.yaml | 9 + 13 files changed, 816 insertions(+) create mode 100644 deploy/README.md create mode 100644 deploy/ecs/task-def.dev.json create mode 100644 deploy/ecs/task-def.prod.json create mode 100644 deploy/k8s/base/configmap.yaml create mode 100644 deploy/k8s/base/deployment.yaml create mode 100644 deploy/k8s/base/external-secret.yaml create mode 100644 deploy/k8s/base/kustomization.yaml create mode 100644 deploy/k8s/base/service.yaml create mode 100644 deploy/k8s/base/serviceaccount.yaml create mode 100644 deploy/k8s/overlays/dev/kustomization.yaml create mode 100644 deploy/k8s/overlays/prod/hpa.yaml create mode 100644 deploy/k8s/overlays/prod/kustomization.yaml create mode 100644 deploy/k8s/overlays/prod/pdb.yaml diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..90e2ed2 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,172 @@ +# Deployment Configurations + +This directory contains deployment configurations for AWS ECS and Kubernetes (EKS). + +## Directory Structure + +``` +deploy/ +├── ecs/ +│ ├── task-def.dev.json # ECS Fargate task definition (dev) +│ └── task-def.prod.json # ECS Fargate task definition (prod) +└── k8s/ + ├── base/ # Base Kubernetes manifests + │ ├── deployment.yaml + │ ├── service.yaml + │ ├── serviceaccount.yaml + │ ├── configmap.yaml + │ ├── external-secret.yaml + │ └── kustomization.yaml + └── overlays/ + ├── dev/ # Dev environment overlay + │ └── kustomization.yaml + └── prod/ # Prod environment overlay + ├── kustomization.yaml + ├── hpa.yaml # Horizontal Pod Autoscaler + └── pdb.yaml # Pod Disruption Budget +``` + +## AWS ECS Deployment + +### Prerequisites + +1. Create secrets in AWS Secrets Manager: + ```bash + # Dev secrets + aws secretsmanager create-secret --name dev/user-service/db \ + --secret-string '{"username":"app_user","password":"your-password"}' + + aws secretsmanager create-secret --name dev/user-service/jwt \ + --secret-string '{"access-secret":"your-64-byte-secret","refresh-secret":"your-64-byte-secret"}' + + # Repeat for prod/user-service/* + ``` + +2. Create ECS Task Execution Role with Secrets Manager access + +### Deploy + +```bash +# Replace variables and register task definition +export AWS_ACCOUNT_ID=123456789 +export AWS_REGION=ap-southeast-2 +export IMAGE_TAG=v1.0.0 + +# Dev +envsubst < deploy/ecs/task-def.dev.json | \ + aws ecs register-task-definition --cli-input-json file:///dev/stdin + +# Prod +envsubst < deploy/ecs/task-def.prod.json | \ + aws ecs register-task-definition --cli-input-json file:///dev/stdin + +# Update service +aws ecs update-service --cluster user-service-dev \ + --service user-service --task-definition user-service-dev +``` + +## Kubernetes (EKS) Deployment + +### Prerequisites + +1. Install [External Secrets Operator](https://external-secrets.io/): + ```bash + helm repo add external-secrets https://charts.external-secrets.io + helm install external-secrets external-secrets/external-secrets \ + -n external-secrets --create-namespace + ``` + +2. Create ClusterSecretStore for AWS Secrets Manager: + ```yaml + apiVersion: external-secrets.io/v1beta1 + kind: ClusterSecretStore + metadata: + name: aws-secrets-manager + spec: + provider: + aws: + service: SecretsManager + region: ap-southeast-2 + auth: + jwt: + serviceAccountRef: + name: external-secrets + namespace: external-secrets + ``` + +3. Create secrets in AWS Secrets Manager (same as ECS) + +### Deploy with Kustomize + +```bash +# Preview dev manifests +kubectl kustomize deploy/k8s/overlays/dev + +# Deploy to dev +kubectl apply -k deploy/k8s/overlays/dev + +# Deploy to prod +kubectl apply -k deploy/k8s/overlays/prod +``` + +### Deploy with kubectl directly + +```bash +# Build and apply +kustomize build deploy/k8s/overlays/dev | kubectl apply -f - +``` + +## Environment-Specific Configuration + +| Setting | Dev | Prod | +|---------|-----|------| +| Replicas | 1 | 2 (HPA: 2-10) | +| CPU Request | 100m | 250m | +| CPU Limit | 500m | 1000m | +| Memory Request | 256Mi | 512Mi | +| Memory Limit | 512Mi | 1Gi | +| DB Pool Max | 5 | 20 | +| DB Pool Min | 2 | 5 | + +## Secrets Structure in AWS Secrets Manager + +``` +dev/user-service/db # {"username": "...", "password": "..."} +dev/user-service/jwt # {"access-secret": "...", "refresh-secret": "..."} +dev/user-service/mail # {"username": "...", "password": "..."} +dev/user-service/oauth2 # {"google-client-id": "...", "google-client-secret": "...", ...} + +prod/user-service/db +prod/user-service/jwt +prod/user-service/mail +prod/user-service/oauth2 +``` + +## CI/CD Integration + +### GitHub Actions Example + +```yaml +- name: Deploy to ECS + run: | + envsubst < deploy/ecs/task-def.${{ env.ENVIRONMENT }}.json > task-def.json + aws ecs register-task-definition --cli-input-json file://task-def.json + aws ecs update-service --cluster ${{ env.CLUSTER }} --service user-service \ + --task-definition user-service-${{ env.ENVIRONMENT }} +``` + +### ArgoCD (Kubernetes) + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: user-service-dev +spec: + source: + repoURL: https://github.com/your-org/java-springboot + path: deploy/k8s/overlays/dev + destination: + server: https://kubernetes.default.svc + namespace: user-service-dev +``` diff --git a/deploy/ecs/task-def.dev.json b/deploy/ecs/task-def.dev.json new file mode 100644 index 0000000..017ee39 --- /dev/null +++ b/deploy/ecs/task-def.dev.json @@ -0,0 +1,99 @@ +{ + "family": "user-service-dev", + "networkMode": "awsvpc", + "requiresCompatibilities": ["FARGATE"], + "cpu": "512", + "memory": "1024", + "executionRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecsTaskExecutionRole", + "taskRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/user-service-task-role", + "containerDefinitions": [ + { + "name": "user-service", + "image": "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/user-service:${IMAGE_TAG}", + "essential": true, + "portMappings": [ + { + "containerPort": 8080, + "protocol": "tcp", + "name": "http" + }, + { + "containerPort": 9090, + "protocol": "tcp", + "name": "grpc" + } + ], + "environment": [ + {"name": "SPRING_PROFILES_ACTIVE", "value": "dev"}, + {"name": "SERVER_PORT", "value": "8080"}, + {"name": "GRPC_PORT", "value": "9090"}, + {"name": "DB_URL", "value": "jdbc:postgresql://user-service-dev.cluster-xxx.ap-southeast-2.rds.amazonaws.com:5432/users"}, + {"name": "DB_POOL_MAX", "value": "5"}, + {"name": "DB_POOL_MIN", "value": "2"}, + {"name": "KAFKA_BOOTSTRAP_SERVERS", "value": "b-1.user-service-dev.xxx.kafka.ap-southeast-2.amazonaws.com:9092"}, + {"name": "CORS_ALLOWED_ORIGINS", "value": "https://dev.example.com"}, + {"name": "MAIL_HOST", "value": "email-smtp.ap-southeast-2.amazonaws.com"}, + {"name": "MAIL_PORT", "value": "587"}, + {"name": "APP_MAIL_FROM", "value": "noreply@example.com"}, + {"name": "JAVA_OPTS", "value": "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0"} + ], + "secrets": [ + { + "name": "DB_USERNAME", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:dev/user-service/db:username::" + }, + { + "name": "DB_PASSWORD", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:dev/user-service/db:password::" + }, + { + "name": "JWT_ACCESS_SECRET", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:dev/user-service/jwt:access-secret::" + }, + { + "name": "JWT_REFRESH_SECRET", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:dev/user-service/jwt:refresh-secret::" + }, + { + "name": "MAIL_USERNAME", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:dev/user-service/mail:username::" + }, + { + "name": "MAIL_PASSWORD", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:dev/user-service/mail:password::" + }, + { + "name": "OAUTH2_GOOGLE_CLIENT_ID", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:dev/user-service/oauth2:google-client-id::" + }, + { + "name": "OAUTH2_GOOGLE_CLIENT_SECRET", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:dev/user-service/oauth2:google-client-secret::" + }, + { + "name": "OAUTH2_GITHUB_CLIENT_ID", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:dev/user-service/oauth2:github-client-id::" + }, + { + "name": "OAUTH2_GITHUB_CLIENT_SECRET", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:dev/user-service/oauth2:github-client-secret::" + } + ], + "healthCheck": { + "command": ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1"], + "interval": 30, + "timeout": 10, + "retries": 3, + "startPeriod": 60 + }, + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/ecs/user-service-dev", + "awslogs-region": "ap-southeast-2", + "awslogs-stream-prefix": "ecs" + } + } + } + ] +} diff --git a/deploy/ecs/task-def.prod.json b/deploy/ecs/task-def.prod.json new file mode 100644 index 0000000..de41af4 --- /dev/null +++ b/deploy/ecs/task-def.prod.json @@ -0,0 +1,99 @@ +{ + "family": "user-service-prod", + "networkMode": "awsvpc", + "requiresCompatibilities": ["FARGATE"], + "cpu": "1024", + "memory": "2048", + "executionRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecsTaskExecutionRole", + "taskRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/user-service-task-role", + "containerDefinitions": [ + { + "name": "user-service", + "image": "${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/user-service:${IMAGE_TAG}", + "essential": true, + "portMappings": [ + { + "containerPort": 8080, + "protocol": "tcp", + "name": "http" + }, + { + "containerPort": 9090, + "protocol": "tcp", + "name": "grpc" + } + ], + "environment": [ + {"name": "SPRING_PROFILES_ACTIVE", "value": "prod"}, + {"name": "SERVER_PORT", "value": "8080"}, + {"name": "GRPC_PORT", "value": "9090"}, + {"name": "DB_URL", "value": "jdbc:postgresql://user-service-prod.cluster-xxx.ap-southeast-2.rds.amazonaws.com:5432/users"}, + {"name": "DB_POOL_MAX", "value": "20"}, + {"name": "DB_POOL_MIN", "value": "5"}, + {"name": "KAFKA_BOOTSTRAP_SERVERS", "value": "b-1.user-service-prod.xxx.kafka.ap-southeast-2.amazonaws.com:9092"}, + {"name": "CORS_ALLOWED_ORIGINS", "value": "https://example.com"}, + {"name": "MAIL_HOST", "value": "email-smtp.ap-southeast-2.amazonaws.com"}, + {"name": "MAIL_PORT", "value": "587"}, + {"name": "APP_MAIL_FROM", "value": "noreply@example.com"}, + {"name": "JAVA_OPTS", "value": "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0"} + ], + "secrets": [ + { + "name": "DB_USERNAME", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:prod/user-service/db:username::" + }, + { + "name": "DB_PASSWORD", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:prod/user-service/db:password::" + }, + { + "name": "JWT_ACCESS_SECRET", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:prod/user-service/jwt:access-secret::" + }, + { + "name": "JWT_REFRESH_SECRET", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:prod/user-service/jwt:refresh-secret::" + }, + { + "name": "MAIL_USERNAME", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:prod/user-service/mail:username::" + }, + { + "name": "MAIL_PASSWORD", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:prod/user-service/mail:password::" + }, + { + "name": "OAUTH2_GOOGLE_CLIENT_ID", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:prod/user-service/oauth2:google-client-id::" + }, + { + "name": "OAUTH2_GOOGLE_CLIENT_SECRET", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:prod/user-service/oauth2:google-client-secret::" + }, + { + "name": "OAUTH2_GITHUB_CLIENT_ID", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:prod/user-service/oauth2:github-client-id::" + }, + { + "name": "OAUTH2_GITHUB_CLIENT_SECRET", + "valueFrom": "arn:aws:secretsmanager:ap-southeast-2:${AWS_ACCOUNT_ID}:secret:prod/user-service/oauth2:github-client-secret::" + } + ], + "healthCheck": { + "command": ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1"], + "interval": 30, + "timeout": 10, + "retries": 3, + "startPeriod": 60 + }, + "logConfiguration": { + "logDriver": "awslogs", + "options": { + "awslogs-group": "/ecs/user-service-prod", + "awslogs-region": "ap-southeast-2", + "awslogs-stream-prefix": "ecs" + } + } + } + ] +} diff --git a/deploy/k8s/base/configmap.yaml b/deploy/k8s/base/configmap.yaml new file mode 100644 index 0000000..fda9bdd --- /dev/null +++ b/deploy/k8s/base/configmap.yaml @@ -0,0 +1,18 @@ +# Base ConfigMap - values overridden by environment overlays +apiVersion: v1 +kind: ConfigMap +metadata: + name: user-service-config +data: + SPRING_PROFILES_ACTIVE: "dev" + SERVER_PORT: "8080" + GRPC_PORT: "9090" + DB_URL: "jdbc:postgresql://localhost:5432/users" + DB_POOL_MAX: "10" + DB_POOL_MIN: "2" + KAFKA_BOOTSTRAP_SERVERS: "localhost:9092" + CORS_ALLOWED_ORIGINS: "http://localhost:3000" + MAIL_HOST: "smtp.gmail.com" + MAIL_PORT: "587" + APP_MAIL_FROM: "noreply@example.com" + JAVA_OPTS: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0" diff --git a/deploy/k8s/base/deployment.yaml b/deploy/k8s/base/deployment.yaml new file mode 100644 index 0000000..db896ec --- /dev/null +++ b/deploy/k8s/base/deployment.yaml @@ -0,0 +1,66 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: user-service + labels: + app: user-service +spec: + replicas: 1 + selector: + matchLabels: + app: user-service + template: + metadata: + labels: + app: user-service + spec: + serviceAccountName: user-service + containers: + - name: user-service + image: user-service:latest # Overridden by kustomize + ports: + - name: http + containerPort: 8080 + protocol: TCP + - name: grpc + containerPort: 9090 + protocol: TCP + envFrom: + - configMapRef: + name: user-service-config + - secretRef: + name: user-service-secrets + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "1000m" + livenessProbe: + httpGet: + path: /actuator/health/liveness + port: 8080 + initialDelaySeconds: 60 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /actuator/health/readiness + port: 8080 + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 3 + securityContext: + runAsNonRoot: true + runAsUser: 1000 + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + volumeMounts: + - name: tmp + mountPath: /tmp + volumes: + - name: tmp + emptyDir: {} diff --git a/deploy/k8s/base/external-secret.yaml b/deploy/k8s/base/external-secret.yaml new file mode 100644 index 0000000..2395af9 --- /dev/null +++ b/deploy/k8s/base/external-secret.yaml @@ -0,0 +1,57 @@ +# External Secrets Operator - fetches secrets from AWS Secrets Manager +# Requires: https://external-secrets.io/ installed in cluster +# +# Alternative: Use sealed-secrets or SOPS for GitOps +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: user-service-secrets +spec: + refreshInterval: 1h + secretStoreRef: + name: aws-secrets-manager + kind: ClusterSecretStore + target: + name: user-service-secrets + creationPolicy: Owner + data: + - secretKey: DB_USERNAME + remoteRef: + key: user-service/db + property: username + - secretKey: DB_PASSWORD + remoteRef: + key: user-service/db + property: password + - secretKey: JWT_ACCESS_SECRET + remoteRef: + key: user-service/jwt + property: access-secret + - secretKey: JWT_REFRESH_SECRET + remoteRef: + key: user-service/jwt + property: refresh-secret + - secretKey: MAIL_USERNAME + remoteRef: + key: user-service/mail + property: username + - secretKey: MAIL_PASSWORD + remoteRef: + key: user-service/mail + property: password + - secretKey: OAUTH2_GOOGLE_CLIENT_ID + remoteRef: + key: user-service/oauth2 + property: google-client-id + - secretKey: OAUTH2_GOOGLE_CLIENT_SECRET + remoteRef: + key: user-service/oauth2 + property: google-client-secret + - secretKey: OAUTH2_GITHUB_CLIENT_ID + remoteRef: + key: user-service/oauth2 + property: github-client-id + - secretKey: OAUTH2_GITHUB_CLIENT_SECRET + remoteRef: + key: user-service/oauth2 + property: github-client-secret diff --git a/deploy/k8s/base/kustomization.yaml b/deploy/k8s/base/kustomization.yaml new file mode 100644 index 0000000..969f3da --- /dev/null +++ b/deploy/k8s/base/kustomization.yaml @@ -0,0 +1,16 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +metadata: + name: user-service + +resources: + - deployment.yaml + - service.yaml + - serviceaccount.yaml + - configmap.yaml + - external-secret.yaml + +commonLabels: + app.kubernetes.io/name: user-service + app.kubernetes.io/component: backend diff --git a/deploy/k8s/base/service.yaml b/deploy/k8s/base/service.yaml new file mode 100644 index 0000000..93358ce --- /dev/null +++ b/deploy/k8s/base/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: user-service + labels: + app: user-service +spec: + type: ClusterIP + ports: + - name: http + port: 80 + targetPort: 8080 + protocol: TCP + - name: grpc + port: 9090 + targetPort: 9090 + protocol: TCP + selector: + app: user-service diff --git a/deploy/k8s/base/serviceaccount.yaml b/deploy/k8s/base/serviceaccount.yaml new file mode 100644 index 0000000..d7cc1a3 --- /dev/null +++ b/deploy/k8s/base/serviceaccount.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: user-service + annotations: + # For AWS EKS: IAM Role for Service Account (IRSA) + # eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT_ID:role/user-service-role diff --git a/deploy/k8s/overlays/dev/kustomization.yaml b/deploy/k8s/overlays/dev/kustomization.yaml new file mode 100644 index 0000000..3e25468 --- /dev/null +++ b/deploy/k8s/overlays/dev/kustomization.yaml @@ -0,0 +1,105 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: user-service-dev + +resources: + - ../../base + +namePrefix: dev- + +commonLabels: + environment: dev + +# Image configuration +images: + - name: user-service + newName: 123456789.dkr.ecr.ap-southeast-2.amazonaws.com/user-service + newTag: dev-latest + +# Replicas +replicas: + - name: user-service + count: 1 + +# ConfigMap patches +patches: + - target: + kind: ConfigMap + name: user-service-config + patch: |- + - op: replace + path: /data/SPRING_PROFILES_ACTIVE + value: "dev" + - op: replace + path: /data/DB_URL + value: "jdbc:postgresql://user-service-dev.cluster-xxx.ap-southeast-2.rds.amazonaws.com:5432/users" + - op: replace + path: /data/DB_POOL_MAX + value: "5" + - op: replace + path: /data/DB_POOL_MIN + value: "2" + - op: replace + path: /data/KAFKA_BOOTSTRAP_SERVERS + value: "b-1.user-service-dev.xxx.kafka.ap-southeast-2.amazonaws.com:9092" + - op: replace + path: /data/CORS_ALLOWED_ORIGINS + value: "https://dev.example.com" + - op: replace + path: /data/MAIL_HOST + value: "email-smtp.ap-southeast-2.amazonaws.com" + - op: replace + path: /data/APP_MAIL_FROM + value: "noreply@dev.example.com" + + # External Secret - use dev secrets path + - target: + kind: ExternalSecret + name: user-service-secrets + patch: |- + - op: replace + path: /spec/data/0/remoteRef/key + value: "dev/user-service/db" + - op: replace + path: /spec/data/1/remoteRef/key + value: "dev/user-service/db" + - op: replace + path: /spec/data/2/remoteRef/key + value: "dev/user-service/jwt" + - op: replace + path: /spec/data/3/remoteRef/key + value: "dev/user-service/jwt" + - op: replace + path: /spec/data/4/remoteRef/key + value: "dev/user-service/mail" + - op: replace + path: /spec/data/5/remoteRef/key + value: "dev/user-service/mail" + - op: replace + path: /spec/data/6/remoteRef/key + value: "dev/user-service/oauth2" + - op: replace + path: /spec/data/7/remoteRef/key + value: "dev/user-service/oauth2" + - op: replace + path: /spec/data/8/remoteRef/key + value: "dev/user-service/oauth2" + - op: replace + path: /spec/data/9/remoteRef/key + value: "dev/user-service/oauth2" + + # Lower resource limits for dev + - target: + kind: Deployment + name: user-service + patch: |- + - op: replace + path: /spec/template/spec/containers/0/resources + value: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" diff --git a/deploy/k8s/overlays/prod/hpa.yaml b/deploy/k8s/overlays/prod/hpa.yaml new file mode 100644 index 0000000..d785c04 --- /dev/null +++ b/deploy/k8s/overlays/prod/hpa.yaml @@ -0,0 +1,41 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: user-service +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: prod-user-service + minReplicas: 2 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 + behavior: + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Percent + value: 10 + periodSeconds: 60 + scaleUp: + stabilizationWindowSeconds: 0 + policies: + - type: Percent + value: 100 + periodSeconds: 15 + - type: Pods + value: 4 + periodSeconds: 15 + selectPolicy: Max diff --git a/deploy/k8s/overlays/prod/kustomization.yaml b/deploy/k8s/overlays/prod/kustomization.yaml new file mode 100644 index 0000000..ee33d2f --- /dev/null +++ b/deploy/k8s/overlays/prod/kustomization.yaml @@ -0,0 +1,108 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: user-service-prod + +resources: + - ../../base + - hpa.yaml + - pdb.yaml + +namePrefix: prod- + +commonLabels: + environment: prod + +# Image configuration +images: + - name: user-service + newName: 123456789.dkr.ecr.ap-southeast-2.amazonaws.com/user-service + # Use specific tag for prod, not 'latest' + newTag: v1.0.0 + +# Replicas (base, HPA will scale) +replicas: + - name: user-service + count: 2 + +# ConfigMap patches +patches: + - target: + kind: ConfigMap + name: user-service-config + patch: |- + - op: replace + path: /data/SPRING_PROFILES_ACTIVE + value: "prod" + - op: replace + path: /data/DB_URL + value: "jdbc:postgresql://user-service-prod.cluster-xxx.ap-southeast-2.rds.amazonaws.com:5432/users" + - op: replace + path: /data/DB_POOL_MAX + value: "20" + - op: replace + path: /data/DB_POOL_MIN + value: "5" + - op: replace + path: /data/KAFKA_BOOTSTRAP_SERVERS + value: "b-1.user-service-prod.xxx.kafka.ap-southeast-2.amazonaws.com:9092" + - op: replace + path: /data/CORS_ALLOWED_ORIGINS + value: "https://example.com" + - op: replace + path: /data/MAIL_HOST + value: "email-smtp.ap-southeast-2.amazonaws.com" + - op: replace + path: /data/APP_MAIL_FROM + value: "noreply@example.com" + + # External Secret - use prod secrets path + - target: + kind: ExternalSecret + name: user-service-secrets + patch: |- + - op: replace + path: /spec/data/0/remoteRef/key + value: "prod/user-service/db" + - op: replace + path: /spec/data/1/remoteRef/key + value: "prod/user-service/db" + - op: replace + path: /spec/data/2/remoteRef/key + value: "prod/user-service/jwt" + - op: replace + path: /spec/data/3/remoteRef/key + value: "prod/user-service/jwt" + - op: replace + path: /spec/data/4/remoteRef/key + value: "prod/user-service/mail" + - op: replace + path: /spec/data/5/remoteRef/key + value: "prod/user-service/mail" + - op: replace + path: /spec/data/6/remoteRef/key + value: "prod/user-service/oauth2" + - op: replace + path: /spec/data/7/remoteRef/key + value: "prod/user-service/oauth2" + - op: replace + path: /spec/data/8/remoteRef/key + value: "prod/user-service/oauth2" + - op: replace + path: /spec/data/9/remoteRef/key + value: "prod/user-service/oauth2" + + # Higher resource limits for prod + - target: + kind: Deployment + name: user-service + patch: |- + - op: replace + path: /spec/template/spec/containers/0/resources + value: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "1000m" diff --git a/deploy/k8s/overlays/prod/pdb.yaml b/deploy/k8s/overlays/prod/pdb.yaml new file mode 100644 index 0000000..837f268 --- /dev/null +++ b/deploy/k8s/overlays/prod/pdb.yaml @@ -0,0 +1,9 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: user-service +spec: + minAvailable: 1 + selector: + matchLabels: + app: user-service