왜 중복 배포가 문제인가

같은 브랜치에 짧은 간격으로 여러 커밋이 푸시되면 배포 워크플로가 각각 실행된다. 문제는 이들이 서로 다른 시점의 아티팩트를 같은 환경에 밀어넣는다는 점이다. 커밋 A와 B가 거의 동시에 트리거되면, 실행 순서가 아니라 완료 순서에 따라 최종 상태가 결정된다. 느린 A 잡이 늦게 끝나면서 B가 배포한 최신 버전을 덮어쓰는 역전 현상이 생긴다.

인프라 프로비저닝은 더 위험하다. 두 잡이 동시에 terraform apply를 실행하면 상태 파일 락 충돌이나 리소스 중복 생성으로 이어진다. 결국 관측된 배포 결과와 저장소의 HEAD가 어긋난다.

concurrency 키의 동작 원리

GitHub Actions는 워크플로 또는 잡 수준에서 concurrency 그룹을 지원한다. 같은 그룹 이름을 가진 실행은 동시에 하나만 진행된다. 나머지는 대기(pending)하거나, 옵션에 따라 취소된다.

name: deploy
on:
  push:
    branches: [main]

concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/deploy.sh

핵심은 group 표현식이다. github.ref를 넣으면 브랜치별로 그룹이 분리되어 main 배포와 staging 배포가 서로를 막지 않는다.

cancel-in-progress의 선택 기준

cancel-in-progress는 배포 성격에 따라 정반대로 설정해야 한다. 잘못 고르면 오히려 사고가 난다.

설정동작적합한 경우
true진행 중 실행을 취소하고 새 실행 시작PR 미리보기, 최신 커밋만 의미 있는 경우
false진행 중 실행을 끝까지 두고 뒤를 대기시킴운영 배포, 마이그레이션 등 중단 불가

운영 배포에서 true를 쓰면 DB 마이그레이션이 중간에 잘려 반쯤 적용된 상태로 남을 수 있다. 반대로 PR 프리뷰에서 false를 쓰면 낡은 커밋 빌드가 큐에 쌓여 자원을 낭비한다.

그룹 키 설계 실무 패턴

그룹 키는 "동시에 실행되면 안 되는 단위"를 정확히 표현해야 한다. 환경별로 직렬화하되 환경 간에는 병렬을 허용하는 것이 일반적이다.

concurrency:
  group: ${{ github.workflow }}-${{ inputs.environment || 'prod' }}
  cancel-in-progress: false

환경을 키에 넣으면 prod 배포가 진행 중이어도 staging 배포는 대기 없이 시작된다. 워크플로 이름을 접두어로 두면 다른 워크플로와의 우연한 충돌도 막는다.

동시성만으로 부족한 경계

concurrency같은 저장소의 같은 워크플로 실행 사이에서만 작동한다. 외부 CD 도구, 수동 kubectl, 다른 저장소의 배포 잡이 같은 환경을 건드리면 통제 밖이다. 이 경우 배포 스크립트 자체에 원격 락을 두는 방어가 필요하다.

import time, sys
import redis

r = redis.from_url("redis://cache:6379")
lock_key = "deploy:prod"

# NX: 없을 때만 설정, EX: 600초 후 자동 만료(교착 방지)
if not r.set(lock_key, sys.argv[1], nx=True, ex=600):
    print("다른 배포가 진행 중입니다. 중단합니다.")
    sys.exit(1)

try:
    # 실제 배포 로직
    time.sleep(1)
finally:
    r.delete(lock_key)

만료 시간(ex)을 반드시 함께 설정해야 잡이 강제 종료돼도 락이 영구히 남지 않는다.

주의점 정리

대기 큐는 최신 한 건만 유지된다. 실행 중 1건 + 대기 1건 상태에서 새 실행이 들어오면 기존 대기 건은 취소된다. 순차 배포가 전부 반영될 것으로 기대하면 안 된다. 또한 대기 중인 잡도 동시성 슬롯을 점유하므로, 배포가 오래 걸리면 후속 커밋의 피드백이 그만큼 늦어진다. 배포 잡은 짧게 유지하고, 검증·빌드는 별도 잡으로 분리해 병목을 줄이는 편이 낫다.