Big Project · AI's EYE 41

배포 'green' 신호, 무엇을 믿고 무엇을 의심할 것인가?

container가 running이고 `/health`가 200이면 최신 schema와 안전한 credential로 배포됐다고 믿기 쉽다. 어떤 증거가 빠져 있는지 알아차리기 어렵다.

근거 · 프로젝트 문서 · 코드 · 테스트

1장: 안전한 배포, credential부터 다시 묻다

솔라는 방금 슬랙 채널에 올라온 한 줄짜리 시스템 개선 공지를 멍하니 바라보고 있었다.

[Infra] GCP 배포 워크플로우에 OIDC/WIF를 적용하여 장기 인증키(JSON) 사용을 제거했습니다.

분명 좋은 소식이었다. ‘장기 인증키’ 같은 고정된 비밀값이 사라지고 더 안전한 방식이 도입되었다는 뜻일 테니. 하지만 솔라의 마음 한구석에는 명확히 설명되지 않는 찝찝함이 남았다.

‘그래서 이게 내가 하는 배포랑 무슨 상관이지? 내 코드가 테스트를 통과하고, 컨테이너가 돌고, 헬스 체크에서 초록불이 뜨면 성공한 배포 아닌가? 인증 방식이 바뀌었다는 건 그냥 인프라팀의 일인 것 같은데…’

솔라는 자신의 배포 파이프라인 화면을 띄웠다. 최근에 올린 기능이 성공적으로 배포되어 ‘Active’ 상태를 나타내는 초록색 점이 선명했다. 저 초록색 점이 알려주는 ‘성공’의 의미에 방금 본 공지사항이 포함되어 있는 걸까? 솔라는 고개를 갸웃거리며 혼잣말을 중얼거렸다.

“배포가 안전해졌다는 건 알겠는데, 뭐가 어떻게 안전해졌다는 건지 손에 잡히질 않네.”

“뭐가 손에 안 잡혀?”

옆에서 조용히 자신의 모니터를 보던 루나가 고개를 돌려 물었다. 루나의 차분한 목소리에 솔라는 기다렸다는 듯이 자신의 화면을 가리켰다.

“언니, 이것 봐. 방금 인프라팀에서 공지가 올라왔는데, 이제 배포할 때 장기 인증키를 안 쓴대. OIDC니 WIF니 하는 걸로 바꿨다는데… 이게 개발자인 나한테도 중요한 변화인 걸까? 어차피 배포 시스템에 직접 접속하는 것도 아니고, 파이프라인이 알아서 해주잖아. 그냥 운영팀의 보안 강화 정도로 생각하면 되는 거 아니야?”

솔라의 말에는 ‘내 책임 범위 밖의 일’이라는 뉘앙스가 섞여 있었다. 배포 성공 여부는 빌드, 테스트, 실행 상태로 판단하는 것이고, 그 과정의 인증 방식까지 개발자가 속속들이 알아야 할 필요는 없다는 생각이었다.

루나는 솔라의 모니터에 떠 있는 초록색 점과 공지사항을 잠시 번갈아 보더니, 의자 등받이에 몸을 기댔다.

“음, 그렇게 볼 수도 있겠네. 솔라, 우리 예전 배포 방식이랑 지금 방식을 아주 간단한 그림으로 한번 그려볼까?”

루나는 책상 위 포스트잇 한 장을 뜯어내 펜을 들었다. 거창한 아키텍처 다이어그램이 아니었다. 루나는 포스트잇을 반으로 나눠 왼쪽에는 ‘이전 방식’, 오른쪽에는 ‘현재 방식’이라고 적었다.

“이전 방식은 이거랑 비슷해.”

루나는 왼쪽 칸에 작은 열쇠 그림 하나를 그리고, 그 옆에 ‘영구적인 만능 열쇠’라고 썼다.

“우리 서버에 누가 들어갈 수 있는지 증명하는 서비스 계정 JSON 키 파일이야. 이 열쇠만 있으면 누구든, 언제든 우리 GCP 프로젝트의 자원에 접근할 수 있어. 배포 스크립트가 실행될 때마다 이 만-능-열쇠를 잠시 빌려 쓰는 거지.”

“그 열쇠가 유출되면 큰일 나겠네.” 솔라가 당연하다는 듯 말했다.

