GitHub ActionsCI/CDDeployment

GitHub Actions로 배포 자동화 설정하기

수동 배포에서 자주 놓치던 순서를 GitHub Actions로 정리하면서, 테스트와 빌드와 서버 반영을 어떻게 하나의 흐름으로 묶었는지 기록했습니다.

Srue2026년 3월 28일
GitHub Actions로 배포 자동화 설정하기

배포를 수동으로 하던 시절에는 늘 비슷한 실수가 있었습니다.
코드는 이미 머지됐는데 서버 반영을 늦게 하거나, 빌드는 했는데 환경 변수 확인을 빼먹거나, 테스트를 안 돌린 채 배포해서 뒤늦게 다시 붙는 식이었습니다.

한두 번은 괜찮아도 배포 횟수가 늘어나면 이런 순서 실수는 금방 피곤해집니다.
그래서 어느 시점부터는 "배포를 잘하는 것"보다 "배포 절차를 잊지 않게 만드는 것"이 더 중요하다고 느꼈습니다.

그때 정리한 게 GitHub Actions였습니다.

처음 목표는 단순했다

처음부터 화려한 파이프라인이 필요했던 건 아닙니다.
딱 세 가지만 자동화하고 싶었습니다.

  1. main 브랜치에 머지되면 테스트가 돈다
  2. 빌드가 성공한 경우에만 배포가 진행된다
  3. 서버 반영 순서가 항상 같게 유지된다

이 세 가지만 지켜져도 수동 배포 때 자주 놓치던 문제 대부분은 줄어들었습니다.

다만 여기서 먼저 선을 긋고 싶은 부분이 있습니다.
이 글에서 다루는 흐름은 개인 프로젝트 기준입니다. 혼자 운영하는 서비스나 작은 사이드 프로젝트에서는 main에 머지된 뒤 바로 배포하는 방식이 꽤 실용적일 수 있습니다.

반대로 실서비스 운영에서는 이 구조를 그대로 가져가는 건 조심해야 합니다.
보통은 스테이징 검증, 승인 절차, 배포 시간 통제, 롤백 전략 같은 장치가 더 필요합니다.

그래서 이 글은 "팀 단위 운영의 정답"이라기보다, 개인 프로젝트에서 배포 실수를 줄이기 위해 자동화를 붙인 기록으로 읽는 편이 맞습니다.

워크플로우는 작게 시작했다

가장 먼저 만든 워크플로우는 정말 단순했습니다.

name: deploy
 
on:
  push:
    branches:
      - main
 
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
 
    steps:
      - uses: actions/checkout@v4
 
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 21
 
      - name: Grant execute permission
        run: chmod +x ./gradlew
 
      - name: Test
        run: ./gradlew test
 
      - name: Build
        run: ./gradlew bootJar

여기까지만 해도 배포 전 최소한의 안전망은 생겼습니다.
main에 들어간 코드는 자동으로 검증되고, 빌드가 깨지면 거기서 바로 멈췄습니다.

배포는 SSH로 붙였다

처음에는 더 거창한 방식을 고민했지만, 운영하고 있는 서버가 EC2 한 대였기 때문에 SSH 방식이 가장 현실적이었습니다.

배포 서버에 접속해서 새 jar를 복사하고, 기존 프로세스를 내리고, 다시 올리는 흐름으로 정리했습니다.

      - name: Copy artifact
        uses: appleboy/scp-action@v0.1.7
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          source: build/libs/*.jar
          target: /home/ubuntu/app
 
      - name: Deploy
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SERVER_SSH_KEY }}
          script: |
            cd /home/ubuntu/app
            pkill -f 'my-service' || true
            nohup java -jar build/libs/my-service.jar > app.log 2>&1 &

아주 세련된 방식은 아니었지만, 수동으로 하던 일을 한 줄씩 눈에 보이게 정리했다는 점이 좋았습니다.

GitHub Secrets는 초반에 꼭 정리해두는 편이 낫다

자동화를 붙이면서 제일 많이 막혔던 건 스크립트 자체보다 시크릿 관리였습니다.

서버 주소, 사용자명, SSH 키, 환경 변수 값이 워크플로우에 섞이기 시작하면 금방 지저분해집니다.

그래서 아래 값들은 처음부터 Secrets로 분리했습니다.

  • SERVER_HOST
  • SERVER_USER
  • SERVER_SSH_KEY
  • SPRING_PROFILE
  • 필요한 외부 API 키

한 번 정리해두고 나니 워크플로우 파일은 훨씬 읽기 쉬워졌고, 민감한 값을 코드에 남길 일도 줄었습니다.

중간에 한 번 크게 불편했던 점

자동화 초반에는 테스트, 빌드, 복사, 재시작만 있으면 끝이라고 생각했습니다.
그런데 배포 직후 애플리케이션이 실제로 떠 있는지 확인하는 단계가 없으니, "배포는 성공"으로 찍혔는데 서비스는 죽어 있는 경우가 생겼습니다.

그 뒤로는 최소한 health check를 넣었습니다.

      - name: Health check
        run: |
          sleep 15
          curl --fail http://${{ secrets.SERVER_HOST }}:8080/actuator/health

별것 아닌 단계인데, 이 한 줄이 있고 없고의 차이가 꽤 컸습니다.
이제는 배포 성공이라는 말이 조금 더 믿을 만해졌습니다.

배포 로그가 남는다는 것도 장점이었다

수동 배포 때는 누가 언제 어떤 순서로 배포했는지 기억에 의존하는 경우가 많았습니다.
GitHub Actions로 바꾸고 나서는 최소한 아래 정보가 다 남았습니다.

  • 누가 머지했는지
  • 언제 워크플로우가 돌았는지
  • 어느 단계에서 실패했는지
  • 테스트가 통과했는지

특히 장애 직후에는 "마지막 배포가 언제였지?"를 빨리 확인할 수 있다는 것만으로도 생각보다 도움이 됐습니다.

지금도 조심하는 부분

자동화가 붙었다고 해서 배포가 가벼워지는 건 아니었습니다.
오히려 너무 쉽게 배포할 수 있게 되면 검토 없이 넘기기 쉬워집니다.

그래서 지금도 아래는 꼭 봅니다.

  1. main에 들어가기 전 리뷰가 있었는가
  2. 배포 스크립트가 환경 의존 값을 너무 많이 알고 있지 않은가
  3. 실패했을 때 어디서 멈췄는지 바로 보이는가

자동화는 배포를 대신해주는 게 아니라, 사람이 실수하지 않게 보조해주는 쪽에 가깝다고 느꼈습니다.

마무리

GitHub Actions로 배포를 자동화하고 나서 가장 좋아진 건 속도보다 일관성이었습니다.
이제는 누가 배포하든 같은 순서로 진행되고, 같은 체크를 통과해야만 다음 단계로 넘어갑니다.

배포 자동화는 거창한 인프라보다도, 반복하던 절차를 코드로 옮기는 데서 시작된다고 생각합니다.
처음부터 완벽한 파이프라인이 아니어도, 테스트와 빌드와 반영 순서만 고정해도 운영 부담은 꽤 줄었습니다.