TL;DR

macOS terminal에서
나의 local PC에 있는 파일을
원격의 cloud에 있는 private server로 옮기고 싶은가?

다양한 방법이 있지만,
terminal에서 bash 스크립트 단 2줄로 실행할 수 있는 방안을 공유한다.

1. 해당 private server로 연결되는 bastion server를 이용하여, 터널을 생성한다
  ssh -fN -i /path/to/bastion-server-key.pem -L tunnel_port_num:private-server-ip:22 username@bastion-server-ip

2. rsync를 이용하여 터널을 통해 로컬의 파일을 옮긴다
  rsync -avz -e "ssh -i /path/to/bastion-server-key.pem -p tunnel_port_num" /path/to/local/source/ username@localhost:/path/to/remote/destination/

왜 rsync를 선택했는지 궁금하다면
본문의 <2.해결 방안의 도출 과정> 부분을 확인!

명령어에 대해서 자세히 이해하고 싶다면,
본문의 <3.해결 방안의 적용> 부분을 확인!

1.  문제 상황

- 어쩌다 원격의 private server에 로컬의 파일을 업로드하게 되었나?

요즘 크래프톤 정글에서 수행한 마지막 프로젝트를 보완하는 작업을 하고 있다.

팀 멤버중 백엔드를 담당했던 친구 두 명이 해외에 나가게 되면서,
내가 급히 API 수정을 하게 되었고

이 과정에서 AWS의 private subnet에 위치한 API 서버를
다시 배포해야 하는 상황이 생겼다.

쌩초짜에 시간도 촉박했던 우리팀은
Docker사용도, CI/CD 구축도 하지 못한 상태였기 때문에

로컬에서 서버를 빌드한 뒤,
Filezilla를 통해 빌드 폴더를 AWS에 옮기는 방식으로 서버 배포를 진행하고 있었다.

그러나 Filezilla를 사용하면 속도도 느리고 번거로워서

이번 기회에 Docker 사용해봐?
CI/CD 구축해봐? 하는 마음이 있었지만,

지금 당장 API를 고쳐야 하는 상황에서는
간단하면서도 빠르게 서버 배포가 가능한 방법이 필요했다.


2. 해결 방안의 도출 과정

- 왜 SSH와 rsync를 선택했나?

Claude에게
로컬의 파일을 원격의 private server로 업로드하는 방법을 물어보니,
다음의 4가지 방안을 알려주었다.

1. SCP(Secure Copy Protocol)

2. rsync

3. git 사용

4. AWS의 CLI 사용

 

먼저 3, 4 선택지를 바로 제외시켰다.

  • git을 사용하지 않은 이유
    • 우리는 보안 이슈 때문에 git에 build 폴더를 올리지 않았고,
    • 무거운 node_modules도 올리지 않았다.
    • 그러나, git을 사용한다면 private server에서 API 서버를 빌드해야 하고,
      build를 하고 npm의 package를 다운 받는 등의 과정이
      우리의 작고 소즁한 AWS EC2 instance에게는 너무 버거운 일이었다.
    • 그래서 나가리.
  • AWS CLI 사용하지 않은 이유
    • AWS는 사용하면 다 돈이다... S3를 쓰고 싶지 않았다.
    • AWS에 이런저런 복잡한 설정들을 해줘야 한다..
    • 매번 최신화 한 번 하기위해 AWS 접속해서 이것 저것 번거롭게 다 해줘야 한다.
    • 즉, 귀찮다. 그래서 나가리.

 

자, 그럼
1. SCP vs 2. rsync 중에 무얼 택할 것이냐?

