토이 프로젝트를 진행하면서 발생했던 문제에 대한 본인의 생각과 고민을 기록한 글입니다.
기술한 내용이 공식 문서 내용과 상이할 수 있음을 밝힙니다.
외부 Tomcat에 배포를 성공하기까지 무려 43번째만에 성공했다. 어떤 과정에서 이슈가 있었고 해결했는지에 대해 작성하고자 한다.
Tomcat은 자바 웹 애플리케이션을 실행하기 위한 서블릿 컨테이너 및 웹 서버다.
Spring Boot 애플리케이션 개발 시 Tomcat을 사용하는 방법에는 크게 두 가지다.
내장 Tomcat
외부 Tomcat
Tomcat의 conf/tomcat-users.xml
파일에서 사용자에게 여러 역할을 부여할 수 있다. 이 중 manager-script
역할은 스크립트를 통한 애플리케이션 배포와 관리를 가능하게 해주는 역할로 Maven이나 Gradle 같은 빌드 도구를 사용하여 CI/CD 파이프라인에서 자동으로 애플리케이션을 배포할 때 이 역할이 필요하기에 설정해야한다.
이전 글에서 Jenkins Pipeline으로 톰캣 서버에 배포하는 준비과정은 마쳤다.
build
→ test
→ deploy
과정에서 deploy에서 발생한 문제에 대해서 얘기하려고 한다.
Tomcat 9.0.87 에 war 파일을 배포하는 동안 Apache Tomcat 서버의 업로드 제한 크기와 관련하여 다음과 같은 문제에 직면했다.
Caused by: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException the request was rejected because its size (83990938) exceeds the configured maximum (52428800)
<multipart-config>
<!-- 150 MiB max -->
<max-file-size>157286400</max-file-size>
<max-request-size>157286400</max-request-size>
<file-size-threshold>0</file-size-threshold>
</multipart-config>
vi webapps/manager/WEB-INF/web.xml
를 보니 기본 업로드 제한은 50MB로 설정되어 있었고, 내 project WAR file 크기는 80.1MB로 Tomcat 업로드 파일 크기 제한을 기본값에서 150MB로 확장했다. (추가: EC2서버 t2.small로 scale-up)
에러를 보면 masterFlyway가 ddl-script를 DB에 적용하려고 하는데 DB 커넥션을 획득하지 못하고 있다. link failure로 빈생성 문제가 발생했다. (
sudo cat logs/catalina.2024-03-21.log
에서 확인)
생각해보면 당연하다. Tomcat은 AWS, DB는 NCP에 도커 컨테이너로 실행되고 있는 상태이니 말이다.
상황은 다음과 같다. Naver Cloud Platform(이하 NCP) 서버에 Docker Compose로 MySQL, PostgreSQL, Redis 등 DB들을 컨테이너 별로 도커를 실행 중이다. (호스트 포트 : 내부 포트 참조)
SSH 터널링을 통해 AWS와 같은 원격 서버와 NCP 내의 도커 컨테이너 또는 사설망 내부 서비스 간에 안전한 통신 통로를 구성할 수 있다.
SSH 터널링은 안전한 SSH 연결을 통해 원격 컴퓨터와 로컬 컴퓨터 사이에 데이터를 전송하는 방법이다.
SSH 터널은 로컬 애플리케이션(예: 톰캣 서버)이 마치 로컬 네트워크 내에 해당 데이터베이스 서버나 캐시 서버가 존재하는 것처럼 통신할 수 있게 한다. 이는 외부 네트워크를 통한 직접 접근이 허용되지 않는 환경에서 적합하다.
@Component
@Slf4j
public class SSHConnection {
private final String host;
private final Integer port;
private final String sshUser;
private final String sshPw;
public SSHConnection(@Value("${ssh.host}") final String host,
@Value("${ssh.port}") final Integer port,
@Value("${ssh.user}") final String sshUser,
@Value("${ssh.password}") final String sshPw) {
this.host = host;
this.port = port;
this.sshUser = sshUser;
this.sshPw = sshPw;
}
private Session session;
@PreDestroy
public void closeSSH() {
if (session != null) {
session.disconnect();
}
}
public void buildSshConnection() {
try {
Properties config = new Properties();
/**
원격 서버의 호스트 키가 알려진 호스트 목록에 없는 경우
클라이언트 호스트 키를 알려진 호스트 목록에 자동으로 추가
**/
config.put("StrictHostKeyChecking", "no");
JSch jsch = new JSch();
session = jsch.getSession(sshUser, host, port);
session.setPassword(sshPw);
session.setConfig(config);
session.connect();
session.setPortForwardingL(3306, "localhost", 3306);// MySQL Master
session.setPortForwardingL(3307, "localhost", 3306);// MySQL Slave1
session.setPortForwardingL(3308, "localhost", 3306);// MySQL Slave2
session.setPortForwardingL(5432, "localhost", 5432);// PostgreSQL
session.setPortForwardingL(6379, "localhost", 6379);// Redis
log.info("SSH 연결 성공");
} catch (JSchException e) {
log.info("SSH 연결 실패: " + e.getMessage());
e.printStackTrace();
}
}
JSch 라이브러리를 사용하여 원격 서버에 SSH 연결을 구성하고, 로컬 포트 포워딩을 설정하여 MySQL, PostgreSQL, Redis 등의 서비스에 대한 터널을 만든다. host의 지정된 포트(예: 3306, 3307, 3308, 5432, 6379)를 사용하여 해당 서비스에 접근할 때, 실제로는 안전한 SSH 연결을 통해 원격 서버의 해당 서비스에 접근할 수 있다.
SSH 터널링을 통해 이제 NCP 서버로 진입하여 포트포워딩을 통해 도커 컨테이너에 있는 DB 커넥션 연결이 가능해졌다. 하지만, SSH Tunneling과 DataSourceConfig의 순서에 따른 문제가 있다.
시나리오
- DataSourceConfig가 적용되고 SSH Tunneling 접속 시
1. DataSourceConfig는 DataSource 정보를 토대로 DB 연결을 시도한다.
2. DataSource는 localhost:port로 정보가 주어진다.
3. 터널링을 하지 않았으므로, 톰캣 서버를 localhost로 인지해 톰캣 서버에 있는 DB를 찾는다.
4. 당연히 톰캣에는 DB가 없으므로 DB Connection link failure 에러 발생 (이후 터널링 접속)
- SSH Tunneling 접속 후 DataSourceConfig가 적용
1. SSH 터널링을 통해 NCP서버로 진입해 localhost는 톰캣이 아닌 NCP다.
- DataSourceConfig는 DataSource 정보를 토대로 DB 연결을 시도한다.
- DataSource는 localhost:port로 정보가 주어진다.
- NCP 서버를 localhost로 인지해 NCP 서버에서 포트포워딩을 통해 DB 접속 성공
시나리오 2번대로 SSH Tunneling 후 DataSource가 생성되어야 한다.
@Configuration
@Slf4j
@RequiredArgsConstructor
@EnableConfigurationProperties(DatabaseProperties.class)
public class DataSourceConfig {
private final SSHConnection sshConnection;
@PostConstruct
public void initializeSshConnection() {
sshConnection.buildSshConnection();
log.info("SSH 접속이 초기화되었습니다.");
}
...
@Bean("masterDataSource")
public DataSource createMasterDataSource(DatabaseProperties databaseProperties) {
log.info("Master DB detail: {}", databaseProperties.getMaster());
return createDataSource(databaseProperties.getMaster());
}
스프링 컨테이너 생성 → 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸전 콜백 → 스프링 종료
version: '3'
services:
mysql_master:
image: mysql:8.0
env_file:
- ./master/mysql_master.env
container_name: "mysql_master"
restart: "no"
ports:
- 3306:3306
volumes:
- ./master/config_file.cnf:/etc/mysql/conf.d/mysql.conf.cnf
networks:
- db_network
mysql_slave1:
image: mysql:8.0
env_file:
- ./slave/mysql_slave.env
container_name: "mysql_slave1"
restart: "no"
ports:
- 3307:3306
depends_on:
- mysql_master
volumes:
- ./slave/config_file.cnf:/etc/mysql/conf.d/mysql.conf.cnf
networks:
- db_network
mysql_slave2:
image: mysql:8.0
env_file:
- ./slave2/mysql_slave.env
container_name: "mysql_slave2"
restart: "no"
ports:
- 3308:3306
depends_on:
- mysql_master
volumes:
- ./slave2/config_file.cnf:/etc/mysql/conf.d/mysql.conf.cnf
networks:
- db_network
network:
db_network:
RDB를 해결하니 이제는 Redis 접속 문제가 발생했다. RedisConfig는 DataSourceConfig랑 @Configuration이 별개이다.
따라서, SSH Tunneling 후 DataSource 빈 생성 & RedisConfig 빈 생성 순이어야 한다.
@Configuration
@Slf4j
@DependsOn("dataSourceConfig")
public class RedisConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
private static final String REDISSON_HOST_PREFIX = "redis://";
@Bean
public RedisConnectionFactory redisConnectionFactory() {
LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(host, port);
return lettuceConnectionFactory;
}
...
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + host + ":" + port);
return Redisson.create(config);
}
redisConfig 빈은 @DependsOn({"dataSourceConfig"})
을 사용해 dataSourceConfig 빈이 먼저 생성되어야 함을 명시하고 있다. dataSourceConfig 빈의 생성이 redisConfig 빈의 생성보다 앞서 실행됨을 보장하므로 결국 SSH 터널링 후 레디스도 NCP서버에서 localhost:6379로 DB 커넥션을 할 수 있게 되었다.
드디어 초록색으로 도배가 됐다.Tomcat 서버 접속 후 API 테스트를 해보자.
다음 글에서는 Tomcat App 실행 중 새로 배포할 경우에 대한 방안 그리고 HashiCorp/Vault로 데이터 암호화와 접근 제어를 통해 비밀 정보 관리에 대해 기록하겠다.