CI/CD Practice 07

CodeDeploy 배포, 그 '보이지 않는 착륙장'의 비밀

배포 대상 서버를 만들었는데 왜 IAM Role, agent 설치, nginx 준비가 따로 필요한지 클릭 절차처럼 보인다.

근거 · CI/CD 교안 p98-p115

CodeDeploy 배포, 그 '보이지 않는 착륙장'의 비밀 대표 이미지

1장: 루나: EC2는 왜 CodeDeploy에게 ‘신분증’을 보여줘야 할까?

CodeDeploy 배포, 그 ‘보이지 않는 착륙장’의 비밀

1. 루나: EC2는 왜 CodeDeploy에게 ‘신분증’을 보여줘야 할까?

“좋아, 이제 EC2 인스턴스도 준비됐고, 배포할 코드 묶음도 S3에 올려뒀어. CodeDeploy 설정 화면에서 이 둘을 연결만 해주면 되겠지?”

솔라의 손가락이 경쾌하게 노트북 트랙패드를 오갔다. 막 생성된 EC2 인스턴스가 AWS 콘솔에서 초록 불을 밝히며 대기 중이었다. 솔라의 머릿속 배포 시나리오는 단순했다. 비행기(애플리케이션 코드)가 있고, 활주로(EC2 인스턴스)가 있으니, 관제탑(CodeDeploy)에서 ‘착륙 허가’ 버튼만 누르면 끝나는 일이라고 생각했다.

온라인 가이드에서 본 ‘EC2 인스턴스에 IAM 역할을 부여합니다’라는 문장이 잠시 스쳐 지나갔지만, 대수롭지 않게 여겼다. ‘권한이야 당연히 내 계정으로 하는 거니까 문제없겠지. 일단 해보고 안 되면 그때 추가하자.’ 솔라는 배포 그룹 설정을 마치고 ‘배포 생성’ 버튼을 자신 있게 눌렀다.

배포 상태를 나타내는 표시등이 ‘진행 중’으로 바뀌는가 싶더니, 순식간에 붉은색 ‘실패’로 돌변했다. 1분도 채 걸리지 않았다.

“어? 왜?”

솔라는 ‘이벤트 보기’ 링크를 눌러 배포 실패의 원인을 찾아 나섰다. 여러 단계 중 첫 번째 단계부터 에러가 떠 있었다. 복잡한 에러 메시지 속에서 유독 한 단어가 눈에 들어왔다. ‘AccessDenied’.

“접근 거부? 내가 내 계정에 있는 EC2에 배포하는데 누가 뭘 거부한다는 거야?”

솔라의 미간이 좁아졌다. 분명 모든 건 자신의 AWS 계정 안에서 일어나고 있는 일이었다. CodeDeploy도, EC2도 모두 자신의 소유였다. 마치 자기 집 안방에 택배를 배달시키는데, 현관문에서부터 ‘출입 권한이 없습니다’라는 말을 들은 것처럼 황당했다.

그때, 소파에서 책을 읽던 루나가 솔라의 혼잣말을 듣고는 조용히 다가왔다.

“뭐가 잘 안 돼?”

“언니, 이상해. CodeDeploy로 배포를 걸었는데, EC2에 접근을 못 한대. 권한이 없다고. 아니, 이게 다 내 건데 왜 권한이 필요하다는 건지 모르겠어.”

루나는 솔라의 노트북 화면에 떠 있는 붉은색 에러 메시지를 잠시 들여다보더니, 의자 하나를 끌어와 옆에 앉았다.

“음… 솔라 네가 ‘아마존 아파트’의 건물주라고 상상해볼까?”

“아마존 아파트?”

“응. 그리고 CodeDeploy는 네가 고용한 아주 특별한 ‘코드배송’ 전문 업체야. 방금 솔라 네가 한 일은, 코드배송 업체에게 ‘101호(EC2 인스턴스)에 이 택배(애플리케이션 코드) 좀 배달해주세요’라고 전화로 주문한 것과 같아.”

