배치 작업에서 여러 디렉터리를 SFTP 로 올려야 할 때, expect 로 프롬프트를 흉내 내어 비밀번호를 흘려 넣는 스크립트를 쓰는 경우가 많다. 이 방식은 세 가지 이유로 오래 버티지 못한다.
비밀번호가 스크립트에 평문으로 남는다. 프롬프트 문자열이 조금만 달라져도 expect 가 엉뚱한 자리에서 대기하다가 타임아웃으로 끝난다. 그리고 전송 실패를 종료 코드로 구분할 수 없어, 실패했는데도 성공으로 처리된다.
가장 먼저 할 일은 비밀번호를 없애는 것이다.
ssh-keygen -t ed25519 -f ~/.ssh/id_sftp -N ''
ssh-copy-id -i ~/.ssh/id_sftp.pub sftpuser@sftp.example.com
클라우드 스토리지의 SFTP 엔드포인트처럼 ssh-copy-id 가 통하지 않는 곳은 관리 콘솔에서 공개키를 등록한다. 접속 설정은 ~/.ssh/config 에 모아 두면 스크립트가 짧아진다.
Host sftp-target
HostName sftp.example.com
User sftpuser
IdentityFile ~/.ssh/id_sftp
StrictHostKeyChecking accept-new
sftp -b 는 명령을 파일에서 읽어 비대화식으로 실행한다. 명령 하나라도 실패하면 즉시 중단하고 0 이 아닌 코드로 끝나므로, 배치에서 성공 여부를 판별할 수 있다.
cat > /tmp/sftp.batch <<'BATCH'
-mkdir /upload/report
cd /upload/report
lcd /data/report
put -r ./
bye
BATCH
sftp -b /tmp/sftp.batch sftp-target
echo "exit=$?"
명령 앞의 - 는 그 줄이 실패해도 계속 진행하라는 표시다. 이미 있는 디렉터리를 mkdir 할 때 쓴다.
디렉터리를 통째로 올리려면 mput * 가 아니라 put -r 을 쓴다. mput 은 와일드카드로 펼친 파일만 올리고 하위 디렉터리로 내려가지 않는다. 기존 스크립트가 하위 폴더를 빠뜨리는 이유가 대개 이것이다.
원본과 대상 목록을 배열로 두고 한 쌍씩 돈다. 실패한 쌍을 기록해야 다음 날 무엇이 빠졌는지 알 수 있다.
#!/bin/bash
set -u
SRC=("/data/report/a" "/data/report/b")
DST=("/upload/a" "/upload/b")
FAILED=0
for i in "${!SRC[@]}"; do
printf -- '-mkdir %s\ncd %s\nlcd %s\nput -r ./\nbye\n' \
"${DST[$i]}" "${DST[$i]}" "${SRC[$i]}" > /tmp/sftp.$$.batch
if sftp -b /tmp/sftp.$$.batch sftp-target; then
echo "OK ${SRC[$i]} -> ${DST[$i]}"
else
echo "FAIL ${SRC[$i]} -> ${DST[$i]}" >&2
FAILED=1
fi
done
rm -f /tmp/sftp.$$.batch
exit $FAILED
바뀐 파일만 올리거나 삭제까지 맞추려면 sftp 만으로는 부족하다. lftp 의 mirror -R 이 이 용도에 맞다.
lftp -u sftpuser, -e "set sftp:connect-program 'ssh -i /home/batch/.ssh/id_sftp'; \
mirror -R --delete --verbose /data/report /upload/report; bye" sftp://sftp.example.com
상대 서버가 일반 SSH 서버라면 rsync -e ssh 가 가장 빠르고 안전하다. SFTP 전용(chroot + internal-sftp)으로 묶인 계정에서는 rsync 가 동작하지 않으므로 위 방법을 쓴다.
키를 등록할 수 없는 상대라면 비밀번호를 스크립트가 아니라 권한을 제한한 별도 파일이나 시크릿 저장소에서 읽는다. 값을 문서나 저장소에 남기지 않는다.
chmod 600 /etc/sftp/credentials
SFTP_PASSWORD="$(cat /etc/sftp/credentials)"