Mini Project 6 01

자동 배포 환경: 체크리스트를 넘어 운영 흐름으로 읽기

교안의 목표를 보면 CI/CD, EKS, Auto Scaling, CloudWatch가 모두 따로 있는 체크리스트처럼 보인다. 무엇이 하나의 운영 시스템으로 묶이는지 감이 잡히지 않는다.

근거 · 교안 프로젝트 목표/배포 흐름

자동 배포 환경: 체크리스트를 넘어 운영 흐름으로 읽기 대표 이미지

1장: 코드 변경, 운영의 첫걸음

솔라는 미니프로젝트 결과 보고서를 화면에 띄워놓고 한숨을 쉬었다. ‘웹 서비스를 위한 CI/CD 환경 구현’, ‘배포 환경 구성’, ‘모니터링’, ‘수동 승인 기능 구현’. 분명 자신이 완수한 중점 사항 목록인데, 마치 서로 다른 가게에서 사 온 물건들을 아무렇게나 늘어놓은 것 같았다. 각각의 기술은 익숙했지만, 이들이 모여 어떻게 하나의 유기적인 시스템을 이루는지 그림이 그려지지 않았다.

“언니, 나 프로젝트 다 끝냈는데… 뭔가 찜찜해.”

옆에서 조용히 책을 읽던 루나가 고개를 들었다.

“CI/CD, EKS, Auto Scaling, CloudWatch… 보고서에 적힌 단어들은 다 알겠는데, 얘네들이 따로따로 노는 느낌이야. 섬처럼. 이 섬들을 잇는 다리가 있어야 할 것 같은데, 내 머릿속엔 그냥 안개만 자욱해.”

솔라는 마우스 휠을 돌려 GitHub 저장소의 커밋 기록을 화면에 띄웠다. aivle-m-6th-t24/6-mini-project24 저장소의 dev 브랜치에 남은 가장 최근의 기록. ‘Fix: button color style’. 아주 사소한 CSS 수정이었다.

“예를 들면 이거. 내가 방금 코드를 고쳐서 GitHub에 올렸어. 이건 그냥 내 컴퓨터에 있던 코드 파일을 원격 저장소에 복사해서 저장한 거잖아? 그런데 잠시 후에 ECR에 가보면 새로운 이미지가 떡 하니 생겨 있어. 이 둘 사이에 무슨 일이 일어나는지, 나는 과정을 본 게 아니라 결과만 통보받은 느낌이야. 그냥 ‘원래 그렇게 되는 거야’ 하고 넘어가기엔 너무 답답해.”

솔라의 말은 ‘GitHub 푸시는 그저 코드 저장일 뿐, 빌드나 이미지 생성과 직접 연결되지 않는다’는 생각에 머물러 있었다. 코드 저장과 이미지 생성은 별개의 작업이며, 그 사이를 잇는 과정은 보이지 않는 마법처럼 느껴졌다.

루나는 솔라의 책상으로 다가와 모니터를 가만히 들여다보았다. 루나는 보고서의 화려한 기술 목록이나 GitHub의 커밋 메시지를 지나쳐, AWS 콘솔의 CodePipeline 서비스 화면을 열었다.

“목록 전체를 보려고 하니까 막막한 거야. 딱 하나만 따라가 보자. 네가 방금 말한 그 ‘버튼 색상 수정’ 코드, 그 녀석의 여정을 처음부터 끝까지 추적해 보는 거지.”

루나의 말에 솔라는 의아한 표정을 지었다. 코드의 여정이라니.

“자, 파이프라인 목록 좀 봐. 네 dev 브랜치와 연결된 파이프라인이 있지? 가장 최근 실행 기록을 한번 볼까?”

솔라는 루나가 가리키는 파이프라인을 클릭했다. 그러자 익숙하지만 무심코 지나쳤던 단계별 흐름도가 나타났다. 첫 번째 단계는 ‘Source’였다. 그리고 그 아래에는 솔라가 조금 전 GitHub에서 봤던 ‘Fix: button color style’ 커밋 메시지와 고유 ID가 선명하게 찍혀 있었다.

“어?”

솔라의 입에서 짧은 탄성이 터져 나왔다.

“GitHub에 코드를 푸시한 순간, 그게 신호탄이었던 거야. CodePipeline은 계속 GitHub 저장소의 dev 브랜치를 지켜보고 있다가, 새로운 커밋이 들어오자마자 자동으로 이 전체 과정을 시작시킨 거지.”

