이번에 새롭게 코지메이트(Cozymate) 프로젝트에 참가하게 되었습니다. 뭔가 요즘 일을 점점 늘려가는거같은데... 아직까지는 충분히 할 정도의 양인거같아서 하고는 있지만..ㅎㅎ 시간에 쫓긴다면 뭐 잠을 줄이면 해결되는 문제가 아닐까요?
암튼 이번에 프로젝트를 하면서 서버 세팅을 처음부터 진행하고 있는데요. 현재 사용하려는 서비스는 다음과 같습니다.
EC2
RDS(MySQL)
S3
간단하죠? 실제로 프로젝트를 진행하면서 자주 사용하는 조합이기도 하고, 프리티어로 제공하고 있기 때문에 무료로 진행하기에 아주 좋은 선택이라고 저는 항상 생각합니다.
프리티어로 진행하고 있는 만큼 가장 중요하게 생각하고 있는 부분은 비용을 최대한 발생시키지 말자! 인데요. 그렇게 야심차게 진행하던 중 초기 세팅부터 문제가 생겨버렸습니다.
RDS
를 사용할 때Public Access
주소에서 과금이 발생한다!
라는 것입니다. RDS
를 사용할 때 외부에서 Datagrip
을 통해서 접근할 수 있어야 데이터 파악이 편해지는데, 그것을 하기 위해서는 Public Access
를 해야하고... 하지만 Public Access
를 하기 위해서는 비용이 과금되고... 라는 문제가 생겨버렸습니다.
그리고 로컬에서 테스트할 때에도 DB에 접근하기 위해서는 이 Public Access
를 사용해야하는데... 아주 큰 문제입니다.
위 이미지에서 퍼블릭 액세스 가능을 선택하게 된다면 그때부터 1시간에 0.005달러의 비용이 부과되게 됩니다. (이러면 하루에 0.12달러... 한달에 약 4달러...면 거의 5000원...)
이 사실을 간과하고 있던 나머지 비용이 발생하게 되었죠...
지금은 0.01달러로 표시되는데 아마 업데이트된다면 0.05달러는 나오지 않을까 싶네요.(피같은 50원..)
이렇게 비용이 발생했다는걸 깨닫고 이를 우회할 방법을 찾아야했습니다.
여러 방식을 찾아보았지만 가장 명확한 해답을 알려준 사이트는 다음과 같았습니다.
명확한 해답을 알려준 곳
해당 사이트에 따르면 EC2
와 VPC
를 연결해서 하나의 IP 주소
로 같이 사용한다는건데요.
EC2
에서도 고정 IPv4
를 받기 위해서는 RDS
와 마찬가지로 비용이 발생합니다.
하지만 EC2
에서는 고정 IP
를 제외하고도 생성했을 때 받을 수 있는 IP
가 하나 있죠.
퍼블릭 IPv4
입니다. 이 주소는 EC2
를 재시작할 때마다 바뀌는 주소인데요. 한번 실행되면 재시작을 할 때까지 바뀌지 않기 때문에 어느정도는 고정 IP
로 사용할 수 있다는 특징이 있습니다.
따라서 RDS와 EC2를 연결해서 EC2의 SSH 연결을 거쳐서 -> RDS의 주소로 접근한다.
라는 과정으로 진행하면 된다고 합니다.
EC2
와 RDS
를 연결AWS에는 RDS에서 EC2에 연결할 수 있는 기능
을 제공하고 있습니다.
이는 RDS를 생성할 때
에 설정할 수도 있고, 생성한 이후에 RDS 작업
에서 연결할 수도 있습니다.
아래는 RDS를 생성할 때의 옵션
입니다.
아래는 생성 이후 작업에서의 옵션
입니다.
동일한 가용영역에 있어야 실제 무료로 이용이 가능하다고 합니다.
Datagrip
에서의 연결위에 언급한 사이트와 별 다른 차이는 없습니다.
명확한 해답을 알려준 곳에서 자세한 내용을 확인하면 되며, 저 또한 이 사이트에서의 내용을 참고하여 구성한 것입니다. (거의 비슷합니다.)
일단 기본적으로 RDS
를 연결하는 방식과 동일하게 일반 부분을 채웁니다. 현재 프로젝트에서는 MySQL
을 사용하며, 만약 다른 경우 해당하는 DB를 선택해주시면 됩니다.
이후에 SSH
를 구성해주어야 하며, SSH 터널 사용
을 체크해주고, 새로운 SSH를 구성
해주어야 합니다.
새로운 SSH는 EC2의 IP와 22번 포트를 통해 접근
하면 됩니다. 사용자 이름은 OS마다 다른데, 현재 프로젝트에서는 Ubuntu
를 사용하기 때문에 사용자 이름을 ubuntu
로 입력해주면 됩니다. (기본 이름이 ubuntu이며, 만약 이름을 따로 설정했다면 해당 이름으로 입력하면 됩니다.)
마찬가지로 EC2
에 ssh
연결하는 방법처럼 pem
파일을 등록해주면 되며, 설정 파일 관련 설정사항을 체크해주고 연결 테스트를 통해 제대로 연결되었는지 확인해주면 등록할 수 있습니다.
만약 연결 테스트를 했을 때 실패했다면, EC2
에 연결되는 인바운드에 22포트가 열려있는지를 확인
해보아야 하며, 추가로 3306
도 아마 열려있어야 합니다. (아마도...)
마지막으로 구성한 SSH
를 선택해주고, 연결 테스트를 해주면 아마도... 성공이 뜰겁니다. 따로 이 부분에서 에러가 발생하지는 않아서 에러 케이스에 대해 설명하기엔 어렵네요... 그리고 2번에 해당하는 로컬 포트
는 <동적>
으로 비워야 정상 작동하니 참고하시면 됩니다.
EC2에서 접근하는 방법은 명령어 하나면 됩니다.
sudo mysql -h [RDS Endpoint] -u [User] -p
로 접근하면 됩니다.
일반적으로 RDS를 Datagrip에 연결하는게 그냥 SQL을 공부하기 위함이 아니라면, 다른 서버 프레임워크에 연결할겁니다. 이제 Spring
에서 어떻게 이를 연결할지 찾아보고 도전해봤습니다.
현재 RDS를 연결할 수 있는 방식은 EC2에서 RDS의 Endpoint와 Port로 접근하는 방식
입니다. 따라서 RDS를 연결하기 위해서는 EC2와 SSH 연결을 통해서 터널
을 만들고 그 터널을 통해 RDS에 접속
해야하는 것이죠.
위 이미지처럼 말이죠. Local에서 EC2로 SSH 연결
을 만들고, EC2 내부에서 RDS에 접근
하는겁니다.
이 방식을 CLI
에서 명령어로 처리하게 되면 다음과 같습니다.
sudo ssh -v -L [Local Port]:[Remote Server Address]:[Remote Server Port] \
-i [pem 파일 경로(보안키)] [사용자]@[서버 주소]
이를 EC2
와 RDS
로 바꾸게 되면 이렇게 됩니다.
sudo ssh -v -L [Local Port]:[RDS Endpoint]:3306 \
-i [pem 파일 경로(보안키)] ubuntu@[EC2 퍼블릭 주소]
Local Port
는 해당 명령어를 사용하는 컴퓨터의 Port를 의미하므로 사용하지 않는 아무런 포트나 사용해도 문제 없습니다. MySQL이 없는 컴퓨터라면 3306
을 그대로 사용해도 충분하죠!
이제 이 방식을 그대로 Spring에 그대로 적용해봅시다.
본 프로젝트에서는 Gradle을 사용하며, Spring Boot 3 버전을 사용합니다.
Spring에서도 그대로 SSH 연결을 통해 터널
을 만들고 이후에 RDS에 연결
하는 절차를 거칩니다.
터널링 연결에 대한 설정은 여기를 참고하였습니다.
ssh를 연결해서 터널을 만드는 것부터 해야하는데요. 그러기 위해서는 다음의 라이브러리가 추가되어야합니다.
// build.gradle
implementation 'com.github.mwiede:jsch:0.2.16'
이 라이브러리는 com.jcraft:jsch
에서 파생되어나온 것으로 jcraft의 jsch가 더이상 관리되고있지 않음에 따라서 생긴 것이라고 합니다. 이 라이브러리를 사용하게 되면 pem 파일을 보안키로 사용할 수 없기 때문에 SSH 연결이 진행되지 않는 문제가 발생한다고 합니다.
이제 SSH 연결에 관련된 정보
를 추가해주어야합니다.
// application.yml
ec2:
remote_jump_host: [EC2 IP 주소]
ssh_port: 22
user: [user: 기본은 ubuntu]
private_key_path: [pem과 같은 보안키 파일 경로]
database_endpoint: [RDS Endpoint]
database_port: 3306
spring:
datasource:
url: jdbc:mysql://localhost:[forwardedPort]/[Database]
driver-class-name: com.mysql.cj.jdbc.Driver
username: [username]
password: [password]
이후에 Config파일
을 만들어서 다음과 같이 추가해줍니다.
// SshTunnelConfig.java
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import jakarta.annotation.PreDestroy;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
@Slf4j
@Component
@ConfigurationProperties(prefix = "ssh")
@Validated
@Setter
public class SshTunnelConfig {
@Value("${ec2.remote_jump_host}")
private String remoteJumpHost;
@Value("${ec2.ssh_port}")
private int sshPort;
@Value("${ec2.user}")
private String user;
@Value("${ec2.private_key_path}")
private String privateKeyPath;
@Value("${ec2.database_endpoint}")
private String databaseEndpoint;
@Value("${ec2.database_port}")
private int databasePort;
private Session session;
@PreDestroy
public void destroy() {
if (session.isConnected()) {
session.disconnect();
}
}
public Integer buildSshConnection() {
Integer forwardPort = null;
try {
log.info("Connecting to SSH with {}@{}:{} using privateKey at {}", user, remoteJumpHost,
sshPort, privateKeyPath);
JSch jsch = new JSch();
jsch.addIdentity(privateKeyPath);
session = jsch.getSession(user, remoteJumpHost, sshPort);
session.setConfig("StrictHostKeyChecking", "no");
log.info("Starting SSH session connection...");
session.connect();
log.info("SSH session connected");
forwardPort = session.setPortForwardingL(0, databaseEndpoint, databasePort);
log.info("Port forwarding created on local port {} to remote port {}", forwardPort,
databasePort);
} catch (JSchException e) {
log.error(e.getMessage());
this.destroy();
throw new RuntimeException(e);
}
return forwardPort;
}
}
위 코드에서 SSH 터널을 생성하는 터널링 작업
을 해주고, Spring이 동작하는 서버에서 사용 가능한 포트(ex. 12345 포트)와 EC2를 SSH(22 포트) 연결
하고, RDS 주소와 3306포트로 포워딩
하는 역할을 합니다.
이렇게 되면 Spring 서버에서 12345 포트로 접근한다면, RDS의 3306 포트로 바로 연결되게 되면서 MySQL을 사용할 수 있게 됩니다.
// SshDataSourceConfig.java
import javax.sql.DataSource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Slf4j
@Configuration
@RequiredArgsConstructor
public class SshDataSourceConfig {
private final SshTunnelConfig initializer;
@Bean("dataSource")
@Primary
public DataSource dataSource(DataSourceProperties properties) {
log.info("Datasource Properties URL: {}", properties.getUrl());
log.info("Datasource Properties Username: {}", properties.getUsername());
log.info("Datasource Properties Password: {}", properties.getPassword());
log.info("Datasource Properties Driver ClassName: {}", properties.getDriverClassName());
Integer forwardedPort = initializer.buildSshConnection();
String url = properties.getUrl().replace("[forwardedPort]", String.valueOf(forwardedPort));
log.info(url);
return DataSourceBuilder.create()
.url(url)
.username(properties.getUsername())
.password(properties.getPassword())
.driverClassName(properties.getDriverClassName())
.build();
}
}
마지막으로 SSH 터널링 연결을 포함한 DataSource를 반환하는 dataSource Bean
을 구성해서 위처럼 만들어주면, 서버가 실행되었을 때 dataSource에서 반환되는 DataSource를 통해서 MySql에 연결
하게 됩니다. 이 과정에서 jdbc:mysql://localhost:[forwardedPort]/[Database]
dataSource의 uri에서 [forwardedPort]
는 위 코드의 replace
함수를 통해 터널링을 진행한 Port
로 변환되게 됩니다. 또한 localhost로 MySQL에 접근
하는 이유는 SSH 터널링
을 통해 localhost
의 해당 포트가 RDS
와 연결되어있기 때문에 가능한 것으로 터널링이 되어있지 않은 경우에는 사용할 수 없습니다.
이렇게 하고 실행하게 되면 MySQL에 따로 다른 설정을 하지 않았을 때 Connection Timeout
이나 MetaData를 받아올 수 없다
거나 하는 여러 에러가 발생할 수도 있습니다.
저 또한 여러 에러가 많이 발생하고, 이를 해결하느라 시간을 참 많이 사용했는데요. 그 중에 하나가 바로 이 권한 부여 부분입니다.
RDS에서 기본으로 주어지는 admin으로는 SSH 터널링을 했을 때 접근 권한이 없어서 CLI에서 다음과 같은 에러가 발생했습니다.
처음엔 이런 에러가 발생하는줄도 모르고... Spring에서 왜 안되지..? 하면서 엄청나게 고민을 많이했다고...
그래서 새로운 계정을 만들고 권한을 부여해줬습니다.
이렇게 말이죠. 'cozymate'@'%'
라는 유저를 만들어서 Password(모자이크 부분)를 지정해주었습니다. 원래 'username'@'ip주소'
의 형식으로 구성되어야 하는데, %를 사용하여 모든 주소에서 사용
이 가능하도록 처리했습니다. (%는 MySQL에서 여러 문자를 의미합니다)
그리고 이후 모든 권한을 다 줬는데, 이는 우선 빠른 개발을 하기 위함이며, 나중에 실제로 배포할 때가 된다면 권한을 좀 수정할 계획입니다.
이렇게 처리하고, CLI에서 확인하면 정상적으로 연결이 됨을 확인할 수가 있고, 스프링에서도 동작하는 것을 확인할 수 있습니다.
적당히 구성했는데, 제대로 나오는 것을 보니 기분이 좋네요 ㅎㅎ
이렇게 이번 포스팅에서는 RDS의 퍼블릭 액세스를 통한 과금을 줄이고자 EC2에 연결해서 SSH 터널링으로 구성해보았습니다. SSH 터널링이 어떤 개념인지 명확하게 알지 못하는 상황이었는데, 이번 기회를 통해 어느정도 구성되는 형식을 파악할 수 있어서 좋은 기회였던 것 같습니다.
사실 AWS 크레딧이 있어서 굳이 안하고 퍼블릭 액세스를 써도 되는데, 비용을 최대한 줄여보고자 한번 해봤고, 이렇게 아끼게 된 크레딧을 다른 기능에 사용하면 될 것 같네요.(아싸)
실제로 서버에 올릴 때에는 EC2에 올릴거니까 SSH 터널링을 빼야할 것 같은데, 이건 EC2에 올릴 때 해당 파일은 빼거나, Profile을 통해 사용하지 않도록 설정하면 될 것 같네요. CI/CD를 처리하고 생각해봐야할 것 같습니다.