Coding

🔥 CI/CD 파이프라인 오류? 포기하지 마세요! Database Migration부터 Health Check까지, 끈질긴 디버깅으로 성공하기 (feat. Docker & Sequelize)

_Woo_ 2025. 5. 26. 15:36

들어가며

개발 프로젝트에서 CI/CD(지속적 통합/지속적 배포) 파이프라인은 정말 중요하죠! 코드가 변경될 때마다 자동으로 빌드, 테스트, 배포 과정을 거쳐 안정적인 서비스를 유지할 수 있도록 도와줍니다. 하지만 이 파이프라인이 제대로 작동하지 않을 때의 답답함이란... 겪어보지 않은 사람은 모를 거예요.

최근 저도 GitHub Actions를 활용한 백엔드 애플리케이션의 CI/CD 파이프라인 구축 중, 데이터베이스 마이그레이션과 애플리케이션 헬스 체크 단계에서 예상치 못한 오류에 부딪혔습니다. 하지만 포기하지 않고 끈질기게 파고들어 결국 성공할 수 있었죠!

이 글에서는 그 과정을 자세히 공유하며, 혹시 같은 문제로 고통받는 분들께 작은 도움이 되고자 합니다. 초보자분들도 쉽게 이해할 수 있도록 최대한 쉽게 설명해 드릴게요!

🚨 첫 번째 난관: db:create 실패! "Unknown argument: database" 오류

저희 CI 파이프라인은 애플리케이션 배포 전에 기존 데이터베이스를 초기화하고(db:drop), 새로 생성한 후(db:create), 마이그레이션을 실행하는(db:migrate) 스크립트를 사용했습니다.

하지만 첫 번째 실행부터 db:create 단계에서 다음과 같은 오류를 뱉어냈습니다.

Unknown argument: database
❌ Failed to create database

 

💡 문제 진단: Sequelize CLI는 Node.js 환경에서 데이터베이스 작업을 돕는 강력한 도구입니다. 이 오류 메시지는 sequelize-cli db:create 명령어가 --database라는 인자를 이해하지 못한다는 뜻이었습니다. 일반적으로 db:create는 데이터베이스 연결 URL(DATABASE_URL)에 포함된 데이터베이스 이름을 파싱하여 사용하거나, 별도의 설정 파일을 참조합니다. 저희는 이미 DATABASE_URL을 통해 필요한 모든 연결 정보를 넘겨주고 있었습니다.

 

✅ 해결책: sequelize-cli db:create 호출 시 불필요했던 --database "$DB_NAME" 부분을 제거했습니다. DATABASE_URL에 데이터베이스 이름이 포함되어 있으므로, Sequelize CLI가 자동으로 이를 인식하도록 맡기는 거죠.

# 이전: npx sequelize-cli db:create --url "$DATABASE_URL" --database "$DB_NAME"
# 수정:
npx sequelize-cli db:create --url "$DATABASE_URL" || {
  echo "❌ Failed to create database"
  exit 1
}

 

이 수정 후, db:create는 정상적으로 동작했습니다! 이제 데이터베이스는 성공적으로 생성되고, 마이그레이션까지 잘 진행되는 것처럼 보였습니다.

 

⚠️ 두 번째 난관: 서버는 시작했는데 Health Check가 실패해요! (feat. 포트 불일치)

 

데이터베이스 관련 문제는 해결된 것 같아 한숨 돌렸는데, 이번에는 애플리케이션 배포 및 헬스 체크 단계에서 파이프라인이 멈췄습니다.

로그를 보니, Docker 컨테이너에서 애플리케이션은 분명히 Server listening on port 4000이라고 출력하며 정상적으로 시작했습니다. 데이터베이스 연결도 ✅ Database connection established. 메시지로 확인되었습니다.

하지만 다음 라인의 curl 헬스 체크 명령은 계속 실패했습니다.

 
Server listening on port 4000
...
curl -s http://localhost:3000/health || { ... }
Error: Process completed with exit code 1.

 

💡 문제 진단: "서버가 4000번 포트에서 잘 돌고 있는데, 왜 curl은 3000번 포트를 호출하고 있지?" 라는 의문이 들었습니다. 애플리케이션은 4000번 포트에서 실행 중인데, 헬스 체크는 3000번 포트를 바라보고 있었던 거죠!

 

✅ 해결책: curl 명령이 애플리케이션이 실제로 리스닝하는 포트인 4000번을 바라보도록 수정했습니다.

 
# 이전: curl -s http://localhost:3000/health || { ... }
# 수정:
curl -s http://localhost:4000/health || {
  docker logs teamitaka-prod
  exit 1
}

 

이 수정 후, curl은 올바른 포트를 호출하게 되었습니다. 이제 성공할 거라고 확신했죠!

😨 세 번째 난관: 포트도 맞는데 왜 계속 실패? (진짜 범인: Docker 포트 매핑 누락)

curl 명령의 포트를 4000번으로 수정한 후 다시 파이프라인을 실행했습니다. 이번에는 curl -s http://localhost:4000/health라고 로그에 정확히 찍혔습니다. 하지만 여전히 헬스 체크가 실패하고, 애플리케이션 로그에는 /health 엔드포인트에 대한 요청 기록(예: Received /health request)이 전혀 보이지 않았습니다.

 

"혹시 헬스체크 엔드포인트 코드가 없나?" 라는 의문이 들었지만, 확인 결과 sequelize.authenticate()를 포함한 /health 엔드포인트가 이미 잘 구현되어 있었습니다.

 

