Expand JH ERP DB port incident work log

This commit is contained in:
2026-09-22 08:54:29 +09:00
parent 1157d3b9dd
commit c0df76b9a9
@@ -6,6 +6,106 @@
- 상태: 운영 서버 측 포트 발행 및 DB 정상 상태 확인 완료 - 상태: 운영 서버 측 포트 발행 및 DB 정상 상태 확인 완료
외부 동기화 서버에서의 최종 TCP/계정 접속 시험은 별도 재확인 필요 외부 동기화 서버에서의 최종 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. 요청 배경 ## 1. 요청 배경
통합 인사정보 동기화를 위해 다음 경로의 MySQL 접속이 필요했다. 통합 인사정보 동기화를 위해 다음 경로의 MySQL 접속이 필요했다.