운영 서버의 Docker 컨테이너를 원격에서 제어하고, 볼륨을 백업한 뒤 GitHub Actions까지 트리거해야 하는 요구가 있었습니다. 단순 스크립트가 아니라 Java 애플리케이션 내부에서 자동화 흐름을 제어해야 했기 때문에, SSH 접속부터 Docker 명령 실행, GitHub API 호출까지 모두 코드로 구현했습니다.
1. SSH 접속 유틸 클래스 구현
Java에서 SSH 접속을 위해 JSch 라이브러리를 사용했습니다.
핵심은 세션을 생성하고, 작업이 끝난 뒤 안전하게 종료하는 구조입니다.
public Session connectToSSH(String sshUser, String sshHost, int sshPort, String sshPassword) throws JSchException {
JSch jsch = new JSch();
session = jsch.getSession(sshUser, sshHost, sshPort);
session.setPassword(sshPassword);
Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
session.connect();
return session;
}
- StrictHostKeyChecking을 "no"로 설정해 초기 키 검증 예외를 방지했습니다.
- 세션은 반드시 외부에서 disconnect()로 종료합니다.
public void disconnect(Session session) {
if(session != null && session.isConnected()) {
System.out.println("SSH세션 연결종료");
session.disconnect();
}
}
SSH는 연결을 열어두면 리소스를 점유하기 때문에, finally 블록에서 정리하는 구조로 사용했습니다.
2. Scheduler로 자동 실행
정해진 시간에 Docker 백업 작업이 실행되어야 했기 때문에 @Scheduled를 사용했습니다.
@Scheduled(cron = "0 0 09 * * ?")
public void dockerVolumeBackup() {
log.info("dockerVolumeBackup");
Session session = null;
try {
session = sshUtil.connectToSSH(SSH_USER, SSH_HOST, SSH_PORT, SSH_PASSWORD);
dockerManager.dockerVolume(session);
} catch (Exception e) {
e.printStackTrace();
} finally {
sshUtil.disconnect(session);
}
}
- 매일 오전 9시에 실행
- SSH 접속 → Docker 작업 수행 → 세션 종료
이 흐름이 자동화의 시작점입니다.
3. Docker Volume 백업 로직
원격 서버에서 Docker 볼륨을 백업하기 위해 busybox 컨테이너를 사용했습니다.
public void backupDockerVolume(Session session) throws Exception {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String date = now.format(formatter);
log.info("volume backup");
remoteCommand(session,
"docker run --rm -v " + VOLUME_NAME + ":/app -v " + BACKUP_DIR +
":/backup busybox tar cvf /backup/backupName" + date + ".tar /app");
}
- --rm : 작업 완료 후 컨테이너 자동 삭제
- -v runs-data:/app : 기존 볼륨 마운트
- -v /app/backup_dir:/backup : 백업 디렉토리 마운트
- 날짜 기준 tar 파일 생성
4. 원격 명령 실행 구조
모든 Docker 명령은 아래 remoteCommand()를 통해 실행했습니다.
private void remoteCommand(Session session, String command) throws Exception {
ChannelExec channel = (ChannelExec) session.openChannel("exec");
channel.setCommand(command);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
channel.setOutputStream(outputStream);
channel.setErrStream(errorStream);
InputStream inputStream = channel.getInputStream();
channel.connect();
byte[] tmp = new byte[1024];
while (true) {
while (inputStream.available() > 0) {
int i = inputStream.read(tmp, 0, 1024);
if (i < 0) break;
outputStream.write(tmp, 0, i);
}
if (channel.isClosed()) {
if (inputStream.available() > 0) continue;
log.info("exit-status: " + channel.getExitStatus());
break;
}
Thread.sleep(2000);
}
channel.disconnect();
log.info("Command output: " + outputStream.toString());
log.info("Command error: " + errorStream.toString());
}
- stdout / stderr 분리 수집
- exit status 로그 출력
- 2초 간격 대기
이 구조 하나로 Docker stop, volume 삭제, 생성 등 모든 명령을 처리했습니다.
5. Docker 컨테이너 시작 로직 (5분 대기 포함)
컨테이너를 백그라운드로 실행하기 위해 nohup을 사용했습니다.
private void startRemoteContainer(Session session, String command) throws Exception {
ChannelExec channel = (ChannelExec) session.openChannel("exec");
String backgroundCommand = "nohup " + command + " > run.log 2>&1 &";
channel.setCommand(backgroundCommand);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ByteArrayOutputStream errorStream = new ByteArrayOutputStream();
channel.setOutputStream(outputStream);
channel.setErrStream(errorStream);
channel.connect();
channel.disconnect();
Thread.sleep(300000); // 5분 대기
log.info("Command output: " + outputStream.toString());
log.info("Command error: " + errorStream.toString());
}
여기서 중요한 부분이 바로:
Thread.sleep(300000); // 5분 대기
컨테이너가 완전히 기동되고 내부 서비스가 안정화되기까지 시간이 필요했기 때문에 5분 대기 로직을 추가했습니다. 실제 운영 환경에서 기동 시간이 예상보다 길어지는 경우가 있었고, 이 대기를 넣은 이후 후속 작업 실패가 줄어들었습니다.
6. GitHub Actions 트리거
마지막으로 GitHub REST API를 통해 workflow dispatch를 호출했습니다.
- 204 응답 시 성공
- 토큰은 workflow 권한 포함 필요
- 10초 대기 후 다음 작업 진행
전체 자동화 흐름 정리
- Scheduler 실행
- SSH 접속
- Docker 컨테이너 제어
- Volume 백업
- 컨테이너 재시작 (5분 대기 포함)
- GitHub Actions 트리거
- SSH 세션 종료
모든 과정을 Java 코드로 제어하도록 구성했습니다.
느낀 점
처음엔 “이걸 Java로 다 제어하는 게 맞나?” 싶었지만,
직접 구현해보니 인프라 자동화 흐름이 머릿속에 구조적으로 정리되는 경험이었습니다.
- SSH 세션 관리
- 원격 명령 실행 구조
- Docker 볼륨 개념
- GitHub Actions API 호출 방식
이 네 가지가 한 번에 연결되면서,
단순 개발이 아니라 “운영 자동화 설계”를 경험한 느낌이었습니다.
지금 다시 보면 개선할 부분도 보이지만,
그 당시에는 정말 하나하나 검색하면서 붙여 만든 코드였고,
그래도 실제로 돌아갔다는 점에서 의미가 컸던 작업이었습니다.
'IT > java' 카테고리의 다른 글
| Spring Scheduler 실행 밀림 원인과 5가지 체크리스트 (0) | 2026.06.02 |
|---|---|
| Spring Scheduler 중복 실행 해결 가이드 (0) | 2026.05.26 |
| Docker Health Check 기반으로 Thread.sleep 제거하기 (0) | 2026.03.06 |
| 원인을 찾지 못한 트랜잭션 에러, HikariCP를 의심하게 되기까지의 기록 (0) | 2026.01.30 |
| Spring에서 스케줄러를 사용하는 이유(@Scheduled 정리) (0) | 2026.01.29 |