“맞아. 그래서 아주 안전한 금고에 보관하고, 꼭 필요한 사람만 접근하게 하고, 주기적으로 열쇠를 바꾸는 등 노력을 했지. 하지만 근본적으로 ‘언제든 사용 가능한 만능 열쇠’가 존재한다는 사실 자체는 변하지 않았어.”

루나는 이어서 오른쪽 칸으로 펜을 옮겼다. 열쇠 대신, 짧은 점선으로 된 화살표를 그렸다.

“그럼 지금 방식은? ‘만능 열쇠’ 자체가 없어. 대신 배포가 시작되는 바로 그 순간, 이런 일이 일어나.”

루나는 화살표의 시작점에 ‘GitHub Action’이라고 적었다.

“GitHub Action이 GCP에게 말을 거는 거야. ‘안녕? 나는 A 프로젝트의 main 브랜치에서 실행된 배포 작업이야. 내 말이 맞는지 GitHub한테 직접 확인해 봐.’ 그럼 GCP는 GitHub의 OIDC라는 신원 확인 창구를 통해 ‘어, 방금 그 요청 진짜네’ 하고 확인해줘.”

“그럼 GCP가 ‘오케이, 너는 믿을 수 있군’ 하면서 뭘 해주는데?”

“바로 이거.” 루나는 점선 화살표 끝에 아주 작은 티켓 그림을 그리고 ‘10분짜리 임시 출입증’이라고 썼다.

“GCP는 딱 이번 배포에만 필요한, 그것도 아주 짧은 시간 동안만 유효한 임시 토큰을 발급해 줘. 우리 배포 스크립트는 이 ‘10분짜리 출입증’을 가지고 컨테이너를 배포하고 필요한 작업을 수행하는 거야. 작업이 끝나면? 이 출입증은 그냥 휴지조각이 돼. 다음 배포 땐 또 새로운 출입증을 발급받아야 하고.”

솔라는 두 그림을 가만히 들여다보았다. 왼쪽의 ‘만능 열쇠’와 오른쪽의 ‘10분짜리 임시 출입증’. 차이는 명확했다. 하나는 도둑맞으면 모든 문이 열리는 위험을 항상 안고 있는 존재. 다른 하나는 목적을 달성하는 순간 사라져 버리는, 도둑맞을 ‘틈’ 자체가 거의 없는 존재.

“아…”

솔라의 입에서 나지막한 탄성이 터져 나왔다.

“그러니까… 이전에는 ‘만능 열쇠’가 도난당하지 않도록 지키는 게 중요했다면, 지금은 애초에 훔쳐갈 ‘만능 열쇠’ 자체가 없는 거구나. 배포할 때마다 필요한 만큼의 권한만 가진 출입증이 잠깐 생겼다가 사라지는 거고.”

“바로 그거야.”

솔라는 다시 자신의 모니터에 떠 있는 배포 성공의 초록색 점을 바라봤다. 이제는 그 점이 다르게 보였다. 저 초록색 점은 단순히 ‘코드가 서버에서 실행 중’이라는 사실 외에, ‘만능 열쇠 없이, 임시 출입증만으로 안전하게 문을 통과했음’이라는 사실까지 포함하고 있었다. Credential 보안이 더 이상 운영팀만의 관심사가 아니라, 내 배포 품질의 한 축이라는 사실이 선명하게 다가왔다.

자신이 만든 ‘토큰 수명 트레이서’라는 이름의 투명한 돋보기를 들고 배포 과정을 다시 들여다보는 기분이었다. 전에는 보이지 않던 토큰의 생성, 사용, 그리고 소멸 과정이 눈앞에 그려졌다.

“이제 알겠어. 배포가 ‘안전하다’는 말의 의미를 내가 너무 좁게 보고 있었네. 인증 방식의 안전성도 내가 확인해야 할 배포의 일부였어.”

안개처럼 막연했던 불안감의 정체가 드러나자 오히려 마음이 편안해졌다. 이제 배포의 ‘인증’ 영역에 대해서는 확신을 가질 수 있었다. 하지만 하나의 의문이 해소되자, 곧바로 새로운 질문이 고개를 들었다.

“언니, 그럼 이제 배포 ‘인증’은 정말 안전해진 거네. 그런데… 이렇게 안전한 문을 통과해서 들어온 배포 패키지 자체가 올바른지는 어떻게 확신하지? 배포 스크립트 로그에 ‘DB 마이그레이션 성공’이라고 뜨고, 헬스 체크 API가 ‘OK’라고 응답하면… 정말 내 코드가 기대하는 최신 데이터베이스 스키마가 적용됐다고 믿어도 되는 걸까?“

