Backend

서버 접속 배포에서 GitHub Actions 자동 배포까지, CI/CD 구성하기

zerozeroseven 2026. 5. 2. 01:35

 

Docker 기반 실행 구조와 CI/CD 워크플로우 구성

 

AWS 인프라, 콘솔 대신 Terraform으로 관리해보자

그동안 프로젝트를 진행하면서 필요한 리소스들은 AWS 콘솔에서 직접 만들어서 사용했다. EC2, S3, RDS, Redis와 같은 리소스를 하나씩 늘려가면서 작업했다.콘솔에서 필요한 순간에 바로 만들 수 있

zerozeroseven.tistory.com

이전 글과 같이 Terraform으로 인프라 구성을 정리한 뒤에는 애플리케이션 배포 흐름을 잡는 작업을 진행했다.  
이번에는 Dockerfile 추가부터 GitHub Actions 기반 CI/CD 워크플로우 구성까지 정리해보려고 한다.

 

Dockerfile 추가

애플리케이션을 서버에서 일관된 방식으로 실행하려면 먼저 컨테이너 실행 구조가 필요했다.  
로컬에서는 잘 되던 실행 방식이 서버에서는 달라지는 경우가 많았기 때문에, 실행 환경 자체를 이미지로 고정하는 것이 우선이라고 판단했다.

Spring Boot 애플리케이션 이미지는 멀티스테이지 빌드로 구성했다.  
빌드 단계와 실행 단계를 분리하면 최종 이미지 크기를 줄일 수 있고, 실행 컨테이너에 불필요한 빌드 도구가 포함되지 않는 장점이 있다.

# =================
# 애플리케이션 빌드 단계
# =================
FROM gradle:8.14.3-jdk21 AS builder

WORKDIR /app
COPY gradlew .
COPY gradle gradle
COPY build.gradle.kts .
COPY settings.gradle.kts .
COPY src src

RUN chmod +x ./gradlew
RUN ./gradlew bootJar --no-daemon

# =================
# 애플리케이션 실행 단계
# =================
FROM eclipse-temurin:21-jre

WORKDIR /app
COPY --from=builder /app/build/libs/*-SNAPSHOT.jar app.jar

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

 

특히 실행 이미지에 jre 기반 이미지를 사용하면서, 빌드와 실행 책임을 분리한 구조를 초기에 잡아둘 수 있었다.

 

실행 환경 분리

Dockerfile을 추가한 뒤에는 실행 환경을 분리했다.
처음에는 하나의 설정으로 관리해도 괜찮을 것 같았지만, dev/prod/test 환경이 섞이기 시작하면 설정 충돌이 생기기 쉬워서 실행 설정은 아래처럼 나눴다.

  • application-local.yml
  • application-dev.yml
  • application-prod.yml

민감한 값은 코드에 직접 넣지 않고 .env로 분리해 관리했다. 이렇게 해두니 환경별 설정이 훨씬 명확해졌다.

 

docker-compose 구성

다음으로 docker-compose 설정을 추가했다.
당시에는 ECR 기반 이미지 배포 전 단계였기 때문에, EC2에서 직접 빌드하는 방식으로 구성했다.

services:
  app-dev:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: moongchijang-dev
    env_file:
      - .env.dev
    ports:
      - "8081:8080"
    restart: unless-stopped

  app-prod:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: moongchijang-prod
    env_file:
      - .env.prod
    ports:
      - "8082:8080"
    restart: unless-stopped

 

이 구조로 먼저 dev/prod 컨테이너 분리 실행을 확인했고, 실제로 EC2에서 빌드와 실행이 정상 동작하는지 점검했다.

 

GitHub Actions 배포 자동화

컨테이너 실행 흐름이 확인된 뒤에는 배포 자동화를 붙였다.
수동으로 EC2에 접속해서 명령어를 반복하는 방식은 초기 검증 단계에서는 가능했지만, 배포 빈도가 올라가면 실수 가능성과 시간 비용이 커진다.

그래서 GitHub Actions에서 dev/prod 워크플로우를 분리하고, 브랜치 기준으로 배포가 자동으로 연결되도록 구성했다.

Dev 배포 워크플로우

name: Deploy Dev

on:
  push:
    branches:
      - develop

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: dev

    steps:
      - name: Deploy to dev container on EC2
        uses: appleboy/ssh-action@v1.2.0
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            set -e
            cd ${{ secrets.EC2_APP_DIR }}
            git fetch origin
            git checkout develop
            git pull origin develop
            docker compose up -d --build app-dev

 

dev는 빠른 반영이 중요해서 테스트 단계를 배포 workflow 안에 중복으로 두지 않고, 개발 브랜치 기준으로 즉시 반영되는 흐름을 우선했다.

Prod 배포 워크플로우

name: Deploy Prod

on:
  push:
    branches:
      - main

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "21"
      - name: Set up Gradle
        uses: gradle/actions/setup-gradle@v4
      - name: Grant execute permission for gradlew
        run: chmod +x ./gradlew
      - name: Run tests
        run: ./gradlew test --no-daemon

  deploy:
    runs-on: ubuntu-latest
    needs: test
    environment: production

    steps:
      - name: Deploy to prod container on EC2
        uses: appleboy/ssh-action@v1.2.0
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ${{ secrets.EC2_USERNAME }}
          key: ${{ secrets.EC2_SSH_KEY }}
          script: |
            set -e
            cd ${{ secrets.EC2_APP_DIR }}
            git fetch origin
            git checkout main
            git pull origin main
            docker compose up -d --build app-prod

 

prod는 안정성을 우선해서 테스트 통과 후 배포하도록 구성했다.
이렇게 분리해두니 dev와 prod의 운영 목적 차이를 워크플로우 레벨에서 명확하게 가져갈 수 있었다.

 

마무리

이번 단계에서는 Docker 기반 실행 구조를 먼저 고정하고, GitHub Actions를 붙여 dev/prod 배포 흐름을 자동화하는 데 집중했다.

예전처럼 서버에 직접 접속해서 배포하던 방식에서, 브랜치 기준으로 자동 배포되는 흐름으로 바꿀 수 있었다. 아직 개선할 부분은 남아 있지만, 배포 과정 자체는 이전보다 훨씬 단순하고 관리하기 쉬워졌다.

 

다음 글에서는 이 구조를 바탕으로 Nginx 리버스 프록시 구성과 ECR 기반 이미지 배포 방식으로 확장한 과정을 정리해보려고 한다.