[배경]
개발 서버를 따로 운영하고 있다.
따로 로컬에서 db 작업하고 다시 개발서버로 적용하는 일이 번거로워서
애초에 개발서버에서 db 작업 후 바로 적용한다.
기존에는 개발용 데이터베이스를 학교에 두고 사용했다.
근데 종종 전원이 꺼지거나, 랜선이 뽑히는 등의 일이 발생해서
직접 학교에 가서 일일이 작업해야하는 소요가 있었다.
그래서 개발용 db 도 aws로 옮겨버렸다.
하지만 암호화를 사용하지 않고 db 연결해서 사용하면, (예로 3306포트로 다이렉트 연결할 경우)
패킷에 평문이 그대로 노출되기 때문에 종단간 암호화를 해주어야한다.
따라서 ssh tunneling 을 이용해서 공개키 암호화 방식의 프로토콜을 이용하되,
aws 상에서 forwarding 을 해주어 db로 연결하도록 했다.
[연결 개요]
- 원래 ssh 연결하면 22번 포트와 tcp 연결하여 소통하는 것이지만, 터널링은 일종의 프록시로서 외부에는 공개되어 있지 않은 내부 포트 또는 사설망 내부 다른ip에 연결하고자 할 때 연결한다.
- 로컬 내부에 사용하지 않는 포트 한개를 임의로 잡아서, ssh tunnel 연결해준다.
- ssh 연결 세션을 보유하고 있으면, 원격서버에서 설정한 ssh timeout 이 발생하기 전에는 연결이 끊기지 않는다.
- 톰캣(8080포트) 입장에서는 마치 별표친 부분에 DB가 있는 것 같은 것과 같은 효과이다.
[설정방법]
1. gradle 종속성 추가
: jsch 라이브러리를 추가해야한다. ssh, sftp 등 원격 연결할 때 사용되는 라이브러리이다.
/* 로컬 개발용 db ssh tunneling */
// https://mavenlibs.com/maven/dependency/com.jcraft/jsch
implementation 'com.jcraft:jsch:0.1.55'
2. SSH 및 포워딩 설정
# application-local.yml
spring:
datasource:
url: #jdbc:mariadb://localhost:[forwardedPort]/db이름
driver-class-name: #org.mariadb.jdbc.Driver
username: # db사용자 계정
password: # db사용자 비밀번호
ssh:
remote_jump_host: # ssh 연결할 중간 서버 주소
ssh_port: # ssh 프로토콜 포트
user: # ssh 연결할 사용자 계정
private_key: # ssh 연결 시 사용할 개인키 파일 경로
database_port: # 원격서버에 있는 db 포트
@Slf4j
@Profile("local")
@Component
@ConfigurationProperties(prefix = "ssh")
@Validated @Setter
public class SshTunnelingInitializer {
@NotNull private String remoteJumpHost;
@NotNull private String user;
@NotNull private int sshPort;
@NotNull private String privateKey;
@NotNull private int databasePort;
private Session session;
@PreDestroy
public void closeSSH() {
if (session.isConnected())
session.disconnect();
}
public Integer buildSshConnection() {
Integer forwardedPort = null;
try {
log.info("{}@{}:{}:{} with privateKey",user, remoteJumpHost, sshPort, databasePort);
log.info("start ssh tunneling..");
JSch jSch = new JSch();
log.info("creating ssh session");
jSch.addIdentity(privateKey); // 개인키
session = jSch.getSession(user, remoteJumpHost, sshPort); // 세션 설정
Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
log.info("complete creating ssh session");
log.info("start connecting ssh connection");
session.connect(); // ssh 연결
log.info("success connecting ssh connection ");
// 로컬pc의 남는 포트 하나와 원격 접속한 pc의 db포트 연결
log.info("start forwarding");
forwardedPort = session.setPortForwardingL(0, "localhost", databasePort);
log.info("successfully connected to database");
} catch (Exception e){
log.error("fail to make ssh tunneling");
this.closeSSH();
e.printStackTrace();
exit(1);
}
return forwardedPort;
}
}
3. JPA DataSource 등록
:웹 이벤트 리스너, 액션리스너 등을 이용해서 스프링부트가 시작되자 마자 ssh 연결을 시도하는 몇 블로그 글이 있었는데, JPA를 사용하지 않으면 가능한 방법이었다. 하지만 JPA를 사용하게 되면 스프링부트 어플리케이션이 시작되기 전에 db 연결을 먼저 확인하는 과정에서 언급한 방법들은 모두 실패했다.
따라서 JPA의 DataSource 를 수정해서 등록해주어야한다. 여러가지 방법을 시도해보았는데, 아래의 방법이 가장 깔끔하게 나온 것 같다. application.yml에서 설정한 spring.datasource 의 값들로 AutoConfiguration 되어 JPA 연관된 빈들이 등록되게 된다. 그 중에서 DataSourceProperties 라는 빈이 application.yml 에 설정한 정보를 갖고 있다.
dataSource 를 등록하는 과정에서 ssh 터널링을 수행하고, 터널링된 포트번호를 반환하여 url 을 재설정해준다. 이 외의 정보들로 구성되어 빈으로 등록되고 나면 JPA의 다른 컴포넌트들이 DataSource 빈을 이용하여 db에 접속하게 된다.
@Primary는 붙이지 않아도 잘 작동하는데, Autoconfiguration 되는 기본 DataSource가 @ConditionalOrMissingBean 설정이기 때문이다. 하지만 여러 dataSource 빈이 등록되는 경우 충돌을 피하고자, 로컬 개발환경에서는 우선적으로 사용하도록 했다.
@Slf4j
@Profile("local")
@Configuration
@RequiredArgsConstructor
public class SshDataSourceConfig {
private final SshTunnelingInitializer initializer;
@Bean("dataSource")
@Primary
public DataSource dataSource(DataSourceProperties properties) {
Integer forwardedPort = initializer.buildSshConnection(); // ssh 연결 및 터널링 설정
String url = properties.getUrl().replace("[forwardedPort]", Integer.toString(forwardedPort));
log.info(url);
return DataSourceBuilder.create()
.url(url)
.username(properties.getUsername())
.password(properties.getPassword())
.driverClassName(properties.getDriverClassName())
.build();
}
}
[결과 화면]
로그를 보면 ssh 터널링 연결이 수행되고 난 후에,
성공적으로 hibernate hikariDataSource Pool 이 생성되는 것을 볼 수 있다.
ssh 원격으로 연결하게 되면, 항상 암복호화 프로토콜을 거치기 때문에 데이터 전송속도가 느릴 수밖에 없다.
로컬 개발 환경 설정으로는 충분하지만, 실제 서비스에는 적용하지 않도록 하자.
실제 서비스와 같은 경우에는 같은 네트워크로 묶어서,
암복호화 프로토콜을 거치지 않도록 하는것이 더 성능상 바람직하다.
'웹 프로젝트 (IBAS) > SpringBoot api 개편' 카테고리의 다른 글
[Spring Boot] Jpa 테스트 작성 시 영속성 컨텍스트 관련 주의사항 (0) | 2022.08.25 |
---|---|
[SpringBoot] 9. OSIV 설정을 통한 쿼리 최적화 방안 고찰 (0) | 2022.08.02 |
[SpringBoot] 7. SpringSecurity 인증 모듈 개발 (OAuth2, jwt, 소셜로그인) (4) | 2022.07.02 |
DDD, 어그리게이트 분리를 위한 리팩토링 (0) | 2022.06.27 |
OAuth2 naver 회원 id 형식 문제 (네아로) (0) | 2022.05.14 |