2장: Alembic migration 성공, 그래서 스키마는?

솔라는 이전 배포의 파이프라인 로그를 화면에 띄워놓고 있었다. 지난번 루나와의 대화 이후, 배포 성공을 알리는 ‘초록불’의 의미를 다시 생각하게 되었기 때문이다. 이제 ‘인증’이라는 관문이 얼마나 안전하게 지켜지고 있는지는 알겠다. 하지만 그 문을 통과한 배포물 자체의 정합성은 또 다른 문제였다.

솔라의 눈앞에는 성공적으로 완료된 배포 단계들이 초록색 체크 표시와 함께 나열되어 있었다.

▶️ Running step: DB_BACKUP... [SUCCESS]
▶️ Running step: ALEMBIC_UPGRADE_HEAD... [SUCCESS]
▶️ Running step: HEALTH_CHECKS... [SUCCESS]
   - Checking local Dashboard URL... [200 OK]
   - Checking public API URL... [200 OK]
▶️ Deployment successful!

이보다 더 명쾌한 성공의 증거가 있을까? 데이터베이스는 안전하게 백업되었고, Alembic을 이용한 스키마 마이그레이션은 최신 버전(head)까지 성공적으로 적용되었다. 배포된 서비스의 헬스 체크 엔드포인트도 모두 200 OK를 반환했다. 이 정도면 배포된 코드가 기대하는 최신 데이터베이스 스키마 위에서 완벽하게 동작하고 있다고 믿어 의심치 않았던 것이 솔라의 평소 생각이었다.

하지만 이제는 그 믿음에 미세한 균열이 생겼다. 솔라는 이 로그 화면을 그대로 둔 채 옆자리 루나에게 고개를 돌렸다.

“언니, 지난번 QA에서 발견됐다는 이슈 말이야. 배포 스크립트 로그에는 분명히 이렇게 마이그레이션이 성공했다고 떴는데, 어떻게 DB 스키마가 꼬일 수 있었던 거야?”

솔라의 질문은 더 이상 ‘내 책임 밖의 일’이 아니었다. ‘Alembic 마이그레이션 성공’이라는 로그가 보증하는 것의 범위를 정확히 알고 싶다는, 개발자로서의 순수한 호기심이자 불안감이었다.

루나는 솔라의 화면에 떠 있는 로그를 잠시 보더니, 자신의 모니터에서 터미널 두 개를 나란히 띄웠다. 하나는 코드 저장소의 특정 폴더를, 다른 하나는 데이터베이스 클라이언트를 보여주고 있었다.

“이 로그가 말해주는 건 ‘Alembic upgrade head 라는 명령어가 에러 없이 실행을 마쳤다’는 사실이야. 그게 ‘DB 스키마가 코드와 완벽히 동기화되었다’는 뜻과 항상 같지는 않거든.”

루나는 말없이 키보드를 몇 번 두드렸다. 첫 번째 터미널에는 코드베이스에 존재하는 최신 마이그레이션 스크립트 파일의 해시값이 나타났다.

# In Code Repository (main branch)
$ ls migrations/versions/ | tail -n 1
# Output: f00d123_add_user_profile_table.py

“우리 코드의 ‘기대’는 이거야. 데이터베이스 스키마 버전이 f00d123이기를 바라고 있지.”

이어서 루나는 두 번째 터미널인 데이터베이스 클라이언트에 쿼리를 날렸다. Alembic이 현재 스키마 버전을 기록하는 테이블을 조회하는 명령어였다.

-- In Production Database
SELECT version_num FROM alembic_version;

솔라는 숨을 죽이고 결과를 기다렸다. 당연히 f00d123이 나와야 했다. 하지만 화면에 나타난 결과는 전혀 예상 밖이었다.

 version_num
-------------
 e1a2b3c
(1 row)

“어? 다르네. 이전 버전인가?”

솔라가 고개를 갸웃거리며 말했다. 하지만 루나는 더 흥미로운 사실을 알려주려는 듯, 첫 번째 터미널에서 다른 명령을 입력했다. Git 로그에서 e1a2b3c라는 리비전을 검색하는 명령어였다.

