leo.dev
infra

ECS 배포 실패 시 자동 롤백, outcome과 conclusion의 함정

2026.04.263 min read ci-cdgithub-actionsawsecs

1편의 문제 2와 6: 마이그레이션은 성공했는데 ECS 배포가 실패하면, DB는 새 스키마인데 코드는 구버전인 상태가 되고, 자동 복구 수단이 없었다. 새 이미지가 시작 직후 크래시하거나 헬스체크가 실패하면 흔히 일어난다.

미리 저장, 나중에 복구

아이디어는 단순하다.

  1. 배포 전에 현재 실행 중인 Task Definition ARN을 저장
  2. 배포가 실패하면 저장한 ARN으로 되돌림

ECS는 Task Definition을 버전으로 관리한다. 배포할 때마다 새 버전이 등록되고, 이전 버전은 삭제되지 않고 남는다. ARN만 알면 언제든 되돌릴 수 있다.

Configure AWS credentials
→ [신규] Save current Task Definition ← 현재 ARN 저장
→ Build → Register Task Def → Migration
→ [수정] Deploy ECS ← continue-on-error: true
→ [신규] Rollback on failure ← deploy 실패 시에만

현재 ARN 저장

deploy.yml
- name: Save current Task Definition (for rollback)
id: save-task-def
run: |
PREV=$(aws ecs describe-services \
--cluster $CLUSTER --services "$SERVICE" \
--query 'services[0].taskDefinition' \
--output text 2>/dev/null || echo "NONE")
echo "previous=$PREV" >> "$GITHUB_OUTPUT"

describe-services가 현재 서비스의 Task Definition ARN을 준다. 이걸 $GITHUB_OUTPUTprevious로 저장하면 이후 스텝에서 ${{ steps.save-task-def.outputs.previous }}로 꺼낼 수 있다.

2>/dev/null || echo "NONE"은 최초 배포(서비스 미존재)나 권한 문제로 조회가 실패하는 경우를 처리한다. 둘 다 NONE을 저장해 롤백을 건너뛰게 한다. 이 처리가 없으면 조회 실패가 전체 배포를 중단시킨다.

Deploy에 continue-on-error

deploy.yml
- name: Deploy Amazon ECS task definition
id: deploy
continue-on-error: true
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
with:
service: $SERVICE
cluster: $CLUSTER
wait-for-service-stability: true

continue-on-error가 없으면 이 스텝이 실패하는 순간 job이 끝나버려 롤백 스텝이 실행될 기회가 없다. 붙이면 실패해도 다음 스텝으로 넘어간다.

여기서 함정이 하나 있다. continue-on-error가 붙은 스텝은 결과를 두 가지로 기록한다.

속성의미
steps.deploy.outcomefailure스텝의 실제 결과
steps.deploy.conclusionsuccessjob에 보고되는 결과 (마스킹됨)

continue-on-error는 job을 계속 진행시키려고 conclusionsuccess로 바꾼다. 하지만 outcome은 실제 결과를 유지한다. 그래서 롤백 조건은 반드시 outcome을 봐야 한다. conclusion을 보면 항상 success라 롤백이 절대 안 돈다. 처음에 이걸 모르고 conclusion으로 짰다가, 일부러 실패시켜도 롤백이 안 뛰어서 한참 헤맸다.

실패 시에만 롤백

deploy.yml
- name: Rollback on deployment failure
if: steps.deploy.outcome == 'failure'
run: |
PREV="${{ steps.save-task-def.outputs.previous }}"
if [ "$PREV" == "NONE" ] || [ -z "$PREV" ]; then
echo "No previous task definition. Skipping rollback."
exit 1
fi
aws ecs update-service \
--cluster $CLUSTER --service "$SERVICE" \
--task-definition "$PREV"
exit 1

배포가 성공하면 if 조건에서 걸러져 아예 실행되지 않는다. 실패 시 update-service로 구 Task Definition을 다시 물린다. ECS가 롤링으로 구 컨테이너를 띄우기 시작한다. 워크플로우는 롤백 완료를 기다리지 않는다. 트리거만 하고 ECS에 맡긴다.

마지막 exit 1이 중요하다. 롤백을 트리거했다고 배포가 성공한 건 아니다. 워크플로우는 여전히 실패로 기록돼야 개발자가 알림을 받고, PR 자동 머지가 막힌다.

롤백이 못 푸는 것 — 그리고 ADD-only와의 관계

이 롤백은 코드만 되돌린다. DB 마이그레이션은 적용된 상태 그대로다. 그런데도 안전한 이유는 이 프로젝트의 마이그레이션 정책 때문이다.

마이그레이션구 코드 + 새 스키마결과
ADD COLUMN새 컬럼을 모름무시하고 동작 ✅
DROP COLUMN없는 컬럼 참조즉시 크래시 ❌
DROP TABLE없는 테이블 쿼리즉시 크래시 ❌

ADD-only 정책이 없으면 코드 롤백은 오히려 DB 크래시를 부른다. 자동 롤백과 ADD-only 마이그레이션은 분리된 규칙이 아니라 서로를 전제하는 쌍이다. 둘 중 하나만 있으면 위험하다.

남은 문제: 매 FE 배포마다 생기는 CloudFront 캐시 전파 지연. 마지막 편에서 파일 특성에 맞는 캐시 전략으로 푼다.

↑↓ 이동 열기esc 닫기