Add JH ERP production DB port incident record

This commit is contained in:
2026-09-22 08:47:59 +09:00
parent 9a7eb6d969
commit 1157d3b9dd
@@ -0,0 +1,187 @@
# 주)장헌 운영 DB 3306 포트 발행 장애 조치 기록
- 조치일: 2026-09-22
- 대상: 주)장헌 ERP 운영 MySQL 컨테이너
- 목적: 외부 동기화 서버가 운영 DB의 MySQL(3306)에 접속할 수 있도록 제한된 호스트 포트를 발행
- 상태: 운영 서버 측 포트 발행 및 DB 정상 상태 확인 완료
외부 동기화 서버에서의 최종 TCP/계정 접속 시험은 별도 재확인 필요
## 1. 요청 배경
통합 인사정보 동기화를 위해 다음 경로의 MySQL 접속이 필요했다.
| 출발지 | 접속 대상 | 용도 |
| --- | --- | --- |
| 한맥 서버 `10.0.10.31` | `10.0.10.110:3306` | 내부망 동기화 |
| 한맥클라우드 `27.96.135.161` | `118.220.172.202:3306` | 공인 IP/VIP를 통한 동기화 |
방화벽 정책은 담당자가 MySQL 서비스(3306)를 허용하고, 클라우드 경로에는 공인 IP/VIP를 사용하도록 구성했다. 애플리케이션 컨테이너 내부 DB는 정상 동작했지만, 외부에서 `10.0.10.110:3306`으로 TCP 접속하면 거부되었다.
## 2. 최초 증상
한맥 서버에서 다음 결과가 확인되었다.
```powershell
Test-NetConnection 10.0.10.110 -Port 3306
# PingSucceeded : True
# TcpTestSucceeded : False
```
PHP에서도 다음과 같은 연결 오류가 발생했다.
```
Can't connect to MySQL server on '10.0.10.110' (10061)
```
이는 DB 계정 또는 비밀번호 오류가 아니라, 운영 서버의 TCP 3306 수신 포트가 실제로 열리지 않은 상태를 뜻한다.
## 3. 최초 적용과 실패 양상
운영 Compose override에 아래 포트 바인딩을 추가하고 DB 컨테이너만 재생성했다.
```yaml
db:
restart: unless-stopped
ports:
- "10.0.10.110:3306:3306"
```
DB 컨테이너는 정상적으로 재생성되었고 MySQL health check도 통과했다. 그러나 최종 포트 검증은 실패했다.
| 점검 항목 | 결과 |
| --- | --- |
| DB 컨테이너 health | `healthy` |
| 기존 운영 DB 데이터 볼륨 | 정상 유지 |
| `HostConfig.PortBindings` | `10.0.10.110:3306` 설정값 존재 |
| `NetworkSettings.Ports` | `3306/tcp: null` |
| `docker port jang-prod-db 3306/tcp` | 공개 포트 없음 |
| 운영 서버 `ss`의 3306 리스너 | 없음 |
| 운영 서버 자체 TCP 접속 | 실패 |
| 컨테이너 내부 `mysqladmin ping` | 성공 |
즉, Compose 설정에는 포트 요청이 기록되었지만 Docker가 실제 호스트 포트/NAT 규칙을 만들지 않은 상태였다.
## 4. 실패 원인
DB는 아래처럼 `internal: true``backend` 네트워크에만 연결되어 있었다.
```yaml
networks:
backend:
internal: true
```
Docker는 컨테이너가 내부 bridge 네트워크에만 연결된 경우 포트 바인딩 요청을 컨테이너 설정에 남기더라도, 실제 공개 포트 매핑을 만들지 않을 수 있다. 이번 장애의 증거가 바로 다음 조합이었다.
- `HostConfig.PortBindings`에는 `3306` 설정이 있음
- 실제 `NetworkSettings.Ports``null`
- `docker port`, `ss`, 운영 서버 자기 접속이 모두 실패
- DB 자체는 healthy
따라서 방화벽, DB 기동, 계정 권한보다 먼저 Docker 네트워크 구성 자체를 수정해야 했다.
## 5. 최종 해결 방법
기존 `backend` 네트워크의 내부 격리는 유지하고, **DB에만** 일반 bridge 네트워크 `db-publish`를 추가했다.
```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 경로를 제공한다.
- `10.0.10.110:3306`: 운영 서버의 지정 IP에서만 MySQL을 수신한다. `0.0.0.0:3306`으로 전체 인터페이스를 열지 않는다.
- 방화벽/VIP: 허용된 출발지와 공인 IP 경로만 별도로 제어한다.
## 6. 실제 적용 절차
일반 PHP 운영 배포는 실행하지 않았다. DB 전용 Actions 워크플로만 사용했다.
1. 최신 `main`의 DB 전용 Compose control 파일을 확인했다.
2. 현재 운영 DB 컨테이너의 Compose 프로젝트, health 상태, 데이터 볼륨을 사전 검사했다.
3. 현재 운영 DB 데이터 볼륨이 예상된 운영 볼륨과 일치하는지 확인했다.
4. DB 전용 control 파일을 임시 파일로 올리고 Compose 구성을 검증했다.
5. `jang-erp-production_db-publish` 네트워크를 생성했다.
6. DB 컨테이너만 `--no-deps --force-recreate db`로 재생성했다.
7. DB health, Docker 실제 포트 매핑, 호스트 3306 listener, 운영 서버 자체 TCP 접속을 검증했다.
8. 검증 실패 시 이전 Compose override를 복원하고 DB 구성도 이전 상태로 재생성하도록 절차를 구성했다.
실행 결과:
```
Network jang-erp-production_db-publish Created
Container jang-prod-db Recreated
Container jang-prod-db Started
production_db_port_apply=success
db_health=healthy
db_port_binding=10.0.10.110:3306
```
DB 데이터 초기화, 볼륨 삭제, 웹 컨테이너 재시작, PHP 코드 배포는 수행하지 않았다.
## 7. 현재 확인된 결과
운영 서버 측에서 다음을 확인했다.
- DB 컨테이너: 정상(`healthy`)
- 운영 DB 데이터 볼륨: 기존 볼륨 유지
- Docker 실제 포트 발행: `10.0.10.110:3306`
- 운영 서버의 3306 listener: 확인
- 운영 서버에서 `10.0.10.110:3306` TCP 자기 접속: 성공
## 8. 남은 최종 확인
서버 측 처리는 완료되었지만, 아래는 각 출발지 서버에서 한 번씩 확인한다.
### 한맥 서버
```powershell
Test-NetConnection 10.0.10.110 -Port 3306
```
기대 결과:
```
TcpTestSucceeded : True
```
### 한맥클라우드
방화벽 담당자가 안내한 공인 주소로 접속한다.
```
118.220.172.202:3306
```
TCP 연결 후에는 동기화 전용 MySQL 계정으로 실제 로그인 및 대상 테이블 권한을 확인한다. TCP 연결 성공 뒤 `Access denied`가 발생하면 그것은 네트워크 문제가 아니라 DB 계정/권한 설정 단계다.
## 9. 재발 방지 및 운영 원칙
- DB 포트를 추가할 때는 Compose의 `ports` 선언만 보지 말고 `docker port`, `NetworkSettings.Ports`, `ss`, 서버 자체 TCP 접속까지 확인한다.
- DB 컨테이너가 내부 네트워크에만 연결되어 있는지 반드시 확인한다.
- 운영 DB 변경은 일반 애플리케이션 배포 대신 DB 전용 워크플로로 수행한다.
- DB 재생성 전에는 컨테이너 health와 데이터 볼륨 이름을 확인한다.
- 포트는 `0.0.0.0` 대신 필요한 운영 서버 IP에만 바인딩한다.
- 방화벽은 필요한 출발지 IP만 허용한다.
- DB root 계정은 외부 동기화에 사용하지 않고, 동기화 대상·권한을 최소화한 전용 계정을 사용한다.
- 컨테이너 재부팅 복구를 위해 `restart: unless-stopped`를 유지하고, 운영 서버 재부팅 후 컨테이너 및 포트 상태 점검 절차를 운영 체크리스트에 포함한다.
## 10. 관련 운영 자산
- 서비스 저장소: `erp_groupware/jang-legacy-dockerize`
- 운영 Compose override: `docker-compose.prod.yml`
- DB 전용 적용 워크플로: `.gitea/workflows/one_time_prod_db_port_3306.yml`
- DB 포트 진단 워크플로: `.gitea/workflows/diagnose_prod_db_port_3306.yml`
- 실제 적용 Actions 실행: `One-time Apply Production DB Port 3306` 실행 #1310 (성공)