“그렇지. 내가 주문했지.” 솔라가 고개를 끄덕였다.

“자, 이제 코드배송 업체 직원이 101호로 가기 위해 아파트 정문에 도착했어. 그런데 정문 경비원이 앞을 막아서는 거야. ‘누구신데 이 아파트에 들어오려 하십니까? 101호에 방문할 수 있다는 증명서가 필요합니다.’”

“증명서? 내가 이미 전화로 시켰는데?”

“경비원은 네 전화를 받지 못했어.” 루나가 차분하게 말을 이었다. “경비원은 오직 공식적인 절차와 증표만 믿지. 건물주인 네가 직접 ‘이 코드배송 업체는 우리 아파트 101호에 출입해도 좋습니다’라고 서명한 공식 출입증을 발급해서, 그걸 101호 문 앞에 붙여놓거나 경비실에 맡겨 둬야만 하는 거야.”

솔라의 눈이 동그래졌다.

“그 출입증이 없으면, 코드배송 업체는 건물주가 보냈다고 아무리 주장해도 아파트 현관조차 통과할 수 없어. AWS의 서비스들도 똑같아. 기본적으로 모든 서비스는 서로 완벽히 격리된 별개의 존재거든. 네 계정 안에 있다고 해서 CodeDeploy가 마음대로 EC2에 접근할 수 있는 게 아니야.”

“아…!”

솔라는 그제야 뭔가 깨달았다는 듯 탄성을 내뱉었다.

“그럼 아까 스쳐 지나갔던 ‘IAM 역할’이라는 게 바로 그 공식 출입증이구나! 내가 CodeDeploy에게 주는 게 아니라, EC2 인스턴스, 그러니까 101호에 붙여두는 ‘이 배송 업체는 들어와도 좋습니다’라는 증표 같은 거네!”

“바로 그거야. 정확히는 ‘101호(EC2)는 건물주(솔라)의 허락을 받아 코드배송(CodeDeploy) 서비스의 요청을 신뢰하고 따르도록 지정되었습니다’라는 신분증을 EC2 스스로 지니게 되는 거지.”

루나의 설명에 솔라의 머릿속에서 흩어져 있던 조각들이 맞춰졌다. IAM 역할은 CodeDeploy라는 서비스가 특정 EC2 인스턴스에 접근하고 명령을 내릴 수 있도록, EC2 인스턴스 스스로가 AWS 세계에 증명해 보이는 ‘서비스 접근 권한’이었다. 배포 주체인 내가 아니라, 배포 대상인 EC2가 가져야 할 자격 증명이었던 것이다. 이것이 바로 CodeDeploy 배포를 위한 ‘보이지 않는 착륙장’의 첫 번째 필수 조건이었다.

“알겠어. 그럼 이제 그 ‘신분증’을 발급해주고 다시 해볼게!”

솔라는 곧장 IAM 콘솔로 달려가 CodeDeploy를 위한 역할을 생성하고 EC2 인스턴스에 연결했다. 그리고 다시 배포 버튼을 눌렀다. 이번에는 아까와 달랐다. 첫 번째 단계였던 접근 권한 확인 단계가 초록불로 바뀌며 순조롭게 통과했다.

“됐다! 역시 신분증이 문제였어.”

안도의 한숨을 내쉬는 솔라. 하지만 그 기쁨도 잠시, 배포 과정은 다음 단계로 넘어가더니 이내 아무런 진전 없이 시간만 흘려보내기 시작했다. 마치 아파트 정문은 통과했는데, 정작 101호 문 앞에서 아무리 벨을 눌러도 안에서 아무도 나오지 않는 듯한 침묵이었다. 잠시 후, 콘솔 화면에는 새로운 실패 메시지가 떠올랐다.

솔라는 다시 고개를 갸웃거렸다.

