Cloud/GCP

[GCP] Cloud SQL 백업을 타프로젝트 및 다른 인스턴스로 복원하기 (gcloud+shell script)

달빛궁전- 2025. 5. 21. 08:30
GCP Cloud SQL은 GUI를 통해 간편하게 백업을 생성하고 동일 인스턴스로 복원하는 기능을 제공합니다.
하지만 백업을 다른 새 인스턴스나 다른 프로젝트의 인스턴스로 복원해야 할 경우, GUI에서는 직접적인 지원이 없습니다.
GUI에서 지원하지 않는 기능이 CLI에서 지원되는 경우가 있습니다.

Cloud SQL의 내장 백업은 파일 형태로 직접 내보내기(Export)를 지원하지 않으며, 데이터를 파일로 옮기려면 mysqldump와 같은 별도의 내보내기 작업을 통해 Cloud Storage 버킷 등에 저장한 후 다시 가져오기(Import) 해야 합니다.
이와 달리, gcloud CLI를 사용하면 Cloud SQL 백업에서 바로 다른 인스턴스(신규 또는 타 프로젝트)로 복원할 수 있습니다.

이는 mysqldump 후 가져오기 하는 방식보다 중간 저장/관리 단계가 없어 더 간결하고 효율적이며, 특히 전체 인스턴스 상태를 특정 시점으로 복구하고자 할 때 매우 유용합니다.
 
필요권한
소스 프로젝트 : Cloud SQL 뷰어 (roles/cloudsql.viewer) / 프로젝트 내의 백업 목록가 세부정보를 보기 위함
대상 프로젝트 : Cloud SQL 편집자 (roles/cloudsql.editor)⁠ / Cloud SQL 복원 작업을 하기 위함
 
사용 방안 (입력 값) 
 
  • 소스 프로젝트 ID
    소스 Cloud SQL 인스턴스 이름(백업 목록을 가져올 인스턴스)
    복원할 대상 Cloud SQL 인스턴스 이름
    대상 프로젝트 ID

    위의 값이 필요하며, 작성한 스크립트에는 위의 값을 순서대로 입력 받은 후 진행되도록 하였습니다.

구동 순서 : 
  • 입력받은 소스 프로젝트와 인스턴스에 대해 성공(SUCCESSFUL)한 백업 목록을 가져옵니다.
  • 백업 목록을 사용자에게 번호와 함께 표시합니다 (시작 시간, 타입 포함).
  • 사용자가 번호를 선택하면 해당 백업을 사용하여 복원을 진행합니다.
  • 복원 시간을 측정하여 마지막에 출력합니다.
 
  1. Test할 Main-DB 생성 후 테스트 값 입력


  2. 스크립트 실행
    소스 프로젝트, 소스CloudSQL명, 대상(복원) 프로젝트, 대상(복원) CloudSQL명을 입력합니다.





    해당 명령 실행 후에는 시간이 꽤 걸립니다.
    그리고 gcloud는 명령어 실행에 시간이 너무 오래걸리면 에러메시지를 출력하고 종료가 됩니다.
    다만 실제 작업이 종료가 된 것이 아닌 “gcloud beta sql operations wait” 명령을 통해 해당 작업이 제대로 진행 되고 있는지를 확인해야 합니다.
    하여, Faild 메시지 출력이라 하더라도 뒤에 is taking longer than expected.*gcloud beta sql operations wait 메시지가 발생되면 최종적으로 Done메시지가 나올때까지 확인하도록 하였습니다.

확인
스크립트 동작 완료 후 
명시적으로 GUI작업결과에서 복원이 완료 되었는지 확인합니다.



그리고 테스트 값으로 넣은 테이블과 값들이 복원대상 Cloud SQL에 잘 입력되었는지 확인 합니다.

아래는 쉘스크립트 내용 입니다.

#!/bin/bash

#2025.05.21 Choi SeongGi

# 전역 변수로 선택된 백업의 전체 경로 저장
SELECTED_BACKUP_NAME=""

# --- 함수 정의 ---

