macOS에서 autossh로 SSH 터널 자동화하기: 운영 DB 안전하게 접근하는 방법
왜 SSH 터널 자동화가 필요한가
개발을 하다 보면 운영 데이터베이스에 접근해야 할 때가 있습니다. 데이터 확인, 스키마 비교, 마이그레이션 검증 등 다양한 이유가 있죠. 그때마다 터미널을 열고 SSH 터널링 명령어를 입력하는 건 은근히 번거로운 일이었습니다.
특히 저처럼 로컬 Docker MySQL(개발용)과 운영 MySQL(OCI HeatWave)을 동시에 사용하는 환경에서는, 터널을 열 때마다 포트가 겹치지 않는지 신경 써야 했습니다. DBeaver에서 운영 DB를 확인하려면 먼저 터널부터 열어야 하고, 앱을 운영 DB에 연결해서 테스트하려면 또 터널을 확인해야 하고... 이런 반복이 쌓이다 보니, 한번 제대로 자동화해두자는 생각이 들었습니다.
사전 지식
이 글은 다음을 알고 있다고 가정합니다:
- SSH 기본 사용법 (
ssh -i key user@host) - SSH 포트 포워딩 개념 (
-L옵션) - macOS 터미널 기본 조작
전체 구조
먼저 완성된 구조를 보면 이해가 쉽습니다:
맥북 부팅
↓
LaunchAgent가 autossh 실행
↓
autossh가 SSH 터널 생성 (Tailscale VPN 경유)
↓
127.0.0.1:23306 → (SSH 터널) → 운영 MySQL HeatWave:3306
↓
앱, DBeaver 등에서 127.0.0.1:23306으로 접속하면 끝
로컬 Docker MySQL은 별도 포트(13306)에서 돌고 있으므로, 두 DB를 동시에 사용할 수 있습니다:
127.0.0.1:13306 → 로컬 Docker MySQL (개발)
127.0.0.1:23306 → 운영 MySQL HeatWave (SSH 터널)
autossh란
autossh는 SSH 연결을 모니터링하고, 끊어지면 자동으로 재연결하는 도구입니다. 일반 SSH 터널은 네트워크가 불안정하거나 맥이 잠자기에서 깨어날 때 끊어지는데, autossh는 이를 감지하고 다시 연결합니다.
설치는 Homebrew로 간단합니다:
brew install autossh
1단계: SSH config 설정
~/.ssh/config 파일을 생성하거나 수정합니다. 이 파일에 터널 설정을 저장해두면, 매번 긴 명령어를 입력할 필요가 없습니다.
Host heatwave-tunnel
HostName [Bastion 서버 IP]
User [SSH 유저명]
IdentityFile ~/.ssh/[키 파일명]
LocalForward 23306 [MySQL 내부 IP]:3306
ServerAliveInterval 30
ServerAliveCountMax 3
ExitOnForwardFailure yes
각 설정의 의미를 짚어보겠습니다:
- LocalForward 23306 [MySQL IP]:3306: 로컬 23306 포트를 원격 MySQL 3306 포트로 연결합니다
- ServerAliveInterval 30: 30초마다 keepalive 패킷을 전송합니다. 연결이 살아있는지 확인하는 용도입니다
- ServerAliveCountMax 3: keepalive 응답이 3번 연속 실패하면 연결을 끊습니다. 즉, 최대 90초 후에 끊어진 연결을 감지합니다
- ExitOnForwardFailure yes: 포트 포워딩이 실패하면 SSH 자체를 종료합니다. 이게 없으면 포트 포워딩 없이 SSH만 연결된 채로 남아있을 수 있습니다
파일 권한도 설정해야 합니다:
chmod 600 ~/.ssh/config
포트 번호 선택 시 고려사항
로컬 포트를 변경하는 것이 통상적입니다. 원격 측 3306은 실제 MySQL이 리스닝하는 포트이므로 바꿀 이유가 없고, 로컬 포트는 내 머신에서 터널 진입점을 어디로 할지 정하는 것일 뿐입니다. 저는 기존에 로컬 Docker MySQL이 13306을 쓰고 있어서, 운영 터널은 23306으로 정했습니다.
2단계: 수동 테스트
LaunchAgent를 설정하기 전에, 먼저 수동으로 터널이 잘 작동하는지 확인합니다:
# 터널 열기
autossh -M 0 -N heatwave-tunnel
# 다른 터미널에서 확인
lsof -i :23306 -sTCP:LISTEN
-M 0은 별도 모니터링 포트 없이 SSH의 ServerAlive 메커니즘에 의존하겠다는 뜻입니다. 별도 포트를 할당하는 것보다 단순하고 안정적입니다.
MySQL 접속도 테스트해봅니다:
mysql -h 127.0.0.1 -P 23306 -u [유저명] -p
여기서 연결이 되면 터널 설정은 정상입니다.
3단계: macOS LaunchAgent로 자동 실행
매번 터미널에서 autossh를 실행하는 건 자동화가 아닙니다. macOS의 LaunchAgent를 활용하면 부팅 시 자동으로 터널이 열립니다.
~/Library/LaunchAgents/com.heatwave.mysql-tunnel.plist 파일을 생성합니다:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.heatwave.mysql-tunnel</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/autossh</string>
<string>-M</string>
<string>0</string>
<string>-N</string>
<string>heatwave-tunnel</string>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/heatwave-tunnel.log</string>
<key>StandardErrorPath</key>
<string>/tmp/heatwave-tunnel.err</string>
<key>EnvironmentVariables</key>
<dict>
<key>AUTOSSH_GATETIME</key>
<string>0</string>
</dict>
</dict>
</plist>
주요 설정을 살펴보면:
- KeepAlive true: 프로세스가 종료되면 자동으로 다시 실행합니다
- RunAtLoad true: LaunchAgent가 로드될 때(부팅 시) 자동 실행합니다
- AUTOSSH_GATETIME 0: 첫 연결이 실패해도 즉시 재시도합니다. 기본값은 30초인데, 네트워크가 아직 준비되지 않은 부팅 직후에는 0이 적합합니다
LaunchAgent 등록:
launchctl load ~/Library/LaunchAgents/com.heatwave.mysql-tunnel.plist
4단계: 확인 및 관리
# 터널 상태 확인
lsof -i :23306 -sTCP:LISTEN
# 에러 로그 확인
cat /tmp/heatwave-tunnel.err
# 터널 중지
launchctl unload ~/Library/LaunchAgents/com.heatwave.mysql-tunnel.plist
# 터널 재시작
launchctl unload ~/Library/LaunchAgents/com.heatwave.mysql-tunnel.plist
launchctl load ~/Library/LaunchAgents/com.heatwave.mysql-tunnel.plist
끊어졌을 때 어떻게 되는가
autossh의 재연결 흐름은 이렇습니다:
SSH 연결 정상
↓
30초마다 keepalive 전송 (ServerAliveInterval)
↓
3회 연속 응답 없음 (ServerAliveCountMax)
↓
SSH 프로세스 종료 감지
↓
autossh가 새 SSH 프로세스 시작
↓
터널 복구, 23306 포트 다시 사용 가능
여기에 LaunchAgent의 KeepAlive가 이중 안전장치 역할을 합니다. autossh 프로세스 자체가 죽더라도 macOS가 다시 살려줍니다.
실제 사용 모습
이렇게 설정해두니, 이제 터널을 의식할 필요가 없어졌습니다:
- 앱 개발:
MYSQL_PORT=23306으로 환경변수만 바꾸면 운영 DB에 연결됩니다 - DBeaver:
127.0.0.1:23306으로 일반 MySQL 연결을 만들어두면 끝입니다. SSH 탭 설정이 필요 없습니다 - Drizzle Studio: 동일하게 로컬 접속처럼 사용합니다
로컬 Docker MySQL(13306)과 운영 HeatWave(23306)를 동시에 열어두고 자유롭게 전환할 수 있습니다.
보안 고려사항
편리한 만큼 보안에 신경 써야 할 부분이 있습니다.
SSH 키 관리
- SSH 키 파일 권한은 반드시
600으로 설정합니다 (chmod 600 ~/.ssh/your-key) - 키에 passphrase를 설정하고, macOS 키체인에 등록하면 매번 입력하지 않아도 됩니다
- 키 파일은 절대 Git에 커밋하지 않습니다.
.gitignore에*.key,*.pem을 추가하세요
네트워크 보안
- 저는 Tailscale VPN을 통해 Bastion 서버에 접근하고 있습니다. 공용 인터넷에 Bastion을 직접 노출하는 것보다 안전합니다
- Tailscale ACL로 어떤 기기가 어떤 서버에 접근할 수 있는지 제어할 수 있습니다
- VPN 없이 공인 IP로 접근하는 경우, Bastion 서버의 방화벽(Security Group)에서 접근 IP를 제한해야 합니다
DB 접근 권한
- 운영 DB에 접속하는 계정은 읽기 전용(READ ONLY) 권한만 부여하는 것이 안전합니다
- 꼭 쓰기가 필요하다면 별도 계정을 만들고, 필요한 테이블에만 권한을 부여하세요
- MySQL의
GRANT문으로 세밀하게 제어할 수 있습니다
터널 포트 보호
LocalForward는 기본적으로127.0.0.1(localhost)에만 바인딩됩니다. 같은 네트워크의 다른 기기에서 접근할 수 없습니다0.0.0.0:23306으로 바인딩하면 외부에서도 접근 가능하므로, 특별한 이유가 없는 한 피하세요
로그 관리
/tmp/에 로그를 남기고 있는데, 재부팅 시 자동으로 삭제됩니다- 영구 로그가 필요하면
~/Library/Logs/하위에 저장하는 것이 macOS 관례입니다 - 로그에 민감 정보(비밀번호, 키 내용)가 포함되지 않는지 확인하세요
한계와 대안
이 방식이 모든 상황에 적합한 건 아닙니다:
- OCI Bastion Service를 사용하는 경우: OCI Bastion은 세션이 최대 3시간으로 제한되어, autossh로 상시 연결을 유지할 수 없습니다. 매번 OCI CLI로 세션을 생성해야 합니다
- 팀 환경: 개인 개발 환경에는 적합하지만, 팀원 전체가 운영 DB에 접근해야 한다면 VPN + DB Proxy(예: ProxySQL) 같은 중앙화된 방법이 더 적절합니다
- 보안 정책이 엄격한 조직: 운영 DB에 개발자 로컬에서 직접 접근하는 것 자체가 금지된 곳도 있습니다. 이런 경우 읽기 전용 레플리카를 별도로 운영하는 방식을 고려해야 합니다
마무리
돌이켜 보면 별것 아닌 설정인데, 이걸 해두기 전에는 매번 터널 여는 게 은근히 스트레스였습니다. 특히 DBeaver에서 운영 DB를 확인하려고 할 때마다 "아, 터널 먼저 열어야지"하는 순간이 반복되니까요.
한번 설정해두면 맥북을 켤 때마다 자동으로 터널이 열리고, 끊어져도 알아서 재연결됩니다. 개발 흐름이 끊기지 않는다는 점에서, 투자 대비 효과가 꽤 좋은 자동화였다는 생각이 듭니다.
관련 글
macOS에서 OCI HeatWave MySQL 접속하기: Bastion SSH 터널 설정 가이드
Oracle Cloud HeatWave MySQL에 로컬에서 접속하기 위한 OCI CLI 설정과 Bastion SSH 터널 구성 과정을 정리했습니다. API 키 등록부터 터널 스크립트 작성, 트러블슈팅까지 전체 흐름을 다룹니다.
OCI MySQL HeatWave, Bastion 없이 Tailscale로 접속하기
Oracle Cloud MySQL HeatWave는 프라이빗 서브넷에만 배치되어 외부 접속이 번거롭습니다. Bastion Service 대신 Tailscale을 활용해 간편하게 접속하는 방법을 정리했습니다.
OCI HeatWave MySQL + NestJS 연동: Bastion을 통한 보안 연결 가이드
Oracle Cloud HeatWave MySQL을 로컬 개발 환경에서 안전하게 연결하고, NestJS + TypeORM으로 연동하는 전체 과정. Bastion SSH 터널 설정부터 Security List 트러블슈팅까지 실제 경험을 바탕으로 정리했습니다.