“분명 출입증은 보여줬는데… 이제는 또 뭐가 문제인 걸까? 문 안에서 누가 받아줘야 하는 거 아냐?”

2장: 루나: IAM 역할로는 부족해, 누가 EC2 안에서 움직이지?

붉은색 ‘실패’ 메시지가 또다시 솔라의 화면을 채웠다. 하지만 이번에는 양상이 조금 달랐다. 솔라는 ‘이벤트 보기’를 눌러 배포 단계들을 꼼꼼히 살폈다. 지난번 실패의 원인이었던 첫 단계는 이제 선명한 초록불과 함께 ‘Succeeded’ 상태였다. CodeDeploy 서비스가 EC2 인스턴스에 접근하는 데는 성공한 것이다.

문제는 그 다음 단계였다. ‘ApplicationInstall’이라는 항목이 몇 분 동안 ‘Pending’ 상태로 머물러 있다가 결국 ‘Failed’로 바뀌었다. 제한 시간을 초과했다는 메시지가 보였다. 마치 배달원이 아파트 정문은 무사히 통과했지만, 정작 배달할 집 문 앞에서 하염없이 기다리다 지쳐 돌아간 모양새였다.

“분명 안으로 들어갈 허락은 받았는데… 왜 일을 안 하는 걸까?”

솔라는 혼란스러웠다. IAM 역할로 ‘신분증’을 발급해 줬으면, CodeDeploy가 알아서 EC2 인스턴스 내부로 파일을 복사하고 명령어를 실행하는 것까지 전부 처리해야 하는 것 아닌가? 그녀는 IAM 역할이 EC2 내부를 직접 제어하는 만능 열쇠라고 생각하고 있었다.

“언니, 또 막혔어. 이번엔 접근 거부는 아닌데, 배포 과정이 그냥 멈춰버렸어. 시간 초과래.”

솔라의 투덜거림에 루나가 다시 화면을 들여다봤다.

“음, 아까 그 ‘아마존 아파트’ 비유를 계속해볼까? 코드배송 업체 직원이 101호 문 앞까지는 성공적으로 도착했어. 그리고 초인종을 눌렀지. ‘CodeDeploy에서 배송 왔습니다!’ 하고.”

“응, 그런데 아무도 안 나와서 실패한 거잖아. 내 생각엔 그 초인종 소리를 못 듣는 것 같아.”

“정확해. 101호 집 안에 아무도 없거나, 있어도 그 초인종 소리에 반응하도록 훈련된 사람이 없는 거야.”

루나는 솔라의 노트북 옆에 메모장을 펼쳐 아파트 그림을 간단히 그렸다. 아파트 정문 옆에는 ‘IAM 역할 (출입 허가)’라고 적었다. 그리고 101호 집 안에는 물음표가 찍힌 사람 모양 아이콘을 그렸다.

“CodeDeploy 서비스는 AWS라는 거대한 시스템의 외부에 있는 존재야. IAM 역할은 그 외부 서비스가 EC2라는 독립된 공간에 ‘접근’하고 ‘신호를 보낼’ 수 있게 해주는 허가증일 뿐이야. EC2 인스턴스라는 컴퓨터의 운영체제 안으로 직접 들어와서 파일을 만들고, 명령어를 실행하는 것과 같은 ‘내부 작업’을 직접 수행하지는 않아.”

“직접 안 한다고? 그럼 누가 해?”

“바로 그 집 안에 있는 ‘집사’가 하는 거지.” 루나가 101호 안의 물음표 아이콘을 동그라미로 감쌌다. “CodeDeploy의 배송 신호를 전문적으로 받고 처리하기 위해, 101호 집 안에 미리 고용해 둔 상주 인력이 필요해. 이 집사는 항상 귀를 기울이고 있다가 ‘CodeDeploy 배송’이라는 특별한 초인종 소리가 들리면 즉시 문을 열고, 택배(코드 묶음)를 받아서, 지정된 위치(디렉터리)에 풀어놓고, 조립 설명서(배포 스크립트)에 따라 설치까지 마치는 임무를 수행해.”

