이 배포, 오늘은 제가 했지만 다음엔 누가 할 수 있을까요? 제가 휴가 간 사이에 급하게 배포해야 하는 일이 생기면, 다른 팀원이 저 없이도 그대로 따라 할 수 있을까요? 로컬에서 명령어 몇 개를 순서대로 실행하는 이 절차를, 새로 합류한 사람에게는 매번 어떻게 처음부터 설명해야 할까요?
저희 팀이 실제로 마주했던 상황입니다. KOKKOK 플랫폼 안에서 운영하는 서비스가 계속 늘어나면서 배포 빈도도 함께 늘었는데, 배포 방식은 여전히 사람이 직접 하는 수동 작업이었습니다. 담당자가 로컬에서 npm run build를 돌리고, 결과물을 직접 S3에 올린 뒤 CloudFront 캐시를 무효화하는 식이었습니다. 이 과정을 GitHub Actions로 자동화하면서 겪은 문제와 해결 과정을 정리합니다. 비슷한 규모의 모노레포를 운영하는 팀에게 도움이 되길 바랍니다.
앱이 늘어날수록 수동 배포의 비용은 배포 한 번에 걸리는 시간이 아니라, 이 절차를 계속 사람이 반복하고 또 다른 사람에게 그대로 넘겨줘야 한다는 데 있었습니다. 담당자마다 배포 순서를 조금씩 다르게 기억하고 있었고, 담당자가 바뀌거나 새로 합류한 팀원에게 인수인계를 할 때마다 순서를 처음부터 다시 설명해야 했습니다. 문서로 절차를 정리해서 실제 상황에 맞게 계속 반영은 했지만, 그 문서를 받아본 사람이 내용을 100% 이해하지 못하는 게 더 큰 문제였습니다.
여기서 세운 원칙은 두 가지였습니다.
- 배포 절차를 사람 머릿속이 아니라 파이프라인 코드에 둔다. 새로 합류한 사람도 순서를 외울 필요 없이, 정해진 명령 하나만 실행하면 배포할 수 있어야 합니다. 그래야 인수인계도 "코드를 읽는 일"로 끝납니다.
- 인증과 검증은 사람이 아니라 파이프라인이 보장한다. 누가 배포할 수 있는지, 어떤 브랜치에서 빌드할 수 있는지를 사람이 조심해서 지키는 규칙이 아니라, 파이프라인이 매번 자동으로 확인하는 규칙으로 만듭니다.
무엇을 비교했고 왜 IAM Role이었나
자동화 도구는 GitHub Actions로 정했습니다. 이미 코드 저장소로 GitHub를 쓰고 있어서 별도 CI/CD 플랫폼을 새로 들이지 않고 바로 워크플로우를 붙일 수 있었기 때문입니다. 대신 따로 정해야 했던 건 AWS 인증 방식이었습니다. 워크플로우가 배포 때마다 S3·CloudFront에 접근하려면 자격증명이 필요한데, 이 자격증명을 어떻게 다룰지가 문제였습니다.
| 방식 | 설명 | 비고 |
|---|---|---|
| Access Key / Secret | 장기 Access Key를 발급받아 GitHub Secrets에 저장해두고 계속 사용 | 키가 유출되면 만료 전까지 계속 악용될 수 있고, 서비스가 늘어날 때마다 키를 새로 발급·보관해야 함 |
| IAM Role, OIDC (선택) | 워크플로우 실행 시점에 GitHub과 AWS가 OIDC로 신뢰 관계를 맺고, 그때그때 단기 자격증명만 발급받음 | 키 자체를 저장소에 저장할 필요가 없고, 탈취되더라도 유효 시간이 짧아 위험 범위가 줄어듦 |
장기 Access Key를 서비스 수만큼 계속 관리하는 부담과, 유출됐을 때의 위험 범위를 줄이기 위해 IAM Role 방식을 선택했습니다.
이 두 원칙에 따라 처음 만든 파이프라인은 이랬습니다. 10개 서비스를 한 워크플로우에서 한꺼번에 빌드하는 대신, 실행할 때 서비스 하나를 골라 그 서비스만 빌드하도록 했습니다. 그리고 "버튼 한 번으로 끝나는" 편의성을 위해, 빌드부터 S3 업로드·배포 대상 전환·CloudFront 캐시 무효화까지를 워크플로우 실행 한 번에 전부 처리하도록 만들었습니다. 배포를 실행할 수 있는 사람과 빌드를 허용할 브랜치도 처음부터 검증 단계에 넣어뒀습니다.
1차 문제: 버튼 한 번으로 묶어두니 버전 관리가 안 됐다
빌드와 배포를 한 워크플로우로 묶어서 처리하다 보니, 배포본을 버전별로 관리한다는 개념 자체가 없었습니다. S3에는 방금 올라가 바로 서비스에 반영되는 폴더와, 그 직전에 빌드됐던 폴더 딱 2개만 존재했습니다. 문제가 생겨서 두 단계 전 버전으로 되돌리고 싶어도 이미 지워지고 없어서 되돌릴 방법이 없었고, 사실상 롤백이 가능한 범위는 "바로 직전"으로 딱 한 걸음뿐이었습니다.
해결: S3에 결과물을 덮어쓰는 대신, 시맨틱 버전을 폴더명에 포함해 버전별로 쌓아두도록 바꿨습니다.
# 빌드 결과물을 버전별 폴더에 쌓아둔다 (덮어쓰지 않는다)
aws s3 cp --recursive "$BUILD_DIR" \
"s3://$BUCKET/__${ENV}-${SERVICE_NAME}-${VERSION}/"이제 S3에는 __prod-serviceA-v1.2.0, __prod-serviceA-v1.2.1처럼 배포됐던 버전이 폴더 단위로 계속 쌓입니다. 직전 버전뿐 아니라, 필요하면 몇 단계 전 버전으로도 돌아갈 수 있는 발판이 마련된 셈입니다.
2차 문제: 버저닝을 해도 릴리즈 시점을 맞추기 어려웠다
버전별로 결과물을 쌓아두기 시작했지만, 빌드와 배포가 여전히 한 워크플로우에 붙어 있었습니다. 서비스 하나 기준으로 빌드에만 5분 넘게 걸렸는데, 이 말은 특정 시각에 정확히 반영하려면 그보다 한참 전에 워크플로우를 실행해야 했고, 그마저도 정확한 반영 시점을 보장할 수 없었다는 뜻이었습니다. 버저닝을 도입해도, 원하는 버전을 원하는 시점에 올리는 일 자체는 여전히 "처음부터 다시 빌드해서 배포"하는 방법밖에 없었습니다.
해결: 워크플로우를 build(빌드 & 업로드) / release(배포 & 무효화) / validation(공용 검증) 세 개로 나눴습니다. build는 1차 문제에서 만든 버전별 폴더에 결과물을 쌓기만 하고, release는 그중 원하는 버전 폴더를 골라 S3 루트로 복사하고 CloudFront 캐시를 무효화하는 일만 합니다.
# release — 원하는 버전 폴더를 S3 루트로 복사한다 (재빌드 없음)
aws s3 cp --recursive \
"s3://$BUCKET/__${ENV}-${SERVICE_NAME}-${VERSION}/" "s3://$BUCKET/"
aws cloudfront create-invalidation --distribution-id "$CF_ID" --paths "/*"release는 복사 전 대상 버전 폴더에 index.html이 실제로 존재하는지, 용량이 비정상적으로 작지 않은지를 확인하는 무결성 검증도 거칩니다. 복사 도중 실패하면 직전 버전 폴더에서 자동으로 다시 복사해, S3 루트가 반쯤 비워진 상태로 남지 않도록 했습니다. 검증 로직도 build·release 워크플로우 각각에 넣는 대신 validation 워크플로우 하나로 뽑아 workflow_call로 재사용하게 했습니다.
빌드와 배포를 분리하면서 job 구성도 다시 손봤는데, 서비스 하나를 기준으로 실측한 결과 다음과 같이 줄었습니다.
| 워크플로우 | 개선 전 | 개선 후 | 개선율 |
|---|---|---|---|
| build (빌드 & 업로드) | 5m 27s | 2m 21s | 약 43.1% ↓ |
| release (배포) | 1m 41s | 1m 15s | 약 25.7% ↓ |
이제 원하는 시각에 정확히 반영하고 싶으면, 미리 build를 실행해 원하는 버전을 S3에 쌓아두고, 반영하고 싶은 시각에 release만 짧게 실행하면 됩니다. 롤백도 "예전 버전으로 다시 빌드하는 일"이 아니라, release 워크플로우를 예전 버전 번호로 다시 실행하는 일이 됐습니다.
종합 결과
결과적으로 배포는 담당자가 절차를 따로 익히거나 인수인계받을 필요 없이, 정해진 버튼(workflow_dispatch 실행) 하나만 누르면 끝나는 일이 됐습니다. 이게 가능해진 건 아래 장치들이 사람이 하던 판단과 기억을 대신 떠맡았기 때문입니다.
- 배포 권한·빌드 가능 브랜치·버전 형식은 사람이 매번 기억하지 않아도, validation 워크플로우가 실행 때마다 자동으로 확인
- 배포본을 버전별 폴더로 관리해, 어떤 버전이 올라가 있는지 사람이 따로 기록하지 않아도 되고 여러 단계 전 버전으로도 버튼 하나로 롤백 가능
- 빌드와 배포를 분리해 원하는 시각에 정확히 반영하고, 롤백도 "예전 버전 번호로 release 버튼을 다시 누르는" 것으로 끝남
- 장기 Access Key를 직접 발급·보관·교체할 필요 없이 IAM Role(OIDC)이 배포 때마다 알아서 단기 자격증명을 처리
- 빌드/배포 워크플로우 분리와 job 통합으로 빌드 43.1%, 배포 25.7% 시간까지 함께 단축
배운 점
- "버튼 한 번으로 끝나는" 편의성과 "원하는 시점에 정확히 반영하는" 정확성은 다른 축의 문제였습니다. 처음엔 편의성을 위해 빌드와 배포를 하나로 묶었는데, 그 선택이 나중엔 릴리즈 타이밍을 통제할 수 없게 만드는 원인이 됐습니다. 편의성은 "한 번에 되는 것"이 아니라 "필요한 만큼만 실행하는 것"으로 다시 정의해야 했습니다.
- 버저닝과 워크플로우 분리는 따로 풀어야 하는 문제였습니다. 버전별 폴더를 먼저 만들어도 빌드와 배포가 붙어 있으면 그 버저닝을 제대로 활용할 수 없었고, 반대로 워크플로우만 나눠도 버전 관리가 없으면 어느 버전을 배포해야 할지 알 수 없었습니다. 두 문제를 순서대로 풀고 나서야 온전히 맞물렸습니다.
- 롤백은 배포의 반대말이 아니라, 그냥 "다른 버전으로 다시 실행하는 일"입니다. 빌드와 배포를 분리하고 나서야, 롤백을 별도 기능으로 새로 만들 필요 없이 release 워크플로우에 예전 버전 번호를 넘기는 것만으로 해결된다는 걸 알았습니다.
남은 이슈
지금 구조는 배포 전 검증까지는 자동화했지만, 배포 후 상태를 확인하는 건 아직 사람의 몫입니다. 배포가 끝난 뒤 서비스가 실제로 정상 동작하는지는 담당자가 직접 확인하고 있고, 이상이 있으면 그제서야 이전 버전으로 다시 release 워크플로우를 실행합니다. 다음 단계로는 배포 직후 헬스체크를 자동으로 돌리고, 헬스체크가 실패하면 이전 버전으로 자동 롤백하는 흐름까지 포함시키려 합니다.
여러분의 모노레포도 서비스가 늘어날수록 배포가 점점 무거워지고 있나요? 그렇다면 편의성을 위해 하나로 묶어둔 단계가 있는지부터 점검해보시길 권합니다. 저희도 그 지점이 버전 관리와 릴리즈 타이밍을 동시에 발목 잡고 있었습니다.