Cloud Run + Cloud Functions + WebSocket 배포 오류 해결기
Cloud Run + Cloud Functions WebSocket/REST API 통합 배포 삽질기 (with GitHub Actions) - 문제 분석과 해결 과정
안녕하세요, 이번 글에서는 제가 최근 진행한 Cloud Run 기반 WebSocket 서비스와 Cloud Functions 기반 REST API 서비스의 통합 배포 과정에서 발생한 문제 상황과 이를 단계별로 해결해 나간 과정을 상세하게 공유해 보려고 합니다.
이번 작업은 단순 배포 자동화 수준을 넘어서, 실제로 서비스 구조 자체의 안정성/일관성 확보와 배포 프로세스 개선까지 이어진 경험이었기에, 앞으로 비슷한 시도를 하실 분들께 도움이 되었으면 합니다.
본 게시글은 OpenAI GPT 기반 Cloud Assistant가 작성하였습니다.
1️⃣ 프로젝트 구조 및 배포 대상 정리
먼저, 이번 프로젝트의 기본 구조는 다음과 같았습니다:
- 📦 Backend: Monorepo 구조 (NestJS 기반)
- REST API 서비스 → Cloud Functions (2nd Gen)
- WebSocket 서비스 → Cloud Run (Docker 기반)
- 📦 Frontend: React 기반 (Vercel 배포 예정, 이번 정리에서는 연동 제외)
- CI/CD: GitHub Actions 사용
- 초기: deploy-functions.yml (REST API), deploy-websocket.yml (WebSocket) 별도 관리
- 최종: deploy-backend.yml 통합 관리
2️⃣ 문제 발생 상황
초기에는 REST API는 배포가 안정적으로 계속 성공하고 있었고, WebSocket 서비스는 계속 실패하는 상태였습니다. 그런데 어느 순간부터 WebSocket은 성공하고 REST API는 409 오류로 배포 실패가 발생하는 이상한 현상이 반복되었습니다.
구체적인 증상은 다음과 같았습니다:
- 🔸 Cloud Run WebSocket 배포 시
- Revision is not ready
- HealthCheckContainerError
- "PORT=8080 not listening" 오류
- DB SSL 오류 (server does not support SSL connections)
- 🔸 Cloud Functions REST API 배포 시
- 초기에는 정상 배포 → 이후 409 Conflict 오류 발생
- ERROR: (gcloud.functions.deploy) ResponseError: status=[409], code=[Ok], message=[unable to queue the operation]
문제는 이 현상이 코드 변경과 직접적인 연관이 없는 상태에서도 발생했다는 점이었습니다. 즉, 외부적인 배포 프로세스/설정 문제 가능성이 높다고 판단했습니다.
3️⃣ 원인 분석 (Chain-of-Thought 방식)
3.1 DB_HOST 설정 불일치
처음 WebSocket 배포 시 DB_HOST=127.0.0.1
로 설정했는데, Cloud Run에서는 Cloud SQL Auth Proxy를 사용하여 Unix Socket 경로를 사용해야 정상 동작합니다:
DB_HOST=/cloudsql/${GCP_SQL_CONNECTION_NAME}
이걸 수정하지 않으면 NestJS에서 TypeORM 초기화 시점에 DB 연결 오류가 발생 → App crash → HealthCheck 실패 → Revision 실패
3.2 DB_SSL_ENABLED 설정 오류
초기에는 DB_SSL_ENABLED=true
로 되어 있었는데, Cloud Run의 Cloud SQL Proxy는 이미 내부적으로 암호화된 Unix Socket을 사용합니다. SSL 설정이 true이면 Postgres가 이를 거부하며 다음 오류 발생:
"The server does not support SSL connections"
→ DB 연결 실패 → Retry 무한 반복 → HealthCheck 실패 → CrashLoop 발생
3.3 GitHub Actions Workflow 병렬 실행 문제
처음에는 REST API와 WebSocket 배포를 서로 다른 Workflow (deploy-functions.yml, deploy-websocket.yml)에서 병렬로 실행하도록 구성했는데, 이로 인해 예상치 못한 충돌이 발생했습니다:
- 두 Workflow가 거의 동시에 gcloud CLI를 사용하여 동일한 프로젝트 리소스 (IAM, Cloud SQL instance 등)에 접근
- 특히 REST API는 Cloud Functions에서 source packaging 과정이 비교적 느리게 진행되어, WebSocket Workflow와 timing 충돌 발생
- 결과적으로 Cloud Functions 배포 시 409 Conflict 오류 발생
3.4 WebSocket 서비스 특성 (min-instances 필요)
WebSocket 서비스는 REST API와 달리 실시간 연결 유지가 중요한 서비스입니다. Cold Start가 발생하면 클라이언트가 연결 실패 가능성이 높아지므로 Cloud Run에서 다음 옵션 필요:
--min-instances=1
초기에는 이 옵션이 누락되어 있었고, 서비스가 idle 상태에서 Cold Start 발생 → HealthCheck 실패가 더 자주 발생하는 패턴이었습니다.
4️⃣ 최종 해결 방법
4.1 WebSocket deploy 수정
- DB_HOST →
/cloudsql/${GCP_SQL_CONNECTION_NAME}
로 수정 - DB_SSL_ENABLED → false 설정
- min-instances=1 추가
4.2 GitHub Actions Workflow 통합
- REST API와 WebSocket Workflow를
deploy-backend.yml
로 통합 - REST API → WebSocket 순서로 순차 실행 (needs 키워드 활용 가능)
- 이렇게 하면 동일 리소스에 대한 충돌 없이 안정적인 배포 가능
4.3 최종 배포 결과
- WebSocket 서비스 Cloud Run 정상 배포 성공 (Revision ready)
- REST API 서비스 Cloud Functions 정상 배포 성공 (409 오류 사라짐)
- GitHub Actions Workflow 전체 100% 성공
5️⃣ 느낀 점과 배운 점
- Cloud Run과 Cloud Functions는 배포 방식, 리소스 사용 방식이 다르기 때문에 동일한 Workflow를 그대로 복제해 사용하면 문제가 발생할 수 있음
- Cloud SQL 연결 시 반드시 DB_HOST, DB_SSL_ENABLED 설정을 서비스 환경에 맞게 설정해야 함 (Cloud Run vs Cloud Functions 다름)
- GitHub Actions는 기본적으로 병렬 실행이기 때문에 리소스 충돌 가능성을 반드시 고려해야 함
- WebSocket 서비스는 실시간 특성상 min-instances 설정이 필요 (Cold Start 방지)
- 단순히 오류 메시지에만 의존하지 않고 구조적으로 원인을 추적하는 Chain-of-Thought 접근이 매우 효과적
6️⃣ 결론
이번 작업은 단순한 CI/CD 구축을 넘어서, 배포 구조의 일관성 확보와 서비스 안정성까지 고려한 구조 개선 경험이었습니다.
처음에는 오류가 왜 발생하는지 파악하는 것도 어려웠지만, Cloud Assistant와 함께 Chain-of-Thought 방식으로 단계별로 원인을 좁혀가면서 해결책을 찾아갈 수 있었습니다.
이번 글이 비슷한 아키텍처 (Cloud Run + Cloud Functions + WebSocket + GitHub Actions)를 구성하는 분들께 도움이 되길 바랍니다!
본 게시글은 OpenAI GPT 기반 Cloud Assistant가 작성하였습니다.