Browsing Tag

object storage

NCLOUD

[NCLOUD] Cloud DB for PostgreSQL 백업 to Object Storage (관리형 데이터베이스 백업이 실패한다면?)

안녕하세요 MANVSCLOUD 김수현입니다.

지난 포스팅에서 우리는 관리형 데이터베이스의 백업 성공/실패 여부를 Slack으로 알림을 받을 수 있도록 기능을 구현해보았습니다. 백업 알림을 통해 문제 상황을 빠르게 인지할 수 있게 되었지만 여전히 백업 실패 자체는 해결되지 않았습니다. 이제 이어서 백업 실패 문제를 어떻게 근본적으로 해결할 수 있을지 이야기해보고자 합니다.


오늘도 역시 PostgreSQL을 기준으로 작성하며 pg_dump 유틸리티를 이용할 것입니다. 이전 포스팅에서 Cloud Functions을 활용했지만 이번에는 다른 접근 방식을 취하겠습니다. Cloud Functions의 경우 TimeOut 설정값이 짧은 편이라 백업 및 업로드 시간 소요가 길어지면 백업 실패로 이어질 수 있기 때문에 이번에는 서버에서 직접 쉘 스크립트를 실행시키는 방식으로 준비했습니다.

추가로 제가 작성한 쉘 스크립트는 데이터베이스 패스워드를 직접 스크립트에 하드코딩하거나 환경변수로 노출시키지 않기 위해 Secret Manager를 사용하는 기준으로 작성되었습니다. 이는 보안 모범 사례를 따르는 접근법으로 민감한 자격 증명을 안전하게 관리할 수 있게 해줍니다. Secret Manager를 사용하고 있지 않다면 스크립트를 적절히 수정하여 다른 보안 방식을 활용해주세요.

  • 사전 준비 및 확인 사항

이 스크립트를 실행하기 전에 아래 네 가지 항목을 확인해주세요:

1. Ubuntu 22.04 기준 pg_dump 사용을 위한 준비: PostgreSQL 클라이언트 도구 설치가 필요합니다.

apt install postgresql-client

2. AWS CLI 설치 불필요: 이 스크립트는 AWS CLI 없이도 Object Storage와 통신할 수 있도록 작성되었습니다. curl 명령을 활용하여 직접 API 호출을 수행합니다. (물론 설치가 되어있어도 정상 동작합니다.)

3. 네트워크 연결 설정: 스크립트를 실행하는 서버에서 Cloud DB에 연결할 수 있도록 ACG(Access Control Group) 설정이 필요합니다.

4. 환경 변수 설정: 다음 환경 변수를 미리 설정해두어야 합니다.
ACCESS_KEY, SECRET_KEY : Secret Manager GetValue 권한 및 Object Storage Upload 권한이 있는 API 키
SECRET_ID : Secret Manager의 ID

  • 백업 스크립트
    : 아래는 PostgreSQL 데이터베이스를 백업 및 압축하고 Object Storage에 업로드하는 전체 쉘 스크립트입니다
#!/bin/bash