$ git log --grep "e1a2b3c"
# Output: (No results)

결과는 텅 비어 있었다. 코드 저장소 역사상 e1a2b3c라는 리비전은 존재한 적이 없었다.

“이게 어떻게 된 거야? 데이터베이스는 유령 리비전을 가리키고 있잖아!”

솔라의 목소리가 커졌다. 상황은 생각보다 더 심각했다. 단순히 마이그레이션이 한두 단계 누락된 것이 아니었다. 데이터베이스의 스키마 버전이 코드베이스 그 어디에도 존재하지 않는 ‘유령’ 상태를 가리키고 있었다.

“과거에 어떤 개발자가 자기 브랜치에서만 만든 마이그레이션 파일로 테스트하다가, 배포 프로세스가 꼬이면서 그 리비전 번호만 DB에 남고 실제 스키마 변경과 파일은 사라진 케이스였어. 중요한 건, 그 이후에 실행된 배포 스크립트가 이 이상한 상태를 감지하지 못하고 ‘Alembic upgrade’ 명령을 실행했고, Alembic은 어떤 이유에선지 에러를 내뱉지 않고 조용히 종료됐다는 거야.”

로그 파일의 [SUCCESS]라는 글자가 차갑게 느껴졌다. 그것은 과정의 성공이었지, 결과의 정합성을 보증하는 표시는 아니었다. 더 소름 돋는 사실은, 이 ‘유령 리비전’ 상태에서는 새로 추가하기로 한 user_profile 테이블이 당연히 존재하지 않았다는 점이다.

솔라는 자신이 방금 얻은 깨달음을 정리하기 위해, 머릿속에 새로운 도구를 그려보았다. ‘스키마 버전 대조기’. 배포가 끝나면 파이프라인의 초록불만 볼 게 아니라, 직접 코드 저장소의 최신 리비전과 실제 DB의 리비전을 대조해 보는 검증 장치였다.

“이제 알겠어. 마이그레이션 성공 로그는 그냥 참고 자료일 뿐이네. 진짜 증거는 데이터베이스 안에 있는 거였어. 배포의 검증 책임에는 DB 스키마 리비전을 직접 확인하는 것까지 포함되어야 했던 거야.”

솔라가 스스로 내린 결론에 고개를 끄덕였다. 유령 리비전의 미스터리는 풀렸다. 하지만 또 다른, 더 근본적인 질문이 솔라의 머리를 맴돌기 시작했다.

“잠깐만, 언니. 스키마 버전이 엉망이고 테이블까지 없었는데… 왜 헬스 체크는 OK였던 거지? database=ok라면서. 데이터베이스가 진짜 ‘ok’한 상태가 아니었잖아?“

3장: ‘database=ok’의 함정, 검증 깊이를 파고들다

솔라의 모니터에는 이제 배포 로그 대신, 웹 프레임워크의 소스 코드 파일 하나가 열려 있었다. 파일 이름은 health_check.py. 지난번 루나와 함께 ‘유령 리비전’의 미스터리를 파헤친 이후, 솔라의 머릿속을 떠나지 않던 마지막 질문의 근원지였다.

database=ok.

이 단순한 응답이 어떻게 스키마 버전 불일치와 테이블 누락이라는 심각한 문제를 모두 놓칠 수 있었을까? 솔라는 이 질문에 대한 답이 코드 속에 있을 거라 확신했다. 그녀는 화면을 뚫어져라 쳐다보며, 데이터베이스 상태를 확인하는 로직을 찾고 있었다. 하지만 코드는 놀라울 정도로 간단했고, 바로 그 점이 솔라를 더욱 혼란스럽게 만들었다.

“언니, 이거 좀 봐. 도무지 이해가 안 가.”

결국 솔라는 스스로의 탐색을 멈추고 루나를 불렀다. 그녀의 질문은 이제 막연한 불안감이 아니라, 명확한 대상을 가진 분석 요청에 가까웠다.

“스키마가 엉망이고 테이블이 없었는데도 헬스 체크가 OK였던 이유를 찾고 있었어. 이게 그 코드인데… 여기서 뭘 하는 거지?”

루나는 솔라의 자리로 다가와 화면을 들여다봤다. 솔라가 가리키는 부분은 다음과 같았다.

