기존 Docker Registry에서 엔터프라이즈급 Harbor 플랫폼으로 전환하며, Harbor의 내장 Registry 기능을 활용한 자동 마이그레이션과 재해복구 체계를 구축한 프로젝트입니다. 7개 팀, 30명의 사용자가 사용하는 AI/ML 플랫폼의 컨테이너 레지스트리를 무중단으로 전환했습니다.
Infrastructure:
- Kubernetes: v1.28
- Harbor: v2.6.0
- Storage: 300GB (PersistentVolume)
- Nodes: 7대 분산 배포
- TLS: 자체 서명 인증서
Core Components:
- Harbor Core: API 서버, 비즈니스 로직
- Harbor Portal: 웹 UI
- Harbor Registry: Docker Registry v2
- Harbor JobService: 비동기 작업 처리
- PostgreSQL: 메타데이터 저장
- Redis: 세션 캐시
- Trivy: 취약점 스캐너
Harbor Platform
├── Team Projects (10개)
│ ├── team1-dev / team1-prod
│ ├── team2-dev / team2-prod
│ ├── team3-dev / team3-prod
│ ├── team4-dev / team4-prod
│ └── team5-dev / team5-prod
├── Common Resources
│ ├── base-images
│ └── shared-libraries
└── DockerHub Proxy
└── cached-images
#!/bin/bash
"""
Harbor Registry API를 활용한 마이그레이션 스크립트
기존 Docker Registry를 Harbor에 등록하고 복제 정책 생성
"""
# 색상 설정
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Harbor 설정
HARBOR_URL="https://harbor.internal:30443"
HARBOR_USER="admin"
HARBOR_PASS="${HARBOR_PASSWORD}"
# 소스 레지스트리 설정
SOURCE_REGISTRY="legacy-registry.internal:5000"
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}Harbor Registry 마이그레이션 시작${NC}"
echo -e "${BLUE}========================================${NC}"
# 1. 소스 레지스트리를 Harbor에 엔드포인트로 등록
register_source_registry() {
echo -e "${YELLOW}1. 소스 레지스트리 등록 중...${NC}"
REGISTRY_RESPONSE=$(curl -k -s -X POST \
-u "${HARBOR_USER}:${HARBOR_PASS}" \
-H "Content-Type: application/json" \
"${HARBOR_URL}/api/v2.0/registries" \
-d "{
\"name\": \"legacy-docker-registry\",
\"type\": \"docker-registry\",
\"url\": \"${SOURCE_REGISTRY}\",
\"description\": \"기존 Docker Registry (마이그레이션 소스)\",
\"insecure\": true,
\"credential\": {
\"type\": \"basic\",
\"access_key\": \"\",
\"access_secret\": \"\"
}
}")
# Registry ID 조회
REGISTRY_ID=$(curl -k -s -u "${HARBOR_USER}:${HARBOR_PASS}" \
"${HARBOR_URL}/api/v2.0/registries" | \
jq -r '.[] | select(.name=="legacy-docker-registry") | .id')
echo -e "${GREEN}✅ 레지스트리 등록 완료: ID=${REGISTRY_ID}${NC}"
echo $REGISTRY_ID
}
# 2. 팀별 복제 정책 생성
create_team_replication_policies() {
local REGISTRY_ID=$1
echo -e "${YELLOW}2. 팀별 복제 정책 생성 중...${NC}"
# 팀 프로젝트 배열
TEAM_PROJECTS=(
"team1-dev"
"team1-prod"
"team2-dev"
"team2-prod"
"team3-dev"
"team3-prod"
"team4-dev"
"team4-prod"
"team5-dev"
"team5-prod"
)
POLICY_IDS=()
for PROJECT in "${TEAM_PROJECTS[@]}"; do
echo -n " ${PROJECT} 정책 생성..."
POLICY_RESPONSE=$(curl -k -s -X POST \
-u "${HARBOR_USER}:${HARBOR_PASS}" \
-H "Content-Type: application/json" \
"${HARBOR_URL}/api/v2.0/replication/policies" \
-d "{
\"name\": \"migrate-${PROJECT}\",
\"description\": \"Migration policy for ${PROJECT}\",
\"src_registry\": {
\"id\": ${REGISTRY_ID}
},
\"dest_registry\": null,
\"dest_namespace\": \"${PROJECT}\",
\"filters\": [
{
\"type\": \"name\",
\"value\": \"${PROJECT}/**\"
}
],
\"trigger\": {
\"type\": \"manual\"
},
\"enabled\": true,
\"deletion\": false,
\"override\": true,
\"speed\": -1
}")
# Policy ID 추출
POLICY_ID=$(curl -k -s -u "${HARBOR_USER}:${HARBOR_PASS}" \
"${HARBOR_URL}/api/v2.0/replication/policies" | \
jq -r ".[] | select(.name==\"migrate-${PROJECT}\") | .id")
if [ -n "$POLICY_ID" ]; then
POLICY_IDS+=($POLICY_ID)
echo -e " ${GREEN}✅ (ID: ${POLICY_ID})${NC}"
else
echo -e " ${RED}❌${NC}"
fi
done
echo "${POLICY_IDS[@]}"
}
# 3. 병렬 복제 실행
execute_parallel_replication() {
local POLICY_IDS=($@)
echo -e "${YELLOW}3. 병렬 복제 실행 중...${NC}"
EXECUTION_IDS=()
# 모든 정책을 동시에 실행
for POLICY_ID in "${POLICY_IDS[@]}"; do
echo -n " 정책 ${POLICY_ID} 실행..."
EXEC_RESPONSE=$(curl -k -s -X POST \
-u "${HARBOR_USER}:${HARBOR_PASS}" \
-H "Content-Type: application/json" \
"${HARBOR_URL}/api/v2.0/replication/executions" \
-d "{\"policy_id\": ${POLICY_ID}}")
# Execution ID 추출
EXEC_ID=$(curl -k -s -X POST \
-u "${HARBOR_USER}:${HARBOR_PASS}" \
-H "Content-Type: application/json" \
-d "{\"policy_id\": ${POLICY_ID}}" \
-I "${HARBOR_URL}/api/v2.0/replication/executions" | \
grep -i location | sed 's/.*\/\([0-9]*\).*/\1/' | tr -d '\r')
if [ -n "$EXEC_ID" ]; then
EXECUTION_IDS+=($EXEC_ID)
echo -e " ${GREEN}✅ (Execution ID: ${EXEC_ID})${NC}"
else
echo -e " ${RED}❌${NC}"
fi
done
echo "${EXECUTION_IDS[@]}"
}
# 4. 복제 진행 상황 모니터링
monitor_replication_progress() {
local EXECUTION_IDS=($@)
echo -e "${YELLOW}4. 복제 진행 상황 모니터링...${NC}"
while true; do
ALL_COMPLETED=true
TOTAL_SUCCESS=0
TOTAL_FAILED=0
TOTAL_PROGRESS=0
echo -e "\n${BLUE}현재 복제 상태:${NC}"
for EXEC_ID in "${EXECUTION_IDS[@]}"; do
STATUS_INFO=$(curl -k -s -u "${HARBOR_USER}:${HARBOR_PASS}" \
"${HARBOR_URL}/api/v2.0/replication/executions/${EXEC_ID}")
STATUS=$(echo "$STATUS_INFO" | jq -r '.status')
TOTAL=$(echo "$STATUS_INFO" | jq -r '.total // 0')
SUCCESS=$(echo "$STATUS_INFO" | jq -r '.success_task_count // 0')
FAILED=$(echo "$STATUS_INFO" | jq -r '.failed_task_count // 0')
case $STATUS in
"InProgress")
echo " Execution ${EXEC_ID}: 🔄 진행 중 (${SUCCESS}/${TOTAL})"
ALL_COMPLETED=false
TOTAL_PROGRESS=$((TOTAL_PROGRESS + 1))
;;
"Succeed")
echo " Execution ${EXEC_ID}: ✅ 완료 (${SUCCESS} 이미지)"
TOTAL_SUCCESS=$((TOTAL_SUCCESS + SUCCESS))
;;
"Failed")
echo " Execution ${EXEC_ID}: ❌ 실패 (성공: ${SUCCESS}, 실패: ${FAILED})"
TOTAL_FAILED=$((TOTAL_FAILED + FAILED))
;;
*)
echo " Execution ${EXEC_ID}: 📊 ${STATUS}"
ALL_COMPLETED=false
;;
esac
done
echo -e "\n${BLUE}전체 진행 상황:${NC}"
echo " 성공한 이미지: ${TOTAL_SUCCESS}개"
echo " 실패한 이미지: ${TOTAL_FAILED}개"
echo " 진행 중인 작업: ${TOTAL_PROGRESS}개"
if $ALL_COMPLETED; then
echo -e "\n${GREEN}🎉 모든 복제 작업이 완료되었습니다!${NC}"
break
fi
echo -e "\n30초 후 다시 확인..."
sleep 30
done
}
# 메인 실행 로직
main() {
# 1. 소스 레지스트리 등록
REGISTRY_ID=$(register_source_registry)
# 2. 팀별 복제 정책 생성
POLICY_IDS=($(create_team_replication_policies $REGISTRY_ID))
# 3. 병렬 복제 실행
EXECUTION_IDS=($(execute_parallel_replication "${POLICY_IDS[@]}"))
# 4. 진행 상황 모니터링
monitor_replication_progress "${EXECUTION_IDS[@]}"
# 5. 최종 결과 요약
echo -e "\n${BLUE}========================================${NC}"
echo -e "${BLUE}마이그레이션 완료${NC}"
echo -e "${BLUE}========================================${NC}"
echo "총 프로젝트: ${#POLICY_IDS[@]}개"
echo "실행된 작업: ${#EXECUTION_IDS[@]}개"
}
# 스크립트 실행
main
#!/bin/bash
"""
DockerHub 백업 복제 정책 설정 스크립트
Production 이미지를 DockerHub에 자동 복제
"""
# 색상 설정
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Harbor 정보
HARBOR_URL="https://harbor.internal:30443"
HARBOR_USER="admin"
HARBOR_PASS="${HARBOR_PASSWORD}"
# DockerHub 정보
DOCKERHUB_USER="company-backup"
DOCKERHUB_TOKEN="${DOCKERHUB_TOKEN}"
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}DockerHub 백업 복제 설정${NC}"
echo -e "${BLUE}========================================${NC}"
# 1. 기존 복제 정책 정리
cleanup_existing_policies() {
echo -e "${YELLOW}1. 기존 복제 정책 삭제...${NC}"
# DockerHub 백업 관련 정책 조회 및 삭제
POLICIES=$(curl -k -s -u "${HARBOR_USER}:${HARBOR_PASS}" \
"${HARBOR_URL}/api/v2.0/replication/policies" | \
jq -r '.[] | select(.name | contains("dockerhub-backup")) | .id')
for POLICY_ID in $POLICIES; do
echo " 복제 정책 삭제 중: ID=$POLICY_ID"
curl -k -X DELETE -u "${HARBOR_USER}:${HARBOR_PASS}" \
"${HARBOR_URL}/api/v2.0/replication/policies/${POLICY_ID}"
echo -e " ${GREEN}✅ 정책 ${POLICY_ID} 삭제됨${NC}"
done
}
# 2. DockerHub Registry Endpoint 생성
create_dockerhub_endpoint() {
echo -e "${YELLOW}2. DockerHub Registry Endpoint 생성...${NC}"
ENDPOINT_RESPONSE=$(curl -k -s -X POST \
-u "${HARBOR_USER}:${HARBOR_PASS}" \
-H "Content-Type: application/json" \
"${HARBOR_URL}/api/v2.0/registries" \
-d "{
\"name\": \"dockerhub\",
\"type\": \"docker-hub\",
\"url\": \"https://index.docker.io\",
\"insecure\": false,
\"credential\": {
\"type\": \"basic\",
\"access_key\": \"${DOCKERHUB_USER}\",
\"access_secret\": \"${DOCKERHUB_TOKEN}\"
},
\"description\": \"Docker Hub registry for production backup\"
}")
# 생성된 엔드포인트 ID 가져오기
REGISTRY_ID=$(curl -k -s -u "${HARBOR_USER}:${HARBOR_PASS}" \
"${HARBOR_URL}/api/v2.0/registries" | \
jq -r '.[] | select(.name=="dockerhub") | .id')
echo -e "${GREEN}✅ Docker Hub 엔드포인트가 생성되었습니다. (ID: ${REGISTRY_ID})${NC}"
echo $REGISTRY_ID
}
# 3. Production 프로젝트별 백업 정책 생성
create_backup_policies() {
local REGISTRY_ID=$1
echo -e "${YELLOW}3. Production 프로젝트 백업 정책 생성...${NC}"
# Production 프로젝트 배열
PROD_PROJECTS=(
"team1-prod"
"team2-prod"
"team3-prod"
"team4-prod"
"team5-prod"
)
for PROJECT in "${PROD_PROJECTS[@]}"; do
echo -n " ${PROJECT} 백업 정책 생성..."
POLICY_RESPONSE=$(curl -k -s -X POST \
-u "${HARBOR_USER}:${HARBOR_PASS}" \
-H "Content-Type: application/json" \
"${HARBOR_URL}/api/v2.0/replication/policies" \
-d "{
\"name\": \"dockerhub-backup-${PROJECT}\",
\"description\": \"Backup ${PROJECT} images to Docker Hub\",
\"src_registry\": null,
\"dest_registry\": {
\"id\": ${REGISTRY_ID}
},
\"dest_namespace\": \"${DOCKERHUB_USER}\",
\"dest_namespace_replace_count\": 1,
\"filters\": [
{
\"type\": \"name\",
\"value\": \"${PROJECT}/**\"
},
{
\"type\": \"tag\",
\"value\": \"{v*.*.*,latest}\"
}
],
\"trigger\": {
\"type\": \"event_based\"
},
\"enabled\": true,
\"deletion\": false,
\"override\": true,
\"speed\": -1
}")
# Policy ID 확인
POLICY_ID=$(curl -k -s -u "${HARBOR_USER}:${HARBOR_PASS}" \
"${HARBOR_URL}/api/v2.0/replication/policies" | \
jq -r ".[] | select(.name==\"dockerhub-backup-${PROJECT}\") | .id")
if [ -n "$POLICY_ID" ]; then
echo -e " ${GREEN}✅ (ID: ${POLICY_ID})${NC}"
else
echo -e " ${RED}❌${NC}"
fi
done
}
# 4. 테스트 복제 실행
test_backup_replication() {
echo -e "${YELLOW}4. 테스트 복제 실행...${NC}"
# 첫 번째 정책으로 테스트
TEST_POLICY_ID=$(curl -k -s -u "${HARBOR_USER}:${HARBOR_PASS}" \
"${HARBOR_URL}/api/v2.0/replication/policies" | \
jq -r '.[] | select(.name | startswith("dockerhub-backup-")) | .id' | head -1)
if [ -n "$TEST_POLICY_ID" ]; then
echo " 테스트 정책 ID: ${TEST_POLICY_ID}"
EXEC_RESPONSE=$(curl -k -s -X POST \
-u "${HARBOR_USER}:${HARBOR_PASS}" \
-H "Content-Type: application/json" \
"${HARBOR_URL}/api/v2.0/replication/executions" \
-d "{\"policy_id\": ${TEST_POLICY_ID}}")
echo -e "${GREEN}✅ 테스트 복제가 시작되었습니다.${NC}"
# 복제 상태 확인
monitor_test_replication
fi
}
# 5. 복제 상태 모니터링
monitor_test_replication() {
echo -e "${YELLOW}5. 복제 상태 확인 (60초 대기)...${NC}"
for i in {1..12}; do
sleep 5
STATUS=$(curl -k -s -u "${HARBOR_USER}:${HARBOR_PASS}" \
"${HARBOR_URL}/api/v2.0/replication/executions?policy_id=${TEST_POLICY_ID}&page_size=1" | \
jq -r '.[0].status')
echo " 상태 확인 ($i/12): $STATUS"
if [ "$STATUS" = "Succeed" ]; then
echo -e "${GREEN}✅ 복제가 성공적으로 완료되었습니다!${NC}"
# 상세 정보 표시
EXEC_INFO=$(curl -k -s -u "${HARBOR_USER}:${HARBOR_PASS}" \
"${HARBOR_URL}/api/v2.0/replication/executions?policy_id=${TEST_POLICY_ID}&page_size=1" | \
jq -r '.[0]')
echo " 성공: $(echo $EXEC_INFO | jq -r '.success_task_count // 0')개"
echo " 실패: $(echo $EXEC_INFO | jq -r '.failed_task_count // 0')개"
break
elif [ "$STATUS" = "Failed" ]; then
echo -e "${RED}❌ 복제가 실패했습니다.${NC}"
# 실패 원인 확인
EXEC_ID=$(curl -k -s -u "${HARBOR_USER}:${HARBOR_PASS}" \
"${HARBOR_URL}/api/v2.0/replication/executions?policy_id=${TEST_POLICY_ID}&page_size=1" | \
jq -r '.[0].id')
echo "실패 원인:"
curl -k -s -u "${HARBOR_USER}:${HARBOR_PASS}" \
"${HARBOR_URL}/api/v2.0/replication/executions/${EXEC_ID}/tasks" | \
jq -r '.[0] | {status: .status, src_resource: .src_resource, dst_resource: .dst_resource}'
break
fi
done
}
# 메인 실행 로직
main() {
# 1. 기존 정책 정리
cleanup_existing_policies
# 2. DockerHub 엔드포인트 생성
REGISTRY_ID=$(create_dockerhub_endpoint)
# 3. 백업 정책 생성
create_backup_policies $REGISTRY_ID
# 4. 테스트 복제 실행
test_backup_replication
echo ""
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE}설정 완료${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
echo "Docker Hub에서 확인: https://hub.docker.com/r/${DOCKERHUB_USER}"
echo "이제 Production 프로젝트에 이미지를 Push하면 자동으로 DockerHub에 백업됩니다."
echo ""
}
# 스크립트 실행
main
#!/usr/bin/env python3
"""
Harbor 복제 작업 병렬 실행 및 관리
여러 프로젝트를 동시에 마이그레이션
"""
import concurrent.futures
from typing import List, Dict
class ParallelReplicationManager:
def __init__(self, harbor_url: str, harbor_auth: tuple):
self.harbor_url = harbor_url
self.harbor_auth = harbor_auth
self.api_base = f"{harbor_url}/api/v2.0"
def execute_parallel_migration(self, policies: List[int], max_workers: int = 5):
"""여러 복제 정책을 병렬로 실행"""
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {}
# 각 정책별로 복제 실행
for policy_id in policies:
future = executor.submit(self._execute_single_replication, policy_id)
futures[future] = policy_id
# 결과 수집
results = []
for future in concurrent.futures.as_completed(futures):
policy_id = futures[future]
try:
result = future.result()
results.append({
'policy_id': policy_id,
'status': result['status'],
'statistics': result.get('statistics', {})
})
# 실시간 진행 상황 출력
total = result['statistics'].get('total', 0)
success = result['statistics'].get('success', 0)
failed = result['statistics'].get('failed', 0)
print(f"Policy {policy_id}: Total={total}, Success={success}, Failed={failed}")
except Exception as e:
logging.error(f"Policy {policy_id} 실행 실패: {e}")
results.append({
'policy_id': policy_id,
'status': 'failed',
'error': str(e)
})
return results
def _execute_single_replication(self, policy_id: int) -> Dict:
"""단일 복제 정책 실행 및 모니터링"""
# 복제 시작
response = requests.post(
f"{self.api_base}/replication/executions",
json={"policy_id": policy_id},
auth=self.harbor_auth
)
if response.status_code != 201:
raise Exception(f"복제 시작 실패: {response.text}")
execution_id = int(response.headers['Location'].split('/')[-1])
# 완료까지 대기
while True:
exec_response = requests.get(
f"{self.api_base}/replication/executions/{execution_id}",
auth=self.harbor_auth
)
if exec_response.status_code == 200:
execution = exec_response.json()
if execution['status'] in ['Succeeded', 'Failed', 'Stopped']:
# 최종 통계 조회
tasks_response = requests.get(
f"{self.api_base}/replication/executions/{execution_id}/tasks",
auth=self.harbor_auth
)
tasks = tasks_response.json() if tasks_response.status_code == 200 else []
return {
'status': execution['status'],
'statistics': {
'total': len(tasks),
'success': len([t for t in tasks if t['status'] == 'Succeeded']),
'failed': len([t for t in tasks if t['status'] == 'Failed'])
}
}
time.sleep(10)
#!/usr/bin/env python3
"""
Harbor 복제 상태 모니터링 및 리포팅
"""
class ReplicationMonitor:
def __init__(self, harbor_url: str, harbor_auth: tuple):
self.harbor_url = harbor_url
self.harbor_auth = harbor_auth
self.api_base = f"{harbor_url}/api/v2.0"
def generate_migration_report(self) -> Dict:
"""마이그레이션 종합 리포트 생성"""
# 모든 복제 정책 조회
policies = requests.get(
f"{self.api_base}/replication/policies",
auth=self.harbor_auth
).json()
report = {
'total_policies': len(policies),
'active_policies': 0,
'executions': [],
'statistics': {
'total_replicated': 0,
'total_size': 0,
'success_rate': 0
}
}
for policy in policies:
if policy['enabled']:
report['active_policies'] += 1
# 최근 실행 내역 조회
executions = requests.get(
f"{self.api_base}/replication/executions",
params={'policy_id': policy['id'], 'limit': 5},
auth=self.harbor_auth
).json()
for execution in executions:
tasks = requests.get(
f"{self.api_base}/replication/executions/{execution['id']}/tasks",
auth=self.harbor_auth
).json()
exec_summary = {
'policy_name': policy['name'],
'execution_id': execution['id'],
'status': execution['status'],
'start_time': execution['start_time'],
'end_time': execution.get('end_time'),
'total_tasks': len(tasks),
'succeeded': len([t for t in tasks if t['status'] == 'Succeeded']),
'failed': len([t for t in tasks if t['status'] == 'Failed'])
}
report['executions'].append(exec_summary)
report['statistics']['total_replicated'] += exec_summary['succeeded']
# 성공률 계산
total_tasks = sum(e['total_tasks'] for e in report['executions'])
total_succeeded = sum(e['succeeded'] for e in report['executions'])
if total_tasks > 0:
report['statistics']['success_rate'] = (total_succeeded / total_tasks) * 100
return report
메트릭 | 수동 마이그레이션 | Harbor Registry API | 개선율 |
---|---|---|---|
총 이미지 수 | 2,847개 | 2,847개 | - |
마이그레이션 시간 | 48시간 | 7.2시간 | 85% 단축 |
동시 처리 수 | 1개 | 10개 | 10배 향상 |
수동 작업 | 100% | 10% | 90% 자동화 |
오류 복구 | 수동 | 자동 재시도 | 완전 자동화 |
정책 유형 | 개수 | 빈도 | 평균 처리 시간 |
---|---|---|---|
레거시 마이그레이션 | 1 | 1회성 | 7.2시간 |
DockerHub 백업 | 5 | Push 이벤트 | 30초 |
크로스 리전 복제 | 2 | 실시간 | 15초 |
개발→운영 프로모션 | 10 | 수동 트리거 | 2분 |
문제: 10GB 이상 이미지 복제 중 타임아웃 발생
해결:
# Harbor 설정 조정
policy_update = {
"speed": -1, # 속도 제한 해제
"decoration": {
"timeout": 3600 # 타임아웃 1시간으로 증가
}
}
문제: 10개 이상 동시 복제 시 Harbor JobService OOM
해결:
# JobService 리소스 증설
resources:
limits:
memory: 4Gi # 2Gi → 4Gi
cpu: 2000m
requests:
memory: 2Gi
cpu: 1000m
문제: 장시간 복제 중 소스 레지스트리 토큰 만료
해결:
Harbor 내장 기능 활용의 중요성
Event-driven 복제의 효율성
병렬 처리 최적화
Harbor의 내장 Registry 복제 기능을 최대한 활용하여, 복잡한 스크립트 없이도 효율적인 마이그레이션과 백업 체계를 구축했습니다. 특히 병렬 복제 실행과 Event-driven 백업으로 운영 효율성을 극대화했습니다.
기술 스택: Harbor API, Kubernetes, Python, Docker Registry V2, PostgreSQL, Redis