솔라의 눈이 번쩍 뜨였다.

“아! 그럼 내 EC2 인스턴스 안에는 지금 그 ‘집사’가 없다는 거구나! 그냥 텅 빈 집이었던 거야!”

“확인해볼까? 그 집사의 정식 명칭은 ‘CodeDeploy 에이전트’야. EC2 인스턴스에 접속해서 집사가 근무 중인지 확인해봐.”

솔라는 곧바로 터미널을 열어 자신의 EC2 인스턴스에 접속했다. 그리고 루나가 알려준 명령어를 입력했다.

sudo service codedeploy-agent status

결과는 명료했다.

codedeploy-agent: unrecognised service

“세상에, 그런 서비스는 아예 존재하지도 않는대. 집사가 고용되지도 않았던 거네!”

이제야 모든 것이 명확해졌다. IAM 역할은 CodeDeploy 서비스가 EC2 인스턴스라는 ‘문’을 두드릴 수 있게 해주는 ‘외부의 권한’이었다. CodeDeploy 에이전트는 그 노크 소리를 듣고 문을 열어 실제 작업을 수행하는 ‘내부의 실행자’였다. 이 둘은 완전히 다른 차원의 역할을 수행하며 협력해야만 했던 것이다. ‘보이지 않는 착륙장’을 만들기 위한 두 번째 필수 조건, 바로 활주로 위에서 비행기를 유도하고 화물을 내릴 ‘지상 근무 요원’의 존재였다.

“알았어! 이제 진짜 알겠어. 착륙 허가(IAM 역할)와 착륙 유도원(에이전트)은 둘 다 있어야 하는 거였어.”

솔라는 의욕에 불타올랐다. 그녀는 기존 인스턴스를 종료하고, 이번에는 처음부터 CodeDeploy 에이전트를 설치하는 명령어를 ‘사용자 데이터(User Data)’ 스크립트에 포함하여 새 EC2 인스턴스를 생성했다. 인스턴스가 부팅되면서 집사가 자동으로 고용되고 근무를 시작하도록 만든 것이다.

새 인스턴스에 IAM 역할을 부여하고, 다시 CodeDeploy 배포를 실행했다. 솔라는 숨죽여 화면을 지켜봤다. 초록불이 하나씩 켜지기 시작했다. 접근 권한 확인, 코드 다운로드, 그리고 지난번 실패했던 ‘ApplicationInstall’ 단계까지 막힘없이 통과했다. 모든 단계가 ‘Succeeded’로 바뀌며 마침내 ‘배포 성공’ 메시지가 화면 가득 나타났다.

“됐다! 드디어 성공이야!”

솔라는 환호하며 새 EC2 인스턴스의 퍼블릭 IP 주소를 복사해 웹 브라우저 주소창에 붙여넣었다. 멋진 결과 페이지가 그녀를 반겨줄 것이라 기대하며 엔터 키를 눌렀다.

하지만 브라우저 화면에 나타난 것은 환영 메시지가 아니었다.

연결할 수 없음.

솔라의 미소가 순식간에 굳어졌다.

“아니, 왜? 배포는 완벽하게 성공했는데. 코드도 무사히 전달됐고 설치도 끝났는데, 왜 아무것도 안 보이지? 집사가 택배를 받아서 창고에 넣어두기만 하고 가게 문은 안 연 건가?”

3장: 루나: 코드는 도착했지만, 누가 손님을 맞이할까?

브라우저 화면에는 연결할 수 없음.이라는 차가운 메시지만이 떠 있었다. 하지만 바로 옆에 열어둔 AWS 콘솔 탭에서는 모든 단계가 선명한 초록색으로 빛나며 ‘배포 성공’을 알리고 있었다. 성공과 실패라는 모순된 두 개의 창이 솔라의 노트북 화면을 양분하고 있었다.