화면 속에서 ‘Source’ 단계는 이미 초록불과 함께 ‘Succeeded’ 상태였다. 다음 단계인 ‘Build’로 시선을 옮기자, 진행률 표시줄이 활발하게 움직이고 있었다. 잠시 후 ‘Build’ 단계에도 초록불이 들어왔다. 루나는 ‘Build’ 단계의 세부 정보를 눌러 로그 링크를 보여주었다. 복잡한 명령어들이 화면을 가득 채웠지만, 솔라의 눈에 익숙한 단어들이 들어왔다. docker build, docker push

“아…! 여기서 내 코드를 진짜 이미지로 만들고 있었구나. 내 컴퓨터가 아니라, CodeBuild라는 작업 공간에서.”

마지막으로 그들은 ECR 저장소로 이동했다. 조금 전까지는 없었던 새로운 이미지가 목록 맨 위에 자리 잡고 있었다. 이미지 태그는 방금 지나온 빌드 번호와 연결되어 있었다. 시작은 단순한 텍스트 파일이었던 코드가, 이제는 어디에서든 실행할 수 있는 표준화된 패키지, 즉 도커 이미지로 변신해 있었다.

솔라는 잠시 모니터를 응시했다. 안갯속에 흩어져 있던 섬들이 하나의 단단한 땅으로 연결되는 기분이었다. GitHub 푸시는 단순히 파일을 저장하는 행위가 아니었다. 그것은 거대한 자동화 공정의 첫 번째 스위치를 누르는 일이었다. 어떤 코드가, 어떤 과정을 거쳐, 어떤 결과물이 되었는지 명확한 인과관계로 연결된 흐름이었다.

“그럼 GitHub는 그냥 코드 창고가 아니라… 운영 시스템의 맨 처음, 가장 중요한 입력 장치였네.”

스스로 내린 결론에 솔라는 고개를 끄덕였다. 막연했던 ‘CI/CD’라는 개념이 ‘코드 변경을 감지해서 배포 가능한 산출물로 자동 변환해주는 파이프라인’이라는 구체적인 흐름으로 보이기 시작했다. 이제 솔라는 어떤 코드 변경이 어떤 이미지 버전을 만들었는지, 그 과정 전체를 추적할 수 있는 자신만의 ‘파이프라인 흐름 추적기’를 손에 넣은 셈이었다.

새로운 이해에 대한 만족감도 잠시, 솔라의 시선은 ECR에 등록된 말끔한 이미지 목록에 머물렀다.

“좋아, 이제 내 코드가 배포 가능한 이미지로 변신했다는 건 확실히 알겠어. 그런데 이 이미지는… 그냥 여기 ECR 저장소에 있는 거잖아. 아직 창고에 있는 물건일 뿐이고. 이게 어떻게 EKS 클러스터 안으로 들어가서, 사용자들이 접속할 수 있는 진짜 서비스로 살아 움직이게 되는 거지?”

코드 변경이 운영의 첫걸음이라는 것을 깨닫자, 그 다음 걸음이 무엇인지 궁금해졌다. 창고에 도착한 물건을 가게 진열대에 올리는 과정은 또 다른 이야기일 터였다.

2장: EKS, 이미지를 운영 서비스로

솔라의 책상 위, 모니터 한편에는 방금 만들어진 따끈따끈한 도커 이미지가 등록된 ECR 저장소 목록이 떠 있었다. 하지만 솔라의 시선은 그 옆에 나란히 띄워놓은 프로젝트 보고서의 한 문장에 고정되어 있었다.

EKS 클러스터: m-6th-t24-cluster, Managed Node Group: m-6th-t24-nodegroup (Ready: 2)

그녀는 펜을 들어 노트에 간단한 그림을 그렸다. ECR 이미지 저장소라고 적은 네모 상자에서 화살표를 길게 뽑아, m-6th-t24-cluster라고 적은 커다란 네모 상자로 향하게 했다. 그리고 그 화살표 위에 커다란 물음표를 그려 넣었다. 창고에 있는 물건을 가게로 옮기는 것까지는 좋은데, 가게 어느 진열대에, 어떻게 놓이는지가 전혀 상상되지 않았다. 그녀에게 m-6th-t24-cluster는 그저 이름이 붙은 거대한 빈 상자일 뿐이었다.

“언니, ECR에 있는 이미지가 이 클러스터로 배포된다는 건 알겠어. 파이프라인이 그렇게 설정되어 있으니까. 그런데 ‘어디로’ 배포된다는 거지? 이 m-6th-t24-cluster라는 곳은 그냥 텅 빈 공간 아니야? 주소도 없는 거대한 물류 창고에 택배를 던져 넣는 기분이야.”