get_user_inputs() {
    echo "Cloud SQL 백업 복원 자동화 스크립트"
    echo "---------------------------------------"
    read -p "1. 백업이 있는 소스 프로젝트 ID를 입력하세요: " SOURCE_PROJECT_ID
    while [ -z "$SOURCE_PROJECT_ID" ]; do
        read -p "소스 프로젝트 ID는 필수입니다. 다시 입력하세요: " SOURCE_PROJECT_ID
    done

    read -p "2. 백업 목록을 가져올 (필터링 기준이 될) 소스 Cloud SQL 인스턴스 이름을 입력하세요: " SOURCE_INSTANCE_NAME
    while [ -z "$SOURCE_INSTANCE_NAME" ]; do
        read -p "소스 인스턴스 이름은 필수입니다. 다시 입력하세요: " SOURCE_INSTANCE_NAME
    done

    read -p "3. 복원할 대상 프로젝트 ID를 입력하세요 (소스와 동일하면 비워두고 엔터): " TARGET_PROJECT_ID
    if [ -z "$TARGET_PROJECT_ID" ]; then
        TARGET_PROJECT_ID="$SOURCE_PROJECT_ID"
        echo "   -> 대상 프로젝트 ID가 소스 프로젝트 ID($SOURCE_PROJECT_ID)와 동일하게 설정되었습니다."
    fi

    read -p "4. 복원할 대상 Cloud SQL 인스턴스 이름 (복원할 인스턴스): " RESTORE_INSTANCE_NAME
    while [ -z "$RESTORE_INSTANCE_NAME" ]; do
        read -p "복원할 대상 인스턴스 이름은 필수입니다. 다시 입력하세요: " RESTORE_INSTANCE_NAME
    done
    echo "---------------------------------------"
}

