런치챗 초기 배포는 GitHub Actions에서 이미지를 만들고 두 대의 서버에 동시에 반영하는 방식이었습니다. 배포 시간은 짧았지만, 서비스가 실제 사용되기 시작하자 기준이 바뀌었습니다.
운영에서 중요했던 것은 더 빠른 배포가 아니라, 배포 중에도 서비스가 계속 살아 있는가였습니다. 병렬 배포는 다운타임, 롤백 복잡도, 마이그레이션 충돌 가능성을 한 번에 만들고 있었습니다.
이 글은 도구를 바꾼 이야기라기보다, 런치챗에서 배포를 어떤 작업으로 다루게 되었는지 정리한 글입니다.
먼저 배포의 기준을 다시 세웠습니다
배포 전략을 다시 설계하면서 기준을 네 가지로 정리했습니다.
- 배포 중에도 최소 한 대의 서버는 계속 살아 있어야 했습니다.
- 데이터베이스 마이그레이션은 한 경로에서만 실행되어야 했습니다.
- 첫 번째 서버에서 문제가 나면 다음 서버 배포는 멈춰야 했습니다.
- 빌드와 배포 로직은 같은 파일에서 함께 커지지 않아야 했습니다.
처음 방식은 빠르긴 했지만, 이 기준을 거의 만족하지 못했습니다.
병렬 배포는 왜 문제였을까요
초기에는 GitHub Actions 하나로 CI와 CD를 모두 처리했습니다. 빌드, 이미지 생성, 모니터링 배포, 애플리케이션 서버 배포가 하나의 워크플로 안에 들어 있었고, 애플리케이션 서버 두 대는 병렬로 배포했습니다.
name: LunchChat CI/CD Pipeline
on:
push:
branches: [main, 'dev/**']
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Build application
run: ./gradlew build -x test --no-daemon --parallel
- name: Build and push Docker image
uses: docker/build-push-action@v5
deploy-server1:
needs: [build-and-push, deploy-monitoring]
runs-on: ubuntu-latest
deploy-server2:
needs: [build-and-push, deploy-monitoring]
runs-on: ubuntu-latest구성만 보면 효율적으로 보입니다. 문제는 서버 두 대를 동시에 내리는 순간이 생긴다는 점이었습니다. 로드밸런서가 있더라도 배포 타이밍에 따라 두 서버가 함께 재시작되면, 사용자는 짧지만 분명한 단절을 겪게 됩니다.
운영에서 특히 불편했던 지점은 세 가지였습니다.
- 배포 중 연결이 끊기면서 사용자 요청이 실패할 수 있었습니다.
- 한 서버만 실패하면 두 서버의 상태가 달라져 롤백 판단이 어려웠습니다.
- 데이터베이스 마이그레이션을 어느 서버에서 실행해야 하는지 배포 흐름 안에서 애매했습니다.
핵심은 병렬 배포가 느슨한 개발 환경에서는 편할 수 있어도, 실제 트래픽을 받는 서비스에서는 복구와 설명이 어려운 실패를 만든다는 점이었습니다.
그래서 롤링 배포로 바꿨습니다
방향은 명확했습니다. 두 서버를 함께 바꾸는 대신, 하나씩 검증하면서 넘겨야 했습니다.
첫 번째 서버를 배포하고, 헬스체크를 통과한 뒤, 두 번째 서버로 넘어가는 흐름으로 바꿨습니다. 데이터베이스 마이그레이션도 첫 번째 서버에서만 수행하도록 고정했습니다.
deploy-server1:
needs: [build-and-push, deploy-monitoring]
runs-on: ubuntu-latest
steps:
- name: Deploy to Server-1
run: |
export SERVER_NAME="Server-1"
export INCLUDE_DB_MIGRATION="true"
verify-server1:
needs: deploy-server1
runs-on: ubuntu-latest
deploy-server2:
needs: [build-and-push, verify-server1]
runs-on: ubuntu-latest
steps:
- name: Deploy to Server-2
run: |
export INCLUDE_DB_MIGRATION="false"이 변경으로 얻은 이점은 단순했습니다.
- 항상 최소 한 대의 서버가 살아 있었습니다.
- 첫 번째 서버에서 이상이 생기면 다음 서버로 진행하지 않았습니다.
- 마이그레이션 경로가 하나로 고정됐습니다.
배포 시간이 조금 더 걸리는 대신, 배포 실패를 훨씬 더 다루기 쉬운 문제로 바꿀 수 있었습니다.
그런데 GitHub Actions 안에 배포 로직을 계속 키우는 건 또 다른 문제였습니다
롤링 배포로 바꾼 뒤에도 불편은 남아 있었습니다. 배포가 안정적이긴 해졌지만, 배포 로직이 GitHub Actions 안에서 점점 커지고 있었기 때문입니다.
대표적인 문제는 세 가지였습니다.
- 배포 워크플로 파일이 커질수록 읽기와 수정이 어려워졌습니다.
- 시크릿과 환경별 설정이 GitHub Actions 쪽에 과하게 몰렸습니다.
- 배포 로직을 바꾸기 위해 CI 파일까지 계속 수정해야 했습니다.
이 시점부터는 배포 전략의 문제가 아니라, 책임 분리의 문제라고 보는 편이 맞았습니다.
그래서 CI와 CD를 분리했습니다
이후 구조는 다음처럼 정리했습니다.
- GitHub Actions는 빌드와 이미지 생성만 담당했습니다.
- AWX는 배포 실행과 서버 구성 관리를 담당했습니다.
이렇게 나누고 나니 GitHub Actions는 다시 단순해졌습니다.
name: LunchChat CI/CD Pipeline with AWX
jobs:
build-and-push:
runs-on: ubuntu-latest
outputs:
image-tag: ${{ steps.set-tag.outputs.IMAGE_TAG }}
steps:
- name: Build application with Gradle
run: ./gradlew build -x test
- name: Build and push Docker image
uses: docker/build-push-action@v5
deploy-app-servers:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Launch AWX job
run: |
awx job_templates launch "Deploy LunchChat App" \
--extra-vars "$EXTRA_VARS" \
--monitor반대로 실제 배포 절차는 AWX와 Ansible Playbook으로 옮겼습니다.
- name: Deploy LunchChat Application
hosts: app_server
become: yes
gather_facts: no
serial: 1
tasks:
- name: Create docker-compose.yml from template
template:
src: templates/docker-compose.app.yml.j2
dest: "{{ app_dir }}/docker-compose.yml"
- name: Pull the new image and start services
community.docker.docker_compose_v2:
project_src: "{{ app_dir }}"
pull: always
state: present
recreate: always
- name: Wait for application to be healthy
uri:
url: http://localhost:8080/actuator/health
status_code: 200
retries: 30
delay: 20
register: health_check
until: health_check.status == 200여기서 핵심은 serial: 1이었습니다. 롤링 배포의 규칙을 워크플로 스크립트 바깥으로 빼고, 배포 도구 수준에서 강제하게 된 것입니다.
왜 Terraform이 아니라 AWX와 Ansible이었을까요
이 시점의 런치챗은 인프라를 새로 프로비저닝해야 하는 상황이 아니었습니다. 이미 띄워 둔 서버에 어떤 설정을 적용하고, 어떤 순서로 배포할지를 제어하는 것이 더 중요했습니다.
즉, 문제는 인프라 생성보다 구성 관리에 가까웠습니다. 그래서 Terraform보다 Ansible과 AWX가 더 맞았습니다. 필요한 것은 리소스 선언보다 배포 절차의 제어였기 때문입니다.
배포 시간은 느려졌지만 운영은 훨씬 단순해졌습니다
병렬 배포가 배포 시간만 놓고 보면 더 빨랐습니다. 두 서버를 함께 바꾸면 전체 소요 시간이 짧아지기 때문입니다.
하지만 운영 기준으로 보면 질문이 달라집니다. 배포가 3분 빠른 것이 중요한지, 실패했을 때 15분 이상 흔들리지 않는 것이 중요한지 봐야 했습니다.
| 항목 | 병렬 배포 | 롤링 배포 + AWX |
|---|---|---|
| 배포 시간 | 더 짧았습니다. | 더 길었습니다. |
| 서비스 연속성 | 배포 시 단절 가능성이 있었습니다. | 최소 한 대를 유지할 수 있었습니다. |
| 실패 대응 | 상태 불일치가 생기기 쉬웠습니다. | 단계별 중단과 판단이 쉬웠습니다. |
| 마이그레이션 제어 | 배포 흐름 안에서 애매했습니다. | 한 서버에서만 실행하도록 고정할 수 있었습니다. |
| 운영 책임 분리 | CI와 CD가 한 파일에 섞여 있었습니다. | 빌드와 배포를 분리했습니다. |
실제로는 배포 시간이 조금 늘어나는 대신, 실패 시 흔들리는 범위를 크게 줄일 수 있었습니다. 이 변화가 런치챗에서는 더 중요했습니다.
정리
런치챗에서 중요했던 것은 배포 시간을 몇 분 줄이는 일이 아니었습니다. 배포 중에도 서비스를 유지하고, 실패 시 다음 행동을 쉽게 결정할 수 있는 구조를 만드는 일이 더 중요했습니다.
병렬 배포는 빠르지만 상태를 불안정하게 만들었고, 롤링 배포와 AWX는 그 반대였습니다. 런치챗에 필요했던 것은 더 화려한 배포 체계가 아니라, 실패 범위를 작게 만들고 운영 판단을 단순하게 만드는 배포 구조였습니다.