솔라의 생각은 ‘EKS 클러스터는 이미지가 올라가는 공간일 뿐, 내부 구성은 운영 흐름에 중요하지 않다’는 지점에 멈춰 있었다. 그저 이름표가 붙은 가상의 서버 공간, 그 이상도 이하도 아니었다.

루나는 솔라의 노트에 그려진 커다란 네모 상자와 물음표를 잠시 들여다보았다. 그러고는 솔라의 마우스를 부드럽게 가져가 EKS 클러스터 관리 화면을 열었다. m-6th-t24-cluster의 세부 정보 페이지였다.

“그 창고, 정말로 그냥 하나의 큰 방으로 되어 있을까? 안으로 한번 들어가 보자.”

루나는 복잡한 메뉴들 사이에서 ‘네임스페이스(Namespaces)’라는 항목을 클릭했다. 화면에는 default, kube-system 같은 낯선 이름들과 함께, 솔라에게 아주 익숙한 두 개의 이름이 보였다.

dev prod

“어…?”

솔라의 눈이 동그래졌다. “방이… 나뉘어 있네? 개발용 방이랑 실제 운영용 방이 따로 있었던 거야?”

거대한 단일 공간이라 생각했던 클러스터 내부에 명확한 구획이 있다는 사실은 작은 충격이었다. 루나는 여기서 멈추지 않고 dev 네임스페이스를 선택했다. 그러자 그 안에서 실행 중인 워크로드, 즉 파드(Pod)들의 목록이 나타났다.

dev-frontend-xxxxxxxx-xxxxx dev-backend-xxxxxxxx-xxxxx

솔라는 dev-backend 파드의 상세 정보를 눌러보았다. 그리고 ‘컨테이너 이미지’ 항목에서 조금 전 ECR에서 봤던 바로 그 이미지 주소와 태그를 발견했다. 코드 변경으로 시작된 여정이 마침내 실제 작동하는 애플리케이션의 최소 단위, 파드 안에서 살아 숨 쉬고 있는 현장을 목격한 순간이었다.

“찾았다! 내 이미지가 여기 와 있었구나! m-6th-t24-cluster라는 창고의, dev라는 구역에, dev-backend라는 이름표를 달고 실행되고 있었어.”

솔라는 흥분하며 말했다. 하지만 루나는 조금 더 차분하게 화면을 아래로 스크롤해 ‘서비스(Services)’ 섹션을 가리켰다. dev-backend-service라는 이름의 서비스가 있었고, 타입은 ClusterIP로 지정되어 있었다.

“자, 그럼 이건 어때? 이 서비스의 타입이 ‘ClusterIP’라고 되어 있네. 무슨 뜻으로 들려?”

솔라는 잠시 생각에 잠겼다. “클러스터… IP? 그럼… 클러스터 안에서만 쓸 수 있는 주소라는 뜻인가? 외부에서는 접근 못 하고?”

“맞아. 프론트엔드 파드는 이 ClusterIP 주소를 통해 백엔드 서비스와 통신할 수 있지만, 클러스터 바깥 세상에서는 이 주소가 보이지도, 존재하지도 않아.”

순간, 솔라의 머릿속에서 흩어져 있던 점들이 하나의 선으로 이어졌다. 보고서에 있던 EKS 클러스터는 m-6th-t24-cluster…라는 문장은 단순히 서버의 이름이 아니었다. 그것은 ‘어떤 구획(네임스페이스)에, 어떤 모습으로(파드), 어떤 내부 규칙(서비스 타입)으로 배포될지’를 정의하는, 매우 구체적인 주소 체계와 운영 규칙의 시작점이었던 것이다.

이제 솔라는 어떤 이미지가 주어졌을 때, 그것이 클러스터 내에서 어떤 주소를 부여받고 어떻게 실행되고 있는지 추적할 수 있는 자신만의 ‘클러스터 배포 식별자’를 갖게 된 셈이다. 이미지가 어디 있는지 찾고 싶을 때, 클러스터 이름뿐만 아니라 네임스페이스와 파드, 서비스까지 확인해야 한다는 것을 깨달았다.

안개가 또 한 겹 걷혔지만, 새로운 궁금증이 고개를 들었다.

“좋아, 이제 ECR 창고에서 이미지를 꺼내 EKS 클러스터의 dev 구역 진열대(파드)에 올렸고, 내부 직원(프론트엔드)들만 알아볼 수 있는 내부 통로(ClusterIP)도 확인했어. 그런데 정작 중요한 손님, 즉 외부 사용자는 이 가게에 어떻게 들어오지? 가게 문은 어디에 있는 거야?”

3장: 서비스 공개, 외부와 소통의 시작

