[클밋] 스테이징 서버와 운영 서버를 나눠 배포하기

박지운·2024년 1월 22일
0

클라이밍 앱 '클밋'을 제작하며 고민/개발 내용을 기록한 글입니다.

이전 방식

보통 내가 했던 배포 방식은 develop 브랜치에서 주차별 혹은 어느정도의 api가 모이면 바로 main 에 보내는 방식을 썼다.
mainpush 가 되면 깃허브 액션을 통해 바로 서버에 배포했다. 웹을 배포한 후에도 이 방식을 사용해 각자 로컬에서 테스트된 브랜치들을 develop에 모아놨다가 배포된 서버로 보냈다.

하지만 develop에서 충돌 해결하는 과정에서 생기는 컴파일 에러 혹은 배포 과정에서 에러가 발생하는 경우가 있다.
이 경우 모든 프론트 팀원이 스웨거나 포스트맨으로 테스트 자체를 못하게 된다.
만약 서비스가 배포됐다면 작은 실수로 서비스 자체가 먹통이 되는 경우가 생기게 된다.

이를 막고자 두 서버의 환경을 분리하고자 한다.


스테이징 서버와 운영 서버

스테이징 서버란 실제 운영 서버로 배포되기 전에 애플리케이션을 테스트하고 검증하는 역할을 하는 서버를 말한다.
운영 서버로 배포 전 테스트를 하는 것이므로 완벽히 동일한 환경을 구성하는 것이 중요하다.

운영서버는 말그대로 실제 유저가 사용하는 서버이다.
스테이징 서버에서 테스트한 브랜치를 그대로 운영 서버에 배포를 하면된다.

서버를 분리할 경우 깃 브랜치는 어떻게 관리를 해야할까?


깃 전략

우리 팀의 PM 분께서 Git-flow 전략을 제시했다. 확실히 Github-flow보다 복잡했지만 QA에 대한 필요성을 느끼고 있었기에 따르기로 했다.
[번역]정말 프로젝트를 시작할 때부터 QA가 필요할까 - 검은 왕자 블로그

그럼 브랜치는 크게 3가지가 존재한다.

  • develop
  • release
  • main

여기서 release 브랜치를 통해 develop 브랜치와 분리 후 스테이징 서버 배포를 하게 된다.
developfeature 개발 완료된 브랜치들을 계속 통합한다. release 브랜치에서는 main에 배포되기 전 QA를 하며 버그 발견시 바로 브랜치에서 직접 수정을 한다.


서버와 데이터베이스

이제는 두 분리된 환경에서 각각 서버와 데이터베이스는 어떻게 해야할지 고민해보자

서버

이전 서브 도메인을 배울때 prod.climeet.com 과 test.climeet.com처럼 나눈다고 배웠어서 하나의 Ip(서버)에서 두 환경을 모두 구동하는 것인가 고민했다.

먼저 분리 고민 이전에 프리티어 EC2에서 2개의 스프링을 돌릴 수 있는지 확인했다. 도커를 통해 실행하기에 docker stats로 확인을 했다.

요청이 아예 없을 때 300MB이고, ec2 프리티어는 1G의 메모리를 가진다. 스왑 메모리를 사용해도 주 메모리에 비해 속도가 느리기 때문에 2개를 돌리는 것은 무리라고 판단했다.

그리고 무엇보다 api응답 속도와 같이 성능 테스트를 할 경우 서로 영향을 끼칠 수 있기에 환경을 아예 분리시키는 것이 낫다고 판단했다. 그래서 동일한 환경의 ec2 2개를 돌리기로 했다.

DB

DB의 경우 RDS는 스키마를 통해 분리하려했지만, 위와 동일한 이유로 환경을 아예 격리하는 것이 나을 것같아 각각 ec2가 하나씩 연결되게 했다.S3 또한 분리했다.

내가 참고한 라인 게임 기술 블로그에서는 스테이징 이전에 베타 서버를 두었고 staging서버와 prod서버가 같은 DB를 연결해두었다.우리 프로젝트에서는 테스트를 할 수 있는 곳이 staging밖에 없어 분리가 낫다 판단했다.

팀원들과 회의를 하며 간단히 도식화를 해봤다.