솔라는 좌절감에 의자 등받이에 몸을 기댔다. 지난번 실패들과는 달랐다. 이번에는 명백한 오류 메시지조차 없었다. CodeDeploy는 자신의 임무를 완벽히 수행했다고 보고하는데, 정작 결과물은 보이지 않는 상황. 집사가 택배를 받아서 창고에 넣어두기만 하고 가게 문은 닫아버린 걸까. 솔라는 마우스 커서로 배포 성공 메시지를 무의미하게 클릭해 보았다.

“정확한 비유야, 솔라. 가게 문을 안 연 거지. 아니, 어쩌면 가게 자체가 아직 없는 걸지도 몰라.”

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

“가게가 없다고?”

“응. 지금 EC2 인스턴스는 멋진 상품(애플리케이션 코드)이 가득 쌓인 창고일 뿐이야. CodeDeploy 에이전트라는 성실한 창고 관리인이 물건을 받아서 잘 정리까지 해뒀지. 하지만 손님을 맞이하고, 상품을 보여주고, 판매할 ‘매장’은 아직 열지 않았어.”

루나의 말에 솔라는 고개를 갸웃거렸다. 코드 파일만 있으면 컴퓨터가 알아서 웹 페이지로 보여주는 게 아니었던가? 그녀는 코드 배포가 완료되면 애플리케이션이 자동으로 웹을 통해 서비스될 것이라고 막연히 기대하고 있었다.

“정말 창고에 물건이 잘 들어가 있는지부터 확인해볼까?” 루나가 제안했다.

솔라는 다시 터미널을 열고 EC2 인스턴스에 접속했다. CodeDeploy가 파일을 배포하기로 약속된 기본 경로, /var/www/html 디렉터리로 이동해 파일 목록을 확인했다.

ls -l /var/www/html

화면에 index.html 파일이 선명하게 나타났다. 자신이 배포했던 바로 그 파일이었다.

“여기 있네! 파일은 정확히 도착했어. 창고 관리인(에이전트)은 자기 일을 완벽하게 한 게 맞아. 그런데 왜 브라우저는 이 파일을 못 찾는 거지?”

솔라의 질문에 루나는 잠시 생각할 틈을 주더니, 다시 비유를 이어갔다.

“솔라 네가 웹 브라우저 주소창에 EC2의 IP 주소를 입력하고 엔터를 누르는 건, 그 창고 주소로 손님(HTTP 요청)을 보내는 것과 같아. ‘안녕하세요, 여기 가게 맞나요? 구경 좀 할게요!’ 하고 문을 두드리는 거지.”

“응, 그런데 아무 응답이 없는 거고.”

“컴퓨터는, 특히 서버는 약속된 방식이 아니면 절대 스스로 움직이지 않아. 손님이 80번 항구(HTTP 포트)에 도착해서 문을 두드렸을 때, 그 소리를 듣고 문을 열어줄 점원이 필요해. 그 점원은 손님의 요청을 해석해서, ‘아, 기본 페이지를 보여달라는 거구나’ 하고 창고에서 index.html 파일을 가져다 보여주는 역할까지 해야 해.”

그제야 솔라의 머릿속에 번개가 쳤다.

“아! 웹 서버! Nginx나 Apache 같은 거!”

“바로 그거야. 웹 서버가 바로 그 ‘가게 점원’이자 손님을 맞이하는 ‘매장’ 그 자체야. 웹 서버는 80번 포트 같은 특정 통로에서 항상 귀를 기울이고 있다가, 브라우저의 요청이 오면 지정된 디렉터리에서 파일을 찾아 응답해주는 전문 프로그램이지. 지금 솔라의 EC2 인스턴스는 텅 빈 땅에 창고 건물(파일)만 덩그러니 있을 뿐, 손님을 맞이할 매장 건물(웹 서버)은 아직 짓지도 않은 상태인 거야.”