솔라는 지난번 대화의 마지막 질문을 곱씹으며 웹 브라우저에 새 탭을 열었다. “가게 문은 어디에 있는 거야?” 그 질문에 대한 가장 간단한 답을 스스로 찾아보기로 했다. 그녀는 프로젝트 보고서에 적힌 개발 환경 도메인 주소 dev-m-6th-t24.ldhcloud.com을 주소창에 천천히 입력하고 엔터 키를 눌렀다.

잠시 후, 화면에는 익숙한 프로젝트의 프론트엔드 화면이 나타났다. 버튼도 잘 보이고, 텍스트도 정상적으로 표시되었다. 가게는 이미 문을 활짝 열고 영업 중이었다. 솔라는 잠시 어리둥절했다. 분명 백엔드 서비스는 ClusterIP로 클러스터 내부에 꽁꽁 숨겨져 있었는데, 프론트엔드는 어떻게 외부 인터넷을 통해 접속이 가능한 걸까? 그녀는 노트에 새로운 그림을 그렸다. 외부 인터넷을 상징하는 구름 아이콘에서 화살표를 그려 EKS 클러스터로 향하게 했다. 그리고 그 화살표 위에 다시 한번 물음표를 찍었다.

‘외부 공개’라는 게 그냥 서버의 특정 포트를 열어주는 방화벽 설정 같은 거라고 생각했다. 그리고 도메인 주소를 그 서버 IP에 연결하는 건 또 별개의 작업이라고 여겼다. 하지만 지금 눈앞의 상황은 그렇게 단순해 보이지 않았다. 백엔드는 닫혀 있고, 프론트엔드는 열려 있다. 선택적인 공개. 이건 단순한 ‘문 개방’과는 다른 무언가가 있다는 뜻이었다.

“언니, 이거 봐. 사이트가 그냥 잘 열리네.”

솔라의 목소리에 루나가 다가왔다. 루나는 이미 작동하고 있는 화면 대신, 솔라가 방금 그린 노트 위의 물음표를 가만히 들여다보았다.

“그 가게, 정문이 따로 있었나 보네. 백엔드가 직원용 뒷문이라면, 프론트엔드는 손님용 정문인 셈이지. 그 정문의 주소는 어떻게 확인했어?”

“그냥 보고서에 있던 도메인 주소로 접속했어. dev-m-6th-t24.ldhcloud.com. 아마 이 도메인 주소가 우리 클러스터의 대표 IP 주소에 연결되어 있는 거겠지?”

솔라의 대답은 그녀의 믿음, 즉 ‘도메인 연결은 별개의 설정 작업’이라는 생각을 담고 있었다.

루나는 말없이 EKS 클러스터의 서비스 목록 화면을 다시 띄웠다. 지난번 확인했던 dev-backend-service 아래에 dev-frontend-service가 보였다. 솔라가 직접 그 서비스를 클릭했다. 상세 정보가 나타났다.

dev-frontend-service

  • Type: LoadBalancer
  • ClusterIP: 172.20.x.x
  • External endpoint: a1b2c3d4e5f6-123456789.ap-northeast-2.elb.amazonaws.com

솔라의 눈이 ‘Type’ 항목의 LoadBalancer라는 단어에 멈췄다. 백엔드 서비스의 ClusterIP와는 명백히 다른 단어였다. 그리고 그 아래 ‘External endpoint’에는 난수처럼 보이는 긴 주소가 적혀 있었다.

“어… 이건 IP 주소가 아니네. 또 다른 도메인 주소 같은데?”

“맞아. 쿠버네티스에게 프론트엔드 서비스를 외부와 연결해달라고 요청했더니, 쿠버네티스가 AWS에 부탁해서 ‘로드밸런서’라는 외부용 문지기를 하나 만들어준 거야. 저 주소가 바로 그 문지기의 주소지.”

루나의 설명에 솔라는 무언가 깨달은 듯 터미널 창을 열었다. 그리고는 조금 전 접속했던 서비스 도메인 주소에 nslookup 명령어를 실행했다.

$ nslookup dev-m-6th-t24.ldhcloud.com
...
Name:   a1b2c3d4e5f6-123456789.ap-northeast-2.elb.amazonaws.com
...

결과로 나온 Name 필드의 값은, 방금 EKS 서비스 화면에서 봤던 로드밸런서의 ‘External endpoint’ 주소와 정확히 일치했다.

