AWS DMS로 온프레미스 PostgreSQL을 RDS로, pgvector 타입까지 무손실 마이그레이션하기
들어가며..
온프레미스 PostgreSQL을 AWS RDS로 옮겨야 했다. 그냥 옮기는 거라면 어렵지 않았겠지만, Farmily DB에는 AI 콘텐츠 생성용 RAG 테이블에 pgvector 확장의 커스텀 타입 vector(1024) 컬럼이 있었다. 일반적인 DMS 방식은 표준 SQL 타입만 변환하기 때문에 이런 확장 타입을 그대로 옮기지 못한다.
그래서 AWS DMS의 동종(Homogeneous) 마이그레이션으로 Full Load + CDC 방식을 선택했고, 그 과정에서 서로 다른 원인으로 7번 막혔다. 이 글은 마이그레이션 과정에서 실제로 부딪힌 문제와 거기서 얻은 인사이트, 그리고 최종 검증 결과를 정리한다.
1. 왜 동종(Homogeneous) 마이그레이션인가
일반(이종) DMS의 타입 매핑 엔진은 표준 SQL 타입만 변환한다. vector(1024) 같은 확장 커스텀 타입은 매핑 대상에 없어 그대로 보존되지 않는다.
동종 마이그레이션은 다르다. 내부적으로 PostgreSQL 네이티브 도구(pg_dump/pg_restore + 논리 복제)를 사용하기 때문에, 확장과 커스텀 타입을 원본 그대로 옮길 수 있다. 소스와 타깃이 같은 엔진(PostgreSQL)일 때만 쓸 수 있는 방식이라 이번 케이스에 정확히 맞았다.
text1온프레미스 PostgreSQL 15 (pgvector 0.8.2) 2 ↓ DMS 동종 마이그레이션 (Full Load + CDC) 3AWS RDS PostgreSQL 18 (pgvector 0.8.1)
2. 왜 Tailscale 릴레이가 필요했나
온프레미스 DB는 VMware Host-Only 네트워크(192.168.30.0/24)에 있어 AWS에서 직접 접근할 수 없었다. DB를 인터넷에 직접 노출하는 방식은 보안상 제외했다.
대신 Tailscale(WireGuard 오버레이 VPN)로 온프레미스와 AWS를 같은 사설 오버레이 네트워크로 묶고, VPC 안의 릴레이 EC2가 socat으로 5432 포트를 온프레미스 DB로 포워딩하는 구조를 만들었다. DMS는 릴레이 EC2의 사설 IP로 접속하면 온프레미스 DB에 도달한다.
text1[온프레미스 db-server] [AWS prod-vpc] 2 PostgreSQL 15 릴레이 EC2(farmily-bastion) RDS PostgreSQL 18 3 192.168.30.10 ──Tailscale──► 10.1.1.64 (socat :5432) ──► prod-rds 4 (VMnet Host-Only) WireGuard Tailscale IP 100.106.105.53 (DMS/RDS가 접속)
이 "릴레이 경유" 구조가 나중에 다룰 7번째 이슈(CDC subscription)의 원인이 된다.
3. Full Load + CDC — 무중단 전환의 기반
Full Load로 기존 데이터를 통째로 복사하고, 이후 발생하는 변경(INSERT/UPDATE/DELETE)을 CDC(Change Data Capture)로 실시간 반영한다. 두 단계를 함께 쓰면 데이터를 옮기는 동안에도 소스를 계속 운영할 수 있어, 무중단 전환(cutover)의 기반이 된다.
DMS는 Start 시 연결 → 논리 복제 검사 → 스키마 복원 → Full Load → CDC 순서로 진행한다. 이 다섯 단계 각각에서 서로 다른 원인으로 총 7번 차단됐다.
4. 7개의 차단 — 트러블슈팅 여정
에러 메시지가 가리키는 단계를 단서로 원인을 좁혀가며 하나씩 해소했다. 요약하면 이렇다.
| # | 단계 | 증상 | 원인 유형 | 조치 |
|---|---|---|---|---|
| 1 | 타깃 연결 | password authentication failed for user "admin" | 시크릿 username 오타 | admin → farmilyadmin |
| 2 | 타깃 연결 | no pg_hba.conf entry ... no encryption | SSL 모드 불일치 | none → require |
| 3 | 논리 복제 검사 | Logical replication is not enabled for TARGET | RDS 기본값 미설정 | 파라미터그룹 rds.logical_replication=1 + 재부팅 |
| 4 | 소스 연결 | password authentication failed for user "dms_user" | DB 비번 ↔ 시크릿 불일치 | 비번 변경 후 시크릿 동기화 |
| 5 | CDC publication | must be superuser to create FOR ALL TABLES publication | 권한 부족 | 소스 접속 계정을 슈퍼유저(postgres)로 전환 |
| 6 | 스키마 복원 | role "farmily" does not exist | 소유권 롤 부재 | 타깃에 동일 이름 롤 생성(이름표용) |
| 7 | CDC subscription | could not connect to the publisher ... Connection timed out | RDS→릴레이 경로 미허용 | 릴레이 SG에 5432 ← RDS SG 인바운드 추가 |
에러 메시지는 어느 계층이 막혔는지를 알려준다
일곱 번 막히면서 알게 된 건, 에러 메시지의 종류가 곧 "어느 계층에서 막혔는지"를 알려준다는 점이다.
password authentication failed→ 비밀번호 단계까지 도달했다는 뜻. 즉 네트워크는 이미 뚫려 있고, 자격 증명만 틀렸다.no pg_hba.conf entry ... no encryption→ 매칭되는 접속 규칙이 없다는 뜻. SSL 모드나 대역이 안 맞는다.Connection timed out→ 네트워크 자체가 안 뚫렸다는 뜻. 보안 그룹이나 라우팅을 봐야 한다.
이 구분 덕분에 매번 처음부터 전체를 의심하지 않고, 에러 메시지 하나로 확인할 범위를 곧바로 좁힐 수 있었다.
가장 비자명했던 문제 — CDC는 양방향을 요구한다
7개 중 가장 오래 걸린 건 #5와 #7이었고, 둘은 같은 뿌리를 공유한다.
처음 세운 가정은 "DMS만 릴레이에 붙으면 된다"는 단방향 모델이었다. 하지만 동종 마이그레이션의 CDC는 그렇게 동작하지 않았다.
- publication(#5): 소스에서 변경을 내보내려면
FOR ALL TABLESpublication을 생성해야 하는데, 이건 슈퍼유저 권한이 필요하다. 처음 만든dms_user는 SELECT + REPLICATION 권한만 있는 일반 계정이라 거부됐다. - subscription(#7): 타깃(RDS)이 publication을 구독하려면, RDS가 직접 릴레이 EC2에 접속해야 한다. 지금까지 열어둔 경로는 "DMS → 릴레이"뿐이었고, "RDS → 릴레이" 경로는 열려 있지 않았다.
text1DMS → 릴레이(10.1.1.64) ✅ (열려 있었음) 2RDS → 릴레이(10.1.1.64) ❌ → ✅ (추가로 열어야 했음)
정리하면, publication은 소스 측 슈퍼유저 권한을, subscription은 타깃→소스 직접 연결을 요구한다. "DMS가 릴레이에 붙는 것"과 "RDS가 릴레이에 붙는 것"은 별개의 경로이고, CDC를 쓰려면 둘 다 필요하다는 걸 두 번의 차단을 겪고 나서야 이해했다.
5 Whys — #7을 파고들면
| 단계 | 질문 → 답 |
|---|---|
| Why 1 | 왜 막혔나 → RDS가 릴레이(10.1.1.64:5432)에 직접 접속을 시도했으나 timeout |
| Why 2 | 왜 timeout이 났나 → 릴레이 SG가 RDS SG로부터의 5432 인바운드를 허용하지 않았음 |
| Why 3 | 왜 막지 못했나 → 네트워크 경로를 "DMS → 릴레이" 단방향으로만 설계함 |
| Why 4 | 왜 사전에 못 찾았나 → 연결 검증을 psql(베스천 경로)로만 했고, RDS→릴레이 경로는 검증 항목에 없었음 |
| Why 5 (근본) | 동종 마이그레이션 CDC의 데이터 흐름(타깃이 소스에 직접 subscribe)에 대한 이해 부족 |
시크릿이 실제 접속 주체를 결정한다
또 하나 명확히 알게 된 것: DB 쪽에서 비밀번호를 바꾸고 pg_hba.conf를 열어도, Secrets Manager의 username이 여전히 예전 계정을 가리키면 DMS는 계속 그 계정으로 접속을 시도한다. 계정을 전환하는 실제 스위치는 DB 설정이 아니라 시크릿이었다.
5. 최종 성과 및 검증
7개 이슈를 모두 해소한 뒤 재시작해서, 아래 결과로 완주했다.
| 지표 | 결과 |
|---|---|
| 로드된 테이블 | 24개 (오류 0) |
| 경과 시간 | 약 16분 31초 |
| CDC 지연 시간 | 0ms |
| 정합성 (소스=타깃 행 수) | 전 테이블 일치 |
| vector 타입 보존 | vector(1024) 보존 (확장 0.8.2 → 0.8.1) |
데이터 정합성 — 소스와 타깃에서 동일한 행 수 비교 쿼리를 실행해 확인했다. users 501,015건, notification_settings 501,010건을 포함해 검증한 6개 테이블 전부 소스=타깃으로 일치했다. 총 약 540만 행(약 1GB) 규모였다.
타입·스키마 보존 — 이번 마이그레이션의 핵심 목적이었던 부분이다. 타깃 RDS에서 \d recipe_embeddings를 확인한 결과, embedding 컬럼이 vector(1024)로 보존됐고 PRIMARY KEY·UNIQUE 제약·IDENTITY 시퀀스·인덱스 4개·text[] 배열 타입까지 전부 그대로 옮겨졌다. pgvector 확장 버전이 소스(0.8.2)와 타깃(0.8.1)이 달랐음에도 타입과 차원이 정확히 일치함을 확인했다.
recipe_embeddings.embedding값은 현재 전부 NULL이다. 이번 검증의 대상은 "벡터 데이터 이전"이 아니라vector(1024)타입(스키마)·차원·인덱스·제약의 보존이라는 점을 분명히 해둔다.
6. 범위 — 여기까지 했고, 여기부터는 안 했다
이번 작업은 DMS Full Load + CDC로 데이터를 이전하고 정합성·타입 보존을 검증하는 단계까지다. 애플리케이션(WAS)의 DB 연결을 RDS로 바꿔 실제 트래픽을 넘기는 cutover(앱 전환)는 포함하지 않았다.
검증이 끝난 뒤에는 환경을 정리했다 — DMS 삭제, 그리고 CDC가 생성한 replication slot을 온프레미스에서 수동 삭제했다(DMS를 지워도 slot은 자동으로 없어지지 않는다. 방치하면 WAL이 계속 쌓여 디스크를 위협한다). 그래서 지금 시점에는 소스↔타깃 실시간 동기화가 끊긴 상태이고, 실제로 cutover를 하려면 CDC 동기화를 다시 구성해야 한다. "검증"과 "앱 전환"은 다른 단계라는 걸 분명히 해두고 싶었다.
7. 얻은 교훈
잘된 점
에러 메시지의 종류로 막힌 계층을 빠르게 좁힌 것, 그리고 이슈를 하나 해결할 때마다 바로 psql로 재검증한 뒤 다음 단계로 넘어간 것. 덕분에 "고친 줄 알았는데 사실 안 고쳐진" 상태로 다음 단계에 진입하는 일이 없었다.
개선할 점
사전 점검 체크리스트가 있었다면 #1·#3·#6은 애초에 마주치지 않았을 문제였다. 그리고 CDC를 양방향 구조로 이해하지 못한 채 네트워크·권한을 단방향으로만 설계한 게 #5·#7의 공통 원인이었다 — 이건 사전 지식으로 막을 수 있는 문제였다.
다른 마이그레이션에도 적용할 수 있는 인사이트
- 동종 마이그레이션 CDC는 publication(소스, 슈퍼유저)과 subscription(타깃→소스 직접 연결)이라는 양방향 권한·네트워크를 요구한다. 릴레이 경유 구조라면 양쪽 방향 모두 SG를 열어야 한다.
- RDS는 논리 복제가 기본적으로 꺼져 있다. Full Load + CDC를 쓰려면 소스뿐 아니라 타깃에도
rds.logical_replication=1(Static, 재부팅 필요)을 설정해야 한다. - 실제 접속 계정을 바꾸는 스위치는 DB 설정이 아니라 Secrets Manager의 값이다.
- 메이저 버전 차이(PG15→18)나 확장 버전 차이(pgvector 0.8.2→0.8.1)가 있어도, 타입 수준 검증을 직접 하면 무손실 이전을 확인할 수 있다.
마치며
일반 DMS로는 vector 커스텀 타입을 지킬 수 없었기 때문에 동종 마이그레이션을 선택했고, 그 선택이 맞았다는 걸 타입 검증으로 확인했다. 하지만 그 과정이 매끄럽지는 않았다 — 인증, SSL, 논리 복제, 권한, 스키마, 네트워크까지 다섯 단계 전부에서 한 번씩 막혔다.
가장 크게 배운 건 기술적인 해결책 하나가 아니라, CDC라는 메커니즘 자체를 양방향으로 이해해야 한다는 것이었다. "소스에서 타깃으로 데이터가 흐른다"는 그림만 그리고 설계하면, 실제로는 타깃이 소스에 다시 연결하러 오는 순간 막힌다. 이번 7번의 차단 중 절반 이상이 결국 이 오해에서 비롯됐다.
Further Reading
댓글 0개
첫 번째 댓글을 남겨보세요