솔라는 무릎을 탁 쳤다. 배포가 끝이 아니었다. 배포된 코드를 해석하고 사용자에게 서비스할 무언가가 더 필요했다.

“맞아. 우리는 그걸 ‘애플리케이션 런타임’이라고 불러.” 루나가 솔라의 깨달음을 정리해 주었다. “배포된 코드가 사용자 요청에 응답하며 실제로 살아 움직이게 만드는 실행 환경이지. 이 무대를 만드는 걸 깜빡한 거야.”

루나의 말에 모든 조각이 맞춰졌다. 이것이 바로 ‘보이지 않는 착륙장’을 완성하는 마지막 필수 구성 요소였다. 항공기가 활주로에 무사히 착륙하고(IAM 역할), 지상 요원이 화물을 내렸지만(CodeDeploy 에이전트), 그 화물을 승객들에게 전달할 ‘터미널 건물(웹 서버)’이 없었던 셈이다.

“그럼 모든 게 설명이 돼. 권한, 실행자, 그리고 실행 환경. 이 세 가지가 모두 있어야 했구나.”

솔라는 더 이상 망설이지 않았다. 그녀는 CodeDeploy 설정으로 돌아가 배포 프로세스를 정의하는 appspec.yml 파일을 열었다. 그리고 AfterInstall이라는 훅(Hook) 단계에 웹 서버(nginx)를 설치하고 실행하는 스크립트를 추가했다. 집사가 택배를 설치한 후에, 곧바로 가게 문까지 열도록 시키는 지시서였다.

다시 배포 버튼을 눌렀다. 모든 단계가 순식좇으로 초록불로 바뀌며 ‘배포 성공’ 메시지가 떴다. 솔라는 떨리는 마음으로 다시 브라우저에 EC2의 IP 주소를 입력하고 엔터 키를 눌렀다.

이번에는 달랐다. 연결할 수 없음 대신, 그녀가 작성했던 Hello, World! 메시지가 화면 중앙에 선명하게 나타났다.

솔라는 눈앞의 결과물을 보며 자신도 모르게 중얼거렸다. 그녀는 방금 겪은 세 번의 실패와 해결 과정을 머릿속에 그리며, 앞으로 CodeDeploy 배포 문제 진단에 사용할 자신만의 체크리스트를 만들고 있었다.

“좋아. 앞으로 CodeDeploy가 실패하면 이렇게 확인하는 거야.”

  1. 배포 시작부터 ‘Access Denied’가 뜬다?

    • 원인: ‘착륙 허가’가 없다.
    • 해결: EC2 인스턴스에 CodeDeploy용 **IAM 역할(서비스 접근 권한)**이 제대로 부여됐는지 확인.
  2. 배포 중간에 시간 초과로 멈춘다?

    • 원인: ‘지상 요원’이 없다.
    • 해결: EC2 인스턴스에 **CodeDeploy 에이전트(인스턴스 내부 실행기)**가 설치되고 실행 중인지 확인.
  3. 배포는 성공했는데 브라우저에서 연결이 안 된다?

    • 원인: ‘터미널 건물’이 문을 닫았다.
    • 해결: **웹 서버(애플리케이션 런타임)**가 설치되고, 올바른 경로의 파일을 서비스하도록 설정되었는지 확인.

단순히 절차를 따르는 것을 넘어, 각 요소가 왜 필요한지 몸으로 부딪혀 깨달은 순간이었다. 솔라에게 CodeDeploy 배포는 더 이상 알 수 없는 마법이 아니었다. 권한, 통신, 실행이라는 세 개의 축이 유기적으로 맞물려 돌아가는, 명확한 인과관계의 시스템으로 보이기 시작했다. ‘보이지 않는 착륙장’의 모든 비밀이 마침내 모습을 드러낸 것이다.