순간 솔라의 머릿속에서 모든 조각이 맞춰졌다. 사용자가 브라우저에 dev-m-6th-t24.ldhcloud.com을 입력하면, Cloudflare DNS는 이 요청을 AWS 로드밸런서의 주소로 안내한다. 그러면 로드밸런서는 그 요청을 받아 EKS 클러스터 내부의 프론트엔드 파드들 중 하나에게 안전하게 전달해주는 것이었다. 클러스터 자체가 외부로 노출되는 것이 아니라, 로드밸런서라는 공식적인 ‘창구’만이 외부에 공개되고, 모든 요청은 그 창구를 통해서만 내부로 들어올 수 있었다.

솔라는 보고서의 한 문장을 다시 떠올렸다. frontend만 LoadBalancer로 외부 공개하고 backend는 ClusterIP로 내부 노출했다. 이 문장은 이제 기술의 나열이 아니었다. ‘손님은 정문(LoadBalancer)으로만 받고, 직원 통로(ClusterIP)는 내부인만 사용하게 한다’는 명확하고 안전한 운영 전략의 선언문으로 읽혔다. 도메인 연결은 독립된 작업이 아니라, 이 전략을 완성하는 마지막 화룡점정이었던 것이다.

“알겠다. 외부에서 클러스터로 들어오는 모든 길을 추적할 수 있는 진단기를 손에 넣은 기분이야. 사용자가 입력한 주소에서부터 시작해서, DNS를 거쳐, 로드밸런서를 통과하고, 마침내 우리 서비스 파드에 도착하기까지의 전 과정이 이제 눈에 보여.”

내부 서비스가 외부와 어떻게 연결되는지, 그 ‘흐름’을 이해하게 되자 안개가 또 한 꺼풀 걷혔다. 하지만 동시에 새로운 걱정이 피어올랐다.

“좋아, 손님을 가게 안으로 안전하게 안내하는 방법은 이제 알겠어. 그런데 만약에 갑자기 손님이 100배로 몰려들면 어떡하지? 이 문지기 혼자서 감당할 수 있을까? 가게가 좁아서 손님들이 기다리다가 그냥 가버릴 수도 있잖아. 이 서비스가 안정적으로, 그리고 유연하게 운영되고 있다는 건 어떻게 보장하는 거지?”

4장: 확장성과 안정성, 흔들림 없는 운영

솔라는 자신의 노트 위에 새로운 그림을 그리고 있었다. EKS 클러스터를 향해 구름처럼 몰려드는 수많은 사용자 아이콘들. 그 앞에는 LoadBalancer라는 이름의 듬직한 문지기가 서 있었다. 하지만 문지기가 안내하는 가게 내부는 텅 비어 보였다. 솔라는 그 안에 작은 서버 아이콘 하나를 그리고는, 아이콘 위에 땀방울을 그려 넣었다. 문지기가 아무리 손님을 잘 안내해도, 정작 일할 직원이 한 명뿐이라면 가게는 금방 마비될 터였다.

지난번 대화 끝에 떠올랐던 걱정, “갑자기 손님이 100배로 몰려들면 어떡하지?”라는 질문이 머릿속을 떠나지 않았다. 보고서에는 ‘HPA’, ‘RDS MySQL’ 같은 단어들이 나열되어 있었다. 안정성을 위한 장치라는 건 어렴풋이 알았지만, 솔라에게는 그저 개별적인 보험 상품 목록처럼 보였다. 손님이 몰릴 때를 대비한 ‘자동 확장 보험’과 데이터가 날아갈 때를 대비한 ‘데이터 보존 보험’. 둘 다 중요하지만, 이 둘이 어떻게 협력해서 ‘흔들림 없는 운영’이라는 하나의 목표를 달성하는지는 와닿지 않았다.

“언니, 이 그림 좀 봐.”

솔라가 노트를 루나에게 내밀었다.

“로드밸런서가 손님들을 잘 받아주는 건 알겠어. 그런데 결국 일하는 건 이 파드 하나잖아. 손님이 늘어나면 이 파드는 그냥 터져버리는 거 아니야? 그리고 만약 이 파드에 문제가 생겨서 재시작되면, 그 안에 저장되어 있던 사용자 정보나 장바구니 내용은 다 날아가는 거고. 아무리 문을 잘 만들어도 가게 자체가 부실하면 소용없잖아.”

솔라의 생각은 ‘HPA와 RDS는 개별적인 고가용성 기능일 뿐, 전체 시스템의 확장성/안정성과는 느슨하게 연결된다’는 지점에 머물러 있었다. 각자 자기 역할만 하는 독립적인 부품들.

루나는 솔라의 노트에 그려진, 땀 흘리는 서버 아이콘을 손가락으로 가볍게 톡 쳤다.

“이 직원, 정말 혼자서 모든 일을 다 감당해야 할까? 그리고 자기 주머니에 중요한 고객 장부를 넣고 다닐까?”

