From c0df76b9a9ee935c074c4ece12a1233eb8314656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=AC=B8=ED=98=95=EC=84=9D?= Date: Tue, 22 Sep 2026 08:54:29 +0900 Subject: [PATCH] Expand JH ERP DB port incident work log --- ..._DB_3306_포트_발행_장애_조치_2026-09-22.md | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/JH_ERP/05_운영_DB_3306_포트_발행_장애_조치_2026-09-22.md b/JH_ERP/05_운영_DB_3306_포트_발행_장애_조치_2026-09-22.md index 4871311..13372e0 100644 --- a/JH_ERP/05_운영_DB_3306_포트_발행_장애_조치_2026-09-22.md +++ b/JH_ERP/05_운영_DB_3306_포트_발행_장애_조치_2026-09-22.md @@ -6,6 +6,106 @@ - 상태: 운영 서버 측 포트 발행 및 DB 정상 상태 확인 완료 외부 동기화 서버에서의 최종 TCP/계정 접속 시험은 별도 재확인 필요 +## 작업 파일·설정·실행 순서 요약 + +### 변경하거나 만든 파일 + +| 파일 | 이번 작업에서 한 일 | 운영 반영 효과 | +| --- | --- | --- | +| `docker-compose.prod.yml` | DB의 `10.0.10.110:3306:3306` 포트 바인딩과 `db-publish` 네트워크를 선언 | Docker가 운영 서버의 3306을 실제로 DB 컨테이너로 전달 | +| `.gitea/workflows/one_time_prod_db_port_3306.yml` | DB만 안전하게 적용하는 수동 Actions 워크플로 생성·보강 | 앱 배포 없이 DB만 재생성하고 실패 시 기존 Compose 설정으로 복구 | +| `.gitea/workflows/diagnose_prod_db_port_3306.yml` | 포트 발행을 읽기 전용으로 진단하는 워크플로 생성·보강 | DB를 변경하지 않고 Docker 설정값과 실제 포트/NAT 상태의 불일치를 확인 | +| `docker-compose.yml` | **변경하지 않음**. DB가 `backend`에만 연결되고 `backend`가 `internal: true`임을 원인 분석에 사용 | 기존 네트워크 구조의 문제를 확인 | +| `.gitea/workflows/prod_code_pull.yml` | **변경하지 않음**. 일반 PHP 운영 배포용 | 최종 조치에는 사용하지 않음. 최신 PHP/웹 배포 없이 DB만 적용 | + +### Compose 설정이 어떻게 바뀌었는가 + +#### 1차 설정: 포트 선언만 추가 — 실패 + +`docker-compose.prod.yml`의 DB 설정에 최초로 추가한 내용은 아래였다. + +```yaml +db: + restart: unless-stopped + ports: + - "10.0.10.110:3306:3306" +``` + +의미는 “운영 서버 IP `10.0.10.110`의 TCP 3306을 컨테이너 내부 MySQL 3306으로 전달하라”는 것이다. + +그러나 DB가 내부 Docker 네트워크인 `backend`에만 붙어 있어, 이 선언만으로는 실제 호스트 포트가 만들어지지 않았다. + +#### 최종 설정: DB 전용 일반 bridge 네트워크 추가 — 성공 + +최종적으로 같은 `docker-compose.prod.yml`의 `db`에 아래 설정을 추가했다. + +```yaml +services: + db: + restart: unless-stopped + ports: + - "10.0.10.110:3306:3306" + networks: + - backend + - db-publish + +networks: + db-publish: +``` + +- `backend`는 그대로 유지했다. 웹 ↔ DB의 기존 내부 통신과 격리는 변경하지 않았다. +- `db-publish`는 DB 컨테이너만 연결되는 **비내부 일반 bridge 네트워크**다. +- Compose가 실제로 만든 네트워크 이름은 `jang-erp-production_db-publish`이다. +- 이 두 번째 네트워크가 생기면서 Docker가 실제 `10.0.10.110:3306` 포트/NAT 매핑을 만들 수 있었다. +- `0.0.0.0:3306`은 사용하지 않았다. 즉 모든 인터페이스가 아니라 지정된 운영 LAN IP에서만 수신한다. + +### 실제 작업 순서와 각 단계의 결과 + +| 순서 | 수행 내용 | 사용 파일/도구 | 결과 | +| --- | --- | --- | --- | +| 1 | 한맥 서버의 TCP 3306 접속 실패 확인 | `Test-NetConnection 10.0.10.110 -Port 3306` | Ping 성공, TCP 실패. DB 계정이 아니라 포트 수신 문제로 판정 | +| 2 | 운영 Compose 구조 확인 | `docker-compose.yml`, `docker-compose.prod.yml` | DB는 `backend`에만 연결, `backend: internal: true` 확인 | +| 3 | 1차 포트 바인딩 선언 추가 | `docker-compose.prod.yml` | 설정은 운영 서버에 반영됨 | +| 4 | DB만 첫 재생성 | 초기 DB 전용 Actions 실행 #1286 | DB는 healthy였지만 실제 3306 listener 검증 실패 | +| 5 | 읽기 전용 진단 | `.gitea/workflows/diagnose_prod_db_port_3306.yml` | `HostConfig`에는 포트가 있으나 `NetworkSettings.Ports`가 null임을 확인 | +| 6 | 원인 확정 | Docker network inspect 및 포트/NAT 검사 | DB가 internal network에만 연결되어 실제 publish가 만들어지지 않음 | +| 7 | 최종 Compose 수정 | `docker-compose.prod.yml` | `db-publish` 네트워크를 DB에 추가 | +| 8 | DB 전용 적용 절차 보강 | `.gitea/workflows/one_time_prod_db_port_3306.yml` | 최신 control 파일 전달, 사전 검증, DB 단독 재생성, 실패 복구를 포함 | +| 9 | 최종 적용 | Actions 실행 #1310 | `jang-erp-production_db-publish` 생성, DB healthy, `10.0.10.110:3306` 실제 발행 확인 | + +### DB 전용 적용 워크플로의 실제 동작 + +`.gitea/workflows/one_time_prod_db_port_3306.yml`은 다음 순서로 동작하도록 만들었다. + +1. 실행자가 확인 문구 `APPLY-PROD-DB-PORT-3306`을 정확히 입력했는지 확인한다. +2. 최신 `main`에서 `docker-compose.prod.yml`만 가져와 SHA-256 checksum을 계산한다. +3. Gitea에 등록된 운영 SSH 인증정보로 운영 서버에 접속한다. +4. 운영 서버에서 `APP_ENV=production`, Compose 프로젝트명, 현재 DB 컨테이너, DB health, 기존 데이터 볼륨을 검사한다. +5. 현재 DB 볼륨이 기대한 운영 볼륨과 다르면 **즉시 중단**한다. +6. 새 Compose override를 임시 파일로 전송한 뒤, 실제 파일을 바꾸기 전에 `docker compose config --quiet` 검증을 수행한다. +7. 배포 lock을 획득한 뒤 새 override를 적용하고, `db` 서비스만 `--no-deps --force-recreate`로 재생성한다. +8. DB health가 `healthy`가 될 때까지 대기한다. +9. 아래 네 가지를 모두 만족해야 성공 처리한다. + - 설정 요청값: `HostConfig.PortBindings = 10.0.10.110:3306` + - 실제 Docker 매핑: `NetworkSettings.Ports = 10.0.10.110:3306` + - 운영 서버 리스너: `ss`에서 `10.0.10.110:3306` 확인 + - 운영 서버 자체 TCP 접속 성공 +10. 7~9단계에서 실패하면 기존 `docker-compose.prod.yml`을 복원하고 DB도 기존 구성으로 다시 재생성하도록 구성했다. + +### 최초 시도가 실패한 직접적인 이유 + +첫 시도는 `ports:` 한 줄만으로 포트가 열릴 것이라고 예상한 점이 문제였다. + +``` +요청 설정(HostConfig)에는 10.0.10.110:3306이 있음 + ↓ +실제 네트워크 설정(NetworkSettings.Ports)은 null + ↓ +docker port 결과 없음 / ss listener 없음 / TCP 연결 거부 +``` + +즉, 컨테이너가 healthy라고 해서 호스트 포트까지 열렸다는 뜻은 아니다. 최종 해결은 DB를 재초기화하거나 MySQL 설정을 바꾸는 것이 아니라, **DB에 포트 발행이 가능한 Docker bridge 네트워크를 추가하는 것**이었다. + ## 1. 요청 배경 통합 인사정보 동기화를 위해 다음 경로의 MySQL 접속이 필요했다.