RDS의 Public Access로 인한 과금을 피해보자 (feat: EC2)

이플 (Dongsik Ga)·2024년 7월 17일
0

기술

목록 보기
3/11

이번에 새롭게 코지메이트(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원..)

이렇게 비용이 발생했다는걸 깨닫고 이를 우회할 방법을 찾아야했습니다.

어떻게 우회했는가?

여러 방식을 찾아보았지만 가장 명확한 해답을 알려준 사이트는 다음과 같았습니다.
명확한 해답을 알려준 곳
해당 사이트에 따르면 EC2VPC를 연결해서 하나의 IP 주소로 같이 사용한다는건데요.

EC2에서도 고정 IPv4를 받기 위해서는 RDS와 마찬가지로 비용이 발생합니다.
하지만 EC2에서는 고정 IP를 제외하고도 생성했을 때 받을 수 있는 IP가 하나 있죠.

퍼블릭 IPv4

입니다. 이 주소는 EC2를 재시작할 때마다 바뀌는 주소인데요. 한번 실행되면 재시작을 할 때까지 바뀌지 않기 때문에 어느정도는 고정 IP로 사용할 수 있다는 특징이 있습니다.
따라서 RDS와 EC2를 연결해서 EC2의 SSH 연결을 거쳐서 -> RDS의 주소로 접근한다. 라는 과정으로 진행하면 된다고 합니다.

구체적인 방법

EC2RDS를 연결

AWS에는 RDS에서 EC2에 연결할 수 있는 기능을 제공하고 있습니다.
이는 RDS를 생성할 때에 설정할 수도 있고, 생성한 이후에 RDS 작업에서 연결할 수도 있습니다.

아래는 RDS를 생성할 때의 옵션입니다.

아래는 생성 이후 작업에서의 옵션입니다.

동일한 가용영역에 있어야 실제 무료로 이용이 가능하다고 합니다.

Datagrip에서의 연결

위에 언급한 사이트와 별 다른 차이는 없습니다.
명확한 해답을 알려준 곳에서 자세한 내용을 확인하면 되며, 저 또한 이 사이트에서의 내용을 참고하여 구성한 것입니다. (거의 비슷합니다.)


일단 기본적으로 RDS를 연결하는 방식과 동일하게 일반 부분을 채웁니다. 현재 프로젝트에서는 MySQL을 사용하며, 만약 다른 경우 해당하는 DB를 선택해주시면 됩니다.


이후에 SSH를 구성해주어야 하며, SSH 터널 사용을 체크해주고, 새로운 SSH를 구성해주어야 합니다.


새로운 SSH는 EC2의 IP와 22번 포트를 통해 접근하면 됩니다. 사용자 이름은 OS마다 다른데, 현재 프로젝트에서는 Ubuntu를 사용하기 때문에 사용자 이름을 ubuntu로 입력해주면 됩니다. (기본 이름이 ubuntu이며, 만약 이름을 따로 설정했다면 해당 이름으로 입력하면 됩니다.)
마찬가지로 EC2ssh 연결하는 방법처럼 pem 파일을 등록해주면 되며, 설정 파일 관련 설정사항을 체크해주고 연결 테스트를 통해 제대로 연결되었는지 확인해주면 등록할 수 있습니다.
만약 연결 테스트를 했을 때 실패했다면, EC2에 연결되는 인바운드에 22포트가 열려있는지를 확인해보아야 하며, 추가로 3306도 아마 열려있어야 합니다. (아마도...)


마지막으로 구성한 SSH를 선택해주고, 연결 테스트를 해주면 아마도... 성공이 뜰겁니다. 따로 이 부분에서 에러가 발생하지는 않아서 에러 케이스에 대해 설명하기엔 어렵네요... 그리고 2번에 해당하는 로컬 포트<동적>으로 비워야 정상 작동하니 참고하시면 됩니다.

EC2 서버 내부에서의 접근

EC2에서 접근하는 방법은 명령어 하나면 됩니다.

sudo mysql -h [RDS Endpoint] -u [User] -p

로 접근하면 됩니다.

Spring에서 연결

일반적으로 RDS를 Datagrip에 연결하는게 그냥 SQL을 공부하기 위함이 아니라면, 다른 서버 프레임워크에 연결할겁니다. 이제 Spring에서 어떻게 이를 연결할지 찾아보고 도전해봤습니다.

접근 방식

현재 RDS를 연결할 수 있는 방식은 EC2에서 RDS의 Endpoint와 Port로 접근하는 방식입니다. 따라서 RDS를 연결하기 위해서는 EC2와 SSH 연결을 통해서 터널을 만들고 그 터널을 통해 RDS에 접속해야하는 것이죠.
SSH 연결
위 이미지처럼 말이죠. Local에서 EC2로 SSH 연결을 만들고, EC2 내부에서 RDS에 접근하는겁니다.
이 방식을 CLI에서 명령어로 처리하게 되면 다음과 같습니다.

sudo ssh -v -L [Local Port]:[Remote Server Address]:[Remote Server Port] \
-i [pem 파일 경로(보안키)] [사용자]@[서버 주소]

이를 EC2RDS로 바꾸게 되면 이렇게 됩니다.

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 연결

터널링 연결에 대한 설정은 여기를 참고하였습니다.
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 권한 부여

이렇게 하고 실행하게 되면 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를 처리하고 생각해봐야할 것 같습니다.

profile
어제보다 더 나은 오늘의 나를 위해 노력하는 개발자입니다.

0개의 댓글

관련 채용 정보