이번 레벨 3 프로젝트를 시작하면서 처음으로 인간 CI/CD 대신 Github Actions 기반의 CI/CD 파이프라인을 팀원들과 도입하기로 결정했다.
하지만 초기 구상과 달리 인프라 환경에서 22번(SSH) 포트가 제약되어 있는 상황을 맞닥뜨렸고, 이를 해결하는 과정에서 여러 시행착오 끝에 Self-hosted Runner를 도입하게 되었다. 그 과정과 해결 방법을 기록한다.
1. 22번 포트 우회를 위한 시도와 한계
처음에는 일반적인 GitHub Actions + SSH 접속(22번 포트) 방식으로 배포(CD)를 진행하려 했으나, 22번 포트 접근이 차단되어 다른 우회 방법을 모색해야 했다.
시도 1 : Jenkins 도입
EC2 서버 내부에서 Jenkins를 설치하고 GitHub Webhook을 연동하여 빌드 자동화를 시도했다.
하지만 PR Merge 및 Push 시 트리거까지 정상 동작했으나, 프리티어 EC2의 열악한 디스크 용량과 RAM 한계로 인해 빌드 도중 서버가 멈추는 문제가 발생하여 포기했다. 아래 첨부한 글은 참고한 글이다.
https://1minute-before6pm.tistory.com/52
Github WebHooks 연동하여 Jenkins 빌드 자동화
Jenkins로 배우는 CI/CD 파이프라인 구축 1. Install and Run Jenkins With Docker Compose 2. Jenkins Pipeline 개념 및 예제 3. Github WebHooks 연동하여 Jenkins 빌드 자동화 4. Jenkins Multibranch Pipeline: 효과적인 브랜치 관리
1minute-before6pm.tistory.com
시도 2 : Code Deploy와 S3를 사용
이 시도는 팀원이 진행하였고, 성공하였다. 하지만 구현이 매우 복잡하다고 하여 추가 학습이 필요하다.
그래서 다시 방법을 찾아보았고, 그 중에 github에서 제공하는 Self Hosting Runner를 사용해서 배포하는 방법을 알아냈다.
Self-hosted Runner란?
Self-hosted Runner는 사용자가 직접 관리하는 인프라(EC2, 온프레미스 서버 등)에 Runner 애플리케이션을 설치하여 GitHub Actions 워크플로우를 실행하는 주체다.
기존의 CI/CD 서버와 달리 외부에서 들어오는 연결(Inbound)을 받지 않고, Runner가 먼저 GitHub 서버로 연결(Outbound)을 요청하는 Long Polling 방식으로 동작한다.
왜 Self-hosted Runner인가?
- Inbound 포트 차단 상태에서도 동작 가능: 외부에서 들어오는 요청(Inbound)을 받는 방식이 아니라, Runner가 GitHub 서버로 Outbound(443/TCP - HTTPS) 요청을 먼저 보내는 Long Polling 방식으로 동작한다. 따라서 22번 포트나 외부 Inbound 포트를 열 필요가 없어 보안상 매우 유리하다.
- 동작 과정:
- 저장소에 Event(Push, PR 등)가 발생하면 GitHub Actions에 Job이 생성된다.
- Long Polling 연결을 통해 GitHub 서버가 EC2에 설치된 Runner에게 Job을 할당한다.
- Runner가 전달받은 스텝(빌드, 배포 명령어 등)을 EC2 내부에서 직접 수행한다.
2. EC2에 Self-hosted Runner 구축하기
Step 1. Runner 애플리케이션 설치
- CD를 구축할 GitHub 레포지토리의 Settings > Actions > Runners로 이동한다.

2. New self-hosted runner 버튼을 클릭한다.

3. EC2 OS 환경(Linux / x64 등)을 선택한 뒤, 화면에 제공되는 다운로드 및 설정 명령어들을 EC2 터미널에서 순서대로 실행한다.
해당 카테고리에 접근하면 아래와 같은 화면이 있다. new self-hosted runner를 통해서 EC2 서버에 Runner 애플리케이션을 설치하는 가이드가 나온다.

