대충 넘어가지 않는 습관을 위한 기록

개발·운영 환경 분리 적용기 - GitHub Actions를 활용한 배포 자동화 (개선 과정 포함)

uhyvn 2025. 2. 6. 01:58

최근 진행 중인 프로젝트의 CI/CD 파이프라인을 개선하면서, 기존의 단순 배포 방식에서 개발(Dev)과 운영(Prod) 환경을 분리하는 방식으로 변경했다.

이 과정에서 GitHub Actions를 활용하여 자동화된 배포를 구현했다.

 

이번 글에서는 초기 배포 방식부터 개발·운영 환경을 분리하게 된 과정과 최종적으로 설정한 GitHub Actions 배포 파이프라인을 정리해보려 한다.

 

 

 


 

 

 

 

 

처음에는 GitHub Actions에서 간단한 deploy.yml을 작성하여 배포를 자동화했다.

 

기존 deploy.yml 설정

name: deploy

on:
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up JDK 21
        uses: actions/setup-java@v3
        with:
          java-version: '21'
          distribution: 'zulu'
      - name: make application.yml
        run: |
          mkdir -p ./src/main/resources
          cd ./src/main/resources
          touch ./application.yml
          echo "${{ secrets.APPLICATION }}" >> ./application.yml
        shell: bash
      - name: Build with Gradle
        run: ./gradlew bootJar
      - name: web docker build and push
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker build -t ${{ secrets.DOCKER_REPO }}/backend .
          docker push ${{ secrets.DOCKER_REPO }}/backend
  deploy:
    needs:
      - build
    runs-on: ubuntu-latest
    steps:
      - name: deploy
        uses: appleboy/ssh-action@v0.1.7
        with:
          host: ${{ secrets.HOST }}
          username: ubuntu
          key: ${{ secrets.KEY }}
          port: 22
          script: |
            sudo docker pull ${{ secrets.DOCKER_REPO }}/backend
            sudo docker compose down
            sudo docker compose up -d

 

기존 배포 방식의 문제점

  1. 개발과 운영 환경이 분리되지 않음
    • application.yml이 하나뿐이라 운영 환경과 동일한 설정으로 배포됨
    • 개발과 운영을 동일한 서버에서 관리하기 어려움
  2. 배포 브랜치 구분이 없음
    • main 브랜치와 개발 브랜치(dev 등)가 동일한 배포 프로세스를 사용
    • dev 브랜치에서 테스트하려면 별도의 설정이 필요함
  3. 운영 서버에서 직접 실행
    • 운영 서버에 곧바로 docker pull을 실행하여 위험 부담이 존재
    • 여러 환경을 지원하기 어려운 구조

 

 


 

 

 

개선된 배포 방식 : 개발/운영 환경 분리

 

 

개선 목표

  • main 브랜치와 개발 브랜치를 구분하여 서로 다른 서버로 배포
  • 환경에 맞는 application.yml을 자동으로 생성
  • 운영 서버(api.ABC.com)와 개발 서버(ABC.test)를 분리하여 배포 안정성 강화

 

 

개선된 deploy.yml 설정

name: Deploy to Development and Production

on:
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up JDK 21
        uses: actions/setup-java@v3
        with:
          java-version: '21'
          distribution: 'zulu'

      - name: make application.yml
        run: |
          mkdir -p ./src/main/resources
          cd ./src/main/resources
          touch ./application.yml
          echo "${{ secrets.APPLICATION_DEV }}" >> ./application.yml
        shell: bash
        if: github.ref != 'refs/heads/main'

      - name: make application.yml
        run: |
          mkdir -p ./src/main/resources
          cd ./src/main/resources
          touch ./application.yml
          echo "${{ secrets.APPLICATION_PROD }}" >> ./application.yml
        shell: bash
        if: github.ref == 'refs/heads/main'

      - name: Build with Gradle
        run: ./gradlew bootJar

      - name: Docker Build & Push for Development
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker build -t ${{ secrets.DOCKER_REPO }}/dev_backend .
          docker push ${{ secrets.DOCKER_REPO }}/dev_backend
        if: github.ref != 'refs/heads/main'

      - name: Docker Build & Push for Production
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker build -t ${{ secrets.DOCKER_REPO }}/prod_backend .
          docker push ${{ secrets.DOCKER_REPO }}/prod_backend
        if: github.ref == 'refs/heads/main'

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to Development Server
        uses: appleboy/ssh-action@v0.1.7
        with:
          host: ${{ secrets.HOST_DEV }}
          username: ubuntu
          key: ${{ secrets.KEY_DEV }}
          port: 22
          script: |
            sudo docker pull ${{ secrets.DOCKER_REPO }}/dev_backend
            sudo docker compose down
            sudo docker compose up -d
        if: github.ref != 'refs/heads/main'

      - name: Deploy to Production Server
        uses: appleboy/ssh-action@v0.1.7
        with:
          host: ${{ secrets.HOST_PROD }}
          username: ubuntu
          key: ${{ secrets.KEY_PROD }}
          port: 22
          script: |
            sudo docker pull ${{ secrets.DOCKER_REPO }}/prod_backend
            sudo docker compose down
            sudo docker compose up -d
        if: github.ref == 'refs/heads/main'

 

 

 

 


 

 

 

 