루나는 말없이 터미널 창을 열고 명령어를 입력했다. 화면에는 dev 네임스페이스에 설정된 HPA(Horizontal Pod Autoscaler)의 상태가 나타났다.

NAME             REFERENCE                    TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
dev-frontend-hpa Deployment/dev-frontend-app   <unknown>/50%   2         10        2          ...
dev-backend-hpa  Deployment/dev-backend-app   <unknown>/50%   2         10        2          ...

“여기 기준이 있네. ‘CPU 사용량이 50%를 넘으면 조치를 취한다’는 규칙. 지금은 REPLICAS, 즉 직원 수가 둘이야. 우리가 한번 이 가게를 아주 바쁘게 만들어 볼까?”

루나는 부하 테스트 도구를 사용하는 대신, HPA 설정을 직접 수정했다. CPU 사용량 기준을 50%에서 1%로 확 낮춰버렸다. 시스템이 아주 약간의 부하만 느껴도 ‘매우 바쁘다’고 인식하게 만든 것이다.

“자, 이제 다시 직원을 확인해 보자.”

몇 초 후, 솔라가 다시 같은 명령어를 입력하자 놀라운 변화가 나타났다.

NAME             REFERENCE                    TARGETS    MINPODS   MAXPODS   REPLICAS   AGE
dev-frontend-hpa Deployment/dev-frontend-app   2%/1%      2         10        4          ...
dev-backend-hpa  Deployment/dev-backend-app   3%/1%      2         10        4          ...

REPLICAS 숫자가 2에서 4로 늘어나 있었다. EKS 클러스터 관리 화면에서도 새로운 파드들이 생성되어 Running 상태로 바뀌는 것이 실시간으로 보였다. 땀을 뻘뻘 흘리던 서버 아이콘이 스스로를 복제해 순식간에 네 명이 된 것과 같았다.

“와… 직원이 늘어났어! 가게가 바빠지니까 똑같이 생긴 직원을 더 만들어내는 거였구나.”

솔라의 눈이 빛났다. 이것이 바로 ‘확장성’의 실체였다. 하지만 그녀는 곧 다른 질문을 떠올렸다.

“좋아, 일하는 직원은 늘었어. 그런데 이 네 명의 직원이 고객 정보를 어떻게 공유하지? 첫 번째 직원이 받은 주문을, 새로 생긴 네 번째 직원이 어떻게 알고 처리해? 그리고 만약 이 직원 중 하나가 갑자기 사라지면, 그가 갖고 있던 정보는?”

루나는 이번에는 AWS의 RDS 콘솔 화면을 띄웠다. 프로젝트에 사용된 m-6th-t24-rds-mysql 데이터베이스 인스턴스의 정보가 보였다.

“아까 네가 걱정했던 ‘중요한 고객 장부’는 직원 주머니에 들어있지 않아. 저기, 클러스터 바깥에 있는 아주 안전한 중앙 금고에 보관되어 있지.”

루나가 가리킨 화면에는 데이터베이스의 상태, 백업 설정, 다중화(Multi-AZ) 여부 등이 표시되어 있었다.

“파드들은 그냥 계산대 직원 같은 거야. 주문을 받거나 계산을 처리할 뿐, 실제 돈이나 장부는 만지지 않아. 모든 기록은 이 중앙 금고, RDS에 저장되고 관리돼. 그래서 직원이 갑자기 그만두거나(파드 장애), 새로운 직원이 충원되어도(스케일 아웃), 업무 연속성에는 아무 문제가 없는 거지. 누구든 중앙 금고에 접근해서 필요한 정보를 꺼내 쓰면 되니까.”

그 순간 솔라는 머릿속이 환해지는 것을 느꼈다. HPA와 RDS는 별개의 보험이 아니었다. 그것은 ‘역할 분리’라는 하나의 철학 아래 긴밀하게 연결된 시스템이었다. HPA는 언제든 교체 가능한 ‘계산대 직원(파드)’의 수를 유연하게 조절해 확장성을 책임진다. RDS는 절대 잃어버리면 안 되는 ‘중앙 금고(데이터)’를 안전하게 지켜 안정성을 책임진다. 언제든 사라져도 괜찮은 것과 절대 사라지면 안 되는 것을 분리했기 때문에, 시스템은 흔들림 없이 운영될 수 있었다.

보고서에 적힌 HPA, RDS MySQL까지 연결했다는 문장은 이제 기술의 나열이 아니었다. ‘애플리케이션의 상태 변화(Stateless)를 처리하는 부분과 상태 자체(Stateful)를 저장하는 부분을 분리하여, 한쪽은 유연하게 확장하고 다른 한쪽은 굳건하게 지킨다’는 시스템의 핵심 설계 사상을 담은 문장으로 읽혔다.