💡 문제 진단 (가장 중요했던 순간!): curl은 GitHub Actions 러너(호스트 머신)에서 실행됩니다. 반면 teamitaka-prod 애플리케이션은 Docker 컨테이너 내부에서 실행됩니다. 컨테이너 내부의 포트(4000번)는 EXPOSE 4000처럼 Dockerfile에 선언될 수 있지만, 이것만으로는 호스트 머신에서 컨테이너 내부의 포트에 접근할 수 없습니다.

 

호스트에서 컨테이너의 포트에 접근하려면 docker run 명령에 -p 옵션을 사용하여 포트를 명시적으로 노출(매핑)해야 합니다. 저희 CI 파이프라인의 docker run 명령에는 이 -p 옵션이 누락되어 있었던 것입니다! 따라서 curl이 localhost:4000으로 요청을 보내도, 호스트 머신에서는 해당 포트에 연결된 컨테이너가 없었기에 계속 연결 실패가 발생했던 것입니다.

 

✅ 해결책: docker run 명령에 -p 4000:4000 (호스트 포트:컨테이너 포트) 옵션을 추가하여, 컨테이너의 4000번 포트를 호스트의 4000번 포트에 매핑했습니다. 또한, 애플리케이션 초기화에 충분한 시간을 주기 위해 sleep 시간을 20초에서 30초로 늘리고, curl이 HTTP 오류(4xx, 5xx)에도 실패하도록 --fail 옵션을 추가했습니다.

- name: Deploy and Test Application
  run: |
    docker run -d --network cloudsql-net \
      --name teamitaka-prod \
      -p 4000:4000 \ # <--- 이 줄을 추가했습니다!
      -e DATABASE_URL="${{ env.DATABASE_URL }}" \
      -e NODE_ENV=production \
      -e JWT_SECRET="${{ secrets.JWT_SECRET }}" \
      teamitaka-app npm run start
    sleep 30 # 시간 확보
    curl -s --fail http://localhost:4000/health || { # --fail 옵션 추가
      docker logs teamitaka-prod
      exit 1
    }

 

😭 사소하지만 치명적인 실수: - p 오타

위 수정을 적용하고 파이프라인을 재실행했을 때, 또다시 오류가 발생했습니다. 이번에는 docker: invalid reference format 이라는 오류였죠.

 

💡 문제 진단: 로그를 자세히 살펴보니, 제가 워크플로우 파일을 수정할 때 -p 4000:4000이라고 입력해야 할 것을 - p 4000:4000 처럼 -와 p 사이에 공백을 하나 더 넣은 치명적인 오타였습니다! Docker는 이 공백 때문에 -를 독립적인 옵션으로, p를 알 수 없는 인자로 해석하여 오류를 냈던 것입니다.

 

✅ 해결책: -p 옵션의 공백을 제거하여 올바른 문법으로 수정했습니다.

 

# 이전: - p 4000:4000 \
# 수정:
-p 4000:4000 \ # <--- 공백 제거!

 

🎉 마침내 성공!

 

사소한 오타까지 해결하고 파이프라인을 다시 실행하자, 드디어 모든 단계가 초록불을 띄우며 성공적으로 완료되었습니다! curl 명령 아래에 헬스 체크 응답 {"status":"OK","database":"connected"}이 보였을 때의 희열은 말로 표현할 수 없었습니다.

 

주요 학습 내용 및 교훈

 

이번 CI/CD 디버깅 여정을 통해 다음과 같은 중요한 교훈을 얻었습니다.

  1. 에러 메시지를 꼼꼼히 읽기: 대부분의 경우 에러 메시지 안에 답이 있습니다. "Unknown argument", "invalid reference format" 등은 명확한 힌트였죠.
  2. Docker 네트워킹과 포트 매핑의 이해: 컨테이너 내부의 포트와 호스트의 포트가 어떻게 연결되는지(-p 옵션)를 정확히 이해하는 것이 중요합니다.
  3. 애플리케이션 헬스 체크 엔드포인트 구현: CI/CD 파이프라인의 견고함을 위해 /health와 같이 서버 상태를 정확히 반영하는 엔드포인트를 구현하는 것이 필수적입니다. 데이터베이스 연결 상태까지 확인하면 더욱 좋습니다.
  4. 시간과의 싸움 (sleep 시간): 서버가 완전히 초기화되고 요청을 처리할 준비가 되기까지 예상보다 시간이 더 걸릴 수 있습니다. sleep 시간은 단순히 "서버가 떴다"가 아니라 "서버가 정상적으로 요청을 처리할 수 있다"는 지점까지 기다려야 합니다.
  5. 작은 문법 오류의 치명성: -와 p 사이의 공백처럼 사소한 오타 하나가 전체 파이프라인을 멈출 수 있습니다. 복사/붙여넣기 시에도 항상 주의하고, 눈으로 직접 확인하는 습관이 중요합니다.
  6. 체계적인 디버깅: 여러 문제가 얽혀 있을 때는 한 번에 하나씩 원인을 제거해나가는 체계적인 접근 방식이 중요합니다.

CI/CD 파이프라인은 복잡하지만, 이러한 과정을 통해 배우고 성장할 수 있는 소중한 기회가 됩니다. 오류에 부딪히더라도 포기하지 않고 끈질기게 디버깅하면 반드시 성공할 수 있습니다!


이 글은 Gemini가 작성했습니다.