Spring Boot 3에서 SSH 터널을 통한 PostgreSQL 연결을 위한 설정 방법을 알려드리겠습니다.
1. application.yml 설정
spring:
datasource:
url: jdbc:postgresql://localhost:5433/your_database_name
username: your_db_username
password: your_db_password
driver-class-name: org.postgresql.Driver
jpa:
hibernate:
ddl-auto: validate
show-sql: true
properties:
hibernate:
dialect: org.hibernate.dialect.PostgreSQLDialect
format_sql: true
# SSH 터널 설정 (커스텀 프로퍼티)
app:
ssh:
tunnel:
host: your-ssh-server.com
port: 22
username: ssh_username
private-key-path: ~/.ssh/id_rsa
local-port: 5433
remote-host: localhost # SSH 서버에서 본 DB 호스트
remote-port: 5432 # 실제 PostgreSQL 포트
2. SSH 터널 자동 연결을 위한 Configuration 클래스## 3. 필요한 의존성 추가 (build.gradle)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.postgresql:postgresql'
implementation 'com.github.mwiede:jsch:0.2.17'
// 기타 필요한 의존성들...
}
3. ssh key 만들기 예시
ssh-keygen -t rsa -b 4096 -m PEM -f ~/.ssh/id_rsa -C "ceo@pink-spider.io"
- DB가 설치된 서버에 ~/.ssh 등에 authorized_keys (권한 644) 파일에 id_rsa.pub 내용 추가해야 됩니다.
- rsa가 아닌 OpenSSh로 keygen 했을 경우 com.github.mwiede:jsch 가 아닌 다른 라이브러리를 써야 됩니다.
SshTunnelConfig
@Configuration
@Slf4j
@Profile({"local", "dev", "stage", "prod"})
public class SshTunnelConfig {
@Value("${app.ssh.tunnel.host}")
private String sshHost;
@Value("${app.ssh.tunnel.port}")
private int sshPort;
@Value("${app.ssh.tunnel.username}")
private String sshUsername;
@Value("${app.ssh.tunnel.private-key-path}")
private String privateKeyPath;
@Value("${app.ssh.tunnel.local-port}")
private int localPort;
@Value("${app.ssh.tunnel.remote-host}")
private String remoteHost;
@Value("${app.ssh.tunnel.remote-port}")
private int remotePort;
@Value("${app.ssh.tunnel.passphrase:}")
private String passphrase;
private Session session;
@PostConstruct
public void createSshTunnel() {
try {
log.info("JSch SSH 터널 연결 시작...");
// 로컬 포트가 사용 가능한지 확인
if (!isPortAvailable(localPort)) {
throw new RuntimeException("로컬 포트 " + localPort + "가 이미 사용 중입니다.");
}
JSch jsch = new JSch();
// 개인키 파일 경로 처리
String keyPath = privateKeyPath;
if (keyPath.startsWith("~/")) {
keyPath = System.getProperty("user.home") + keyPath.substring(1);
}
// 파일 존재 및 읽기 권한 확인
File keyFile = new File(keyPath);
if (!keyFile.exists()) {
throw new RuntimeException("SSH 개인키 파일을 찾을 수 없습니다: " + keyPath);
}
if (!keyFile.canRead()) {
throw new RuntimeException("SSH 개인키 파일을 읽을 수 없습니다. 파일 권한을 확인하세요: " + keyPath);
}
log.info("SSH 개인키 파일: {}", keyPath);
// 키 형식 확인 (RSA 형식인지 체크)
try (java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(keyFile))) {
String firstLine = reader.readLine();
log.info("키 파일 형식: {}", firstLine);
if (firstLine != null && !firstLine.contains("BEGIN RSA PRIVATE KEY")) {
log.warn("RSA 형식이 아닌 키가 감지되었습니다: {}", firstLine);
log.warn("RSA 형식 키를 생성하려면: ssh-keygen -t rsa -b 4096 -m PEM -f {}", keyPath);
}
}
// 개인키 추가
if (passphrase != null && !passphrase.trim().isEmpty()) {
jsch.addIdentity(keyPath, passphrase);
log.info("패스프레이즈와 함께 개인키 로드 완료");
} else {
jsch.addIdentity(keyPath);
log.info("개인키 로드 완료");
}
// SSH 세션 생성
session = jsch.getSession(sshUsername, sshHost, sshPort);
session.setConfig("StrictHostKeyChecking", "no");
session.setConfig("PreferredAuthentications", "publickey");
session.setTimeout(30000);
log.info("SSH 서버 연결 시도: {}@{}:{}", sshUsername, sshHost, sshPort);
session.connect();
log.info("SSH 연결 성공");
// 포트 포워딩 설정
session.setPortForwardingL(localPort, remoteHost, remotePort);
// 포트가 열릴 때까지 대기
waitForPortToOpen(localPort, 10000);
log.info("SSH 터널이 성공적으로 생성되었습니다: localhost:{} -> {}:{}:{}",
localPort, sshHost, remoteHost, remotePort);
} catch (JSchException e) {
log.error("SSH 터널 생성 실패: {}", e.getMessage(), e);
// 구체적인 에러 메시지 제공
if (e.getMessage().contains("invalid privatekey")) {
log.error("개인키 형식이 잘못되었습니다. RSA 형식 키를 사용하세요.");
log.error("새 RSA 키 생성: ssh-keygen -t rsa -b 4096 -m PEM -f ~/.ssh/level_up_rsa");
} else if (e.getMessage().contains("Auth fail")) {
log.error("인증에 실패했습니다. 공개키가 서버에 등록되어 있는지 확인하세요.");
log.error("공개키 등록: ssh-copy-id -i {} {}@{}", privateKeyPath + ".pub", sshUsername, sshHost);
} else if (e.getMessage().contains("Connection refused")) {
log.error("서버 연결이 거부되었습니다. 호스트와 포트를 확인하세요.");
}
cleanupResources();
throw new RuntimeException("SSH 터널 연결에 실패했습니다: " + e.getMessage(), e);
} catch (Exception e) {
log.error("예상치 못한 오류: {}", e.getMessage(), e);
cleanupResources();
throw new RuntimeException("SSH 터널 설정 중 오류가 발생했습니다: " + e.getMessage(), e);
}
}
@PreDestroy
@EventListener(ContextClosedEvent.class)
public void closeSshTunnel() {
log.info("SSH 터널 종료 시작...");
cleanupResources();
log.info("SSH 터널 종료 완료");
}
private void cleanupResources() {
if (session != null && session.isConnected()) {
try {
session.disconnect();
log.info("SSH 세션이 종료되었습니다.");
} catch (Exception e) {
log.warn("SSH 세션 종료 중 오류: {}", e.getMessage());
}
}
}
private boolean isPortAvailable(int port) {
try (ServerSocket serverSocket = new ServerSocket(port)) {
return true;
} catch (Exception e) {
return false;
}
}
private void waitForPortToOpen(int port, long timeoutMillis) throws InterruptedException {
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis() - startTime < timeoutMillis) {
try (java.net.Socket socket = new java.net.Socket()) {
socket.connect(new java.net.InetSocketAddress("localhost", port), 1000);
log.info("포트 {}가 성공적으로 열렸습니다.", port);
return;
} catch (Exception e) {
// 포트가 아직 열리지 않음, 재시도
Thread.sleep(500);
}
}
log.warn("포트 {}가 {}ms 내에 열리지 않았습니다.", port, timeoutMillis);
}
}