“알겠다… 시스템의 탄력성과 데이터의 영속성을 동시에 확보하는 방법이었구나. 이제 어떤 시스템을 보더라도 ‘어떤 부분이 늘었다 줄었다 하는가?’ 그리고 ‘가장 중요한 데이터는 어디에 보관되는가?’ 이 두 가지를 나눠서 확인하면 되겠어.”

솔라는 시스템의 유연성과 안정성을 평가할 수 있는 자신만의 ‘확장/지속성 평가기’를 얻은 기분이었다. 하지만 새로운 그림을 그리려던 그녀의 펜이 잠시 멈칫했다.

“좋아. 시스템이 스스로를 치유하고 확장하는 원리는 알겠어. 그런데… 이게 정말 잘 작동하고 있는지 우리는 어떻게 알지? 우리가 이렇게 직접 들여다보지 않는 이상, 밤사이에 파드가 100번이나 죽었다 살아났는지, 데이터베이스 연결에 자꾸 문제가 생기는지 어떻게 알아챌 수 있는 거야? 이 똑똑한 자동화 시스템을 믿고, 언제 다음 버전으로 업데이트해도 괜찮다고 판단할 수 있는 근거는 어디서 찾아?”

5장: 관측과 승인, 운영의 최종 방어선

솔라의 노트에는 이제 꽤 복잡한 그림이 그려져 있었다. 코드 변경에서 시작해 이미지로 변신하고, EKS 클러스터 안에서 파드로 실행되며, 로드밸런서를 통해 외부에 공개되고, HPA와 RDS 덕분에 스스로 확장하고 데이터를 지키는 모습까지. 마치 잘 설계된 자동 기계처럼 모든 것이 유기적으로 연결된 흐름도였다. 하지만 솔라는 그 그림 위에 손바닥을 얹고는 깊은 생각에 잠겼다. 이 완벽해 보이는 자동화가 오히려 불안의 근원이었다.

솔라는 프로젝트 보고서의 마지막 항목들을 노려보았다. ‘CloudWatch를 이용한 모니터링’, ‘CodePipeline을 이용한 수동 승인’. 그녀에게 이 항목들은 잘 돌아가는 기계에 억지로 붙여놓은 사족처럼 느껴졌다. 기계가 알아서 잘 돌아가는데 굳이 들여다보는 건 귀찮은 일이고, 자동으로 진행되는 배포를 일부러 멈추는 건 속도를 저해하는 불필요한 절차라고 생각했다. ‘CloudWatch는 단순 로그 수집 도구, 수동 승인은 개발 속도를 늦추는 불필요한 절차다.’ 이 생각은 지난 며칠간 얻은 깨달음 속에서도 여전히 굳건한 바위처럼 남아 있었다.

“언니, 시스템이 알아서 확장하고 스스로 문제를 해결하는 건 알겠어. 그런데… 그게 오히려 무서워.”

솔라의 말에 루나가 조용히 고개를 들었다.

“마치 운전자 없는 자동차 같아. 목적지까지 잘 가고는 있는데, 가는 동안 타이어에 펑크가 났다가 스스로 메웠는지, 엔진 오일이 잠깐 샜다가 막혔는지 운전자는 아무것도 모르는 거야. 이 차를 믿고 다음 목적지를 입력해도 되는지 어떻게 판단해? 근거가 없잖아.”

이전까지의 질문이 ‘어떻게 작동하는가’에 대한 것이었다면, 이제는 ‘어떻게 믿을 수 있는가’에 대한 질문이었다.

루나는 솔라의 ‘운전자 없는 자동차’ 비유를 잠시 곱씹어 보았다. 그리고는 AWS 콘솔에서 CloudWatch 서비스 화면을 열었다.

“운전석에 앉으면 계기판이 보이잖아. 우리 시스템의 계기판은 여기 있어.”

루나가 클릭한 것은 ‘컨테이너 인사이트(Container Insights)’ 대시보드였다. 화면에는 m-6th-t24-cluster의 상태를 보여주는 다채로운 그래프와 표들이 나타났다. 단순한 텍스트 로그의 나열이 아니었다. 클러스터, 노드, 파드, 서비스 단위로 CPU와 메모리 사용량이 실시간 그래프로 그려지고 있었고, 네트워크 사용량과 함께 각 파드가 몇 번이나 재시작되었는지(Pod restart count)가 선명한 숫자로 표시되어 있었다.