select_backup() {
    echo ""
    echo "프로젝트 [$SOURCE_PROJECT_ID]의 백업 목록을 가져와서 인스턴스 [$SOURCE_INSTANCE_NAME] 기준으로 필터링합니다..."

    # gcloud 기본 출력을 사용 ( --instance 플래그 및 --format 플래그 제거 )
    backup_list_raw_output=$(gcloud sql backups list \
        --project="$SOURCE_PROJECT_ID" \
        --sort-by="~windowStartTime")
    # 참고: --filter 옵션은 awk 내부에서 상태 및 인스턴스 필터링을 수행하므로 여기서는 생략

    if [ -z "$backup_list_raw_output" ]; then
        echo "오류: 프로젝트 [$SOURCE_PROJECT_ID]에서 백업 목록을 가져올 수 없거나 출력이 비어있습니다."
        exit 1
    fi

    declare -a backup_full_names
    declare -a backup_display_options

    # awk를 사용하여 기본 출력 파싱
    # SOURCE_INSTANCE_NAME 변수를 awk로 전달
    parsed_backups=$(echo "$backup_list_raw_output" | awk -v target_instance_name="$SOURCE_INSTANCE_NAME" '
    BEGIN { 
        RS = ""; FS = "\n"; OFS = "|";
        name_regex = "^projects/[^/]+/backups/[a-f0-9-]+$";
    }
    {
        name_val=""; wst_val=""; type_val=""; instance_val=""; state_val="";
        
        for (i=1; i<=NF; i++) {
            current_line = $i;
            gsub(/^[ \t]+|[ \t]+$/, "", current_line); # 각 줄의 앞뒤 공백 제거

            if (current_line ~ /^NAME: /) {
                temp_val = current_line;
                sub(/^NAME: /, "", temp_val); 
                gsub(/^[ \t]+|[ \t]+$/, "", temp_val); 
                if (temp_val ~ name_regex) { name_val = temp_val; }
            } else if (current_line ~ /^WINDOW_START_TIME: /) {
                wst_val = current_line;
                sub(/^WINDOW_START_TIME: /, "", wst_val);
                gsub(/^[ \t]+|[ \t]+$/, "", wst_val);
            } else if (current_line ~ /^TYPE: /) {
                type_val = current_line;
                sub(/^TYPE: /, "", type_val);
                gsub(/^[ \t]+|[ \t]+$/, "", type_val);
            } else if (current_line ~ /^INSTANCE: /) {
                instance_val = current_line;
                sub(/^INSTANCE: /, "", instance_val);
                gsub(/^[ \t]+|[ \t]+$/, "", instance_val);
            } else if (current_line ~ /^STATE: /) { 
                state_val = current_line;
                sub(/^STATE: /, "", state_val);
                gsub(/^[ \t]+|[ \t]+$/, "", state_val);
            }
        }
        
        if (name_val != "" && instance_val == target_instance_name && state_val == "SUCCESSFUL") {
            print name_val, wst_val, type_val
        }
    }
    ')

    if [ -z "$parsed_backups" ]; then
        echo "오류: awk로 파싱한 결과, 인스턴스 [$SOURCE_INSTANCE_NAME]에 해당하는 성공한 백업이 없습니다."
        exit 1
    fi
    
    count=0
    while IFS= read -r line || [[ -n "$line" ]]; do
        if [ -z "$line" ]; then
            continue
        fi
        
        local original_ifs="$IFS"
        IFS='|' read -r name_val start_time_val type_val <<< "$line"
        IFS="$original_ifs"

        if [[ -n "$name_val" && "$name_val" == "projects/"* ]]; then
            backup_full_names+=("$name_val")
            
            local formatted_time=$(echo "$start_time_val" | sed 's/T/ /; s/\..*//')
            local display_id_part=$(basename "$name_val")
            backup_display_options+=("시작: $formatted_time, 타입: $type_val (ID: $display_id_part)")
            count=$((count + 1))
        else
            echo "경고: 다음 awk 출력 라인에서 백업 정보를 올바르게 파싱하지 못했습니다: '$line'"
            echo "      (name_val: '$name_val', start_time_val: '$start_time_val', type_val: '$type_val')"
        fi
    done <<< "$parsed_backups"

    if [ ${#backup_full_names[@]} -eq 0 ]; then
        echo "오류: 유효한 백업 목록을 구성하지 못했습니다 (awk 파싱 후)."
        exit 1
    fi

    echo ""
    echo "복원할 백업을 선택하세요 (번호 입력):"
    PS3="번호 선택 (취소하려면 'q' 입력): "
    select opt in "${backup_display_options[@]}" "취소"; do
        if [[ "$REPLY" == "q" || "$opt" == "취소" ]]; then
            echo "복원 작업을 취소했습니다."
            exit 0
        elif [[ "$REPLY" -gt 0 && "$REPLY" -le ${#backup_full_names[@]} ]]; then
            SELECTED_BACKUP_NAME="${backup_full_names[$(($REPLY - 1))]}"
            echo "선택된 백업 경로: $SELECTED_BACKUP_NAME"
            break
        else
            echo "잘못된 선택입니다. 목록에 있는 번호를 입력하거나 'q'를 입력하여 취소하세요."
        fi
    done

    if [ -z "$SELECTED_BACKUP_NAME" ]; then
        echo "백업이 선택되지 않았습니다. 스크립트를 종료합니다."
        exit 1
    fi
}

perform_restore() {
    echo ""
    echo "----------------------------------------------------"
    echo "선택된 백업 ($SELECTED_BACKUP_NAME)을"
    echo "대상 프로젝트 [$TARGET_PROJECT_ID]의 인스턴스 [$RESTORE_INSTANCE_NAME](으)로 복원합니다."
    echo "주의: 대상 인스턴스에 현재 데이터가 있다면 모두 손실됩니다!"
    echo "----------------------------------------------------"

    read -p "정말로 복원을 진행하시겠습니까? (y/N): " confirm_restore
    if [[ ! "$confirm_restore" =~ ^[Yy]$ ]]; then
        echo "복원이 취소되었습니다."
        exit 0
    fi

    GCLOUD_RESTORE_COMMAND="gcloud sql backups restore \"$SELECTED_BACKUP_NAME\" \
        --restore-instance=\"$RESTORE_INSTANCE_NAME\" \
        --project=\"$TARGET_PROJECT_ID\" \
        --quiet"

    echo "실행 명령어: $GCLOUD_RESTORE_COMMAND"
    echo "복원 중입니다... 이 작업은 시간이 오래 걸릴 수 있습니다."
    echo "----------------------------------------------------"

    start_time=$(date +%s)
    
    restore_output_and_error=$(eval "$GCLOUD_RESTORE_COMMAND" 2>&1)
    restore_initial_exit_code=$?
    
    echo "$restore_output_and_error"

    final_exit_code=$restore_initial_exit_code
    restore_status="실패 (초기 상태)"

    if [ $restore_initial_exit_code -eq 0 ]; then
        restore_status="성공 (즉시 완료)"
    else
        if echo "$restore_output_and_error" | grep -q "is taking longer than expected.*gcloud beta sql operations wait"; then
            echo ""
            echo "초기 복원 명령이 예상보다 오래 걸리고 있습니다. Cloud SQL Operation 상태를 확인합니다..."
            
            operation_url=$(echo "$restore_output_and_error" | grep -o 'https://sqladmin.googleapis.com/sql/v1beta4/projects/[^/]*/operations/[^ ]*' | head -n 1)
            operation_id=$(basename "$operation_url")
            wait_project_id="$TARGET_PROJECT_ID"

            if [[ -n "$operation_id" ]]; then
                wait_command_to_run="gcloud beta sql operations wait \"$operation_id\" --project \"$wait_project_id\""
                max_wait_retries=60 
                wait_retry_count=0
                operation_completed_successfully=false

                while [ $wait_retry_count -lt $max_wait_retries ]; do
                    echo ""
                    echo "대기 명령어 실행 (시도: $((wait_retry_count + 1))/$max_wait_retries): $wait_command_to_run"
                    echo "대기 중..."
                    echo "----------------------------------------------------"

                    current_wait_output_and_error=$(eval "$wait_command_to_run" 2>&1)
                    current_wait_exit_code=$?
                    
                    echo "$current_wait_output_and_error"

                    if [ $current_wait_exit_code -eq 0 ]; then
                        # 'gcloud beta sql operations wait' 성공 시, 최종 상태는 'STATUS: DONE' 이어야 함
                        if echo "$current_wait_output_and_error" | grep -q "STATUS: DONE"; then
                            restore_status="성공 (비동기 대기 후 완료)"
                            final_exit_code=0
                            operation_completed_successfully=true
                        else
                            echo "오류: 'wait' 명령어가 성공(0)으로 종료되었으나, 최종 상태가 'STATUS: DONE'이 아닙니다. 작업 결과를 확인하세요."
                            restore_status="실패 (비동기 대기 후 상태 불일치)"
                            final_exit_code=1 
                        fi
                        break 
                    else
                        if echo "$current_wait_output_and_error" | grep -q "is taking longer than expected.*gcloud beta sql operations wait"; then
                            wait_retry_count=$((wait_retry_count + 1))
                            if [ $wait_retry_count -ge $max_wait_retries ]; then
                                echo "오류: 최대 대기 재시도 횟수($max_wait_retries)에 도달했습니다. 작업을 수동으로 확인하세요: $operation_id"
                                restore_status="실패 (대기 시간 초과)"
                                final_exit_code=$current_wait_exit_code 
                                break 
                            fi
                            echo "작업이 여전히 진행 중입니다. 30초 후 다시 확인합니다..."
                            sleep 30
                        else
                            restore_status="실패 (비동기 대기 중 최종 오류)"
                            final_exit_code=$current_wait_exit_code
                            break 
                        fi
                    fi
                done

                if ! $operation_completed_successfully && [ $wait_retry_count -ge $max_wait_retries ]; then
                    restore_status="실패 (최대 대기 재시도 초과)"
                    # final_exit_code은 이미 루프 내에서 설정됨
                fi
            else
                echo "오류: gcloud 에러 메시지에서 Operation ID를 추출하지 못했습니다."
                restore_status="실패 (Operation ID 추출 오류)"
            fi
        else
            restore_status="실패 (초기 복원 명령 실패)"
        fi
    fi

    end_time=$(date +%s)
    duration=$((end_time - start_time))
    hours=$((duration / 3600))
    minutes=$(( (duration % 3600) / 60 ))
    seconds=$((duration % 60))

    echo ""
    echo "----------------------------------------------------"
    echo "Cloud SQL 백업 복원 작업 완료."
    echo "상태: $restore_status"
    echo "총 실행 시간: $duration 초"
    printf "시간 형식: %02d시간 %02d분 %02d초\n" $hours $minutes $seconds
    echo "----------------------------------------------------"

    if [ $final_exit_code -ne 0 ]; then
        exit $final_exit_code
    fi
}

# --- 메인 스크립트 실행 ---
get_user_inputs
select_backup
perform_restore

exit 0
 
참고