애플리케이션이 실행되기도 전에 실패했다
빌드와 배포가 비슷한 시점에 실패했다. 로그를 보니 애플리케이션 코드나 테스트가 아니라 GitHub Actions의 Set up job 단계에서 멈춰 있었다. 외부 액션의 소스 압축파일을 다운로드하는 요청이 404를 반환하고 있었다.
문제가 된 액션은 hashicorp/vault-action이었다. 동료들이 원인을 좁히고 공용 액션의 참조를 수정한 뒤 작업이 다시 정상 동작했다. 해결 과정을 따라가면서 궁금해졌다.
우리는 왜 외부 액션을 특정 커밋에 고정해서 쓰고 있었을까? 그냥 최신 버전을 쓰면 이런 문제를 피할 수 있지 않을까?
이번 일을 계기로 액션의 버전을 지정하는 방법과, 버전을 고정해도 남는 의존성을 알아봤다. 사례에 등장하는 인물과 내부 저장소·서비스 이름은 생략했다.
Vault 액션은 누가 관리하는 코드일까
Vault Action은 HashiCorp가 관리하는 GitHub 액션이다. 워크플로에서 Vault에 인증하고 필요한 비밀정보를 가져오는 등의 작업을 수행한다. 우리가 직접 작성한 코드가 아니라 외부 저장소에서 가져와 실행하는 코드다.
당시 여러 빌드·배포 워크플로는 공용 액션을 통해 이 액션을 사용하고 있었다. 공용 액션 안에서 Vault 액션을 특정 커밋 SHA로 지정했다. SHA는 Git에서 커밋을 식별하는 해시값이다.
yamluses: hashicorp/vault-action@4c06c5ccf5c0761b6029f56cfb1dcf5565918a3b
이 설정은 “실행할 때마다 최신 코드를 가져와라”가 아니라 “이 커밋의 코드를 가져와라”라는 뜻이다. 당시 이 SHA는 v3.4.0에 해당했다.
따라서 우리 애플리케이션 저장소에 변경이 없어도, 러너가 이 외부 코드를 다운로드하지 못하면 작업을 시작할 수 없다. 실제 실패 지점도 Vault에 인증을 요청하는 단계에 도달하기 전이었다.
text빌드·배포 작업 시작 → 사용할 액션 준비 → 공용 액션이 참조하는 Vault 액션 다운로드 → GitHub codeload에서 404 반환 → Set up job 실패
Vault 액션을 가져오지 못했다는 것과 Vault 서버에 장애가 났다는 것은 서로 다른 이야기였다.
404라고 커밋이 사라진 것은 아니었다
처음에는 고정해 둔 커밋이 삭제된 게 아닐까 하는 의문이 나왔다. 하지만 공유된 조사 결과에서는 해당 SHA를 지정한 git fetch가 성공했다. 같은 버전인 v3.4.0도 태그를 지정한 다운로드 경로에서는 정상적으로 내려받을 수 있었다.
당시 확인된 결과를 정리하면 다음과 같다.
| 접근 방법 | 관측 결과 |
|---|---|
| 문제가 된 v3.4.0 SHA의 압축파일 다운로드 | 404 |
해당 SHA를 지정한 git fetch | 성공 |
v3.4.0 태그를 지정한 압축파일 다운로드 | 성공 |
| v4.0.0 SHA의 압축파일 다운로드 | 성공 |
GitHub의 codeload는 저장소 내용을 압축파일로 내려주는 서비스다. 커밋을 Git으로 가져오는 것과, 그 커밋의 압축파일을 HTTP로 내려받는 것은 다른 접근 경로다. 한쪽이 실패했다고 다른 쪽에서도 커밋을 찾을 수 없다고 단정할 수는 없었다.
이때는 특정 SHA의 압축파일을 제공하는 경로에 문제가 있다고 추정했다. 다만 GitHub 내부에서 왜 404가 발생했는지까지 확인한 것은 아니다. 모든 SHA 다운로드가 실패한 것도 아니어서, GitHub 전체 장애라고 일반화할 근거도 없었다.
또 러너에 필요한 액션의 캐시가 없을 때 실패한다는 보고가 있었다. 이미 코드를 가지고 있는 실행과 새로 다운로드해야 하는 실행은 결과가 다를 수 있다는 점도 함께 봐야 했다.
SHA는 그대로 고정하고, 가리키는 커밋을 바꿨다
대응 과정에서는 SHA 고정을 잠시 풀고 v3.4.0 태그를 사용하는 방안이 논의됐다. 같은 버전의 태그 다운로드는 동작했기 때문이다.
실제로 반영된 수정은 조금 달랐다. 한 동료가 정상 다운로드되는 v4.0.0의 SHA로 교체하는 PR을 만들었고, 다른 동료가 이를 반영한 뒤 실패했던 작업을 재실행했다. 이후 재실행한 작업의 성공과 정상 동작 확인이 이어졌다.
diff- uses: hashicorp/vault-action@4c06c5ccf5c0761b6029f56cfb1dcf5565918a3b + uses: hashicorp/vault-action@892a26828f195e65540a40b4768ae4571f51ebfc # v4.0.0
SHA를 고정하는 정책은 유지했다. 다운로드에 실패하던 참조를 정상적으로 가져올 수 있는 참조로 바꾼 것이다.
설정 변경은 한 줄이었지만 실행할 액션의 메이저 버전이 바뀐다는 점은 별도로 봐야 한다. v4.0.0 릴리스 노트에는 액션의 Node.js 실행 환경이 Node 20에서 Node 24로 바뀌었다는 내용이 있다. 다운로드가 성공하는지와 함께 러너 호환성, 사용하는 인증 입력, 실제 실행 결과도 확인해야 하는 변경이다.
그런데 왜 SHA를 고정할까
태그를 쓰면 읽기도 편한데, 굳이 긴 해시값을 쓰는 이유가 궁금했다. 액션 버전을 지정하는 방법을 비교하니 차이가 보였다.
| 참조 방식 | 실행할 코드가 정해지는 기준 |
|---|---|
@main | 실행 시점에 main 브랜치가 가리키는 커밋 |
@v4 | 실행 시점에 v4 태그가 가리키는 커밋 |
@v4.0.0 | 실행 시점에 v4.0.0 태그가 가리키는 커밋 |
@892a268… | 지정한 커밋. 실제 설정에는 전체 SHA 사용 |
브랜치가 갱신되면 @main으로 실행하는 코드도 바뀐다. @v4처럼 메이저 버전 태그를 최신 v4 릴리스로 옮겨 주는 방식도 있다. 이 경우 워크플로 파일을 수정하지 않아도 다음 실행에서는 다른 코드가 동작할 수 있다.
@v4.0.0처럼 버전 전체를 적은 태그는 특정 릴리스를 가리키려는 의도가 더 분명하다. 다만 일반적인 Git 태그 자체가 변경 불가능한 것은 아니다. GitHub의 immutable release로 보호되는 태그와도 구분해야 한다.
SHA로 고정하면 적어도 액션의 소스가 우리 모르게 다른 커밋으로 바뀌는 것을 막을 수 있다. 어제 성공한 배포와 오늘 실패한 배포가 같은 액션 코드를 사용했는지 확인하기도 쉬워진다. 물론 액션이 실행 중 외부에서 가져오는 도구나 러너 환경까지 전부 고정되는 것은 아니다.
보안 측면에서도 의미가 있다. 액션은 워크플로에 부여된 권한과 전달된 비밀정보를 사용할 수 있다. 특히 인증을 담당하는 액션이라면, 외부 저장소의 변경이 검토 없이 실행되는 상황을 줄일 필요가 있다. GitHub의 외부 액션 보안 가이드도 전체 커밋 SHA로 참조를 고정해 외부 코드 변조 위험을 줄이는 방법을 설명한다.
그렇다고 SHA가 코드의 안전성을 검증해 주는 것은 아니다. 무엇을 실행할지 고정하는 것과, 그 코드가 안전한지 검토하는 것은 별개의 일이다.
매번 최신 버전을 사용하면 안 될까
자동으로 최신 버전을 따라가면 업데이트를 직접 챙기는 부담은 줄어든다. 버그 수정이나 보안 패치를 빠르게 받을 수도 있다. 대신 변경이 적용되는 시점도 외부 저장소의 업데이트에 따라 결정된다.
예를 들어 최신 버전에서 지원하는 입력이 바뀌거나 더 새로운 러너를 요구하면, 우리 코드를 수정하지 않았는데도 배포가 실패할 수 있다. @main은 최신 정식 릴리스를 의미하지도 않는다. 아직 릴리스하지 않은 변경까지 따라갈 수 있다.
이번에는 다른 버전으로 바꿔서 복구했지만, 이것만으로 최신 버전 자동 사용이 더 안정적이라고 결론 내리기는 어렵다. 최신 버전에도 버그가 있을 수 있고, 다운로드 서비스 장애는 참조 방식과 관계없이 발생할 수 있다.
내가 이해한 현실적인 운영 방식은 검증한 버전의 SHA를 고정하고, 새 버전은 변경 PR을 통해 받아들이는 것이다. 고정해 두고 영원히 업데이트하지 않는다는 뜻은 아니다.
Dependabot을 사용하면 GitHub Actions 의존성의 새 버전을 확인하고 업데이트 PR을 만드는 과정을 자동화할 수 있다. 사람은 릴리스 변경과 검증 결과를 보고 반영 여부를 결정한다. 직접 작성한 공용 액션 내부의 참조까지 원하는 범위로 탐지하는지는 별도로 확인해야 한다.
이것은 이번 장애에서 적용한 후속 조치라는 뜻은 아니다. 사건을 돌아보며 생각해 본 업데이트 관리 방향이다.
버전 고정과 다운로드 가능성은 별개였다
처음에는 “SHA까지 고정했는데 왜 실패하지?”라고 생각했다. 버전을 고정하면 배포가 안정적일 것이라는 기대가 있었던 것 같다.
하지만 SHA 고정이 통제하는 것은 실행할 코드의 선택이다. 그 코드를 가져오는 서비스가 계속 정상 동작하는지까지 보장하지는 않는다.
이번에 배운 것은 이 두 질문을 나눠 보는 일이었다. 같은 코드를 실행하도록 정했는가? 그리고 그 코드를 지금 가져올 수 있는가? 공용 액션의 참조 한 줄에도 버전 관리, 외부 서비스의 가용성, 인증 권한이 함께 얽혀 있었다.
다음에 배포가 실패하면 애플리케이션 변경부터 의심하기 전에, 어느 단계에서 무엇을 가져오다가 멈췄는지 먼저 확인해 보려고 한다.