여기서 해당 EC2의 운영체제를 선택하고 아래 나오는 코드들을 실행하면 EC2에 Runner 애플리케이션을 설치 할 수 있다.
Step 2. 프리티어 EC2 메모리 부족 해결 (Swap Memory)
Spring Boot 프로젝트를 빌드할 때 프리티어 EC2(RAM 1GB)는 메모리가 부족하여 빌드가 튕기거나 서버가 다운될 수 있다. 이를 방지하기 위해 2GB 스왑 메모리를 설정해준다.
# 1. 2GB 크기의 스왑 파일 생성
sudo fallocate -l 2G /swapfile
# 2. 권한 변경 (root만 읽고 쓸 수 있도록 설정)
sudo chmod 600 /swapfile
# 3. 스왑 공간으로 포맷
sudo mkswap /swapfile
# 4. 스왑 활성화
sudo swapon /swapfile
# 5. 재부팅 시에도 유지되도록 /etc/fstab에 추가
echo '/swapfile swap swap defaults 0 0' | sudo tee -a /etc/fstab
# 6. 스왑 메모리가 적용되었는지 확인 (Swap 항목에 2.0G 할당 확인)
free -h
이렇게 적용후 결과를 확인하면

swap이 설정된 것을 알 수 있다.
여기서 push후 build가 실패한다면, visudo에 설정한 사용자 계정명이 실제 GitHub Actions Runner를 실행 중인 계정명과 다르기 때문이다.
이 경우 EC2 터미널에 접속하여 visudo 설정을 github-runner 계정 기준으로 변경해주면 바로 해결된다.
Step 3. Sudoers 권한 설정 (visudo)
Runner가 빌드 완료 후 서비스를 재시작(systemctl restart)하거나 로그를 확인할 때 비밀번호 없이 실행할 수 있도록 visudo를 통해 최소 권한을 부여한다.
sudo visudo
sudoers 파일 최하단에 아래 내용을 추가한다. (my-app 부분은 본인의 서비스명으로 변경)
# ubuntu 사용자에게 my-app(본인의 서비스명) 서비스 제어 및 journalctl 로그 조회 권한 부여 (비밀번호 없음)
ubuntu ALL=(ALL) NOPASSWD: \
/bin/systemctl restart my-app, \
/bin/systemctl status my-app, \
/bin/systemctl is-active my-app, \
/usr/bin/systemctl restart my-app, \
/usr/bin/systemctl status my-app, \
/usr/bin/systemctl is-active my-app, \
/bin/journalctl, \
/usr/bin/journalctl
설정 후 아래 명령어로 정상 적용되었는지 확인한다.
sudo -l
# (ALL) NOPASSWD: ALL가 뜨면 제대로 작성된 것이다.
Step 4. 트러블슈팅: 권한 문제로 배포가 실패할 때
Push 후 워크플로우 빌드가 권한 문제(sudo: a password is required)로 실패한다면, Runner 서비스가 실행 중인 계정명과 visudo에 설정한 계정명(ubuntu)이 다를 확률이 높습니다.
이 경우 아래의 순서대로 Runner 서비스를 현재 계정(ubuntu) 기준으로 재설치 및 재시작해주면 해결됩니다.
# 1. actions-runner 폴더로 이동
cd ~/actions-runner
# 2. 기존 서비스 중지 및 제거
sudo ./svc.sh stop
sudo ./svc.sh uninstall
# 3. 현재 계정(ubuntu)으로 서비스 다시 설치 및 시작
sudo ./svc.sh install ubuntu
sudo ./svc.sh start
# 4. 상태 확인 (Active: active (running) 확인)
sudo ./svc.sh status
마치며
22번 포트가 차단된 제약 조건과 EC2 프리티어 메모리 한계라는 두 가지 난관이 있었지만, Self-hosted Runner의 Long Polling 방식과 Swap Memory 설정을 통해 성공적으로 무중단에 가까운 자동 배포 환경을 구축할 수 있었다.
비슷한 환경에서 CI/CD 구축을 고민 중이신 사람들에게 도움이 되길 바란다 👍🏻
