노트

[SolveGO #18] 배포도 알아서 해주세요 - CD 적용 본문

Project/SolveGO

[SolveGO #18] 배포도 알아서 해주세요 - CD 적용

forwarder 2026. 7. 12. 11:30

 

 

 

 

 

현재 SolveGO는 CI 파이프라인을 통해 코드 변경 시 테스트가 자동으로 수행되는 단계까지 구축된 상태였다.또한 Docker를 이용해 프로젝트를 패키징함으로써, Docker를 실행할 수 있는 호환 환경에서 일관된 실행 환경을 제공할 수 있게 되었으며 EC2에도 배포할 수 있는 상태가 되었다. 그러나 실제 배포는 여전히 개발자가 직접 수행해야 하는 수동 배포(manual deployment) 단계에 머물러 있다.

 

 

사진 출처 : https://www.blackduck.com/glossary/what-is-cicd.html#F

 

 

 

현재 이 상황을 알기 쉽게 나타내보면 아래와 같다.

 

 

 

프로젝트를 도커로 패키징하여 Docker를 실행할 수 있는 환경에서 일관된 실행 환경으로 EC2에도 배포를 하였지만, 현재 방식은 소스 코드를 수정하면 EC2에 SSH로 계속 직접 수동으로 접속하여 최신 코드로 Docker 이미지를 다시 빌드하여 컨테이너를 재실행해야 한다. 그리고 이는 배포 자동화가 아니다. 나는 코드를 수정하면, CI 뿐만 아니라 CD, 다시 말해 배포 과정까지 자동화하기를 원한다. 

 

그렇다면 문제는 정의된다. "배포 과정을 자동화하자"

그러면 한 번 질문할 필요가 있다. 언제 배포 과정이 필요할까? 매 순간? 아니다. 변경 이전의 코드는 이미 서버에서 실행 중이므로 추가적인 배포는 필요하지 않다. 따라서 새로운 커밋이 main 브랜치에 반영되는 순간에만 배포가 필요하다. 그러면 "변경"이라는 이벤트를 감지하는 것이 중요하다는 의미이다. 지금까지는 이 이벤트를 개발자인 내가 감지하여 사람이 배포 과정을 진행하였다면, 이제 이 이벤트를 감지하여 배포를 하는 책임을 기계에게 맡김으로써 자동화를 진행하면 된다.

 

여기서 한 번 정리해 보자. main 브랜치에 새로운 커밋이 push되는 순간이 배포를 시작하는 이벤트가 된다. GitHub Actions는 이 이벤트를 Trigger로 Workflow Run과 Job을 생성하고, Runner는 생성된 Job을 전달받아 실제 배포 명령을 실행한다. 이를 구현하는 방법으로 GitHub-hosted Runner가 SSH를 통해 EC2에 접속하는 방식과, EC2 내부의 Self-hosted Runner가 배포 Job을 직접 실행하는 방식을 고려하였다.

 

 

처음에는 1번 GitHub-hosted Runner 방식을 사용하려고 했다. 이미 CI를 구축하면서 GitHub Actions Workflow가 정상적으로 동작하는 것을 확인했기 때문에, 코드 변경이라는 이벤트를 소비(consume)하는 주체 또한 GitHub Actions로 구성하면 CI와 CD 모두 GitHub Actions를 중심으로 동작하는 일관된 구조를 만들 수 있다고 생각했기 때문이다. 그러나 이 방식에는 한 가지 문제가 있었다. GitHub-hosted Runner는 GitHub가 실행 시마다 생성하는 임시 서버에서 동작하는데, 이 서버의 IP 주소는 고정되어 있지 않다. 따라서 EC2의 Security Group에서 SSH 접근을 특정 IP로 제한하기가 어려웠고, 현실적으로는 SSH 22번 포트를 광범위하게 허용하거나 GitHub의 변경되는 IP 대역을 지속적으로 관리해야 했다. 다른 포트라면 모르겠지만, SSH는 서버를 원격에서 직접 제어할 수 있는 프로토콜이기 때문에 보안 측면에서 이러한 구성이 마음에 들지 않았다. 대안으로는 정적 IP를 지원하는 GitHub Larger Runner를 사용할 수 있지만 Larger Runner는 GitHub Team 또는 Enterprise Cloud를 사용하는 조직과 엔터프라이즈에서 제공되며, 사용량에 따른 추가 비용이 발생한다. 따라서 현재 개인 프로젝트 규모에는 과한 선택이라고 판단했다.

 

따라서 자연스럽게 2번 Self-hosted Runner 방식을 고려하게 되었다. Self-hosted Runner는 EC2 내부에서 실행되는 GitHub Actions Runner 프로그램으로, GitHub와 지속적으로 연결된 상태에서 Workflow Job이 생성되면 이를 수신하여 배포 작업을 수행한다. 즉, GitHub가 EC2에 SSH로 접속하는 것이 아니라 EC2 내부에서 Runner가 직접 배포를 수행하므로, SSH 22번 포트를 외부에 개방할 필요가 없다. 물론 이 방식을 사용하려면 Runner가 동작하는 동안 EC2가 실행 중이어야 한다는 조건이 있다. 하지만 어차피 서비스를 운영하고 배포를 수행하기 위해서는 EC2를 켜두어야 하므로 실질적인 단점은 아니라고 판단하였다. 또한 GitHub-hosted Runner에 고정 IP를 제공하기 위한 별도 구성을 사용하는 것보다 현재 운영 중인 EC2를 그대로 Self-hosted Runner로 활용하는 편이 비용 측면에서도 유리했다. 특히 현재는 AWS Free Tier를 사용하고 있으므로 추가적인 운영 비용에 대한 부담도 크지 않았다. 이러한 이유로 SolveGO는 GitHub-hosted Runner를 이용한 SSH 방식 대신, EC2 내부에서 Self-hosted Runner가 직접 배포를 수행하는 구조를 선택했다.

 


 

 

EC2 내부 Self-hosted Runner가 직접 배포를 수행하는 구조

 

GitHub 문서에 따라 EC2 운영체제에 맞는 Self-hosted Runner를 설치한다.

 

Getting started with self-hosted runners for your enterprise - GitHub Enterprise Cloud Docs

You can configure a runner machine for your enterprise so your developers can start automating workflows with GitHub Actions.

docs.github.com

 

EC2 환경에 맞게 운영체제와 아키텍처를 선택하면 GitHub가 Runner 패키지를 내려받고 등록하기 위한 명령어를 제공한다.

 

 

이를 이용하여 EC2에 접속하여 설치를 진행하면 된다.

 

sudo ./svc.sh install
sudo ./svc.sh start
sudo ./svc.sh status


그리고 EC2에서 설치된 Self-hosted Runner가 항상 실행되도록 리눅스 서비스로 등록한다. 이를 통해 부팅 시 자동으로 Runner가 실행되게 된다. (단순히 ./run.sh를 이용하여 실행하게 되면 현재 터미널 세션에서만 동작한다. SSH 연결이 종료되거나 EC2가 재부팅되면 Runner도 함께 종료될 수 있다)

 

 

 

 

정상적으로 EC2에서 Self-hosted Runner가 실행되는 중이라면 GitHub에서 Runner가 동작 중임을 위 사진처럼 확인할 수 있다.  현재 상태는 아래와 같다. EC2 내부의 Self-hosted Runner가 GitHub에 아웃바운드 HTTPS 443 연결을 수립하고, GitHub Actions가 전달할 Workflow Job을 기다리는 상태이다.

 

 

 

 

이제 main branch에 push라는 이벤트가 발생하게 되면, 그것을 GitHub이 Publish하고 EC2내부의 Self-hosted Runner에게 명령을 내리기 위한 workflow 파일을 아래와 같이 작성하자. 

 

참고로 Self-hosted Runner는 EC2 내부에서 직접 명령을 실행할 수 있으므로 외부 사용자가 수정한 Workflow를 그대로 실행해서는 안 된다. 따라서 Pull Request의 테스트는 GitHub-hosted Runner에서 수행하고, Self-hosted Runner를 사용하는 배포 Job은 main 브랜치에 push가 발생한 경우에만 실행되도록 제한했다.

name: Branch CI/CD

jobs:
	# test Job 생략
	deploy:
        name: Deploy to EC2
        needs: test
        if: github.ref == 'refs/heads/main'
        runs-on: [self-hosted, Linux, X64]
        timeout-minutes: 15

        steps:
          - name: Deploy SolveGO
            run: |
              cd /home/ubuntu/solvego

              git fetch origin main
              git reset --hard origin/main

              docker compose up --build -d

              curl --fail \
                --retry 20 \
                --retry-delay 5 \
                --retry-all-errors \
                http://localhost:8080/actuator/health

 

이 workflow 파일은 main branch에 push라는 이벤트가 발생하였을 경우 지정된 Runner에게 깃허브의 최신 커밋을 바탕으로 이미지를 빌드하고 그 성공 유무를 보고하도록 명령한다. 시퀀스 다이어그램은 아래와 같다.

 

 

따라서 이제 개발자가 EC2에 직접 SSH로 접속하여 배포 명령을 실행하지 않아도 된다. main 브랜치에 변경 사항이 반영되면 EC2 내부의 Self-hosted Runner가 자동으로 최신 코드를 가져와 애플리케이션을 다시 배포한다. 

 

 

결과적으로 이제 코드에 수정 사항이 발생하여 작업 브랜치에서 개발을 진행한 뒤 main 브랜치에 병합하면 다음 과정이 모두 자동으로 수행된다.

  1. Continuous Integration (CI)
    • 테스트 자동 실행
    • JaCoCo 테스트 커버리지 보고서 생성
    • Coverage Report를 GitHub Actions Artifact로 저장
  2. Continuous Deployment (CD)
    • EC2의 Self-hosted Runner가 최신 main 커밋을 가져옴
    • Docker 이미지를 다시 빌드하고 컨테이너를 재실행
    • Health Check를 수행한 뒤 배포 결과를 GitHub Actions에 보고

 

다만 현재 CD는 자동 배포와 Health Check까지 구현한 기본 구조이다. 배포 실패 시 이전 버전으로 자동 롤백되지는 않으며, 무중단 배포와 지속적인 모니터링도 아직 적용하지 않았다. 이러한 운영 안정성 관련 기능은 이후 단계에서 보완할 예정이다.

 

아무튼, 코드 검증부터 운영 서버 배포까지 모두 자동화된 CI/CD 파이프라인을 구축했다. 이제 프로젝트의 1차 구현과 배포 자동화를 마쳤으므로, README를 정리한 뒤 미뤄 두었던 회원가입 동시성 문제를 해결하고 부하 테스트와 성능 개선을 진행해보자.