Beanstalk 배포 중 발생한 rpm 잠금 충돌과 OOM
문제해결 | | 13분 읽기
Astro로 만든 페이지를 AWS Elastic Beanstalk(이하 EB)에 배포하는 과정에서 배포가 여러 차례 실패했다. 로그를 확인하며 원인을 찾고 대응한 과정을 정리했다.
배포 환경
당시 페이지 요청은 CloudFront를 거쳐 EB의 nginx와 Astro 서버로 전달됐고, 정적 파일은 S3에서 제공했다.
브라우저 → CloudFront ─┬─ JS/CSS/SVG → S3
└─ HTML → EB (nginx → Astro SSR)- 배포 환경: AWS Elastic Beanstalk
- 초기 인스턴스: test와 prod 모두
t4g.micro(메모리 1GB, 스왑 없음) - 배포 파이프라인: CodePipeline → CodeBuild → EB
타임라인
| 단계 | 시각 (KST) | 환경 | 일어난 일 |
|---|---|---|---|
| 1 | 10/20 14:45 등 | test | 배포가 반복해서 실패함. 새 서버가 뜨면서 AWS 자동 업데이트와 brotli 설치가 충돌 |
| 2 | — | test | 배포 방식을 Rolling으로 전환 (prod는 원래 Rolling) |
| 3~4 | 10/22 10:16, 14:03 | test | 다시 실패. 이번엔 npm install 메모리 부족 |
| 5 | 14:34 ~ 14:37 | prod | 같은 메모리 부족으로 실패. 14:40 ~ 14:42 사이트 502 → 재배포로 복구 |
| 6 | 15:03 ~ 15:07 | prod | 서버를 t4g.small로 올리고, 최소 2대로 변경 |
| 7 | 15:13 | prod | 배포 방식을 바꾸자 1단계와 같은 충돌 발생 → 원인 분석 시작 |
1. 테스트 서버 배포가 계속 실패
test 환경에서 배포가 자꾸 실패해서 로그를 열어 봤다. 실패한 배포에서는 새로 뜬 서버에서 아래 일이 거의 동시에 일어나고 있었다.
시각 (UTC) 일어난 일
05:45:12 새 인스턴스가 부팅되고 SSM Agent가 Systems Manager에 등록됨
05:45:14 SSM Quick Setup(AWS-QuickSetup-SSM-UpdateSSMAgent)이 새 인스턴스에 즉시 실행됨
→ updater → install.sh → yum이 rpm 잠금을 잡음
05:45:37 배포 단계의 nginx_brotli.sh가 dnf install -y nginx-mod-brotli 실행
05:45:49 error: can't create transaction lock on /var/lib/rpm/.rpm.lock
(Resource temporarily unavailable) → hook이 exit 1로 끝나서 배포 실패/var/log/messages에도 관련 로그가 남아 있었다.
잠금을 잡고 있던 쪽이 SSM Agent 업데이트였다는 뜻이다.
error: can't create transaction lock on /var/lib/rpm/.rpm.lock
Key import failed
GPG check FAILED로그를 기준으로 정리하면 다음과 같다.
- test는 배포할 때마다 새 서버를 하나 더 띄워서 거기에 먼저 배포하는 방식이었다. (추가 배치 롤링 업데이트)
- 새 서버가 켜지면 두 가지 설치가 거의 동시에 시작된다.
- AWS가 자동으로 하는 서버 관리 프로그램(SSM Agent) 업데이트
- 우리 코드가 하는 압축 모듈(brotli) 설치(
nginx_brotli.sh)
dnf와yum은 패키지 설치 시 rpm 잠금을 사용하기 때문에 동일한 인스턴스에서 설치 작업이 겹치면 충돌할 수 있다.- SSM Agent 업데이트가 rpm 잠금을 사용 중이어서 brotli 모듈 설치가 실패했고, 해당 배포도 중단됐다.
💡 brotli 모듈을 prebuild에서 설치한 이유
EB는 배포 초반에 실행할 스크립트를 넣어 둘 수 있는데, 이걸 prebuild라고 부른다.
nginx 설정 파일이 "brotli 모듈이 설치돼 있다"고 가정하고 불러오기 때문에, nginx 설정이 적용되기 전에 설치가 끝나 있어야 한다. 그래서 prebuild에 추가되어 있다.
초기 판단
- prod는 Rolling 방식으로 기존 인스턴스에 배포했기 때문에 같은 충돌이 발생하지 않을 것으로 판단했다.
- 다만 prod에서도 새 인스턴스가 생성되면 같은 충돌이 발생할 가능성은 있었다.
2단계. test를 Rolling으로 전환
일단 test 배포 방식을 prod와 같은 Rolling으로 바꿨다. 새 서버를 띄우지 않으니 설치 충돌은 피할 수 있었다.
또한 당장 테스트 서버에 작업 사항을 배포하여 확인이 필요했기 때문에 추후에 확인하기로 했다.
3~4단계. 또 실패 - 메모리 부족
Rolling으로 바꾼 뒤에도 배포가 또 실패했고, 이번엔 원인이 달랐다.
배포 중 npm install이 강제 종료(Killed)되고 있었다.
// S3 배포 로그 (resources/environments/logs/deployments/{환경ID}/eb-deployment-xxxx.log)
2026/10/22 05:03:11 [INFO] Starting application deployment
2026/10/22 05:03:11 [INFO] Executing prebuild platform hooks...
2026/10/22 05:03:15 [INFO] Ran hook: nginx_brotli.sh ← brotli 설치는 이번엔 정상
2026/10/22 05:03:15 [INFO] Completed prebuild platform hooks
2026/10/22 05:03:16 [INFO] Running npm install...
2026/10/22 05:03:42 [INFO] --- npm install ---
Killed ← 강제 종료
2026/10/22 05:03:42 [ERROR] npm install failed
2026/10/22 05:03:42 [ERROR] Failed application deployment- 서버 로그에도 "메모리가 부족해서 npm install을 강제 종료했다"는 기록이 남아 있었다. npm install이 약 430MB를 쓰고 있었다.
- 서버는 **t4g.micro(메모리 1GB)**이다.
- Rolling으로 바꾸면서, 이제는 사이트가 돌고 있는 서버에서 그대로
npm install을 한다. - 1GB 안에서 실행 중인 사이트와
npm install이 메모리를 나눠 쓰다가 부족해졌다. - 메모리가 부족할 때 사용할 스왑 공간도 설정되지 않았다.
- 바로 다시 배포하면 성공했다. 두 번 모두 1~5분 안에 재배포가 성공했다.
- 코드 변경 때문에 생긴 문제는 아니었다.
배포 방식과의 관계
- 추가 배치 롤링: 아무것도 돌지 않는 새 서버에서 npm install을 해서 메모리가 넉넉했다(대신 설치 충돌이 있었다).
- 롤링: 사이트가 돌고 있는 서버에서 npm install을 해서, 1GB를 나눠 써야 했다.
Rolling으로 변경한 뒤에는 기존 인스턴스에서 의존성을 설치하면서 메모리 부족 문제가 발생했다.
💡 스왑(swap)
스왑은 메모리가 부족할 때 디스크 공간을 임시로 사용하는 기능이다.
메모리 사용량이 일시적으로 늘어날 때 도움이 될 수 있지만 성능이 저하될 수 있고, OOM을 반드시 방지하는 것은 아니다.
5단계. prod 배포도 실패
prod도 같은 1GB 서버였고 배포 방식도 같았다. 결국 prod에서도 같은 이유로 실패했다.
- 14:34 ~ 14:37: 배포 중 npm install 강제 종료(같은 메모리 부족)
- 14:40 ~ 14:42: 사이트 502 / 응답 없음
- 14:42: 재배포 성공으로 복구
6단계. 서버 사양 상향
일단 메모리부터 늘렸다.
- 서버 종류: t4g.small, t4g.medium(메모리 2GB 이상)
- 최소 서버 수: 2대
7단계. prod 배포 방식을 바꾸려다 1단계가 재현됐다
서버가 2대가 됐으니 prod 배포 방식을 추가 배치 롤링으로 바꿨다(15:03 ~ 15:07).
그런데 15:13 배포에서 새로 뜬 서버가 1단계와 똑같은 설치 충돌로 실패했다. (예견된 일 ..)
테스트 서버 배포 때 확인한 rpm 잠금 충돌이 prod에서도 재현됐다.
우선 상용 환경의 배포 방식을 Rolling으로 되돌리고 원인을 추가로 확인했다.
brotli 설치 필요성 검토
처음 확인한 응답 헤더에는 gzip이 적용돼 있었다. brotli 모듈이 실제로 사용되고 있는지 확인했다.
💡 HTTP 응답 압축
HTML 응답에는 gzip 또는 brotli(br) 압축을 적용할 수 있다. brotli는 응답 내용에 따라 gzip보다 더 높은 압축률을 보일 수 있다.
요청의Accept-Encoding은 브라우저가 지원하는 압축 방식을, 응답의Content-Encoding은 실제 적용된 압축 방식을 나타낸다.
💡 Next.js와 Astro의 응답 압축 차이
Next.js는 기본으로 알아서 gzip 압축을 해서 보낸다.
Astro 서버는 HTML을 압축하지 않고 보낸다. 그래서 앞단의 nginx에서 압축을 켜야 했다.
nginx에서 gzip은 설정만 켜면 되지만, brotli는 따로 설치해야 한다.
1단계와 7단계에서 충돌한 게 바로 이 설치였다.
💡 CodeBuild와 EB의 brotli 용도
프로젝트에 brotli 설치가 두 군데 있다.
• 빌드 서버(CodeBuild): JS/CSS/SVG 파일을 미리 압축해서 S3에 올리는 데 쓴다.
• EB 서버: 요청이 올 때마다 HTML을 압축하는 데 쓴다. 문제가 된 건 이쪽.
서로 관계가 없어서, 한쪽을 빼도 다른 쪽에는 영향이 없다.
측정 경로 확인
https://unpa.me/category의 응답을 확인하니 brotli가 적용돼 있었다.
요청 헤더의 accept-encoding: 브라우저가 br을 풀 수 있다고 알린다.
응답 헤더의 content-encoding: br: 서버가 brotli로 압축해서 보냈다.
Size 열에 마우스를 올리면 실제로 받은 크기와 압축을 푼 원래 크기를 함께 볼 수 있다.
측정 결과 (unpa.me/category)
| 압축 방식 | 받은 크기 |
|---|---|
| 압축 안 함 | 50,652 B |
| gzip | 7,695 B |
| brotli | 6,966 B |
- gzip만 써도 85% 가까이 줄어든다. brotli와 gzip의 차이는 **약 730 B(1KB도 안 됨)**이다.
- 게다가 CloudFront가 HTML을 10분 동안 저장해 두고 그대로 내보내기 때문에, 서버가 실제로 압축하는 횟수도 많지 않다.
결론: brotli를 빼고 gzip만 남긴다
측정한 페이지에서 brotli와 gzip의 응답 크기 차이는 1KB 미만이었다. 별도 모듈 설치로 인한 배포 실패 가능성을 고려해 EB에서 brotli를 제거하기로 했다.
- brotli 설치 스크립트(
nginx_brotli.sh) 삭제 - nginx 설정에서 brotli 관련 부분 삭제(gzip은 그대로)
배포 후에는 이렇게 바뀐다.
- Astro 페이지의 HTML은 gzip으로 나간다. 배포 직후 최대 10분 동안은 CloudFront에 저장돼 있던 brotli 응답이 나갈 수 있다.
- JS/CSS 파일은 빌드할 때 미리 압축해 둔 brotli 그대로 나간다.
- Next.js 페이지는 바뀌지 않는다.
검토했지만 쓰지 않은 방법
- brotli 설치 재시도: 설치가 실패하면 잠시 기다렸다가 다시 시도하는 방법이다. 실패 가능성은 줄지만, 배포할 때마다 설치하는 구조는 그대로 남는다.