def check_database_health():
    try:
        # DB 엔진에 연결하여 간단한 쿼리를 실행한다.
        db.engine.execute("SELECT 1")
        return {"database": "ok"}
    except Exception as e:
        logger.error(f"Database health check failed: {e}")
        return {"database": "error"}

“흠, 이 코드가 뭘 하는 것처럼 보여?”

루나는 바로 답을 알려주는 대신, 질문을 되돌려주었다. 솔라는 코드를 다시 한번 천천히 읽어 내려갔다.

“일단… 데이터베이스에 연결해서 SELECT 1이라는 쿼리를 실행하네. 그리고 아무런 에러가 없으면 database=ok를 반환하고, 뭔가 예외가 발생하면 error를 반환하는 구조야.”

“맞아. 그럼 SELECT 1 쿼리는 데이터베이스에게 뭘 하라고 시키는 걸까?”

“그냥… 숫자 1을 선택하라는 거니까… 사실상 아무것도 안 하는 거나 마찬가지 아닌가? 데이터베이스가 살아있는지, 아주 기본적인 명령에 응답하는지만 확인하는 용도 같은데.”

거기까지 말한 솔라는 순간 말을 멈췄다. 머릿속에서 뭔가 ‘아차’ 하는 느낌이 스쳤다. 그녀는 방금 자기가 한 말을 되짚어 보았다.

‘데이터베이스가 살아있는지… 응답하는지만 확인…’

“잠깐만. 이 쿼리는 우리가 지난번에 발견했던 user_profile 테이블이 있든 없든, alembic_version 테이블의 값이 유령이든 아니든 상관없이 항상 성공하겠네? 그냥 데이터베이스 서버와 연결만 되면!”

솔라의 목소리에 깨달음의 흥분이 실렸다. database=ok의 비밀이 풀리는 순간이었다. 그것은 ‘우리 서비스가 사용할 데이터베이스의 상태가 완벽하게 준비되었습니다’라는 뜻이 아니었다. 그저 ‘저기 어딘가에 있는 데이터베이스라는 녀석과 성공적으로 통신했습니다’라는, 지극히 얕은 수준의 신호였을 뿐이다.

루나는 말없이 고개를 끄덕이며, 솔라가 스스로 결론에 도달할 때까지 기다려주었다.

“결국 이것도 똑같았네. 배포 스크립트의 ‘마이그레이션 성공’ 로그처럼, 헬스 체크의 ‘OK’ 신호도 그저 ‘주어진 명령을 에러 없이 수행했다’는 사실만 알려줄 뿐, 그 결과가 우리가 기대한 ‘정합성’을 보증하지는 않았던 거야.”

솔라는 마치 배신당한 기분마저 들었다. 그동안 초록색 OK 사인을 보며 느꼈던 안도감이 얼마나 근거 없는 믿음 위에 서 있었는지 알게 된 것이다. 인증, 마이그레이션, 헬스 체크. 배포의 각 단계에서 ‘성공’이라는 단어는 모두 다른 깊이와 다른 의미를 가지고 있었다.

그녀는 책상 위에 놓여 있던 포스트잇 한 장을 가져와 펜을 들었다. 더 이상 배포 파이프라인의 초록불에 속지 않기 위한, 자기 자신을 위한 검증 장치를 만들어야겠다고 다짐했다.

배포 ‘Green’ 신호 평가 체크리스트

  1. 인증 (Auth): ‘안전한 문’인가? (장기 Secret 없이, 단기 토큰으로?)
  2. 정합성 (Schema): ‘올바른 집’인가? (마이그레이션 로그뿐 아니라, 코드와 DB의 revision이 정말 일치하는가?)
  3. 상태 (Health): ‘집 안의 가구’는 제대로 있는가? (database=ok는 무슨 깊이의 ‘ok’인가? 단순 연결 확인? 아니면 필수 테이블과 로직 검증까지?)

솔라는 자신이 만든 작은 체크리스트를 모니터 옆에 붙였다. 이제 ‘Active’ 상태를 알리는 초록색 점은 솔라에게 더 이상 최종 목적지가 아니었다. 그것은 이 체크리스트를 꺼내 들어야 할 시작 신호가 되었다. 하나의 초록불이 안전, 최신, 정합을 모두 증명해주지 않는다는 사실을, 솔라는 이제 명확하게 이해했다. 배포의 진짜 주인은 파이프라인이 아니라, 이 질문들을 던지는 자기 자신이라는 것도.