더 알아봐야겠지만 정말 정확한 테스트를 위해서는 앞서 말했듯이 staging이전에 분리된 db를 갖는 서버를 두어 테스트를 진행한 후 staging과 prod서버가 동일한 db를 갖는 것이 맞는 것 같다.


배포

깃허브 액션 을 통한 도커 배포를 자동화했다.
먼저 빌드 후 테스트를 진행한다.
이후 두 어느 브랜치이냐에 따라 도커 이미지를 프로필에 맞춰 빌드한다. 이후 브랜치에 따른 ec2에 접속해 빌드한 도커 이미지를 pull받아 실행한다.

FROM openjdk:17-oracle

ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar

ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=${SPRING_PROFILE}", "/app.jar"]
name: CLIMEET_DEPLOY

on:
  push:
    branches: ["main", "release"]
jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      #체크아웃
      - name: Checkout code
        uses: actions/checkout@v3

      # JDK 설치
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

      #서브 모듈 접근
      - name: Checkout repo
        uses: actions/checkout@v3
        with:
          token: ${{ secrets.CLIMEET_ACTION_TOKEN }}
          submodules: true

      # 서브 모듈 변경 점 있으면 update
      - name: Git Submodule Update
        run: |
          git submodule update --remote --recursive

      # gradlew 권한 변경
      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      # 빌드(test는 제외) :아직 테스트 코드를 만들지 않아 제외했음 추후 변경
      - name: Build with Gradle
        uses: gradle/gradle-build-action@v2
        with:
          arguments: clean build -x test

      # Docker 이미지 빌드 (Main Branch)
      - name: Docker 이미지 빌드 (Main Branch)
        if: github.ref == 'refs/heads/main'
        run: docker build --build-arg SPRING_PROFILE=prod -t gourderased/spring-project:latest .

      # Docker 이미지 빌드 (Release Branch)
      - name: Docker 이미지 빌드 (Release Branch)
        if: github.ref == 'refs/heads/release'
        run: docker build --build-arg SPRING_PROFILE=dev -t gourderased/spring-project:latest .

      # DockerHub 로그인
      - name: Docker - Login to DockerHub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      # Docker Hub 이미지 푸시
      - name: Docker Hub 퍼블리시
        run: docker push gourderased/spring-project:latest

      # Deploy to EC2 (Main Branch)
      - name: Deploy to EC2 (Main Branch)
        if: github.ref == 'refs/heads/main'
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.MAIN_EC2_HOST }}
          username: ubuntu
          key: ${{ secrets.MAIN_SSH_KEY }}
          script: |
            sudo docker stop $(sudo docker ps -a -q)
            sudo docker rm $(sudo docker ps -a -q)
            sudo docker pull gourderased/spring-project:latest
            sudo docker run -d -p 8080:8080 --name climeet-prod-server gourderased/spring-project:latest

      # Deploy to EC2 (Release Branch)
      - name: Deploy to EC2 (Release Branch)
        if: github.ref == 'refs/heads/release'
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.RELEASE_EC2_HOST }}
          username: ubuntu
          key: ${{ secrets.RELEASE_SSH_KEY }}
          script: |
            sudo docker stop $(sudo docker ps -a -q)
            sudo docker rm $(sudo docker ps -a -q)
            sudo docker pull gourderased/spring-project:latest
            sudo docker run -d -p 8080:8080 --name climeet-dev-server -e SPRING_PROFILE=dev gourderased/spring-project:latest

ec2는 분리되어있으니 배포또한 따로 해야한다.
그렇다면 도커의 이미지는 왜 분리해야할까? 바로 스프링 프로파일때문이다.

스프링은 실행시 profile에 dev또는 local, prod등을 넣어주면 해당 프로필에 맞는 yml을 찾아 실행시킨다.

자세한건 아래 포스트를 참고하자.
스프링 프로파일 통한 환경분리

마치며

만약 이렇게 구성을 한다면 스케일링 업/다운시에도 똑같이 해줘야하는거 아닌가 생각이 든다.
환경 분리를 하고 관리하는 것은 시간과 노력이 들지만 QA나 추후 기능 추가, 리팩토링시 매우 큰 도움이 될 수 있을 것 같다.

출처

[LINE Rangers 신입사원의 서버 분석기] - 개발 환경

profile
앞길막막 전과생

0개의 댓글