Add JH ERP production DB port incident record
This commit is contained in:
@@ -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 (성공)
|
||||
Reference in New Issue
Block a user