실제 GitHub Actions 장애를 겪으며 든 질문을 정리했다. 특정 조직, 저장소, 인프라를 식별할 수 있는 정보는 걷어내고 자동화의 성질과 운영 원칙만 남겼다.
GitHub Actions가 멈추자 생각보다 많은 일이 함께 멈췄다.
PR의 타입 체크와 테스트가 시작되지 않았다. 자동 리뷰와 코멘트도 오지 않았다. 개발·검증 환경으로의 배포는 물론 운영 배포까지 대기열에 섰다. 코드는 로컬에 있고 Git 저장소에도 올라갔지만, 그 코드를 검증하고 배포 단계로 넘기는 실행 제어면이 멈춘 상태였다.
처음 든 생각은 단순했다.
GitHub Actions도 이중화해야 하는 것 아닌가?
그런데 저장소의 워크플로 파일을 펼쳐보면 문제가 금방 커진다. 수십 개의 YAML 파일에는 checkout, 캐시, 산출물(artifact), 클라우드 권한, PR 코멘트, 알림, 공용 워크플로, 자체 composite action이 서로 얽혀 있다. 이것을 다른 CI 제품의 문법으로 한 벌 더 옮기면 이중화는 가능할지 몰라도, 두 번째 파이프라인 자체가 새로운 제품이 된다.
그리고 곧 더 현실적인 질문이 남는다.
평소에는 거의 쓰지 않는 두 번째 파이프라인을 누가 계속 최신 상태로 유지할 것인가?
TL;DR
- 모든 GitHub Actions 워크플로를 이중화할 필요는 없다. 고객 장애 복구와 운영 배포에 필요한 경로부터 보호한다.
- self-hosted runner도 GitHub Actions 서비스에서 작업을 받아야 하므로 Actions 제어면 장애의 대안이 아니다.
- GitHub YAML을 다른 CI YAML로 복사하지 않는다. 빌드·배포 로직을 공통 명령이나 컨테이너로 빼고, 각 CI는 그것을 호출하는 어댑터(adapter)가 된다.
- 운영 배포를 두 구현으로 만들면 변경할 때마다 둘이 어긋난다. 핵심 구현은 하나, 실행 제어면은 둘이어야 한다.
- 유휴 비상 경로는 문서만으로 유지되지 않는다. 개발 환경의 모의 배포와 롤백 훈련으로 계속 살아 있는지 확인해야 한다.
- 이중화 범위는 불안감이 아니라 RTO, 즉 목표 복구 시간으로 결정한다.
멈춘 것은 runner가 아니라 제어면이었다
GitHub Actions를 쓰다 보면 실행 주체를 runner라고 생각하기 쉽다. 실제 빌드와 테스트는 runner에서 일어나기 때문이다. 그래서 장애를 겪으면 GitHub-hosted runner 대신 self-hosted runner를 두면 되지 않을까 생각하게 된다.
하지만 self-hosted runner도 GitHub Actions 서비스에 연결해 작업 할당을 받는다. GitHub 공식 문서에도 runner가 GitHub에 연결해 job을 받는 구조가 명시돼 있다. 실행할 서버가 회사 안에 있어도 작업을 생성하고 배정하는 제어면이 멈추면 새로운 job은 출발하지 않는다.
GitHub Actions 제어면
-> 워크플로 해석
-> job 생성과 대기열 관리
-> runner에 작업 할당
-> 시크릿·OIDC·산출물 연결
-> 결과와 check 상태 기록
runner를 두 벌 두는 것은 실행 자원 장애에는 도움이 된다. 하지만 제어면 장애까지 견디려면 GitHub와 무관하게 작업을 시작할 수 있는 두 번째 실행 경로가 필요하다.
이 구분이 중요하다. runner 이중화와 CI/CD 제어면 이중화는 다른 문제다.
모든 자동화가 같은 중요도를 갖지는 않는다
장애가 나면 멈춘 것들이 한꺼번에 눈에 들어온다. 그렇다고 전부 같은 속도로 복구해야 하는 것은 아니다.
PR에 자동으로 라벨을 붙이지 못한 것과, 고객 장애를 고칠 운영 배포를 시작하지 못한 것은 영향이 다르다. Chromatic 결과가 늦게 나오는 것과, 이전 정상 버전으로 롤백하지 못하는 것도 다르다.
자동화를 다음처럼 나눠볼 수 있다.
| 등급 | 성질 | 장애 시 대응 | 준비 수준 |
|---|---|---|---|
| Tier 0 | 고객 장애 복구, 운영 배포, 롤백 | 즉시 실행해야 함 | 독립된 비상 경로 |
| Tier 1 | 빌드, 테스트, 필수 품질 검사 | 수동 또는 보조 CI로 실행 | 플랫폼 독립 명령 |
| Tier 2 | 시각 회귀, 정기 리포트, 개발 생산성 | 복구 후 재실행 | 재시도 가능하게 유지 |
| Tier 3 | PR 라벨, 코멘트, 알림 | 기다려도 됨 | 이중화하지 않음 |
등급은 기술 난이도가 아니라 멈췄을 때 고객 대응이 막히는가로 정한다.
이 기준을 적용하면 해야 할 일이 크게 줄어든다. PR 편의 자동화 수십 개를 다른 플랫폼에 그대로 옮길 필요가 없다. 운영 장애 때 반드시 필요한 것은 대개 훨씬 좁다.
검증된 버전을 고른다
-> 이미지와 정적 파일을 확인한다
-> 승인한다
-> 배포하거나 이전 버전으로 되돌린다
-> 상태를 검증한다
먼저 이 경로를 GitHub Actions 없이 실행할 수 있게 만드는 편이 전체 자동화를 복제하는 것보다 현실적이다.
PR 자동화는 복제보다 분리가 먼저다
PR에 걸린 워크플로 중에는 실제 검사와 GitHub UI 연동이 한 덩어리로 묶인 것이 많다.
예를 들어 의존성 규칙을 검사하는 Action이 있다고 하자.
두 커밋 사이의 의존성 변경 계산
-> 규칙 위반 판정
-> Markdown 리포트 생성
-> 기존 PR 코멘트 검색
-> 코멘트 생성 또는 수정
-> required check 성공·실패 기록
앞의 세 단계는 Node.js가 있는 곳이면 어디서나 실행할 수 있다. 뒤의 세 단계는 GitHub API와 PR 컨텍스트가 필요하다. 이 둘을 composite action 안에 함께 두면 검사 로직까지 GitHub Actions 전용처럼 보인다.
경계를 나누면 구조가 달라진다.
scripts/ci/dependency-check.ts # 순수 검사, 리포트와 exit code 생성
.github/actions/... # GitHub 코멘트와 체크 어댑터
secondary-ci/... # 보조 CI 결과 어댑터
타입 체크, 테스트, lint, 애플리케이션 빌드도 같은 방식으로 다룰 수 있다.
pnpm ci:type-check
pnpm ci:test
pnpm ci:lint
pnpm ci:build
GitHub Actions는 이 명령을 실행하고 결과를 PR에 표시한다. Actions가 멈추면 개발자는 로컬에서 같은 명령을 실행하거나 보조 CI에서 실행할 수 있다. GitHub 코멘트가 남지 않더라도 검증 자체는 계속할 수 있다.
PR 자동화 전체를 이중화하지 않아도 검사의 핵심을 GitHub 밖으로 꺼내는 것만으로 종속성을 줄일 수 있다.
여기서 체크를 두 개로 늘리는 것은 조심해야 한다. 기존 GitHub Actions 체크와 보조 CI 체크를 모두 required로 지정하면, 둘 중 하나가 멈췄을 때 두 결과를 모두 기다리느라 머지가 막힌다. 이중화하려다 의존성을 하나 더 추가하는 셈이다.
보조 CI를 연결한다면 대표 체크 하나가 실행기별 결과를 반영하게 하거나, 장애 상황에서만 승인된 담당자가 사용할 수 있는 우회 절차를 별도로 정해야 한다. GitHub 전체가 멈추면 PR 화면과 머지 자체도 사용할 수 없으므로, 라벨과 코멘트까지 복제하는 것보다 정확한 커밋을 검증하고 배포할 수 있는 경로가 우선이다.
배포는 두 벌의 YAML이 아니라 하나의 계약이어야 한다
배포는 더 어렵다. 보통 하나의 워크플로 안에 다음 작업이 이어진다.
배포 버전 생성
-> Docker 이미지 빌드
-> 컨테이너 레지스트리 게시
-> 정적 파일 추출
-> 산출물 전달
-> 오브젝트 스토리지 업로드
-> 배포 매니페스트 갱신
-> CD 도구 동기화
-> 상태와 알림 기록
이 YAML을 다른 CI 문법으로 번역해 두면 당장은 이중화처럼 보인다. 하지만 이미지 태그 규칙, 빌드 인자, 정적 파일 경로, 권한, 배포 매니페스트 형식이 바뀔 때마다 두 파이프라인을 모두 수정해야 한다.
한쪽만 고치면 비상 경로는 조용히 오래된 상태가 된다. 실제 장애가 발생한 날 처음 실행했을 때 잘못된 경로에 파일을 올리거나, 존재하지 않는 시크릿을 찾거나, 현재 애플리케이션과 맞지 않는 매니페스트를 만들 수 있다.
그래서 목표는 두 구현이 아니다.
핵심 구현은 하나, 실행 제어면은 둘.
배포의 핵심을 공통 명령이나 빌드 컨테이너로 만든다.
release prepare
release build
release publish
release promote
release rollback
release verify
정상 상황에서는 GitHub Actions가 이 명령을 호출한다. 비상 상황에서는 별도 CI나 승인된 운영 도구가 같은 명령을 호출한다.
GitHub Actions ─┐
├─ 공통 release CLI·컨테이너 ─ registry·storage ─ 배포
비상 실행기 ───┘
이렇게 해야 배포 규칙이 바뀌어도 공통 구현 한 곳을 수정할 수 있다. 두 실행기는 실행 조건, 시크릿 주입, 로그와 승인 UI만 다르게 담당한다.
그래도 두 경로의 정합성은 관리해야 한다
공통 구현을 사용한다고 운영 비용이 사라지는 것은 아니다. 실행 환경, 권한, 네트워크, 트리거, 승인 방식은 여전히 다르다.
배포 관련 변경이 생길 때마다 최소한 다음을 확인해야 한다.
- 두 실행기가 같은 release CLI 또는 동일한 digest의 컨테이너를 사용하는가
- 필수 입력과 기본값이 같은가
- 이미지와 정적 파일의 버전 규칙이 같은가
- 시크릿 이름이 아니라 실제로 요구하는 권한이 같은가
- 산출물 위치와 보존 기간이 비상 복구 시간보다 충분히 긴가
- 배포 매니페스트 스키마가 두 경로에서 동일하게 검증되는가
- health check와 롤백 조건이 같은가
- 정상 경로 변경이 비상 경로의 테스트를 함께 요구하는가
이를 사람의 기억에 맡기면 놓친다. 배포 변경 PR에는 자동으로 다음 검증이 따라야 한다.
release contract test
-> 동일 입력으로 동일한 version 산출
-> 동일한 image tag·digest 규칙 확인
-> 동일한 정적 산출물 경로 확인
-> 매니페스트 스키마 검증
-> 실제 배포 없이 드라이런 결과 비교
공통 로직을 한 곳에 뒀더라도 어댑터가 새 입력을 전달하지 않으면 비상 경로는 깨진다. 따라서 배포 파일, 빌드 인자, 산출물 구조, IAM 권한, 매니페스트 스키마가 바뀌는 PR은 비상 경로 검증을 필수 조건으로 묶는 편이 안전하다.
이중화의 유지비는 두 YAML을 읽는 비용이 아니라, 두 경로가 여전히 같은 배포 계약을 지키는지 증명하는 비용이다.
유휴 비상 경로는 어떻게 관리할까
비상 파이프라인을 매일 돌리는 active-active 구조는 비용이 크다. 같은 코드를 두 번 빌드하고 두 시스템의 실행기, 캐시, 시크릿, 로그, 권한을 계속 운영해야 한다.
대부분의 팀에는 능동-대기(active-passive), 그중에서도 필요할 때 실행 자원을 만드는 콜드 스탠바이(cold standby)가 더 현실적이다.
평상시
- GitHub Actions가 정상 배포
- 공통 release 명령은 매 배포에서 사용
- 비상 실행기는 중지 또는 최소 상태
정기 점검
- 개발 환경에서 비상 경로로 모의 배포
- 운영과 같은 산출물·매니페스트 계약 확인
- 단기 권한 발급과 승인 절차 확인
장애 시
- 비상 실행기 시작
- 검증된 산출물 선택
- 2인 승인
- 배포·롤백과 상태 확인
- GitHub 복구 후 Git 기록과 실제 배포 상태 맞추기
중요한 것은 컴퓨팅을 계속 켜두는 일이 아니라, 다음 항목이 유효한지 계속 확인하는 일이다.
- 마지막 성공 훈련일
- 실행 이미지와 도구 버전
- 단기 자격 증명 발급 가능 여부
- 레지스트리와 오브젝트 스토리지 접근 권한
- 배포 대상과 health check 접근성
- 최근 정상 산출물과 롤백 가능 버전
- 담당자와 승인자
예를 들어 월별 개발 환경 모의 배포와 분기별 롤백 훈련을 기준으로 시작할 수 있다. 정확한 주기는 서비스 RTO와 변경 빈도에 따라 조정해야 하지만, 실제로 실행하지 않는 비상 경로는 문서가 아무리 좋아도 신뢰할 수 없다.
비상 권한은 평상시 권한의 복사본이 아니다
정상 파이프라인은 GitHub OIDC로 클라우드 역할(role)을 얻는 경우가 많다. Actions가 멈추면 그 자격 증명 발급 경로도 사용할 수 없다.
비상 경로에는 GitHub와 독립된 권한이 필요하다. 그렇다고 개인 PC에 장기 액세스 키를 저장하면 장애 대응을 위해 더 큰 보안 위험을 만든다.
필요한 것은 다음과 같은 비상용(break-glass) 권한이다.
- SSO와 MFA를 거친 단기 자격 증명
- 배포와 롤백에 필요한 최소 권한
- 사용 시 별도 승인
- 발급과 실행에 대한 감사 로그
- 짧은 만료 시간
- 정기적인 발급 테스트
비상 권한은 편리해야 하는 권한이 아니라, 필요할 때 확실히 열리되 사용 흔적이 분명히 남는 권한이어야 한다.
전체 GitHub 장애까지 대비할 것인가
Actions만 멈추고 Git clone과 push가 된다면 보조 실행기는 GitHub에서 커밋을 가져와 빌드할 수 있다. 하지만 GitHub 전체가 멈추면 소스 코드와 배포 매니페스트 저장소에도 접근하지 못한다.
이 수준까지 대비하려면 범위가 더 커진다.
- 소스 저장소 미러
- 배포 저장소 미러 또는 독립된 릴리스 매니페스트 저장소
- GitHub webhook 없이 시작할 수 있는 트리거
- GitHub Secrets와 무관한 시크릿 관리 시스템
- GitHub Actions 산출물을 사용하지 않는 외부 산출물 저장소
- GitHub API에 결과를 쓰지 못해도 확인 가능한 별도 로그와 승인 기록
여기까지 가면 재난 복구 시스템에 가깝다. 모든 팀이 가져야 하는 기본값은 아니다. 고객 장애를 처리하는 동안 새 핫픽스를 몇 분 또는 몇 시간 안에 배포해야 하는지 먼저 정해야 한다.
배포 지연을 몇 시간 허용한다
-> GitHub 복구 대기
운영 롤백은 15분 안에 필요하다
-> 산출물 기반 비상 롤백 경로
새 핫픽스도 1시간 안에 필요하다
-> 소스 미러와 보조 빌드·배포 제어면
이중화 범위는 불안감으로 정하면 끝이 없다. 서비스가 요구하는 RTO로 정해야 한다.
필요한 것부터 단계적으로 준비한다
한 번에 두 번째 CI/CD 플랫폼을 완성하려 하면 프로젝트가 너무 커진다. 다음 순서가 현실적이다.
1. 자동화 목록과 중요도 분류
각 워크플로의 목적, 담당자, Tier, 허용 중단 시간, GitHub 의존 요소, 대체 절차를 적는다. 무엇이 멈췄는지조차 한눈에 보이지 않는 상태에서는 이중화 범위를 정할 수 없다.
2. 이전 정상 버전 롤백
Actions 없이 이미 배포된 이전 버전으로 돌아갈 수 있는지 확인한다. 고객 장애 대응에서 신규 빌드보다 먼저 필요한 능력이다.
3. 산출물 독립
컨테이너 이미지는 digest로, 정적 파일은 버전별 경로로, 배포 정보는 release manifest로 보관한다. GitHub의 일시적인 산출물만으로 운영 복구 경로를 만들지 않는다.
4. 공통 실행 계약 추출
빌드·게시·승격·롤백·검증을 YAML에서 꺼내 공통 CLI나 컨테이너로 만든다. GitHub Actions는 주 실행 어댑터가 된다.
5. 비상 실행기와 권한
기존에 운영하는 다른 CI나 클라우드 빌드 서비스를 우선 사용한다. 비상 상황에만 쓰려고 새로운 플랫폼을 하나 더 들이면 그 플랫폼 자체의 운영 부담이 커진다.
6. 훈련과 변경 관리
개발 환경에서 정기적으로 비상 경로를 실행한다. 배포 계약이 바뀌는 PR에는 비상 경로 드라이런(dry-run)을 함께 요구한다. 실패한 훈련은 문서상 예외로 넘기지 않고 비상 경로의 실제 장애로 취급한다.
결국 관리해야 하는 것은 두 번째 YAML이 아니다
GitHub Actions 장애를 겪고 나면 무엇이든 두 벌씩 두고 싶어진다. 하지만 자동화 전체를 복제하면 장애 대응 능력보다 유지보수할 코드와 권한이 더 빠르게 늘어난다.
중요한 것은 모든 자동화가 계속 돌아가는 상태가 아니다. 고객 장애를 복구하는 데 필요한 최소 경로가 특정 제어면 하나에 갇히지 않는 상태다.
그래서 다음 원칙으로 정리할 수 있다.
- 자동화를 고객 영향과 복구 시간으로 분류한다.
- PR 편의 자동화는 복구를 기다린다.
- 검사 로직은 GitHub 밖에서도 실행할 수 있게 만든다.
- 운영 배포와 롤백은 독립 실행 경로를 준비한다.
- 배포 구현은 복제하지 않고 공통 계약으로 관리한다.
- 비상 경로는 정기 훈련으로 살아 있음을 증명한다.
이중화는 복사본을 하나 더 만드는 일이 아니었다. 중요한 기능을 플랫폼에서 분리하고, 드물게 쓰는 경로를 계속 믿을 수 있게 관리하는 일이었다.
이 글이 배포 경로를 지키는 기술적 준비를 다뤘다면, 이어지는 장애 기록은 저장으로 끝나지 않는다에서는 기록과 훈련을 실제 대응으로 연결하기 위한 조건을 정리했다.