Spring Boot와 JSch 활용한 ElastiCache SSH 터널링 설계하기
by rowing0328Intro
이번 글에서는 Spring Boot 애플리케이션에서 JSch 라이브러리를 활용해SSH 터널링을 설정하는 방법을 정리한다.
이 방법은 AWS ElastiCache와 같이 Private 네트워크에 위치한 서비스에 접근해야 할 때 매우 유용하다
SSH Tunneling
AWS ElastiCache와 같은 인프라는 기본적으로 Private 네트워크안에 존재한다. 따라서 외부에서 접근할 수 없다.
이를 해결하기 위해 SSH 터널링을 활용하면,
Bastion 서버를 통해 안전하게 Private 리소스에 접근할 수 있다.
의존성 추가
implementation 'com.github.mwiede:jsch:0.2.20'
SSH 터널링을 위한 환경 설정 추가
ssh:
host: ${SSH_HOST} # Bastion 서버의 호스트 주소
port: ${SSH_PORT} # Bastion 서버의 포트 (기본: 22)
user: ${SSH_USER} # SSH 접속 사용자명
private_key_path: ${SSH_PRIVATE_KEY_PATH} # SSH 개인 키 경로
spring:
data:
redis:
host: ${AWS_ELASITCACHE_REDIS_URL} # ElastiCache Redis 엔드포인트
port: ${AWS_ELASITCACHE_REDIS_PORT} # ElastiCache Redis 포트
application.yml에 SSH 접속 정보와 Spring Data Redis 설정을 추가한다.
위 설정을 통해 Bastion 서버를 경유해 Private 네트워크의 Redis에 안전하게 접근할 수 있는 환경을 구성한다.
SshTunnelingInitializer 구현하기
SshTunnelingInitializer는 JSch 라이브러리를 활용해 SSH 세션을 생성하고, Private 네트워크에 있는 데이터베이스 또는 리소스에 접근하기 위한 포트 포워딩을 설정한다.
코드 예제
package org.toastit_v2.core.common.infrastructure.ssh;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import jakarta.annotation.PreDestroy;
import jakarta.validation.constraints.NotNull;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import org.toastit_v2.core.common.application.code.CommonExceptionCode;
import org.toastit_v2.core.common.application.exception.RestApiException;
@Slf4j
@Validated
@Component
public class SshTunnelingInitializer {
private final Session session;
public SshTunnelingInitializer(
@NotNull @Value("${ssh.host}") String host,
@NotNull @Value("${ssh.user}") String user,
@NotNull @Value("${ssh.port}") Integer sshPort,
@NotNull @Value("${ssh.private_key_path}") String privateKeyPath
) {
try {
log.debug("SSH 연결을 시작합니다: 사용자={}, 호스트={}, 포트={}, 개인 키 경로={}", user, host, sshPort, privateKeyPath);
JSch jsch = new JSch();
jsch.addIdentity(privateKeyPath);
this.session = jsch.getSession(user, host, sshPort);
Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
log.debug("SSH 세션이 생성되었습니다. 연결을 시도합니다...");
session.connect();
log.debug("SSH 연결이 성공적으로 확립되었습니다.");
} catch (Exception e) {
log.error("SSH 연결 실패: {}", e.getMessage());
throw new RestApiException(CommonExceptionCode.SSH_CONNECTION_ERROR);
}
}
@PreDestroy
public void closeSSH() {
if (session.isConnected()) {
session.disconnect();
log.debug("SSH 연결이 성공적으로 종료되었습니다.");
}
}
public Integer buildSshConnection(String databaseUrl, int databasePort) {
Integer forwardedPort = null;
try {
log.debug("포트 포워딩을 시작합니다...");
forwardedPort = session.setPortForwardingL(0, databaseUrl, databasePort);
log.debug("데이터베이스에 성공적으로 연결되었습니다.");
} catch (Exception e) {
log.error("포트 포워딩 설정에 실패했습니다: {}", e.getMessage());
closeSSH();
throw new RestApiException(CommonExceptionCode.SSH_PORT_FORWARDING_ERROR);
}
return forwardedPort;
}
}
- SSH 연결 생성
JSch 객체를 생성하고 addIdentity를 통해 SSH 개인 키를 등록한다.
session을 생성하여 SSH 세션을 설정하며, StrictHostKeyChecking을 비활성화하여 비공인 호스트에도 연결할 수 있도록 설정한다. - 포트 포워딩
setPortForwarding(0, databaseUrl, databasePort) 메서드를 호출해 로컬 포트를 동적으로 할당하고 원격 리소스에 연결한다.
성공 시 포워딩된 로컬 포트를 반환한다. - 예외 처리
SSH 연결 실패 또는 포트 포워딩 실패 시 RestApiException을 던져 애플리케이션에서 예외를 처리할 수 있도록 한다. - 자원 정리
@PreDestroy를 활용해 애플리케이션 종료 시 SSH 세션을 안전하게 닫는다.
SSH 터널링 사용하기
아래는 SshTunnelingInitializer를 사용해 ElastiCache 또는 데이터베이스에 포트 포워딩을 설정하는 예제이다.
코드 예제
import jakarta.validation.constraints.NotNull;
import org.toastit_v2.core.common.infrastructure.ssh.SshTunnelingInitializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.validation.annotation.Validated;
@Validated
@Configuration
public class DevRedisConfig {
private final String host;
private final Integer port;
private SshTunnelingInitializer sshTunnelingInitializer;
public DevRedisConfig(
@NotNull @Value("${spring.data.redis.host}") String host,
@NotNull @Value("${spring.data.redis.port}") Integer port,
SshTunnelingInitializer sshTunnelingInitializer
) {
this.host = host;
this.port = port;
this.sshTunnelingInitializer = sshTunnelingInitializer;
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
Integer forwardedPort = sshTunnelingInitializer.buildSshConnection(host, port);
RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration();
standaloneConfiguration.setHostName("localhost");
standaloneConfiguration.setPort(forwardedPort);
return new LettuceConnectionFactory(standaloneConfiguration);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
}
- 포트 포워딩 설정
SshTunnelingInitializer를 사용해 SSH 터널링을 설정하고, Private 리소스(ElastiCache 또는 데이터베이스)에 접근하기 위한 포트를 로컬에 포워딩한다. - Redis 연결 설정
RedisStandaloneConfiguration을 사용해 포워딩된 로컬 포트를 기반으로 Redis에 연결한다.
Redis 템플릿(RedisTemplate)을 구성하여 키와 값을 직렬화한다. - 유효성 검사
@NotNull로 필수 속성을 검증하여 누락된 설정을 방지한다.
실행 및 확인
Redis CLI를 사용해 Bastion 서버 접근 확인
ssh -i <SSH_PRIVATE_KEY_PATH> <SSH_USER>@<BASTION_HOST> -L <LOCAL_PORT>:<REDIS_HOST>:<REDIS_PORT>
redis-cli -h 127.0.0.1 -p <LOCAL_PORT> ping
SSH 키와 설정 정보를 사용하여 Bastion 서버를 통해 Redis에 접근 가능한지 확인한다.
애플리케이션 실행 및 로그 확인
SSH Tunnel established on localhost:<FORWARDED_PORT> to <REDIS_HOST>:<REDIS_PORT>
애플리케이션을 실행한 뒤 SSH 터널링이 성공적으로 설정되었는지 로그를 확인한다.
성공 시 로그에 다음과 같은 메세지가 포함될 수 있다.
Redis 데이터 접근 확인
애플리케이션에서 Redis 관련 작업을 실행하여 동작이 정상적인지 검증한다.
'📌ETC > Development Log' 카테고리의 다른 글
K6와 InfluxDB를 활용한 성능 테스트 및 모니터링 설계하기 (2) | 2025.01.02 |
---|---|
Prometheus와 Grafana 활용한 Spring Boot 모니터링 설계하기 (0) | 2025.01.02 |
Spring Boot 3.x JVM 통계, Grafana 대시보드로 분석하기 (4) | 2024.12.16 |
블로그의 정보
코드의 여백
rowing0328