반복되는 코드를 줄이기 위한 리팩토링

위에서 작성한 deploy.yml을 보면 Deploy to Development Server와 Deploy to Production Server가 거의 동일한 구조로 반복되고 있다.

이렇게 반복되는 코드는 유지보수하기 어렵고 실수할 가능성도 높아진다고 생각했다.

그래서 GitHub Actions의 matrix 전략을 활용하여 코드 중복을 줄이는 방식으로 리팩토링을 했다.

 

 

리팩토링 전 문제점

  • 개발 서버와 운영 서버 배포 코드가 거의 동일한데, 중복이 많음.
  • 같은 로직을 두 번 작성해야 하므로 유지보수 시 실수할 가능성이 큼.
  • 불필요한 if 문을 줄일 방법이 필요함.

 

 

리팩토링 후 개선된 deploy.yml

name: Deploy to Development and Production

on:
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up JDK 21
        uses: actions/setup-java@v3
        with:
          java-version: '21'
          distribution: 'zulu'

      - name: Make application.yml
        run: |
          mkdir -p ./src/main/resources
          cd ./src/main/resources
          touch ./application.yml
          if [ "${{ github.ref }}" == "refs/heads/main" ]; then
            echo "${{ secrets.APPLICATION_PROD }}" >> ./application.yml
          else
            echo "${{ secrets.APPLICATION_DEV }}" >> ./application.yml
          fi
        shell: bash

      - name: Build with Gradle
        run: ./gradlew bootJar

      - name: Docker Build & Push
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          if [ "${{ github.ref }}" == "refs/heads/main" ]; then
            IMAGE_TAG="prod_backend"
          else
            IMAGE_TAG="dev_backend"
          fi
          docker build -t ${{ secrets.DOCKER_REPO }}/$IMAGE_TAG .
          docker push ${{ secrets.DOCKER_REPO }}/$IMAGE_TAG

  deploy:
    needs: build
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: [dev, prod]
    steps:
      - name: Deploy
        uses: appleboy/ssh-action@v0.1.7
        with:
          host: ${{ matrix.environment == 'prod' && secrets.HOST_PROD || secrets.HOST_DEV }}
          username: ubuntu
          key: ${{ matrix.environment == 'prod' && secrets.KEY_PROD || secrets.KEY_DEV }}
          port: 22
          script: |
            if [ "${{ matrix.environment }}" == "prod" ]; then
              IMAGE_TAG="prod_backend"
            else
              IMAGE_TAG="dev_backend"
            fi
            sudo docker pull ${{ secrets.DOCKER_REPO }}/$IMAGE_TAG
            sudo docker compose down
            sudo docker compose up -d
        if: |
          (matrix.environment == 'prod' && github.ref == 'refs/heads/main') ||
          (matrix.environment == 'dev' && github.ref != 'refs/heads/main')

 

 

 

리팩토링 후 개선점

  1. matrix 전략 사용 → environment: [dev, prod]로 설정하여 하나의 Job에서 개발/운영 배포를 모두 처리할 수 있음.
  2. hostkey 참조 방식 개선 → && || 연산자로 환경에 따라 secrets 값을 자동 선택.
  3. 중복 코드 제거 → script 내부에서 환경에 따라 이미지 태그를 선택하도록 변경하여 가독성 향상.
  4. 유지보수 용이 → 새로운 환경 추가 시 matrix.environment에 값만 추가하면 됨.

 

 


 

 

이렇게 리팩토링을 마무리하면서, 더 간결하고 유지보수하기 쉬운 CI/CD 파이프라인을 구축할 수 있었다.

 

이제 개발/운영 환경을 쉽게 배포할 수 있고, 코드 수정 시 실수할 가능성도 줄어들었다!

 

 

Matrix란?

더보기

matrix는 GitHub Actions에서 여러 환경을 동시에 실행할 수 있도록 도와주는 기능이다.
즉, 같은 작업을 여러 번 실행해야 할 때, 중복을 줄이고 코드의 가독성을 높일 수 있다.

예를 들어, 내 상황처럼 deploy.yml에서 개발(dev)과 운영(prod) 환경에 배포해야 했다면,
기존 방식이라면 같은 steps를 두 번 작성해야 했지만, matrix 전략을 활용하면 한 번만 작성하면 된다.

 

기본 사용법

strategy:
  matrix:
    환경변수: [값1, 값2, 값3]

위처럼 정의하면, 해당 환경변수에 대해 각각의 값으로 Job이 실행된다.

 

예제 (배포 환경을 dev와 prod로 구분)

strategy:
  matrix:
    environment: [dev, prod]

 

이렇게 설정하면, GitHub Actions는 environment가 dev일 때 한 번, prod일 때 한 번 총 두 번 실행하게 된다.
즉, 하나의 deploy Job이 두 번 실행되면서 dev와 prod에 맞춰 동작하는 것이다.