CI/CD Practice 10
최종 CI/CD 흐름에서 Source, Build, Deploy, 검증을 한 줄로 읽는 법
마지막에 pipeline이 성공했다고 해도 어디에서 무엇이 성공했는지, 실패하면 어느 단계의 문제인지 판단하기 어렵다.
근거 · CI/CD 교안 p138-p141
1장: 소스 변경 감지: 파이프라인의 시작점
솔라는 노트북 화면에 떠 있는 두 개의 창을 번갈아 보며 입술을 깨물었다. 왼쪽에는 방금 git push 명령이 성공적으로 완료되었음을 알리는 터미널이, 오른쪽에는 몇 번을 새로고침해도 아무런 변화가 없는 웹사이트가 있었다.
“이상하네… 분명히 코드를 수정해서 올렸는데.”
작은 푸념과 함께 솔라의 손가락이 다시 F5 키를 눌렀다. 하지만 돌아온 것은 똑같은 파란색 헤더의 웹페이지뿐이었다. 분명 ‘Blue Header’라고 되어 있는 부분을 ‘Green Header’로 바꾸는 아주 간단한 수정이었는데.
“코드도 올렸고, 파이프라인도 자동으로 돌아갈 텐데 왜 그대로일까. 전체가 한 번에 성공하거나 실패하는 거 아니었나?”
혼잣말은 점점 더 커졌다. 마치 거대한 기계에 재료를 넣었는데, 아무런 제품도 나오지 않고 기계는 묵묵부답인 상황 같았다. 솔라는 파이프라인 전체가 하나의 거대한 블랙박스처럼 느껴졌다.
그때 방에 들어온 루나가 솔라의 노트북 화면을 슬쩍 보더니, 꼼짝 않는 웹페이지와 덩그러니 남은 터미널 창을 보고는 조용히 물었다.
“아직이야?”
“언니, 이것 좀 봐. 분명히 코드를 push했거든? 그런데 웹사이트는 바뀔 생각을 안 해. 파이프라인이 내 코드를 못 본 척하는 것 같아.”
솔라는 거의 하소연하듯 말했다. 그녀의 말에는 ‘나는 내 할 일을 다했다’는 억울함이 묻어 있었다.
루나는 의자를 끌어와 솔라 옆에 앉았다. 그리고는 웹사이트 창이 아니라, 솔라가 방금 닫으려던 터미널 창을 가리켰다.
“솔라, 네가 코드를 push한 건 사실이지. 그런데 그게 ‘파이프라인이 시작되었다’는 뜻과 같을까?”
“당연히 같은 거 아니야? Git에 코드가 올라가면 파이프라인이 자동으로 그걸 감지해서 시작해야지. Source 단계가 바로 그 역할이잖아. ‘Source 단계는 Git commit/push로 새 revision을 감지한다.’ 이렇게 배웠는데.”
솔라는 자기가 아는 문장을 그대로 내뱉었다. 그녀에게 ‘코드를 올리는 행위’와 ‘파이프라인의 시작’은 분리할 수 없는 하나의 사건이었다.
루나는 고개를 끄덕이면서도 시선은 다른 곳을 향하고 있었다.
“맞아, 그 문장은 틀리지 않았어. 하지만 ‘감지한다’는 건 파이프라인 입장에서의 일이야. 우리는 그 일이 실제로 일어났는지 확인해 볼 필요가 있지 않을까? 우리 집 현관문이 자동으로 잠긴다고 믿는 것과, 손잡이를 돌려서 잠겼는지 확인하는 건 다른 것처럼 말이야.”
솔라는 그 비유가썩 마음에 들지는 않았지만, 틀린 말은 아니라고 생각했다.
“그럼… 확인은 어디서 하는데?”
“파이프라인이 사는 곳에서 해야지. AWS 콘솔, CodePipeline 화면으로 가보자.”
솔라는 마지못해 새 브라우저 탭을 열고 AWS CodePipeline 화면으로 들어갔다. 자신이 설정한 파이프라인의 상태를 보여주는 익숙한 다이어그램이 나타났다.
[Source] -> [Build] -> [Deploy]
“자, 여기서 첫 번째 관문이 어디지?”
루나의 질문에 솔라는 다이어그램의 맨 왼쪽, Source라고 적힌 상자를 가리켰다.
“여기. Source 단계.”
“그 상자 안을 봐. 뭐라고 쓰여 있어?”
솔라는 눈을 가늘게 뜨고 Source 상자 아래의 작은 글씨들을 읽었다. 상자는 선명한 초록색으로 채워져 있었고, 그 아래에는 ‘Succeeded’라는 단어가 보였다. 그리고 그 옆에는 자신이 방금 커밋한 메시지, ‘Change header color to green’과 함께 고유한 커밋 ID가 적혀 있었다.
순간 솔라의 머릿속에서 무언가 ‘탁’ 하고 맞아 들어가는 느낌이 들었다.
“아…!”
“뭐가 보여?”
“여기… Source 단계가 ‘성공’이라고 되어 있어. 그리고 내가 올린 커밋 메시지도 정확히 있어. 이건… 내 코드를 파이프라인이 성공적으로 ‘물고’ 들어왔다는 증거네.”
솔라의 목소리에는 놀라움이 섞여 있었다. 지금까지는 git push를 하고 나서 파이프라인 화면을 바로 확인해 볼 생각을 하지 못했다. 그저 최종 결과물인 웹사이트만 쳐다보고 있었던 것이다.
“맞아. 이제 우리는 한 가지 사실을 확실히 알게 됐어. 문제가 어디에 있든, 적어도 파이프라인이 코드를 인식하지 못하는 문제는 아니라는 걸.”
루나는 말을 마치고 자리에서 일어섰다.
솔라는 이제 다른 눈으로 파이프라인 다이어그램을 보기 시작했다. Source 단계는 더 이상 전체 과정의 일부가 아니라, 독립적으로 성공과 실패를 판단할 수 있는 첫 번째 검문소였다. ‘코드를 올렸다’는 나의 행위와, ‘파이프라인이 소스를 감지했다’는 시스템의 상태를 명확히 구분할 수 있게 된 것이다. 만약 이 Source 단계가 빨간색으로 ‘Failed’였다면, 문제는 Git 저장소 연결이나 권한 문제일 것이고, 다른 곳은 쳐다볼 필요도 없다는 뜻이다.
이제 Source 단계의 초록불은 든든한 확신이 되었다. 하지만 동시에 솔라의 시선은 자연스럽게 오른쪽 다음 단계로 옮겨갔다.
“언니, 그런데 이상해. Source는 성공했는데, Build 단계는 왜 빨간색으로 ‘실패’라고 되어 있지? Build는 도대체 뭘 하는 단계고, 뭘 확인해야 하는 거야?“
2장: 빌드 Artifact 확인: 성공적인 변환의 증거
솔라의 시선은 파이프라인 다이어그램의 두 번째 상자, 붉은색으로 빛나는 Build에 고정되었다. 바로 옆 Source 단계의 초록불이 머나먼 과거의 성공처럼 느껴졌다. 진짜 문제는 바로 여기, 멈춰버린 파이프라인이었다.
솔라는 Build 상자 안에 있는 ‘Details’ 링크를 눌렀다. 새로운 브라우저 탭이 열리며 AWS CodeBuild 콘솔 화면이 나타났다. 화면을 가득 채운 것은 처음 보는 빽빽한 로그의 행렬이었다. 스크롤바를 맨 아래로 내리자, 역시나 최종 상태는 Failed. 그 위에는 이런 메시지가 적혀 있었다.
Phase context status code: COMMAND_EXECUTION_ERROR Message: Error while executing command: ng build. Reason: exit status 1
“하… COMMAND_EXECUTION_ERROR… exit status 1… 이건 그냥 에러가 났다는 말이잖아. 대체 어디서부터 잘못된 건지 어떻게 알아?”
막막했다. 수백 줄에 달하는 로그를 처음부터 끝까지 다 읽어야만 원인을 찾을 수 있을 것 같았다. 이전에는 Build 단계의 성공 여부를 맨 마지막 줄의 Succeeded 혹은 Failed라는 단어 하나로만 판단했다. Failed가 떴으니 실패인 것은 알겠지만, ‘왜’ 실패했는지, ‘무엇을’ 확인해야 하는지는 전혀 감이 잡히지 않았다.
“언니, Build가 실패했대. 로그를 보라는데… 이건 그냥 암호문 같아.”
솔라의 불평에, 어느새 다가온 루나는 노트북 화면을 들여다보았다. 그녀는 복잡한 로그 대신, 솔라의 어깨를 가볍게 두드리며 질문을 던졌다.
“음, 실패한 건 확실하네. 그런데 솔라, 원래 Build 단계가 성공하면 우리에게 뭘 줘야 하지? 이 단계의 임무가 뭐야?”
“임무? 그야… 소스 코드를 웹사이트로 ‘빌드’하는 거겠지. buildspec.yml 파일에 정의된 대로. ‘Build 단계는 buildspec.yml을 이용해 Angular dist artifact를 생성한다’고 했으니까. 결과물로 dist라는 폴더, 즉 아티팩트가 나와야 해.”
솔라는 자기가 아는 것을 쏟아냈다. 하지만 말하면서도 어딘가 이상했다. 아는 것과 지금 눈앞의 문제를 연결하지 못하고 있었다.
“바로 그거야. Build 단계의 성공은 단순히 에러 없이 끝났다는 뜻이 아니야. 약속된 결과물, 즉 ‘아티팩트’를 성공적으로 만들어냈다는 증거가 있어야 해.”
루나는 솔라가 보는 화면에서 마우스 커서를 움직여 로그 창 상단의 검색창을 가리켰다.
“이 모든 로그를 읽을 필요는 없어. 우리가 찾아야 할 증거는 정해져 있으니까. 이 로그는 Build 공장에서 벌어진 일을 기록한 작업 일지 같은 거야. 우리는 제품이 완성됐는지, 아니면 어떤 공정에서 불량이 났는지만 확인하면 돼.”
“작업 일지… 그럼 뭘 검색해야 하는데?”
“빌드 명세서(buildspec.yml)를 보면, 빌드가 끝난 후 결과물을 포장하는 단계가 있어. 보통 artifacts라는 이름의 단계지. 한번 검색해볼래?”
솔라는 반신반의하며 로그 검색창에 artifacts라고 입력했다. 엔터를 치자, 수백 줄의 로그가 순식간에 몇 줄로 걸러졌다.
[Container] 2023/10/26 14:30:00 Running command echo "Uploading artifacts..."
Uploading artifacts...
[Container] 2023/10/26 14:30:01 Phase complete: UPLOAD_ARTIFACTS State: FAILED
[Container] 2023/10/26 14:30:01 Phase context status code: CLIENT_ERROR Message: no matching files have been found for the path dist/**/*
“어?”
솔라의 입에서 짧은 탄성이 터져 나왔다. UPLOAD_ARTIFACTS 단계가 FAILED였고, 그 이유는 dist 경로에서 아무 파일도 찾지 못했다는 CLIENT_ERROR였다. 모든 로그를 읽지 않고도 문제의 핵심에 곧바로 접근한 것이다.
“아… ng build 명령 자체가 실패해서 dist 폴더가 아예 안 만들어졌고, 그래서 아티팩트를 포장하려니까 ‘파일이 없다’고 에러가 난 거구나!”
이전에는 그저 COMMAND_EXECUTION_ERROR라는 막연한 실패 메시지만 보였는데, 이제는 실패의 과정이 눈에 보이기 시작했다.
루나는 고개를 끄덕였다. “만약 빌드가 성공했다면 저 부분은 어떻게 보일까? 예전에 성공했던 빌드 로그를 한번 찾아보자.”
3장: CodeDeploy 배포: 서버에 전달된 변화
솔라는 의기양양하게 키보드를 두드려 이전 빌드 실패의 원인이었던 코드 오타를 수정했다. 그리고 다시 git commit과 git push를 실행했다. 이번에는 자신감이 있었다. Build 단계의 실패 원인이 ‘결과물(artifact) 부재’라는 것을 명확히 알았으니, 수정된 코드는 분명히 dist 폴더를 제대로 만들어낼 것이다.
잠시 후, 솔라는 CodePipeline 대시보드를 새로고침했다. 예상대로였다. 파이프라인 다이어그램의 Source와 Build 단계가 모두 선명한 초록색으로 빛나고 있었다.
[Source (Succeeded)] -> [Build (Succeeded)] -> [Deploy (In Progress)]
“됐어! 빌드 성공!”
솔라는 작게 환호성을 질렀다. 막혔던 흐름이 뚫리는 시원한 기분이었다. 이제 마지막 Deploy 단계만 통과하면, 그토록 보고 싶었던 녹색 헤더의 웹사이트를 만날 수 있을 터였다. 몇 번 더 새로고침하자 Deploy 단계마저 초록색 Succeeded로 바뀌었다. 모든 관문이 초록불이었다.
솔라는 망설임 없이 웹사이트 탭으로 이동해 F5 키를 눌렀다. 하지만 화면에 나타난 것은 여전히 시퍼런 헤더의 예전 웹사이트였다.
“어? 왜…?”
김이 확 빠지는 기분이었다. 파이프라인의 모든 단계가 ‘성공’이라고 알려주고 있는데, 왜 최종 결과물은 바뀌지 않은 걸까. 솔라는 다시 CodeDeploy 콘솔로 들어가 배포 상세 정보를 확인했다. 그곳에도 ‘Succeeded’라는 단어와 함께, 모든 배포 생명주기 이벤트(ApplicationStop, DownloadBundle, Install, ApplicationStart 등) 옆에 초록색 체크 표시가 붙어 있었다.
“언니, 이건 진짜 이상해. 파이프라인도, 빌드도, 배포도 전부 성공이래. 콘솔에서는 거짓말을 할 리가 없잖아. 그런데 왜 웹사이트는 그대로인 거야? Deploy가 성공했다는 건, 배포가 끝났다는 뜻 아니야?”
솔라의 목소리에는 당혹감과 불신이 섞여 있었다. 그녀에게 CodeDeploy의 ‘성공’은 곧 ‘웹사이트에 변경 사항이 적용되었다’는 최종 선언과 같았다. 이 둘 사이에 다른 과정이 끼어들 여지가 있다고는 생각지 못했다.
솔라의 옆에서 이 과정을 지켜보던 루나가 조용히 입을 열었다.
“솔라, 택배 앱에서 ‘배송 완료’ 알림을 받으면, 그게 무슨 뜻일까?”
“응? 당연히 물건이 문 앞에 도착했다는 뜻이지.”
“맞아. 그런데 그 알림이 ‘내가 주문한 물건이, 파손되지 않은 상태로, 정확히 도착했다’는 것까지 보장해 주지는 않지. 상자를 열어보기 전까지는 모르는 거잖아.”
루나는 CodeDeploy 배포 상세 화면을 가리켰다.
“CodeDeploy의 ‘Succeeded’도 마찬가지야. 이건 ‘배송 기사가 약속된 위치에 상자를 내려놓고, 초인종을 누르는 것까지 성공했다’는 보고서에 가까워. ‘Deploy 단계는 CodeDeploy를 통해 EC2에 artifact를 배치하고 appspec.yml로 Nginx 재시작을 관리한다’는 문장의 역할은 거기까지야.”
솔라는 루나의 비유를 곱씹었다. 배송 완료와 실제 물건 확인은 다른 단계. 그렇다면 CodeDeploy의 ‘배송 완료’ 보고서 너머, 실제 ‘문 앞’을 확인해야 한다는 뜻이었다.
“그럼… 실제 서버에 들어가서 확인해봐야 한다는 거야?”
“직접 상자를 열어보는 거지. 우리 웹 서버가 있는 EC2 인스턴스로 들어가 보자.”
루나의 말에 솔라는 터미널 창을 열고 EC2 인스턴스에 접속하는 SSH 명령어를 입력했다.
ssh ec2-user@<EC2_PUBLIC_DNS> -i "my-key-pair.pem"
낯선 프롬프트가 깜빡이는 검은 화면에 들어서자, 마치 서버의 문을 열고 안으로 들어온 기분이 들었다.
“자, 택배 상자는 어디에 배달되기로 약속되어 있었지? appspec.yml 파일에서 배송지를 확인해 봐.”
솔라는 로컬 컴퓨터에 저장된 appspec.yml 파일을 떠올렸다. 웹 콘텐츠가 배포될 경로는 /var/www/html/ 이었다.
“/var/www/html/ 폴더야. 거기로 가볼게.”
솔라는 터미널에 명령어를 입력했다.
cd /var/www/html/
ls -l
엔터를 치자, 폴더 안의 파일 목록이 나타났다. 그중에는 index.html 파일도 있었다. 솔라는 파일의 수정 날짜를 확인했다. 방금 배포가 일어난 시각과 거의 일치했다.
“파일은… 제대로 온 것 같아. 날짜가 최신이야.”
“상자는 도착했네. 이제 상자 안을 확인해 볼 차례지. 파일 내용이 정말 우리가 보낸 녹색 헤더가 맞을까?”
솔라는 cat 명령어로 index.html 파일의 내용을 화면에 출력했다. 그리고 그 안에서 결정적인 한 줄을 발견했다.
...
<h1 style="color: green;">Green Header</h1>
...
“맞아! ‘Green Header’로 바뀌어 있어! 그럼 파일은 제대로 배포된 게 확실하네.”
이제야 한시름 놓을 수 있었다. 적어도 빌드 결과물이 엉뚱한 곳으로 증발한 것은 아니었다.
“좋아. 마지막으로 확인할 게 하나 더 있어. 상자를 집에 들여놨으면, 이제 이걸 사용해야지. appspec.yml은 파일 복사 후에 웹 서버를 재시작하라고 되어 있었어. 배달원이 초인종을 눌렀을 때, 집 안의 Nginx가 제대로 응답했는지 확인해 보자.”
루나의 말에 솔라는 Nginx 서비스의 상태를 확인하는 명령어를 입력했다.
sudo systemctl status nginx
터미널 화면에 Nginx의 상태 정보가 출력되었다.
● nginx.service - The nginx HTTP and reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; vendor preset: disabled)
Active: active (running) since Thu 2023-10-26 15:10:05 UTC; 2min ago
...
Active: active (running) 이라는 문구가 선명하게 보였다. 그리고 재시작 시간도 방금 배포가 완료된 시점과 일치했다.
솔라는 비로소 깨달았다. CodeDeploy의 Succeeded는 끝이 아니었다. 그것은 단지 ‘서버에 파일을 복사하고, 명시된 스크립트를 실행하는 임무를 완수했다’는 신호일 뿐이었다. 배포의 진짜 성공 여부는 서버 안으로 직접 들어와 ① 파일이 제대로 도착했는지(파일 존재 및 내용 확인), 그리고 **② 그 파일을 서빙할 서비스가 제대로 작동하는지(Nginx 상태 확인)**를 직접 눈으로 봐야만 판단할 수 있는 것이었다.
이제 Deploy 단계의 초록불은 더 이상 최종 성공의 보증수표가 아니라, ‘서버를 확인해 볼 준비가 되었다’는 출발 신호로 보이기 시작했다.
그런데 모든 것을 확인한 솔라의 마음속에 새로운 의문이 피어올랐다.
“언니, 이상하다. 서버에 파일도 제대로 들어왔고, Nginx도 잘 돌고 있어. 그런데 왜… 내 브라우저에서는 아직도 파란색 헤더가 보이는 거지? 모든 게 완벽하게 배포됐는데, 도대체 뭐가 문제일까?“
4장: 브라우저 엔드포인트: 최종 사용자 경험 확인
솔라의 손가락이 기계적으로 F5 키를 연타했다. 하지만 모니터 속 웹사이트는 묵묵부답. 왼쪽 화면에는 모든 단계가 초록색 ‘성공’으로 빛나는 파이프라인 대시보드가, 오른쪽 화면에는 여전히 시퍼런 헤더의 예전 웹페이지가 띄워져 있었다. 이 완벽한 모순 앞에서 솔라는 길을 잃은 기분이었다.
터미널 창에서 확인했던 서버의 상태가 머릿속을 맴돌았다. EC2 인스턴스 안의 /var/www/html/ 경로에는 분명 ‘Green Header’ 코드가 담긴 index.html 파일이 최신 날짜로 존재했다. Nginx 서비스 역시 오류 없이 active (running) 상태였다. 서버 세상에서는 모든 것이 완벽했다. 하지만 사용자의 세상인 브라우저에서는 아무것도 바뀌지 않았다.
마침내 F5 키를 누르던 손을 멈춘 솔라가 망연자실한 표정으로 루나를 돌아보았다. 그 눈빛이 ‘모든 게 완벽한데, 도대체 왜?‘라고 소리치고 있었다. 루나는 파이프라인 대시보드나 터미널 창이 아닌, 솔라가 보고 있던 브라우저 창을 턱짓으로 가리켰다.
“지금까지 우리가 확인한 건 전부 서버라는 ‘창고’ 안에서 일어난 일이야. 파일이라는 상품이 잘 도착했고, Nginx라는 점원이 제자리에 잘 정리까지 해뒀지. 그런데 솔라, 넌 지금 ‘창고’를 보고 있는 게 아니라, 사용자들이 보는 ‘매장 쇼윈도’를 보고 있는 거잖아.”
“매장 쇼윈도…?”
“응. 파이프라인의 성공은 창고에 새 상품이 들어왔다는 소식일 뿐이야. 쇼윈도에 진열된 상품이 바뀌었다는 보장은 아니지. 어쩌면 쇼윈도를 담당하는 직원이 게으름을 피우고 있는지도 몰라. 새 상품이 들어온 걸 모르고 계속 예전 상품만 진열해두는 거지.”
루나의 비유에 솔라는 고개를 갸웃했다. 쇼윈도를 담당하는 직원이라니?
“그 직원이 누군데?”
“바로 네 브라우저.”
루나는 솔라의 키보드 위로 손을 뻗어 F12 키를 눌렀다. 화면 오른쪽에 복잡한 글자들이 가득한 개발자 도구 창이 나타났다.
“브라우저는 똑똑해서 한 번 방문한 웹사이트의 파일들을 저장해 둬. 다음에 또 방문할 때 서버까지 가지 않고, 저장해 둔 파일을 바로 보여주려고. 그게 훨씬 빠르니까. 그걸 ‘캐시(Cache)‘라고 부르지. 지금 네 브라우저는 ‘어, 이 웹사이트 저번에 왔었네? 여기 저장해 둔 거 보여줘야지’ 하면서, 서버에 새 파일이 도착했는지 확인조차 안 하고 있는 거야.”
이전까지 솔라에게 ‘웹사이트가 작동한다’는 것은 파이프라인의 모든 단계가 성공했으니 당연히 따라오는 결과였다. 하지만 이제 서버의 성공과 브라우저의 화면 사이에 ‘캐시’라는 보이지 않는 장벽이 있을 수 있다는 사실을 처음 마주했다.
루나는 개발자 도구의 여러 탭 중에서 ‘Network’ 탭을 클릭했다. 그리고 그 안에 있는 작은 체크박스 하나를 가리켰다.
“자, 여기 ‘Disable cache’라고 쓰인 체크박스 보이지? 이걸 선택하면, 브라우저에게 ‘저장해 둔 거 쓰지 말고, 무조건 서버에 가서 새로 가져와!’라고 명령하는 거야. 한번 체크하고 새로고침 해볼래?”
솔라는 반신반의하며 마우스로 ‘Disable cache’ 체크박스를 클릭했다. 그리고 다시 F5 키를 눌렀다.
순간, 마법 같은 일이 일어났다.
몇 번을 눌러도 꿈쩍 않던 파란색 헤더가 사라지고, 그토록 보고 싶었던 선명한 초록색 헤더가 화면에 나타났다.
“세상에…!”
솔라의 입에서 탄성이 터져 나왔다.
“캐시 때문이었다고? 그럼 파이프라인도, 빌드도, 배포도, 심지어 서버도… 전부 아무 잘못이 없었던 거네. 그냥 내 브라우저가 옛날 화면을 붙들고 안 놔주고 있었던 거야.”
허탈함과 함께 명쾌한 깨달음이 밀려왔다. 지금까지의 모든 확인 과정은 서버라는 기계의 입장에서의 검증이었다. 하지만 최종 결과물은 사용자의 컴퓨터, 즉 브라우저에서 소비된다. ‘검증은 브라우저 엔드포인트 확인을 통해 이루어진다’는 문장의 진짜 의미는 바로 이것이었다. 단순히 URL을 주소창에 쳐보는 행위가 아니라, 최종 사용자의 경험에 영향을 미칠 수 있는 캐시와 같은 변수까지 고려하여, 진짜 ‘새것’이 보이는지를 확인하는 것. 그것이 진정한 최종 검증이었다.
이제 솔라는 파이프라인의 마지막 단계를 어떻게 통과해야 하는지 알게 되었다. 그것은 AWS 콘솔의 초록불이 아니라, 내 눈앞 브라우저의 변화였다.
[최종 검증 절차]
1. 웹 브라우저에서 F12 키를 눌러 개발자 도구를 연다.
2. 'Network' 탭으로 이동한다.
3. 'Disable cache' 옵션을 체크한다.
4. 페이지를 새로고침(F5 또는 Cmd+R)하여 변경 사항이 반영되었는지 확인한다.
솔라는 이제 각 단계의 검문소를 자신 있게 통과할 수 있을 것 같았다. 하지만 모든 조각이 맞춰지자, 새로운 질문이 고개를 들었다.
“언니, 이제 각 단계에서 뭘 봐야 하는지는 확실히 알겠어. Source는 파이프라인 콘솔에서, Build는 빌드 로그에서 아티팩트를, Deploy는 EC2 서버 안에서 파일과 서비스를, 그리고 마지막 Verification은 브라우저에서. 그런데 만약 문제가 생겼을 때, 이 네 군데를 처음부터 하나씩 다 뒤져봐야 하는 거야? 뭔가 더 똑똑하게, 전체 흐름을 따라가면서 문제 지점을 바로 찾아내는 방법은 없을까?“
5장: 파이프라인 흐름 진단: 단계별 증거 연결
솔라는 방금 자신이 정리한 최종 검증 절차 메모를 보며 만족스러운 미소를 지었다. 소스 코드 변경부터 브라우저 캐시 문제까지, 각 단계를 독립적으로 점검할 수 있는 네 개의 열쇠를 모두 손에 쥔 기분이었다. 이제 어떤 문제가 생겨도 이 열쇠들로 차근차근 문을 열어보면 될 터였다.
하지만 만족감도 잠시, 솔라는 네 개의 검문소 사이를 어떻게 효율적으로 오갈지 막막했다. 그녀는 손가락으로 책상 위를 짚어가며 상상 속의 선을 그었다. CodePipeline 콘솔에서 CodeBuild 로그로, 다시 EC2 터미널로, 마지막으로 브라우저 개발자 도구로. 점들은 뚜렷했지만, 그 점들을 잇는 최적의 경로는 보이지 않았다. 문제가 생겼을 때, 이 모든 지점을 무작정 순서대로 확인하는 것이 최선일까?
루나는 솔라의 고민을 읽었다는 듯, 노트북을 자기 쪽으로 살짝 돌렸다. 그리고는 몇 번 키보드를 두드리고 나서, 의미심장한 미소와 함께 다시 솔라에게 화면을 보여주었다.
“자, 모의 훈련 시간이야. 내가 방금 파이프라인 어딘가에 작은 함정을 하나 파뒀어. 네가 배운 열쇠들로 한번 문제를 진단해 봐. 단, 조건이 있어. 최대한 적은 확인 과정으로 원인을 찾아내는 거야.”
솔라의 눈앞에 펼쳐진 것은 익숙한 CodePipeline 대시보드였다. 루나는 이미 새로운 코드 변경을 push 해둔 상태였다. 잠시 후 파이프라인이 움직이기 시작했다.
Source 단계가 초록색으로 바뀌었다. 그리고 Build 단계 역시 초록색 Succeeded로 빛났다. 하지만 잠시 뒤, 솔라의 예상과 달리 Deploy 단계에서 선명한 붉은색 Failed 글자가 나타났다.
[Source (Succeeded)] -> [Build (Succeeded)] -> [Deploy (Failed)]
이전이라면 솔라는 그저 “배포가 실패했네”라며 막막해했을 것이다. 하지만 이제 그녀의 사고는 달랐다. 그녀는 파이프라인 전체를 하나의 흐름으로 보기 시작했다.
“음, 일단 Source랑 Build는 성공했어.”
솔라는 혼잣말을 하며 자신의 생각을 정리했다.
“이건 내 코드가 파이프라인에 잘 전달됐고, 빌드 서버가 내 코드를 웹사이트 파일 묶음(아티팩트)으로 변환하는 데까지는 성공했다는 뜻이야. 문제는 그 아티팩트를 실제 서버로 옮기는 과정에서 터진 거네.”
그녀는 더 이상 Source 단계를 다시 확인하거나 git 명령을 의심하지 않았다. Build 로그를 샅샅이 뒤져볼 필요도 없었다. 파이프라인의 상태가 이미 ‘문제는 Deploy 이후에 있다’고 명확히 알려주고 있었기 때문이다. 용의선상에서 앞선 두 단계가 확실히 제외된 것이다.
솔라는 망설임 없이 Deploy 단계의 ‘Details’ 링크를 클릭하여 CodeDeploy 콘솔로 이동했다. 배포 이벤트 목록이 나타났다.
Deployment lifecycle events
- ApplicationStop (Succeeded)
- DownloadBundle (Succeeded)
- Install (Succeeded)
- ApplicationStart (Failed)
- ...
“찾았다. ApplicationStart 훅에서 실패했네.”
솔라의 손가락이 ‘View logs’ 링크를 향했다. 이젠 로그를 보는 것이 두렵지 않았다. 무엇을 찾아야 하는지 알기 때문이었다. appspec.yml 파일에서 ApplicationStart 단계는 Nginx 웹 서버를 재시작하는 스크립트를 실행하도록 되어 있었다. 문제는 바로 그 스크립트 실행 중에 발생한 것이다.
로그 파일의 마지막 몇 줄에 실패의 원인이 명확히 드러나 있었다.
Error: Unit nginxx.service not found.
“‘nginxx’? 아… 오타다!”
솔라는 허탈한 웃음을 터뜨렸다. 루나가 appspec.yml 파일의 Nginx 재시작 명령어 systemctl restart nginx를 systemctl restart nginxx로 교묘하게 바꿔놓은 것이었다. 존재하지 않는 서비스를 재시작하려니 당연히 오류가 날 수밖에.
“언니, 범인은 appspec.yml 파일의 오타였어.”
“정확해.” 루나가 빙그레 웃으며 고개를 끄덕였다. “무작정 처음부터 다 뒤져본 게 아니라, 파이프라인의 상태를 보고 범위를 좁혀 들어갔지. 그게 바로 파이프라인 흐름 진단이야. 각 단계의 성공은 다음 단계의 전제 조건이 되니까, 실패 지점은 바로 그 앞 단계까지는 모두 정상이라는 가장 강력한 증거가 돼.”
솔라는 방금 자신이 한 행동에 ‘파이프라인 흐름 진단’이라는 이름이 붙자, 그 과정이 한결 명확하게 정리되는 것을 느꼈다. 무작정 모든 곳을 확인한 것이 아니었다. 파이프라인의 실패 지점(Deploy)을 보고 진단 범위를 좁혔고, CodeDeploy의 실패 이벤트(ApplicationStart)를 보고 다시 EC2 서버의 해당 스크립트 로그로 파고들었다. 마치 의사가 환자의 증상을 보고 청진할 부위를 정하고, 이상 음을 듣고 나서야 엑스레이를 찍는 것과 같았다.
그녀는 비로소 처음에는 그저 하나의 문장으로 보였던 말을 새로운 눈으로 읽을 수 있게 되었다.
“소스 변경부터 빌드, CodeDeploy 배포, 웹 사이트 확인까지 전체 pipeline을 검증합니다.”
이 문장은 더 이상 ‘모든 것을 한 번에 확인한다’는 막연한 지시가 아니었다. 이제 솔라에게는 잘 짜인 진단 순서도로 보였다. 그녀는 노트북 옆에 놓인 메모장을 펼쳐 펜을 들었다. 그리고 방금 머릿속으로 그렸던 흐름을 써 내려가기 시작했다.
CI/CD 파이프라인 문제 진단 흐름
1. 브라우저에서 변경이 안 보일 때, 어디부터 볼까?
- CodePipeline 대시보드를 연다.
2. 파이프라인 상태는 어떤가?
- [Source] 실패? → Git 저장소 연결, 권한, 브랜치 이름 확인.
- [Build] 실패? → CodeBuild 로그 확인. 특히
UPLOAD_ARTIFACTS단계에서 ‘파일 없음’ 에러가 있는지 본다. 빌드 명령어 자체의 실패 여부 확인. - [Deploy] 실패? → CodeDeploy 이벤트 로그 확인. 어떤 생명주기 훅(e.g.,
ApplicationStart)에서 실패했는지 찾고, EC2 인스턴스의 해당 로그(/var/log/aws/codedeploy-agent/…)를 확인하여 스크립트 오류를 찾는다. - [모두 성공]? → 사용자 측 문제일 가능성 높음. 브라우저 개발자 도구(F12) > Network 탭 > ‘Disable cache’ 체크 후 새로고침.
솔라는 자신이 만든 간결한 진단 지도를 보았다. 이것이야말로 흩어져 있던 네 개의 열쇠를 하나로 꿰어 만든 마스터키였다. 이제 어떤 문제가 발생하더라도, 이 흐름도를 따라가면 막힘없이 원인을 찾아낼 수 있다는 자신감이 생겼다. 파이프라인은 더 이상 블랙박스가 아니었다. 각 단계의 입출력과 증거가 명확하게 연결된, 투명한 운영 흐름이었다.