# Secret Manager에서 비밀번호 가져오기 함수
get_db_password() {
    # 환경 변수 확인
    if [ -z "$ACCESS_KEY" ] || [ -z "$SECRET_KEY" ] || [ -z "$SECRET_ID" ]; then
        echo "필요한 환경 변수가 설정되지 않았습니다."
        echo "다음 환경 변수를 설정해주세요: ACCESS_KEY, SECRET_KEY, SECRET_ID"
        exit 1
    fi

    # 현재 시간을 밀리초로 변환
    TIMESTAMP=$(date +%s%3N)

    # API 호출 정보
    API_HOST="secretmanager.apigw.ntruss.com"
    REQUEST_URL="/api/v1/secrets/${SECRET_ID}/values"
    REQUEST_METHOD="GET"

    # 서명 생성 (API Gateway signature v2)
    create_signature() {
        local method=$1
        local url=$2
        local timestamp=$3
        local access_key=$4
        local secret_key=$5
        
        # 서명 대상 문자열 생성
        local message="${method} ${url}
${timestamp}
${access_key}"
        
        # Base64로 인코딩된 HMAC-SHA256 서명 생성
        local signature=$(echo -n "${message}" | openssl dgst -sha256 -hmac "${secret_key}" -binary | base64)
        
        echo ${signature}
    }

    # 서명 생성
    SIGNATURE=$(create_signature ${REQUEST_METHOD} ${REQUEST_URL} ${TIMESTAMP} ${ACCESS_KEY} ${SECRET_KEY})

    # API 호출
    response=$(curl -s \
        -H "x-ncp-apigw-timestamp:${TIMESTAMP}" \
        -H "x-ncp-iam-access-key:${ACCESS_KEY}" \
        -H "x-ncp-apigw-signature-v2:${SIGNATURE}" \
        -H "Content-Type:application/json" \
        "https://${API_HOST}${REQUEST_URL}")

    # 응답에서 active의 cdbPassword 추출
    password=$(echo "${response}" | python -c '
import json, sys
try:
    data = json.load(sys.stdin)
    if data["code"] == "SUCCESS":
        active_json = json.loads(data["data"]["decryptedSecretChain"]["active"])
        print(active_json["cdbPassword"])
    else:
        print("Error: " + json.dumps(data), file=sys.stderr)
        sys.exit(1)
except Exception as e:
    print(f"Error parsing response: {e}", file=sys.stderr)
    print(sys.stdin.read(), file=sys.stderr)
    sys.exit(1)
')

    # 오류 확인
    if [ $? -ne 0 ]; then
        echo "비밀번호 가져오기 실패"
        exit 1
    fi

    echo "$password"
}

# 메인 백업 스크립트
main() {
    # 데이터베이스 연결 정보
    DB_HOST="pg-32lqva.vpc-cdb-kr.ntruss.com"
    DB_USER="manvscloud"
    DB_PORT="15432"
    DB_NAME="manvscloud"
    BUCKET_NAME="manvscloud-psql-backup"

    # Secret Manager에서 DB 비밀번호 가져오기
    echo "Secret Manager에서 데이터베이스 비밀번호를 가져오는 중..."
    DB_PASSWORD=$(get_db_password)
    
    if [ -z "$DB_PASSWORD" ]; then
        echo "데이터베이스 비밀번호를 가져오지 못했습니다."
        exit 1
    fi
    echo "비밀번호를 성공적으로 가져왔습니다."

    # 현재 날짜와 시간 정보 가져오기
    CURRENT_DATE=$(date +"%Y%m%d%H%M")
    YEAR=$(date +"%Y")
    MONTH=$(date +"%m")
    DAY=$(date +"%d")

    # 백업 파일 경로 및 이름 설정
    BACKUP_DIR="/tmp"
    BACKUP_FILE="${DB_NAME}_${CURRENT_DATE}.sql"
    COMPRESSED_FILE="${BACKUP_FILE}.gz"
    OBJECT_PATH="${YEAR}/${MONTH}/${DAY}/${COMPRESSED_FILE}"

    # 백업 디렉토리가 없으면 생성
    mkdir -p ${BACKUP_DIR}
    echo "PostgreSQL 데이터베이스 백업을 시작합니다..."

    # pg_dump를 사용하여 데이터베이스 백업
    PGPASSWORD="${DB_PASSWORD}" pg_dump -h ${DB_HOST} -U ${DB_USER} -p ${DB_PORT} -d ${DB_NAME} -F p > ${BACKUP_DIR}/${BACKUP_FILE}

    # 백업 성공 여부 확인
    if [ $? -ne 0 ]; then
        echo "데이터베이스 백업 실패"
        exit 1
    fi
    echo "데이터베이스 백업이 완료되었습니다. 파일 압축을 시작합니다..."

    # 백업 파일 압축
    gzip -f ${BACKUP_DIR}/${BACKUP_FILE}

    # 압축 성공 여부 확인
    if [ $? -ne 0 ]; then
        echo "백업 파일 압축 실패"
        exit 1
    fi
    echo "파일 압축이 완료되었습니다. NAVER Cloud Object Storage에 업로드를 시작합니다..."

    # curl을 사용하여 Object Storage에 업로드
    echo "curl을 사용하여 Object Storage에 업로드합니다..."
    
    # S3 날짜 형식 (RFC 1123)
    DATE=$(date -u +"%a, %d %b %Y %H:%M:%S GMT")
    
    # 업로드할 파일 경로
    FILE_PATH="${BACKUP_DIR}/${COMPRESSED_FILE}"
    
    # Content-Type 결정
    CONTENT_TYPE="application/gzip"
    
    # 버킷과 오브젝트 이름
    RESOURCE="/${BUCKET_NAME}/${OBJECT_PATH}"
    
    # 서명 생성을 위한 문자열
    STRING_TO_SIGN="PUT\n\n${CONTENT_TYPE}\n${DATE}\n${RESOURCE}"
    
    # HMAC-SHA1 서명 생성
    SIGNATURE=$(echo -en "${STRING_TO_SIGN}" | openssl sha1 -hmac "${SECRET_KEY}" -binary | base64)
    
    # curl을 사용하여 파일 업로드
    UPLOAD_RESULT=$(curl -s -X PUT \
        -T "${FILE_PATH}" \
        -H "Host: kr.object.ncloudstorage.com" \
        -H "Date: ${DATE}" \
        -H "Content-Type: ${CONTENT_TYPE}" \
        -H "Authorization: AWS ${ACCESS_KEY}:${SIGNATURE}" \
        "https://kr.object.ncloudstorage.com${RESOURCE}")
    
    # 업로드 성공 여부 확인 (HTTP 상태 코드로 확인)
    CURL_EXIT_CODE=$?
    if [ ${CURL_EXIT_CODE} -ne 0 ]; then
        echo "Object Storage 업로드 실패: curl 오류 (코드: ${CURL_EXIT_CODE})"
        echo "응답: ${UPLOAD_RESULT}"
        exit 1
    fi
    
    # 업로드가 성공적으로 완료되었는지 확인
    # (curl이 성공적으로 완료되면 빈 응답을 반환함)
    if [[ -n "${UPLOAD_RESULT}" && "${UPLOAD_RESULT}" == *error* ]]; then
        echo "Object Storage 업로드 실패: ${UPLOAD_RESULT}"
        exit 1
    fi
    echo "백업 파일이 성공적으로 NAVER Cloud Object Storage에 업로드되었습니다."
    echo "경로: ${BUCKET_NAME}/${OBJECT_PATH}"

    # 임시 백업 파일 정리
    rm -f ${BACKUP_DIR}/${COMPRESSED_FILE}
    echo "임시 파일이 정리되었습니다. 백업 프로세스가 완료되었습니다."
}

# 스크립트 실행
main

실제 환경에 맞게 스크립트를 수정하여 사용하시려면 main() 함수 시작 부분에 있는 데이터베이스 연결 정보와 버킷 정보를 변경해주셔야 합니다.

main() {
    # 데이터베이스 연결 정보
    DB_HOST="example.vpc-cdb-kr.ntruss.com"
    DB_USER="manvscloud"
    DB_PORT="15432"
    DB_NAME="manvscloud"
    BUCKET_NAME="manvscloud-psql-backup"

백업이 정상적으로 완료되면 지정한 Object Storage에 데이터베이스 백업이 있어야합니다.

# 스크립트 동작 방식 상세 설명

이 스크립트는 크게 다음과 같은 순서로 동작합니다:

  1. Secret Manager에서 데이터베이스 비밀번호 검색:
    • NAVER Cloud Secret Manager API를 호출하여 데이터베이스 비밀번호를 안전하게 가져옵니다.
    • API 호출 시 적절한 서명을 생성하여 인증을 수행합니다.
    • 응답에서 Python 스크립트를 이용해 비밀번호 값을 추출합니다.
  2. 데이터베이스 백업 생성:
    • pg_dump 명령어를 사용하여 데이터베이스의 백업을 생성합니다.
    • 백업 파일은 임시 디렉토리(/tmp)에 저장됩니다. (DB 및 볼륨 용량에 따라 경로 변경 필수)
    • 날짜와 시간을 포함한 파일명으로 생성하여 백업 버전 관리가 용이하게 합니다.
  3. 백업 파일 압축:
    • 생성된 SQL 백업 파일을 gzip으로 압축하여 용량을 줄입니다.
  4. Object Storage에 업로드:
    • AWS S3 호환 API를 통해 NAVER Cloud Object Storage에 백업 파일을 업로드합니다.
    • 년/월/일 형식의 폴더 구조로 저장하여 백업 파일을 체계적으로 관리합니다.
    • curl을 사용하여 직접 API 호출을 수행하므로 AWS CLI가 필요하지 않습니다.
  5. 정리 및 완료:
    • 업로드가 완료된 후 임시 파일을 삭제하여 디스크 공간을 확보합니다.

이제 작성된 스크립트를 실행하여 테스트해보고 정상적으로 실행된다면 다음과 같은 방법으로 자동화할 수 있습니다.

1) Crontab을 활용한 정기 백업 스케줄링

# 매일 새벽 2시에 백업 스크립트 실행
0 2 * * * /path/to/backup_script.sh >> /var/log/db_backup.log 2>&1

2) 기존 알림 시스템과의 통합
: 지난 포스팅에서 구현한 백업 성공/실패 알림 기능을 업그레이드하여 관리형 데이터베이스 백업 실패 시 자동으로 이 스크립트를 실행하도록 구성할 수 있습니다.

3) 백업 수명 주기 관리
: Object Storage의 수명 주기 정책을 설정하여 오래된 백업을 자동으로 삭제하거나 Archive Storage로 이동시킬 수 있습니다.


오늘 포스팅을 통해 관리형 데이터베이스에서 제공하는 백업이 실패하더라도 우리는 자체적인 백업 시스템을 구축하여 데이터를 안전하게 보관할 수 있게 되었습니다.

물론 이 방식에도 관리형 데이터베이스의 기본 백업 기능을 완전히 대체할 수는 없는 한계가 있습니다. 특히 특정 시점으로의 복구(Point-in-Time Recovery) 같은 고급 기능은 직접 구현하기 복잡할 수 있습니다. 따라서 가능하다면 관리형 백업과 이 스크립트를 병행하여 이중 백업 체계를 구축하는 것이 가장 이상적입니다.

이 글이 관리형 데이터베이스 백업에 대한 고민을 해결하는 데 도움이 되길 바랍니다.

긴 글 읽어주셔서 감사합니다.