최종 선택은 rsync!

  • SCP를 사용하지 않은 이유
    • SCP는 파일 전체를 완전 새로 전송하는 방식.
    • 그러나, 우리는 node_modules 등 대용량 전송이 필요하고,
      가능하다면 변경된 것만 전송하여 효율을 높이는 것이 중요하다.
    • 매번 전부 새로 전송하니까 느리고, 비효율적인 SCP도 나가리!
  • rsync를 사용한 이유
    • 우리처럼 파일 변경이 잦고, 최신화도 자주 해야하는 환경에서는
      바뀐 것만 업데이트 해주는 rsync의 기능이 딱 알맞다.
    • 심지어 압축 전송 기능까지 있어서 네트워크 대역폭도 절약 가능하다
    • 자주 일어나는 일은 아니지만, 전송이 중단되면 중단된 시점부터 다시 시작하는 안정성까지 보장 받을 수 있다
    • 어디에 사용하게될지는 모르겠지만, 파일의 메타데이터까지도 보존할 수 있다.

 

결론,

우리처럼
서버 수정 사항 생길 때마다
로컬에서 마음놓고 CPU 사용해서 build하고,
로컬에서 마음놓고 네트워크 사용해서 package 다운받고,

이 과정에서 생긴 많은 양의 파일을
빠른 시간 안에 효율적으로 뚝-딱 전송할 수 있는 방법은

rsync였다!


3. 해결 방안의 적용

- SSH와 rsync는 어떻게 사용하는가?

원격의 Private server로 나의 Local에 있는 파일을 업로드하려면 세 과정을 거쳐야 한다.

첫째, SSH를 사용하여, Bastion Host를 통해 Private으로 가는 터널을 로컬에 생성하는 작업 (백그라운드에서 실행되도록 함)
둘째, 로컬의 파일을 앞선 과정에서 열어둔 터널을 통해 Private server로 전송하는 작업
셋째, 앞서 열어둔 터널을 종료하는 작업

  • 이 과정에서 필요한 준비물
    • Bastion server에 접근하는 ssh key
    • Private server에 접근하는 ssh key 파일

준비가 다 되었다면,
terminal을 열고 bash 명령어를 작성하면 된다.


# STEP 1: 터널 열기

ssh -fN -i /path/to/bastion-key.pem -L tunnel_port_num:private-server-ip:22 username@bastion-public-ip

# 예시: ssh -fN -i ./app.pem -L 2222:12.3.45.678:22 ubuntu@1.23.345.67
  • 명령어 해석
    • Secure Shell아, 백그라운드에서 실행하고, 원격 셸은 열지 말아줘. 이 키 파일을 사용해서 Bastion host에 연결하고, 로컬 머신의 2222 포트를 private-server의 22번 포트로 포워딩해줘. 연결할 Bastion host의 주소는 여기야.
  • 옵션별 의미
    • -f: 백그라운드 실행
    • -N: 원격 셸을 열지 않음. (만약 원격 셸을 열었다면 이 터미널을 끄지말고, 다른 터미널에서 STEP2를 진행해주면 됨!)
    • -i: identity file (SSH 키 파일 지정)
    • -L: Local port forwarding (사용구문 => -L [bind_address:]port:host:hostport) bind_address를 명시하지 않으면 localhost로 자동 지정됨
    • tunnel_port_num: 터널을 사용하기 위해 로컬에서 여는 임의의 포트번호. 1024 이상의 겹치지 않을만한 번호로 지정해야 함
    • 22: SSH 포트 번호

 

# STEP 2: 파일 전송

rsync -avz -e "ssh -i /path/to/bastion-key.pem -p tunnel_port_num" /path/to/local/source/ username@localhost:/path/to/remote/destination/

# 예시: rsync -avz -e "ssh -i ./app.pem -p 2222" ./app/backend/dist ubuntu@localhost:./api_server
  • 명령어 해석
    • rsync야 전송하는 파일의 각종 정보 싹 보존해주고, 전송 과정을 터미널에 자세히 출력해주는데, 전송할때는 압축해서 보내줘. SSH 연결은 이 명령어로 설정된 터널을 통해 해. 내가 로컬에서 보낼 파일은 여기에 있고, 원격에서 저장할 경로는 여기야.
  • 옵션별 의미
    • -a: archive (전송하는 파일의 다양한 정보를 전부 보존하여 보냄)
    • -v: verbose (전송과정을 자세히 출력)
    • -z: zip (파일 전송할 때 압축함. zip방식은 아니지만 관례적으로 압축 옵션하면 zip이 연상되는 z를 씀. CPU 사용량이 올라갈 수 있음)
    • -e: external (원격 쉘 지정)
    • destination의 host가 localhost인 것은 터널을 이용하여 private으로 연결된 상태이기 때문임.
      즉, 이미 private server가 원격 셸의 로컬이 되어버린 것!
      만약 터널 사용없이 public server에 원격 전송을 한다면, username@public-server-ip가 되어야 함.

 

