From a38aeb0a58f3e51e5e7b76b798740dca65582054 Mon Sep 17 00:00:00 2001 From: JIWONKIMS Date: Tue, 14 Oct 2025 15:29:42 +0900 Subject: [PATCH 1/3] feat(be): Switch to docker-compose for RabbitMQ deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add docker-compose installation in Terraform user_data - Create infra/rabbitmq-docker-compose.yml as deployment template - Update main.tf to use docker-compose instead of docker run - Update local docker-compose.yml with proper network configuration - RabbitMQ container name: rabbitmq_1 for consistency πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- docker-compose.yml | 16 +++++-- infra/main.tf | 76 ++++++++++++++++++++----------- infra/rabbitmq-docker-compose.yml | 31 +++++++++++++ 3 files changed, 91 insertions(+), 32 deletions(-) create mode 100644 infra/rabbitmq-docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml index 3722ff1..94e58a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,20 +4,26 @@ services: rabbit: image: rabbitmq:3-management # 버전 λͺ…μ‹œ ꢌμž₯ (예: 3.13-management) container_name: rabbitmq-1 + networks: + - common # 이미 μ‘΄μž¬ν•˜λŠ” 곡용 λ„€νŠΈμ›Œν¬ μ‚¬μš© ports: - "5672:5672" # AMQP (Spring ↔ RabbitMQ) - "61613:61613" # STOMP (Relayκ°€ 여기에 λΆ™μŒ) - "15672:15672" # Management UI (λ‘œμ»¬μ—μ„œλ§Œ) environment: + TZ: Asia/Seoul # νƒ€μž„μ‘΄ μ„€μ • RABBITMQ_DEFAULT_USER: admin - RABBITMQ_DEFAULT_PASS: admin - volumes: # μ˜μ†ν™”κ°€ ν•„μš”ν•  λ•Œλ§Œ μœ μ§€ - - ./dockerProjects/rabbitmq-1/volumes/etc/rabbitmq:/etc/rabbitmq - - ./dockerProjects/rabbitmq-1/volumes/var/lib/rabbitmq:/var/lib/rabbitmq - - ./dockerProjects/rabbitmq-1/volumes/var/log/rabbitmq:/var/log/rabbitmq + RABBITMQ_DEFAULT_PASS: "$PASSWORD_1" + volumes: + - ./volumes/etc/rabbitmq:/etc/rabbitmq + - ./volumes/var/lib/rabbitmq:/var/lib/rabbitmq + - ./volumes/var/log/rabbitmq:/var/log/rabbitmq command: > sh -c " rabbitmq-plugins enable rabbitmq_management && rabbitmq-plugins enable rabbitmq_stomp && rabbitmq-server " +networks: + common: + external: true # 이미 λ§Œλ“€μ–΄μ§„ λ„€νŠΈμ›Œν¬λ₯Ό μ‚¬μš© \ No newline at end of file diff --git a/infra/main.tf b/infra/main.tf index ba9c8be..8ccbe92 100644 --- a/infra/main.tf +++ b/infra/main.tf @@ -263,33 +263,55 @@ CREATE DATABASE \"${var.app_1_db_name}\" OWNER team11; GRANT ALL PRIVILEGES ON DATABASE \"${var.app_1_db_name}\" TO team11; " -# rabbitmq μ„€μΉ˜ -docker run -d \ - --name rabbitmq_1 \ - --restart unless-stopped \ - --network common \ - -p 5672:5672 \ - -p 61613:61613 \ - -p 15672:15672 \ - -e RABBITMQ_DEFAULT_USER=admin \ - -e RABBITMQ_DEFAULT_PASS=${var.password_1} \ - -e TZ=Asia/Seoul \ - -v /dockerProjects/rabbitmq_1/volumes/data:/var/lib/rabbitmq \ - rabbitmq:3-management - -# RabbitMQκ°€ 쀀비될 λ•ŒκΉŒμ§€ λŒ€κΈ° -echo "RabbitMQκ°€ 기동될 λ•ŒκΉŒμ§€ λŒ€κΈ° 쀑..." -until docker exec rabbitmq_1 rabbitmqctl status &> /dev/null; do - echo "RabbitMQκ°€ 아직 μ€€λΉ„λ˜μ§€ μ•ŠμŒ. 5초 ν›„ μž¬μ‹œλ„..." - sleep 5 -done -echo "RabbitMQκ°€ 쀀비됨. STOMP ν”ŒλŸ¬κ·ΈμΈ ν™œμ„±ν™” 쀑..." - -# RabbitMQ STOMP ν”ŒλŸ¬κ·ΈμΈ ν™œμ„±ν™” -docker exec rabbitmq_1 rabbitmq-plugins enable rabbitmq_stomp -docker exec rabbitmq_1 rabbitmq-plugins enable rabbitmq_management - -echo "RabbitMQ μ„€μΉ˜ 및 μ„€μ • μ™„λ£Œ!" +# docker-compose μ„€μΉ˜ +echo "docker-compose μ„€μΉ˜ 쀑..." +curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +chmod +x /usr/local/bin/docker-compose +ln -sf /usr/local/bin/docker-compose /usr/bin/docker-compose +docker-compose --version + +# RabbitMQ docker-compose.yml 생성 +mkdir -p /dockerProjects/rabbitmq_1 +cat > /dockerProjects/rabbitmq_1/docker-compose.yml <<'RABBITMQ_COMPOSE' +version: "3.8" + +services: + rabbitmq: + image: rabbitmq:3-management + container_name: rabbitmq_1 + restart: unless-stopped + networks: + - common + ports: + - "5672:5672" + - "61613:61613" + - "15672:15672" + environment: + TZ: Asia/Seoul + RABBITMQ_DEFAULT_USER: admin + RABBITMQ_DEFAULT_PASS: \${PASSWORD_1} + volumes: + - /dockerProjects/rabbitmq_1/volumes/etc/rabbitmq:/etc/rabbitmq + - /dockerProjects/rabbitmq_1/volumes/var/lib/rabbitmq:/var/lib/rabbitmq + - /dockerProjects/rabbitmq_1/volumes/var/log/rabbitmq:/var/log/rabbitmq + command: > + sh -c " + rabbitmq-plugins enable rabbitmq_management && + rabbitmq-plugins enable rabbitmq_stomp && + rabbitmq-server + " + +networks: + common: + external: true +RABBITMQ_COMPOSE + +# RabbitMQ μ‹œμž‘ +echo "RabbitMQ μ‹œμž‘ 쀑..." +cd /dockerProjects/rabbitmq_1 +docker-compose up -d + +echo "RabbitMQ docker-compose μ„€μΉ˜ μ™„λ£Œ!" echo "${var.github_access_token_1}" | docker login ghcr.io -u ${var.github_access_token_1_owner} --password-stdin diff --git a/infra/rabbitmq-docker-compose.yml b/infra/rabbitmq-docker-compose.yml new file mode 100644 index 0000000..c682e85 --- /dev/null +++ b/infra/rabbitmq-docker-compose.yml @@ -0,0 +1,31 @@ +version: "3.8" + +services: + rabbitmq: + image: rabbitmq:3-management + container_name: rabbitmq_1 + restart: unless-stopped + networks: + - common + ports: + - "5672:5672" + - "61613:61613" + - "15672:15672" + environment: + TZ: Asia/Seoul + RABBITMQ_DEFAULT_USER: admin + RABBITMQ_DEFAULT_PASS: ${PASSWORD_1} + volumes: + - /dockerProjects/rabbitmq_1/volumes/etc/rabbitmq:/etc/rabbitmq + - /dockerProjects/rabbitmq_1/volumes/var/lib/rabbitmq:/var/lib/rabbitmq + - /dockerProjects/rabbitmq_1/volumes/var/log/rabbitmq:/var/log/rabbitmq + command: > + sh -c " + rabbitmq-plugins enable rabbitmq_management && + rabbitmq-plugins enable rabbitmq_stomp && + rabbitmq-server + " + +networks: + common: + external: true From e202b108b6d6a2f87321b8d2b35081f9b70420ab Mon Sep 17 00:00:00 2001 From: JIWONKIMS Date: Tue, 14 Oct 2025 16:14:09 +0900 Subject: [PATCH 2/3] refactor(infra): Move docker-compose installation to logical position MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Moved docker-compose installation block right after Docker installation - Better script organization: Docker β†’ docker-compose β†’ containers - Removed duplicate installation code --- infra/main.tf | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/infra/main.tf b/infra/main.tf index 8ccbe92..51ac310 100644 --- a/infra/main.tf +++ b/infra/main.tf @@ -210,6 +210,13 @@ yum install docker -y systemctl enable docker systemctl start docker +# docker-compose μ„€μΉ˜ +echo "docker-compose μ„€μΉ˜ 쀑..." +curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose +chmod +x /usr/local/bin/docker-compose +ln -sf /usr/local/bin/docker-compose /usr/bin/docker-compose +docker-compose --version + # 도컀 λ„€νŠΈμ›Œν¬ 생성 docker network create common @@ -263,13 +270,6 @@ CREATE DATABASE \"${var.app_1_db_name}\" OWNER team11; GRANT ALL PRIVILEGES ON DATABASE \"${var.app_1_db_name}\" TO team11; " -# docker-compose μ„€μΉ˜ -echo "docker-compose μ„€μΉ˜ 쀑..." -curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose -chmod +x /usr/local/bin/docker-compose -ln -sf /usr/local/bin/docker-compose /usr/bin/docker-compose -docker-compose --version - # RabbitMQ docker-compose.yml 생성 mkdir -p /dockerProjects/rabbitmq_1 cat > /dockerProjects/rabbitmq_1/docker-compose.yml <<'RABBITMQ_COMPOSE' From f710cf8608fcf4eb08a4a338ba38b106373d81fb Mon Sep 17 00:00:00 2001 From: JIWONKIMS Date: Tue, 14 Oct 2025 17:15:02 +0900 Subject: [PATCH 3/3] docs(be): Update CLAUDE.md and fix dev RabbitMQ config - Add comprehensive RabbitMQ integration documentation - Add profile-based configuration strategy explanation - Add infrastructure & deployment section - Fix: Exclude RabbitMQ autoconfiguration in dev profile to prevent health check DOWN --- CLAUDE.md | 158 +++++++++++++++++++++++++++-- src/main/resources/application.yml | 1 + 2 files changed, 150 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 52510b8..bbe2a23 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,6 +24,9 @@ Korean Travel Guide Backend - A Spring Boot Kotlin application providing OAuth a # Run specific test ./gradlew test --tests "WeatherServiceTest" + +# Compile Kotlin only (faster than full build) +./gradlew compileKotlin ``` ### Code Quality @@ -54,6 +57,24 @@ open http://localhost:8080/h2-console curl http://localhost:8080/actuator/health ``` +### Local RabbitMQ Setup +```bash +# Start RabbitMQ with docker-compose (for WebSocket testing) +docker network create common +docker-compose up -d + +# Check RabbitMQ status +docker logs rabbitmq-1 + +# Access RabbitMQ Management UI +open http://localhost:15672 +# Username: admin +# Password: (from .env PASSWORD_1 or default: qwpokd153098) + +# Stop RabbitMQ +docker-compose down +``` + ### Redis (Optional - for caching) ```bash # Start Redis with Docker @@ -73,7 +94,7 @@ The codebase follows Domain-Driven Design with clear separation: ``` com/back/koreaTravelGuide/ β”œβ”€β”€ common/ # Shared infrastructure -β”‚ β”œβ”€β”€ config/ # App, Security, Redis, AI configs +β”‚ β”œβ”€β”€ config/ # App, Security, Redis, AI, DevConfig β”‚ β”œβ”€β”€ security/ # OAuth2, JWT, filters β”‚ β”œβ”€β”€ exception/ # Global exception handler β”‚ └── ApiResponse.kt # Standard API response wrapper @@ -86,7 +107,8 @@ com/back/koreaTravelGuide/ β”‚ β”‚ └── tour/ # Tourism API integration β”‚ β”œβ”€β”€ userChat/ # WebSocket chat between Guest-Guide β”‚ β”‚ β”œβ”€β”€ chatroom/ # Chat room management -β”‚ β”‚ └── chatmessage/ # Message persistence +β”‚ β”‚ β”œβ”€β”€ chatmessage/ # Message persistence & publishing +β”‚ β”‚ └── stomp/ # WebSocket config (Simple/Rabbit) β”‚ └── rate/ # Rating system for AI sessions & guides ``` @@ -95,15 +117,43 @@ com/back/koreaTravelGuide/ 1. **Each domain is self-contained** with its own entity, repository, service, controller, and DTOs 2. **Common utilities live in `common/`** - never duplicate config or security logic 3. **AI Chat uses Spring AI** with function calling for weather/tour tools -4. **User Chat uses WebSocket** (STOMP) for real-time messaging -5. **Global exception handling** via `GlobalExceptionHandler.kt` - just throw exceptions, they're caught automatically +4. **User Chat uses WebSocket** with profile-based configuration: + - **Dev**: SimpleBroker (in-memory, single server) + - **Prod**: RabbitMQ STOMP Relay (scalable, multi-server) +5. **Port-Adapter pattern** for message publishing (`ChatMessagePublisher` interface with `SimpleChatMessagePublisher` and `RabbitChatMessagePublisher` implementations) +6. **Global exception handling** via `GlobalExceptionHandler.kt` - just throw exceptions, they're caught automatically ### Critical Configuration Files - **build.gradle.kts**: Contains BuildConfig plugin that generates constants from YAML files (area-codes.yml, prompts.yml, etc.) -- **application.yml**: Dev config with H2, Redis optional, OAuth2 providers +- **application.yml**: Dev config with H2, Redis optional, OAuth2 providers, RabbitMQ for local testing +- **application-prod.yml**: Production config with PostgreSQL, Redis required, RabbitMQ with connection stability settings - **SecurityConfig.kt**: Currently allows all requests for dev (MUST restrict for production) - **AiConfig.kt**: Spring AI ChatClient with OpenRouter (uses OPENROUTER_API_KEY env var) +- **DevConfig.kt**: Auto-generates 2 dummy GUIDE users on startup (dev profile only) + +### Profile-Based Configuration Strategy + +The application uses Spring profiles (`@Profile` annotation) to switch implementations: + +**Development Profile (`dev`):** +- H2 in-memory database +- `SimpleChatMessagePublisher` - uses Spring's SimpleBroker (no RabbitMQ needed) +- `UserChatSimpleWebSocketConfig` - basic WebSocket with in-memory broker +- Redis optional (session.store-type: none) +- Dummy guide data auto-generation + +**Production Profile (`prod`):** +- PostgreSQL database +- `RabbitChatMessagePublisher` - publishes to RabbitMQ +- `UserChatRabbitWebSocketConfig` - STOMP Broker Relay to RabbitMQ +- Redis required (session.store-type: redis) +- Connection stability settings (timeouts, heartbeats) + +When adding new features that differ between dev/prod, follow this pattern: +1. Create an interface in the domain layer +2. Create separate implementations with `@Profile("dev")` and `@Profile("prod")` +3. Inject via the interface, Spring will wire the correct implementation ## Working with Spring AI @@ -121,6 +171,44 @@ fun getTourSpots(area: String): TourResponse **Important**: System prompts are managed in `src/main/resources/prompts.yml` and compiled into BuildConfig at build time. +## WebSocket & Real-Time Messaging + +### Architecture +User-to-user chat uses WebSocket with STOMP protocol. The implementation switches based on profile: + +**Development**: Uses Spring's SimpleBroker (in-memory) +- Suitable for single-server development +- No external dependencies +- Messages stored in memory only + +**Production**: Uses RabbitMQ STOMP Relay +- Scales across multiple server instances +- Messages persist in RabbitMQ +- Handles reconnection and failover + +### Message Flow +1. Client connects to WebSocket endpoint: `/ws/userchat` +2. Client sends message to: `/pub/chat/send` +3. Server processes and publishes to: `/topic/chat/{roomId}` +4. `ChatMessagePublisher` interface abstracts the publishing mechanism +5. Messages are persisted to database via `ChatMessageService` + +### RabbitMQ Configuration +Located in `application.yml` and `application-prod.yml`: +```yaml +spring: + rabbitmq: + host: ${RABBITMQ_HOST} + port: ${RABBITMQ_PORT} + username: ${RABBITMQ_USERNAME} + password: ${RABBITMQ_PASSWORD} + stomp-port: ${RABBITMQ_STOMP_PORT} # Default: 61613 +``` + +RabbitMQ requires two plugins enabled: +- `rabbitmq_management` - Management UI (port 15672) +- `rabbitmq_stomp` - STOMP protocol support (port 61613) + ## Testing Strategy Tests are in `src/test/kotlin` mirroring the main structure: @@ -152,7 +240,7 @@ Always run `./gradlew ktlintCheck` before committing - it's enforced by git hook - **Development**: H2 in-memory (jdbc:h2:mem:testdb), resets on restart - **Production**: PostgreSQL (configured in application-prod.yml) -- **JPA Strategy**: `ddl-auto: create-drop` in dev (wipes DB on restart) +- **JPA Strategy**: `ddl-auto: create-drop` in dev (wipes DB on restart), `update` in prod Main entities: - `User` - OAuth users with roles (GUEST/GUIDE/ADMIN) @@ -166,7 +254,7 @@ Required `.env` file (copy from .env.example): ```bash # AI (Required) OPENROUTER_API_KEY=sk-or-v1-... -OPENROUTER_MODEL=anthropic/claude-3.5-sonnet +OPENROUTER_MODEL=z-ai/glm-4.5-air:free # OAuth (Required for auth) GOOGLE_CLIENT_ID=... @@ -183,7 +271,14 @@ TOUR_API_KEY=... # JWT (Required for production) CUSTOM__JWT__SECRET_KEY=... -# Redis (Optional - caching) +# RabbitMQ (Required for prod WebSocket) +RABBITMQ_HOST=localhost +RABBITMQ_PORT=5672 +RABBITMQ_USERNAME=admin +RABBITMQ_PASSWORD=qwpokd153098 +RABBITMQ_STOMP_PORT=61613 + +# Redis (Optional in dev, required in prod) REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD= @@ -217,6 +312,33 @@ Common exceptions mapped to HTTP status: 5. **Commit**: `{type}(scope): summary` (e.g., `feat(be): Add weather caching`) 6. **PR title**: `{type}(scope): summary (#{issue})` (e.g., `feat(be): Add weather caching (#42)`) +## Infrastructure & Deployment + +### Terraform Infrastructure (`infra/main.tf`) +EC2 instance setup with: +1. Docker & docker-compose installation +2. Container network (`common`) +3. Nginx Proxy Manager (ports 80, 443, 81) +4. Redis (port 6379) +5. PostgreSQL 16 (port 5432) +6. RabbitMQ with management & STOMP plugins (ports 5672, 15672, 61613) + +**Order matters**: Docker β†’ docker-compose β†’ network β†’ containers + +### CI/CD Pipeline (`.github/workflows/deploy.yml`) +Blue-Green deployment strategy: +1. Build on GitHub Actions runner +2. Transfer JAR to EC2 +3. Deploy to blue/green container based on availability +4. Health check before switching traffic +5. Environment variables injected via `docker run -e` + +### Docker Compose Files +- `docker-compose.yml` (root) - Local development RabbitMQ +- `infra/rabbitmq-docker-compose.yml` - EC2 production RabbitMQ template + +Both require `common` network to be pre-created: `docker network create common` + ## Common Issues & Solutions ### Build fails with "BuildConfig not found" @@ -228,13 +350,20 @@ Common exceptions mapped to HTTP status: - Start Redis: `docker run -d -p 6379:6379 --name redis redis:alpine` - Check: `docker logs redis` +### RabbitMQ connection issues +- In dev: RabbitMQ is optional (SimpleBroker used instead) +- In prod: RabbitMQ is required for WebSocket +- Start local RabbitMQ: `docker-compose up -d` +- Check logs: `docker logs rabbitmq-1` +- Verify plugins: `rabbitmq_management` and `rabbitmq_stomp` must be enabled + ### ktlint failures - Auto-fix: `./gradlew ktlintFormat` - Pre-commit hook enforces this - setup via `./setup-git-hooks.sh` ### Spring AI errors - Verify `OPENROUTER_API_KEY` in .env -- Check model name matches OpenRouter API (currently: anthropic/claude-3.5-sonnet) +- Check model name matches OpenRouter API (currently: z-ai/glm-4.5-air:free) - Logs show AI requests: `logging.level.org.springframework.ai: DEBUG` ### OAuth login fails @@ -242,6 +371,12 @@ Common exceptions mapped to HTTP status: - Check redirect URIs match OAuth provider settings - Dev: `http://localhost:8080/login/oauth2/code/{provider}` +### WebSocket connection fails +- Check profile: dev uses SimpleBroker, prod uses RabbitMQ +- In prod: Ensure RabbitMQ is running and accessible +- Verify STOMP port (61613) is open and reachable +- Check `UserChatStompAuthChannelInterceptor` for authentication issues + ## Important Notes - **Never commit .env** - it's gitignored, contains secrets @@ -250,3 +385,8 @@ Common exceptions mapped to HTTP status: - **Global config in common/** - don't duplicate security/config in domains - **BuildConfig is generated** - don't edit manually, modify YAML sources - **Redis is optional in dev** - required for production caching/sessions +- **RabbitMQ is optional in dev** - required for production WebSocket scaling +- **Profile-based beans** - use `@Profile` to switch implementations between dev/prod +- **Dummy data in dev** - 2 guide users auto-generated on startup (DevConfig.kt) +- **Docker network required** - `common` network must exist before running docker-compose +- **Terraform order matters** - Docker installation before docker-compose, containers after both diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4786d61..661b05d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -11,6 +11,7 @@ spring: exclude: - org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration # Redis 없어도 μ‹€ν–‰ κ°€λŠ₯ν•˜λ„λ‘ λ³€κ²½ - org.springframework.boot.autoconfigure.session.SessionAutoConfiguration # Redis 없어도 μ‹€ν–‰ κ°€λŠ₯ν•˜λ„λ‘ λ³€κ²½ + - org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration # RabbitMQ 없어도 μ‹€ν–‰ κ°€λŠ₯ν•˜λ„λ‘ λ³€κ²½ (dev용) config: import: - "optional:file:.env[.properties]"