diff --git a/docs/20260914_배포검증readiness및deployment엔드포인트/plan-task.md b/docs/20260914_배포검증readiness및deployment엔드포인트/plan-task.md new file mode 100644 index 00000000..ab02ea61 --- /dev/null +++ b/docs/20260914_배포검증readiness및deployment엔드포인트/plan-task.md @@ -0,0 +1,144 @@ +# OCI Blue/Green 배포 검증 endpoint 구현 계획 + +| 문서 항목 | 내용 | +|---|---| +| 상태 | 구현 완료 | +| 작성일 | 2026-09-14 | +| 요구사항 기준 | `docs/20260914_배포검증readiness및deployment엔드포인트/prd.md` | +| 현재 Phase | Phase 1 배포 검증 endpoint | +| 현재 활성 Goal | 없음 | + +## 목표 + +배포 스크립트가 애플리케이션 포트에서 readiness를, management 포트에서 readiness와 배포 식별자를 확인할 수 있다. + +## 현재 상태 + +| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 | +|---:|---|---:|---|---| +| 1 | 완료 | `4/4` | 없음 | 없음 | + +## 범위 + +### 포함 + +- `spring-boot-starter-actuator` 의존성 추가. +- `application.yml`(main/test)의 포트·probe·exposure·deployment property 설정. +- `DeploymentEndpoint` custom Actuator endpoint 구현. +- `SecurityConfig`의 `/readyz` permitAll 최소 변경. +- 위 동작을 검증하는 통합 test. + +### 제외 + +- Kubernetes 의존성·설정. +- 배포 스크립트(`scripts/`, `appspec.yml`) 변경. +- 신규 추상화/라이브러리, 기존 API 변경. + +## 기술적 제약 + +- 기술 스택: Kotlin 1.6.21, Spring Boot 2.7.14, Gradle Kotlin DSL, JUnit 5. +- 배포 식별자는 property placeholder만 사용하고 별도 환경변수 파싱·fallback 코드를 만들지 않는다. +- 응답 필드명은 snake_case 고정(`application_artifact_version`, `config_commit`). +- 검증: 신규 test 우선 실행 후 `./gradlew test` 전체 회귀(공통 보안·설정 변경이므로 전체 회귀 필요). + +## Phase 1 배포 검증 endpoint + +**Phase 결과:** readiness와 deployment endpoint가 지정된 포트 경계에 맞게 동작한다. + +**선행조건:** 없음. + +**Phase 완료 조건:** `P1-T1`~`P1-T4` 완료, `./gradlew test`와 `./gradlew bootJar` 성공 기록. + +### 구현 항목 + +#### Task 1.1 Actuator 의존성과 기본 설정 + +**Goal 실행 `P1-T1`:** Actuator를 추가하고 포트·exposure·deployment property 계약을 설정에 반영한다. + +- **시작 조건:** PRD `DEPLOY-001`, `DEPLOY-002`, `DEPLOY-005`. +- **완료 증거:** 설정 파일 diff와 애플리케이션 컨텍스트 기동 test 통과. +- **범위 밖:** endpoint 구현, security 변경. + +**Files:** + +- Modify: `build.gradle.kts` +- Modify: `src/main/resources/application.yml` +- Modify: `src/test/resources/application.yml` +- Test: `src/test/kotlin/kr/co/vividnext/sodalive/deployment/DeploymentEndpointIntegrationTest.kt` +- Test: `src/test/kotlin/kr/co/vividnext/sodalive/deployment/DeploymentEndpointInjectedValueIntegrationTest.kt` + +- [x] **RED:** `DeploymentEndpointIntegrationTest`에서 management 포트 readiness/deployment 호출 test를 작성했다. +- [x] **RED 확인:** Actuator 부재로 `Could not resolve placeholder 'local.management.port'` 실패를 확인했다. +- [x] **GREEN:** `spring-boot-starter-actuator`와 포트·exposure·deployment 설정을 추가했다. +- [x] **GREEN 확인:** `./gradlew test --tests 'kr.co.vividnext.sodalive.deployment.*'` 통과. +- [x] **REFACTOR:** deprecated `LocalServerPort` import 정리, `./gradlew ktlintCheck` 성공. + +#### Task 1.2 readiness 접근 허용 + +**Goal 실행 `P1-T2`:** `/readyz`와 management readiness가 인증 없이 200을 반환한다. + +- **시작 조건:** `P1-T1` 완료. +- **완료 증거:** readiness test 통과. +- **범위 밖:** 기타 endpoint의 security 정책 변경. + +**Files:** + +- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt` +- Test: `src/test/kotlin/kr/co/vividnext/sodalive/deployment/DeploymentEndpointIntegrationTest.kt` + +- [x] **RED:** `/readyz` 200 기대 test 작성(`DeploymentEndpointIntegrationTest`). +- [x] **RED 확인:** Actuator/보안 미적용 상태 실패 확인. +- [x] **GREEN:** `SecurityConfig`에 `/readyz` permitAll과 `EndpointRequest.toAnyEndpoint()` permitAll 추가. +- [x] **GREEN 확인:** readiness test 2건 통과. +- [x] **REFACTOR:** 추가 변경 없음. + +#### Task 1.3 deployment custom Actuator endpoint + +**Goal 실행 `P1-T3`:** `/actuator/deployment`가 snake_case 배포 식별자를 반환한다. + +- **시작 조건:** `P1-T1` 완료. +- **완료 증거:** 기본값·주입값 test 통과. +- **범위 밖:** 추가 필드. + +**Files:** + +- Create: `src/main/kotlin/kr/co/vividnext/sodalive/deployment/DeploymentEndpoint.kt` +- Test: `src/test/kotlin/kr/co/vividnext/sodalive/deployment/DeploymentEndpointIntegrationTest.kt` + +- [x] **RED:** 기본값(`local`, 40자리 zero)과 주입값 검증 test 작성. +- [x] **RED 확인:** endpoint 미구현 실패 확인. +- [x] **GREEN:** `@Endpoint(id = "deployment")` + `@ReadOperation` 구현. +- [x] **GREEN 확인:** 기본값·주입값 test 통과. +- [x] **REFACTOR:** `./gradlew ktlintCheck` 성공. + +#### Task 1.4 포트 접근 경계 검증 + +**Goal 실행 `P1-T4`:** 애플리케이션 포트에서 `/actuator/deployment`가 노출되지 않음을 검증한다. + +- **시작 조건:** `P1-T3` 완료. +- **완료 증거:** 애플리케이션 포트 접근 test 통과. +- **범위 밖:** 그 외 경로 노출 정책. + +**Files:** + +- Test: `src/test/kotlin/kr/co/vividnext/sodalive/deployment/DeploymentEndpointIntegrationTest.kt` + +- [x] **RED:** 애플리케이션 포트 접근이 200이 아님을 기대하는 test 작성. +- [x] **RED 확인:** 초기 404 기대 단정이 `401 UNAUTHORIZED`로 실패함을 확인. +- [x] **GREEN:** management 포트 분리로 애플리케이션 포트에서는 endpoint가 매핑되지 않고 401로 차단됨을 확인, 단정을 "200 아님 + 배포 식별자 미포함" 계약으로 정정. +- [x] **GREEN 확인:** 해당 test 통과. +- [x] **REFACTOR:** `./gradlew test` 전체 회귀 성공. + +### Phase Gate `P1-GATE` + +- [x] `./gradlew test` 성공. +- [x] `./gradlew bootJar` 성공 및 실행 가능한 JAR 생성 확인. + +## 검증 기록 + +- 2026-09-14 `./gradlew test --tests 'kr.co.vividnext.sodalive.deployment.*'` → RED 5 failed → GREEN 5 passed. +- 2026-09-14 `./gradlew ktlintCheck` → BUILD SUCCESSFUL. +- 2026-09-14 `./gradlew test` → BUILD SUCCESSFUL (전체 회귀). +- 2026-09-14 `./gradlew bootJar` → `build/libs/sodalive-0.0.1-SNAPSHOT.jar`(141MB), `Main-Class: org.springframework.boot.loader.JarLauncher` 확인. +- 참고: 기존 test 실행에는 `JWT_SECRET` 환경변수가 필요하며 이는 이번 변경 이전부터 동일한 전제다. +- 참고: 애플리케이션 포트에서 `/actuator/deployment`는 404가 아니라 401로 차단된다. `EndpointRequest` matcher가 management 포트 분리 시 애플리케이션 포트 요청에 매칭되지 않기 때문이며, 노출되지 않는다는 요구는 충족한다. diff --git a/docs/20260914_배포검증readiness및deployment엔드포인트/prd.md b/docs/20260914_배포검증readiness및deployment엔드포인트/prd.md new file mode 100644 index 00000000..4c2f80e7 --- /dev/null +++ b/docs/20260914_배포검증readiness및deployment엔드포인트/prd.md @@ -0,0 +1,101 @@ +# OCI Blue/Green 배포 검증용 readiness 및 deployment endpoint PRD + +## 문서 정보 + +| 항목 | 내용 | +|---|---| +| 문서 상태 | 구현 완료 | +| 작성일 | 2026-09-14 | +| 최종 수정일 | 2026-09-14 | +| 대상 제품 | sodalive server 배포 검증 endpoint | +| 작성자·결정권자 | 요청 사용자 | +| 관련 구현 계획 | `docs/20260914_배포검증readiness및deployment엔드포인트/plan-task.md` | +| 관련 review | 없음 | + +## 1. Overview + +OCI Blue/Green 배포에서 배포 스크립트가 새로 기동한 인스턴스의 기동 완료 여부와, 실제로 어떤 artifact/config가 올라갔는지를 +HTTP로 확인할 수 있어야 한다. 이를 위해 Spring Boot Actuator의 readiness probe와 배포 식별 정보를 반환하는 custom Actuator +endpoint를 제공한다. + +## 2. Problem Statement + +- 현재 프로젝트에는 Actuator 의존성과 health endpoint가 없어 배포 스크립트가 기동 완료를 판정할 방법이 없다. +- 어떤 artifact 버전과 config commit이 배포되었는지 런타임에서 확인할 수 없다. +- 모든 요청이 Spring Security의 `anyRequest().authenticated()`로 보호되어 있어 인증 없는 probe 호출이 불가능하다. + +해결 판단 기준: 배포 스크립트가 인증 없이 `GET :8080/readyz`로 기동을 판정하고, 운영 전용 포트에서 +`GET :8082/actuator/deployment`로 배포 식별자를 확인할 수 있다. + +## 3. Goals + +- 애플리케이션 포트(8080)에서 인증 없이 readiness 확인 가능. +- Management 포트(8082)에서 readiness와 배포 식별 정보 확인 가능. +- 배포 식별자는 환경변수로 주입하고, 환경변수가 없어도 로컬/테스트 기동이 가능해야 한다. + +## 4. Non-Goals + +- Kubernetes 관련 의존성·설정 추가. +- liveness 기반 자동 재기동 정책, 배포 스크립트 자체 변경. +- health details/components 공개, 신규 추상화 계층 도입. + +## 5. 권한 + +- 인증 주체: 없음(배포 스크립트/로드밸런서). +- `/readyz`: 애플리케이션 포트에서 인증 없이 허용. +- `/actuator/**`: management 포트(8082)에서만 노출. 애플리케이션 포트에서는 노출하지 않는다. +- `/readyz` 응답에는 민감 정보나 배포 식별자를 포함하지 않는다. + +## 6. 기능 요구사항 + +| ID | 상태 | 요구사항 | 수용 기준 | Goal 연결 | +|---|---|---|---|---| +| `DEPLOY-001` | 확정 | `spring-boot-starter-actuator` 의존성을 추가한다(중복 추가 금지). | `build.gradle.kts`에 1회만 존재 | `P1-T1` | +| `DEPLOY-002` | 확정 | 애플리케이션 포트 8080, management 포트 8082로 분리한다. | `application.yml`에 `server.port: 8080`, `management.server.port: 8082` | `P1-T1` | +| `DEPLOY-003` | 확정 | health probes와 additional path를 활성화해 `/readyz`를 제공한다. | `GET :8080/readyz` → 200 | `P1-T2` | +| `DEPLOY-004` | 확정 | management 포트에서 readiness를 제공한다. | `GET :8082/actuator/health/readiness` → 200 | `P1-T2` | +| `DEPLOY-005` | 확정 | web exposure는 `health`, `deployment`만 포함하고 health details/components는 공개하지 않는다. | `management.endpoints.web.exposure.include: health,deployment`, `show-details: never`, `show-components: never` | `P1-T1` | +| `DEPLOY-006` | 확정 | `DEPLOY_ARTIFACT_VERSION`/`DEPLOY_CONFIG_COMMIT`을 property placeholder로 연결한다. | 환경변수 없으면 `local`, 40자리 zero commit | `P1-T3` | +| `DEPLOY-007` | 확정 | `/actuator/deployment`를 custom Actuator endpoint로 구현한다. | REST controller가 아닌 `@Endpoint` 구현체 존재 | `P1-T3` | +| `DEPLOY-008` | 확정 | 응답 필드는 정확히 `application_artifact_version`, `config_commit`이다. | snake_case JSON 검증 test 통과 | `P1-T3` | +| `DEPLOY-009` | 확정 | 애플리케이션 포트에서 `/actuator/deployment`가 노출되지 않는다. | `GET :8080/actuator/deployment` → 200 아님 | `P1-T4` | +| `DEPLOY-010` | 확정 | Security 정책 최소 변경으로 readiness가 401/403/redirect 되지 않는다. | `SecurityConfig`에 `/readyz` permitAll 추가 | `P1-T2` | + +## 7. API 계약 + +### 7.1 `GET :8080/readyz` + +- 인증 불필요. 응답 body는 Actuator 기본 health 응답(`{"status":"UP"}`), 세부 정보 비공개. + +### 7.2 `GET :8082/actuator/health/readiness` + +- 응답: `{"status":"UP"}`. + +### 7.3 `GET :8082/actuator/deployment` + +```json +{ + "application_artifact_version": "local", + "config_commit": "0000000000000000000000000000000000000000" +} +``` + +## 8. 성공 기준 + +- [x] `GET :8080/readyz`가 200을 반환한다. (`DEPLOY-003`, `DEPLOY-010`) +- [x] `GET :8082/actuator/health/readiness`가 200을 반환한다. (`DEPLOY-004`) +- [x] `GET :8082/actuator/deployment`가 주입값을 snake_case JSON으로 반환한다. (`DEPLOY-006`~`DEPLOY-008`) +- [x] `GET :8080/actuator/deployment`가 200을 반환하지 않는다(401로 차단). (`DEPLOY-009`) +- [x] `./gradlew test`, `./gradlew bootJar` 성공. + +## 9. Open Questions + +없음. + +## 10. Decision Log + +| 날짜 | ID | 상태 | 결정 | 근거 | 영향 요구사항 | +|---|---|---|---|---|---| +| 2026-09-14 | `DEC-001` | 확정 | Kubernetes 의존성 없이 Actuator 기본 probe만 사용 | 사용자 요구사항 범위 제한 | `DEPLOY-003`, `DEPLOY-004` | +| 2026-09-14 | `DEC-002` | 확정 | 배포 식별자는 별도 파싱 코드 없이 Spring property placeholder로만 연결 | 사용자 요구사항, 단순성 유지 | `DEPLOY-006` | +| 2026-09-14 | `DEC-003` | 확정 | `/actuator/deployment`는 `@Endpoint` 기반 custom Actuator endpoint로 구현 | 사용자 요구사항(REST controller 금지) | `DEPLOY-007` |