# STEP 3: 터널 종료

pkill -f "ssh -f -N -L tunnel_port_num:private-server-ip:22 username@bastion-server-ip"

# 예시: pkill -f "ssh -f -N -L 2222:12.3.45.678:22 ubuntu@1.23.345.67"
  • 명령어 해석
    • pkill아, 다음 쌍따옴표 안의 명령어와 완전히 똑같은 명령으로 실행되고 있는 process를 종료(kill)해줘.
  • 옵션별 의미
    • -f: full 혹은 fixed string. (전체 명령어 라인을 매칭시켜라)

 


아 근데
이 세 줄 매번 일일이 쳐주기 너무 귀찮다.

그래서 bash 명령어 3줄만 띡띡띡 작성해서 스크립트를 만드려고 했다.

그런데,

이 명령어들이 동기적으로 실행된다고 해도,
앞선 명령어 실행하다 에러가 발생하면 도미노처럼 무너지지 않을까? 라는 생각이 들어서

Claude에게 에러 핸들링을 할 수 있는 형태로 짜달라고 부탁했다!

그리고 이것이 최종 결과!

#!/bin/bash

# 변수 설정
BASTION_HOST="user@bastion-host-ip"
PRIVATE_SERVER="private-server-ip"
LOCAL_FILE="/path/to/local/file"
REMOTE_PATH="/path/on/remote/server"
REMOTE_USER="username"
TUNNEL_PORT="2222"
SSH_KEY_PATH="/path/to/private_key"

# SSH 옵션 설정
SSH_OPTIONS="-i $SSH_KEY_PATH"


# STEP 1.터널 생성 함수
create_tunnel() {
	ssh -fN $SSH_OPTIONS -L $TUNNEL_PORT:$PRIVATE_SERVER:22 $BASTION_HOST
    
    if [ $? -ne 0 ]; then
        echo "Failed to create SSH tunnel"
        exit 1
    fi
    sleep 5
}


# STEP 2.파일 전송 함수

transfer_file() {
    rsync -avz -e "ssh $SSH_OPTIONS -p $TUNNEL_PORT" $LOCAL_FILE $REMOTE_USER@localhost:$REMOTE_PATH
    if [ $? -ne 0 ]; then
        echo "File transfer failed"
        return 1
    fi
}


# STEP 3.터널 종료 함수
close_tunnel() {
    pkill -f "ssh $SSH_OPTIONS -f -N -L $TUNNEL_PORT:$PRIVATE_SERVER:22 $BASTION_HOST"
    while pgrep -f "ssh $SSH_OPTIONS -f -N -L $TUNNEL_PORT:$PRIVATE_SERVER:22 $BASTION_HOST" > /dev/null; do
        sleep 1
    done
}


# 스크립트 전체 종료 함수
cleanup() {
	echo "Performing cleanup..."
    close_tunnel
}


#----------------
# 스크립트가 어떤 방식으로든 끝나게 되면 실행될 종료 구문 설정
trap cleanup EXIT

# 메인 실행 과정
# SSH 키 확인
if [ ! -f "$SSH_KEY_PATH" ]; then
    echo "SSH key not found at $SSH_KEY_PATH"
    exit 1
fi


# STEP 1
echo "Creating SSH tunnel..."
create_tunnel

# STEP 2
echo "Transferring file..."
if transfer_file; then
    echo "File transfer completed successfully."
else
    echo "File transfer failed. Exiting."
    exit 1
fi

echo "Script completed successfully."

+ Recent posts