솔라는 마우스를 움직여 dev 네임스페이스의 파드 목록을 유심히 살폈다. 다행히 재시작 횟수는 모두 ‘0’이었지만, 만약 밤사이에 문제가 생겨 특정 파드가 수십 번 재시작되었다면 그 기록이 여기에 고스란히 남을 터였다.

“아… 이건 그냥 로그 창고가 아니었구나. 시스템의 건강검진 결과표네.”

솔라의 입에서 탄성이 터져 나왔다. 보이지 않던 자동차 내부의 상태가 계기판에 명확한 수치와 그래프로 드러난 순간이었다. ‘관측’은 수동적인 기록 수집이 아니라, 시스템의 건강 상태를 능동적으로 진단하는 행위였던 것이다.

“좋아, 이제 차의 상태를 알려주는 계기판은 찾았어.”

솔라가 말하자, 루나는 기다렸다는 듯 다른 탭에 열어 두었던 CodePipeline 화면을 보여주었다. main 브랜치, 즉 실제 운영 환경으로 배포하는 파이프라인이었다. ‘Build’와 ‘DeployToDev’ 단계는 초록불이 켜져 있었지만, 그 다음 단계인 ‘ManualApprovalProd’는 주황색 불과 함께 ‘진행 중(In Progress)’ 상태로 멈춰 있었다. 그리고 그 아래에는 ‘검토(Review)’라는 이름의 파란색 버튼이 깜빡이고 있었다.

“자, 의사 선생님. 환자(시스템)의 건강검진 결과(CloudWatch)는 확인했어. 이제 새로운 처방(코드 변경)을 실제 환자에게 적용할지 최종 결정을 내려야 할 때야. 저 버튼을 누를 거야, 말 거야?”

그 순간, 솔라의 머릿속을 가로막고 있던 마지막 바위가 깨지는 소리가 들렸다. 수동 승인은 흐름을 방해하는 장애물이 아니었다. 그것은 자동화된 파이프라인 위에서 인간이 책임을 지고 결정을 내리는, 가장 중요하고 유일한 ‘관문’이었다. 그리고 그 결정을 내리기 위한 근거는 바로 조금 전 확인한 CloudWatch 계기판에 있었다.

보고서에 나열되어 있던 CloudWatch Container Insights, Manual Approval까지 연결했다는 문장은 이제 분리된 두 기능의 목록이 아니었다. 그것은 ‘시스템의 상태를 객관적인 데이터로 관측하고, 그 근거를 바탕으로 운영 배포 여부를 신중하게 승인한다’는, 자동화된 흐름의 최종 방어선이자 가장 책임감 있는 운영 절차를 설명하는 하나의 완결된 문장이었던 것이다.

솔라는 자신의 노트를 펼쳤다. 처음 미니프로젝트를 시작했을 때 그렸던, 섬처럼 흩어져 있던 기술들의 이름. CI/CD, EKS, Auto Scaling, CloudWatch… 그녀는 그 페이지를 찢어 버리고 새 페이지에 다시 그림을 그리기 시작했다.

코드 변경(GitHub)에서 시작된 화살표는 빌드/이미지 생성(CodePipeline)을 거쳐 이미지 저장소(ECR)로 향했다. 다음 화살표는 배포(EKS)를 가리켰고, EKS 상자 안에는 확장(HPA)안정성(RDS)이 서로를 떠받치고 있었다. EKS에서 뻗어 나온 화살표는 외부 공개(LoadBalancer)로 이어졌다.

그리고 가장 중요한 마지막 단계. 솔라는 EKS 상자에서 굵은 화살표를 뽑아 관측(CloudWatch)으로 연결했다. 그리고 관측에서 나온 화살표는 사람 아이콘과 함께 그려진 승인(Manual Approval) 관문을 통과했다. 이 관문을 통과한 최종 화살표만이 운영 환경 배포라는 목적지에 도달했다. 모든 기술이 하나의 거대한 강물처럼, 시작부터 끝까지 막힘없이 그러나 신중하게 흘러가는 완벽한 흐름도였다.

“이제야 알겠다.”

솔라는 완성된 흐름도를 보며 나지막이 말했다.

“이건 그냥 기술의 체크리스트가 아니었어. 코드 한 줄의 변화가 사용자에게 안전하게 도달하기까지의, 처음부터 끝까지 이어지는 하나의 길이었네. 그리고 나는 이제 이 길 전체를 읽을 수 있어.”

안개는 완전히 걷혔다. 솔라의 손에는 이제 어떤 복잡한 시스템이라도 그 시작과 끝, 그리고 그 안의 모든 연결고리를 추적하고 이해할 수 있는 자신만의 ‘운영 건전성 평가기’가 들려 있었다.