Mini Project 6 03

소스 코드에서 EKS 배포 단위까지: 빌드팩, 도커파일, ECR의 여정

CodeBuild가 성공했다는 말과 ECR에 이미지가 올라갔다는 말이 어떻게 같은 배포 흐름 안에서 이어지는지 흐릿하다.

근거 · 교안 빌드·이미지 배포 파트

소스 코드에서 EKS 배포 단위까지: 빌드팩, 도커파일, ECR의 여정 대표 이미지

1장: CodeBuild 성공, 그 다음은 어디로?

솔라의 모니터에 파이프라인의 모든 단계가 초록색으로 빛나고 있었다. ‘성공’. 그 단어 하나가 주는 안도감도 잠시, 솔라는 알 수 없는 찜찜함에 미간을 찌푸렸다. 소스 코드를 커밋했고, CodePipeline이 변경을 감지해 CodeBuild를 실행했다. 그리고 빌드는 성공했다.

“성공이라는데, 왜 이렇게 애매하게 느껴지지?”

혼잣말처럼 중얼거리는 솔라의 목소리에, 옆에서 조용히 자신의 코드를 보던 루나가 고개를 들었다.

“뭐가 애매해? 파이프라인은 전부 통과했잖아.”

“언니, 봐봐. CodeBuild가 성공했어. 이건 내 코드가 에러 없이 잘 빌드됐다는 뜻이잖아. 그런데 그 다음은? 우리가 최종적으로 배포할 곳은 EKS 클러스터인데, 이 ‘빌드 성공’이 어떻게 EKS 배포까지 이어지는지 흐름이 보이지 않아. 특히 CodeBuild가 만든 결과물은 어디로 갔을까? 내 노트북에서 사라진 파일처럼 증발한 건 아닐 테고.”

솔라는 마우스를 움직여 CodeBuild의 성공 로그 링크를 가리켰다. 그녀의 초기 생각은 단순했다. ‘CodeBuild 성공’은 그저 코드를 기계가 이해하는 언어로 바꾸는 과정, 즉 컴파일이 문제없이 끝났다는 신호일 뿐이라고. 그 결과물이 ECR이라는 이미지 저장소에 올라가야 한다는 사실은 알고 있었지만, ‘성공’이라는 두 글자가 그 과정까지 포함하는지, 아니면 별개의 과정인지 확신이 서지 않았다. 마치 목적지가 서울인 기차를 탔는데, 차장이 ‘출발 성공’이라고만 외치고는 대전역에 도착했는지, 부산역으로 향하고 있는지 알려주지 않는 느낌이었다.

“그럼 그 ‘성공’이 뭘 의미하는지 직접 확인해보면 되겠네.”

루나는 자신의 의자에서 일어나 솔라의 뒤에 섰다. 그리고는 모니터의 CodeBuild 로그 링크를 가리켰다.

“빌드가 뭘 했는지 가장 정직하게 보여주는 건 결과 메시지가 아니라 실행 기록이야. 저 로그를 한번 열어보자. 정말 ‘컴파일’만 하고 끝났을까?”

솔라는 잠시 망설이다 로그 링크를 클릭했다. 수백 줄의 텍스트가 화면을 가득 채웠다. 익숙한 라이브러리 다운로드 메시지와 빌드 과정들이 스쳐 지나갔다.

“이건 그냥… 빌드 로그인데. 예상했던 그대로야.”

“조금만 더 내려봐. 빌드의 마지막 단계, ‘post_build’ 근처를 한번 볼래?”

루나의 말에 솔라는 스크롤바를 거의 끝까지 내렸다. 그리고 그곳에서, 그녀의 생각을 흔드는 몇 줄을 발견했다.

...
[Container] 2023/10/26 14:30:00 Running command docker build -t <ACCOUNT_ID>.dkr.ecr.ap-northeast-2.amazonaws.com/m-6th-t24-backend:latest .
...
[Container] 2023/10/26 14:31:00 Successfully built a1b2c3d4e5f6
[Container] 2023/10/26 14:31:05 Running command docker push <ACCOUNT_ID>.dkr.ecr.ap-northeast-2.amazonaws.com/m-6th-t24-backend:latest
The push refers to repository [<ACCOUNT_ID>.dkr.ecr.ap-northeast-2.amazonaws.com/m-6th-t24-backend]
...
latest: digest: sha256:abcdef... size: 1234

“어…?”

솔라의 눈이 커졌다. 로그의 마지막 부분에는 단순히 빌드가 성공했다는 메시지뿐만 아니라, docker builddocker push 명령어가 명백하게 기록되어 있었다. 목적지는 놀랍게도 ECR 저장소 주소였다.

“이건… CodeBuild가 Docker 이미지를 만들고, 그걸 ECR에 직접 밀어 넣었다는 거잖아?”

“맞아. 우리가 본 ‘성공’은 단순히 ‘컴파일 성공’이 아니었던 거지. ‘소스 코드를 가져와서, 빌드하고, Docker 이미지로 포장한 뒤, ECR 저장소에 안전하게 배송하는 것까지 성공’이라는 의미였던 거야.”

루나는 책상 위 메모지에 간단한 흐름도를 그리기 시작했다. 소스 코드CodeBuildECR. 그리고 CodeBuild에서 ECR로 향하는 화살표 위에 작게 ‘push’라고 적었다.

솔라는 방금 자신이 본 로그와 루나가 그린 간단한 지도를 번갈아 보았다. 흩어져 있던 점들이 하나의 선으로 이어지는 순간이었다. CodeBuild의 초록불은 종착역이 아니라, 다음 단계로 넘어가는 환승역의 신호등이었다. 빌드는 결과물을 만들어내는 행위에서 끝나는 것이 아니라, 그 결과물을 다음 주자가 사용할 수 있도록 정확한 장소에 ‘전달’하는 것까지 포함된 약속이었던 것이다.

이제 솔라의 마음속 지도는 훨씬 명확해졌다. 소스 코드 커밋은 ECR이라는 이미지 창고에 새로운 버전의 ‘배포 단위’를 입고시키는 전체 과정의 시작점이었다.

“그렇구나. CodeBuild는 그냥 빌드만 하는 게 아니라, 정해진 목적지까지 물건을 배달하는 배송기사 역할까지 했던 거네. 그럼 이 배송기사는 어떤 지시를 보고 움직이는 거지? 로그에 있던 docker push 같은 명령어는 누가, 어디에 적어둔 걸까? 그 buildspec.yml이라는 파일이 이 모든 걸 지시하는 작업 지시서 같은 건가?”

솔라의 질문은 더 이상 막연한 불안감이 아니었다. 방금 찾아낸 연결고리를 바탕으로, 이제는 흐름의 다음 단계를 향한 구체적인 호기심이었다.

2장: buildspec.yml: 빌드와 ECR 푸시의 지휘자

솔라의 손가락이 키보드 위에서 잠시 멈췄다. 이전 장에서 발견한 CodeBuild 로그의 흔적을 따라, 그녀는 마침내 프로젝트의 루트 디렉터리에 있는 buildspec.yml 파일을 화면에 열었다. 어제까지만 해도 그저 수많은 설정 파일 중 하나로 보였던 이 파일이, 이제는 모든 비밀을 쥐고 있는 암호문처럼 느껴졌다. 파일의 내용은 언뜻 보기에 간단했다. 여러 명령어들이 나열되어 있었다.

솔라는 파일을 위에서 아래로 훑어보았다. echo 'Building backend...', gradle build, docker build..., 그리고 마침내 익숙한 docker push... 까지. 그녀의 첫인상은 ‘단순한 명령어 목록’이라는 생각에서 크게 벗어나지 않았다. 이 배송기사는 그저 위에서 아래로 순서대로 적힌 주소지를 따라가는 것뿐일까? 하지만 그렇다고 하기엔 파일의 구조가 어딘가 이상했다. phases라는 단어 아래로 pre_build, build, post_build 같은 낯선 이름들이 들여쓰기 되어 있었다. 마치 연극 대본의 1막, 2막, 3막처럼.

“언니, 이 buildspec.yml 파일 찾았어. 지난번에 로그에서 봤던 명령어들이 여기 다 있네. 그런데… 그냥 순서대로 적어놓은 명령어 목록 같아 보이는데, 맞아?”

솔라의 질문에 루나는 솔라의 화면을 들여다보았다. 그녀는 파일의 내용을 훑어보지 않고, 솔라가 방금 지나쳤던 구조를 손가락으로 짚었다.

“정말 그럴까? 만약 그냥 위에서 아래로 흐르는 스크립트라면, phasesbuild, post_build 같은 제목들은 왜 있을까? 마치 요리 레시피에 ‘재료 손질’, ‘볶기’, ‘담아내기’ 같은 단계가 나뉜 것처럼 보이지 않아?”

루나의 말에 솔라는 다시 화면으로 눈을 돌렸다. ‘그냥 목록’이라는 생각에 사로잡혀 무시했던 들여쓰기와 구조가 그제야 눈에 들어왔다. 그녀는 루나의 비유를 따라 파일의 내용을 ‘단계별’로 다시 읽기 시작했다.

솔라는 pre_build 섹션을 먼저 보았다. 그곳에는 ECR 저장소에 로그인하는 명령어가 있었다. ‘아, 이건 요리로 치면 오븐을 예열하는 것과 같구나. 본격적인 작업을 시작하기 전에 필요한 준비를 하는 단계네.’

version: 0.2

phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com

  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build -t $REPOSITORY_URI:latest .
      - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG

  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker images...
      - docker push $REPOSITORY_URI:latest
      - docker push $REPOSITORY_URI:$IMAGE_TAG

다음은 build 섹션이었다. 여기에는 소스 코드를 빌드하고(gradle build 같은 명령이 여기에 있을 것이다), 그 결과물을 가지고 docker build 명령을 실행하여 Docker 이미지를 만드는 작업이 정의되어 있었다. ‘재료 손질이 끝났으니, 이제 본격적으로 볶고 끓이는 메인 요리 단계구나. 여기서 비로소 ‘배포 단위’라는 완제품의 원형이 만들어지는 거였어.’

그리고 마침내 post_build 섹션. 그곳에는 지난번 로그에서 솔라의 눈을 번쩍 뜨이게 했던 docker push 명령어가 자리 잡고 있었다.

“아…!”

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

“이건 그냥 순서가 아니었어. pre_build에서 먼저 ECR에 로그인해서 길을 터놓고, build 단계에서 이미지를 만들고, 마지막으로 post_build에서 잘 만들어진 이미지를 ECR이라는 창고로 밀어 넣는 거였네! 각 단계별로 해야 할 일이 명확하게 나뉘어 있었던 거야.”

단순한 명령어의 나열이 아니었다. 이것은 CodeBuild라는 배우에게 전달되는, 아주 잘 짜인 ‘빌드 작업 지시서’였다. 각 막(phase)마다 배우가 해야 할 행동(commands)이 명확히 정의되어 있었다. ‘성공’이라는 초록불은 이 모든 막의 모든 지시가 문제없이 수행되었음을 의미하는 커튼콜이었던 셈이다.

솔라는 이제 buildspec.yml을 완전히 다른 눈으로 보게 되었다. 이것은 더 이상 암호문이 아니었다. 소스 코드가 EKS 배포 단위로 변환되는 긴 여정의 첫 번째 관문을 지휘하는 오케스트라의 악보였다.

“그래. 배송기사가 무작정 달리는 게 아니라, ‘창고 접근 권한 획득’, ‘물품 포장 및 상차’, ‘창고로 배송 출발’ 같은 명확한 단계가 적힌 운행일지를 받은 거였네. 그럼 이 지시서 덕분에 CodeBuild는 자기가 뭘 해야 할지 정확히 알 수 있었던 거구나.”

솔라의 시선은 다시 build 단계의 docker build -t ... . 라인에 머물렀다. 이제 이 지시서가 전체 흐름을 어떻게 지휘하는지는 알겠다. 하지만 지휘자가 손을 흔든다고 바이올린이 저절로 소리를 내지는 않는다.

“그런데 언니, 여기 docker build라는 명령은 알겠는데, 이 명령은 정확히 뭘 보고 이미지를 만드는 거지? 이 buildspec.yml 파일은 ‘이미지를 만들어라’고 지시만 할 뿐, ‘어떻게’ 만들지에 대한 레시피는 여기에 없는 것 같아. 아마 저기 옆에 있는 Dockerfile이라는 게 진짜 레시피 아닐까?”

솔라의 호기심은 buildspec.yml이라는 지휘자를 넘어, 이제 실제 요리를 담당하는 셰프의 레시피 북, Dockerfile로 향하고 있었다.

3장: Dockerfile: 소스 코드를 컨테이너 이미지로 포장하는 법

솔라의 손가락 끝이 모니터 위 Dockerfile이라는 파일 이름을 가리킨 채 멈춰 있었다. buildspec.yml이 지휘자라면, docker build라는 지휘에 맞춰 연주하는 연주자는 누구일까. 그 해답이 바로 이 파일 안에 있을 거라는 예감이 들었다. 그녀는 파일을 더블 클릭해 열었다.

화면에 나타난 내용은 buildspec.yml보다도 더 간결해 보였다. FROM, WORKDIR, COPY, ENTRYPOINT… 영어 단어 몇 개와 경로가 전부였다. 솔라는 코드를 훑어보며 고개를 갸웃거렸다. 그녀의 첫인상은 이 파일이 단순히 ‘실행 환경을 설정하는 파일’이라는 생각에 머물렀다. 맨 첫 줄의 FROM amazoncorretto:17-alpine은 마치 텅 빈 서버에 자바 17 버전을 설치하라는 지시처럼 보였다. 그냥 필요한 소프트웨어를 설치하는 스크립트가 아닐까?

“언니, 이 Dockerfile이라는 게 buildspec.yml이 참고하는 레시피가 맞는 것 같긴 한데… 내용을 보니까 그냥 자바 실행 환경을 만드는 설정 파일 같아. 서버 세팅 스크립트 같은 거. 우리가 만든 소스 코드는 대체 어느 단계에서 여기에 합쳐지는 거지? buildspec.yml에서 소스 코드를 빌드하라고 시키고, Dockerfile에서는 그냥 자바를 준비하라고 시키면 둘이 따로 노는 거 아냐?”

루나는 솔라의 화면을 들여다보았다. 그리고는 지난번처럼, 정답을 말해주는 대신 솔라가 직접 레시피를 따라가 보도록 유도했다.

“그럴듯한 추측이네. 그럼 이 레시피를 처음부터 한 줄씩 따라가 보자. docker build라는 요리사가 이 파일을 읽는다고 상상하고. 첫 줄 FROM은 뭐라고 말하고 있어?”

“음… amazoncorretto:17-alpine 이미지를 기반으로 시작하라고. 이건 알겠어. 운영체제랑 자바 17이 이미 설치된 깨끗한 컴퓨터 한 대를 준비하는 거네.”

솔라는 마우스를 스크롤하며 다음 줄로 시선을 옮겼다. 루나는 아무 말 없이 솔라가 스스로 단계를 밟아나가길 기다렸다.

솔라는 backend 프로젝트의 Dockerfile을 열어, 루나의 말대로 요리사가 된 것처럼 한 줄씩 그 의미를 곱씹어보기 시작했다.

# 1. 베이스 이미지: 운영체제와 자바 17이 설치된 환경
FROM amazoncorretto:17-alpine

# 2. 작업 공간 만들기
WORKDIR /app

# 3. 소스 코드 빌드 결과물(.jar 파일)을 이미지 안으로 복사
COPY build/libs/*.jar app.jar

# 4. 컨테이너가 시작될 때 실행할 명령어
ENTRYPOINT ["java", "-jar", "app.jar"]

FROM은 깨끗한 컴퓨터. WORKDIR /app은 그 컴퓨터 안에 /app이라는 작업 폴더를 만드는 것. 여기까지는 여전히 서버 초기 설정의 범주에서 벗어나지 않았다. 하지만 세 번째 줄에 이르렀을 때, 솔라의 눈빛이 흔들렸다.

COPY build/libs/*.jar app.jar

“잠깐만… COPY?”

솔라는 이 한 단어에 모든 것이 담겨 있음을 직감했다. build/libs/*.jar라는 경로는 낯이 익었다. buildspec.ymlbuild 단계에서 gradle build 명령이 실행되면 생성되는, 바로 그 자바 애플리케이션의 빌드 결과물이었다. 이 명령어는 그 결과물인 .jar 파일을 이미지 안의 /app 폴더로, app.jar라는 이름으로 ‘복사’하라는 뜻이었다.

“아…!”

퍼즐 조각이 맞춰지는 소리가 머릿속에서 울렸다. Dockerfile은 단순히 환경만 설정하는 스크립트가 아니었다. 그것은 외부의 재료, 즉 컴파일된 내 애플리케이션 코드를 가져와 준비된 환경 안에 집어넣는, 명백한 ‘조립’ 과정을 포함하고 있었다. 마지막 ENTRYPOINT 명령어는 이 조립된 완제품의 전원 버튼과 같았다. 이 이미지를 실행하면 java -jar app.jar 명령을 내려서 방금 복사한 내 애플리케이션을 구동시키라는 의미였다.

솔라는 이제 Dockerfile을 완전히 새로운 시선으로 바라보았다. 이것은 단순한 설정 파일이 아니라, 하나의 완제품을 만들기 위한 ‘컨테이너 패키징 명세서’였다. 어떤 종류의 빈 상자(FROM)를 가져와서, 그 안에 무엇을(COPY), 어떻게 담고(WORKDIR), 마지막으로 어떻게 켜는지(ENTRYPOINT)까지 모든 포장 과정과 사용법을 기록한 상세한 설명서. buildspec.yml이 “백엔드 이미지 만들어!”라고 외치면, docker build라는 작업자는 바로 이 명세서를 보고 소스 코드 빌드 결과물을 가져와 런타임 환경과 함께 하나의 완결된 상자로 포장하고 있었던 것이다.

“그랬구나. buildspec.yml은 ‘이미지를 만들어라’고 지시만 내리는 게 맞았어. ‘어떻게’ 만들지에 대한 구체적인 방법은 전부 이 Dockerfile이라는 레시피에 적혀 있었던 거네. 소스 코드가 이 COPY 명령어를 통해 비로소 ‘배포 단위’인 이미지의 일부가 되는 거였어.”

이제 소스 코드가 어떻게 독립적으로 실행 가능한 이미지로 변환되는지에 대한 의문이 풀렸다. 흐름의 연결고리가 하나 더 명확하게 이어졌다.

솔라의 시선은 다시 buildspec.ymlpost_build 단계로 향했다. 그곳에는 이렇게 정성껏 포장된 이미지를 ECR 저장소로 밀어 넣는 docker push 명령이 있었다.

“좋아, 이제 이 레시피로 완벽한 배송 상자를 만들었어. 그리고 작업 지시서에 따라 이 상자를 ECR이라는 창고로 보낸다는 것도 알아. 그런데 CodeBuild는 어떻게 ECR 창고의 문을 열고 들어가는 거지? 아무나 물건을 막 집어넣을 순 없을 텐데. pre_build 단계에서 로그인하는 걸 보긴 했지만… 그 인증 과정은 어떻게 안전하게 이루어지는 걸까?“

4장: ECR: 빌드된 이미지를 위한 안전한 저장소

솔라의 시선은 AWS 콘솔의 ECR(Elastic Container Registry) 화면에 머물러 있었다. 화면에는 m-6th-t24-backendm-6th-t24-frontend라는 이름의 저장소 두 개가 나란히 놓여 있었다. 마치 깔끔하게 정리된 두 개의 폴더 같았다. 이전 단계에서 Dockerfile이라는 레시피로 정성껏 포장한 이미지 상자가 바로 이곳에 보관된다. 하지만 볼수록 이상한 기분이 들었다.

단순해 보이는 이 ‘폴더’에 아무나 파일을 올릴 수는 없을 것이다. 솔라는 이 저장소가 마치 특별한 회원만 출입할 수 있는 창고와 같을 것이라고 생각했다. 그렇다면 CodeBuild는 어떻게 회원증을 보여주고 이 창고에 들어갔을까? buildspec.yml 파일의 pre_build 단계에서 로그인 명령을 본 기억이 났지만, 그게 전부라면 어딘가 허술하게 느껴졌다. 그녀의 머릿속 ECR은 그저 이미지를 보관하는, 조금 더 똑똑한 파일 서버와 다르지 않았다. 그리고 파일 서버에 접근하려면 아이디와 비밀번호가 필요하다.

“언니, 이거 좀 이상해. CodeBuild가 ECR에 이미지를 푸시하려면 로그인을 해야 하잖아. buildspec.yml에도 로그인 명령이 있고. 그럼 CodeBuild는 ECR의 비밀번호를 어딘가에 저장해두고 쓴다는 건데, 빌드 스크립트에 비밀번호를 그대로 적어놓는 건 너무 위험하지 않아? 마치 창고 열쇠를 창고 문 앞에 걸어두는 거나 마찬가지잖아.”

솔라의 질문은 ECR을 단순한 파일 저장소로 보는 시각에서 출발한, 지극히 합리적인 의문이었다. 중요한 비밀번호를 평문으로 코드에 노출하는 것은 보안의 기본 원칙을 위배하는 일이었다.

루나는 솔라의 질문을 듣고 잠시 생각에 잠기더니, 솔라의 화면 대신 자신의 터미널 창을 열었다.

“그럼 그 ‘비밀번호’라는 걸 우리가 직접 한번 받아볼까? CodeBuild가 아니라, 지금 네가 쓰는 이 노트북에서 말이야. buildspec.ymlpre_build 단계에 있던 그 명령어를 그대로 한번 실행해보는 거지.”

루나는 buildspec.yml 파일에서 봤던 명령어 한 줄을 솔라에게 보여주었다.

aws ecr get-login-password --region ap-northeast-2

“CodeBuild가 했던 것처럼, 우리도 ECR에 로그인하기 위한 비밀번호를 달라고 AWS에 직접 요청해보는 거야.”

솔라는 고개를 갸웃거리며 자신의 터미널에 그 명령어를 그대로 입력하고 엔터를 쳤다. 잠시 후, 화면에 아주 길고 복잡한 문자열이 나타났다. 알파벳 대소문자와 숫자가 무작위로 뒤섞인, 도저히 사람이 외울 수 없는 형태였다.

“이게… 비밀번호라고? 너무 긴데. 이걸 어디다 복사해서 쓰는 건가?”

“한 번 더 실행해볼래? 바로 이어서.”

루나의 말에 솔라는 키보드의 위쪽 화살표 키를 눌러 방금 실행한 명령어를 다시 불러온 뒤 엔터를 쳤다. 화면에는 또다시 긴 문자열이 출력되었다. 그런데 무언가 달랐다.

솔라는 눈을 가늘게 뜨고 두 개의 문자열을 비교했다. 첫 번째와 두 번째 문자열은 완전히 다른 값이었다.

“어? 비밀번호가 바뀌었어! 실행할 때마다 매번 다른 값을 주네.”

그 순간 솔라의 머릿속을 스치는 깨달음이 있었다. 그녀가 ‘비밀번호’라고 생각했던 것은 고정된 열쇠가 아니었다. 이것은 매번 새로 발급되는, 딱 한 번만 짧은 시간 동안 유효한 ‘임시 출입증’에 가까웠다.

루나가 조용히 설명을 덧붙였다.

“맞아. ECR은 고정된 비밀번호를 사용하는 방식이 아니야. CodeBuild 같은 신뢰할 수 있는 서비스가 IAM 역할(Role)이라는 권한을 가지고 ‘지금 ECR에 들어가고 싶어’라고 요청하면, AWS는 그 요청이 유효한지 확인하고 아주 짧은 시간만 쓸 수 있는 임시 인증 토큰을 발급해줘. aws ecr get-login-password 명령어가 바로 그 토큰을 받아오는 역할을 하는 거고. CodeBuild는 이 임시 토큰으로 ECR에 로그인해서 이미지를 푸시한 다음, 그 토큰을 그냥 버리는 거야. 다음 빌드가 실행되면 또 새로운 토큰을 받아서 쓰고.”

이제 모든 조각이 맞춰졌다. ECR은 아무나 드나드는 파일 서버가 아니었다. 엄격한 신원 확인 절차를 거쳐, 매번 임시 출입증을 발급받아야만 들어갈 수 있는 보안 구역이었다. CodeBuild와 ECR 사이의 연결은 허술한 열쇠가 아니라, 실시간으로 신원을 확인하고 권한을 부여하는 안전한 통신 채널이었던 것이다. 이것이 바로 이미지 저장소 연결점의 진짜 모습이었다.

솔라는 자신이 그린 흐름도 위에, CodeBuild에서 ECR로 향하는 화살표를 굵게 덧칠했다. 그리고 그 위에 ‘임시 토큰 인증’이라고 작게 적었다. 더 이상 불안한 연결고리가 아니었다.

“그렇구나. 창고 열쇠를 문 앞에 걸어둔 게 아니라, 신분증을 보여주고 매번 새로운 출입 카드를 발급받는 시스템이었네. 훨씬 안전하네.”

이제 완벽하게 포장된 이미지 상자가 어떻게 안전한 창고에 보관되는지 명확히 이해했다. 소스 코드에서 시작된 여정의 중요한 한 단계가 선명해졌다. 솔라의 시선은 자연스럽게 흐름도의 다음 단계로 옮겨갔다.

“좋아, 이제 안전한 ECR 창고에 최신 버전의 배포 단위가 잘 도착했어. 그럼 이 창고에 있는 물건은 최종 목적지인 EKS 클러스터에서 어떻게 가져다 쓰는 거지? EKS도 CodeBuild처럼 매번 신분증을 보여주고 임시 출입증을 발급받아서 이미지를 꺼내 오는 걸까?“

5장: EKS: ECR 이미지, 배포의 최종 목적지

솔라의 책상 위, 이전 장들에서 하나씩 이어 붙인 흐름도 메모가 놓여 있었다. 소스 코드에서 시작해 CodeBuild를 거쳐 ECR이라는 안전한 창고에 도착하기까지의 여정. 이제 마지막 한 조각, EKS로 향하는 길만 남아 있었다. 솔라는 그 마지막 연결고리를 찾기 위해, EKS에 애플리케이션을 배포할 때 사용하는 deployment.yaml 파일을 화면에 띄워 놓았다.

그녀의 예상은 이랬다. CodeBuild가 ECR에 로그인할 때 임시 토큰을 사용했듯, EKS 역시 ECR에서 이미지를 가져오려면 복잡하고 안전한 인증 절차를 거칠 것이다. 어쩌면 이 배포 명세서 어딘가에 get-token 같은 명령어나 숨겨진 인증 블록이 있을 거라고 생각했다. 하지만 화면에 떠 있는 YAML 파일은 너무나도 담백했다. 설정값들 사이에서 눈에 띄는 image라는 키워드가 있었지만, 그 값은 그저 ECR 이미지 주소 한 줄이 전부였다.

“이게 다라고? 그냥 주소만 적혀있잖아.”

솔라는 혼란스러웠다. 그녀가 보기에 이건 마치 보안문이 설치된 창고 주소만 덜렁 적어놓고, ‘이 주소로 가서 물건을 가져오세요’라고 말하는 것과 같았다. 어떻게 문을 열지에 대한 설명은 어디에도 없었다. EKS는 어떤 이미지든 이름만 알면 마음대로 가져올 수 있는 만능 열쇠라도 가진 걸까?

“언니, 마지막 단계에서 막혔어. EKS 배포 파일에는 그냥 ECR 이미지 주소만 덩그러니 있는데, 이걸로 어떻게 이미지를 가져오지? CodeBuild처럼 EKS도 임시 출입증을 발급받아서 ECR 창고에 들어가는 거 아니었어? 그런 명령어는 어디에도 안 보여.”

솔라의 질문에 루나는 솔라의 화면을 함께 들여다보았다. 그녀는 복잡한 시스템 아키텍처 다이어그램 대신, 솔라가 보고 있는 바로 그 YAML 파일의 한 부분을 손가락으로 가리켰다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
      - name: backend-container
        image: <ACCOUNT_ID>.dkr.ecr.ap-northeast-2.amazonaws.com/m-6th-t24-backend:latest
        ports:
        - containerPort: 8080

“바로 그 image 필드가 맞아. 그런데 솔라, 한번 다시 생각해 봐. CodeBuild는 ‘빌드 환경’ 그 자체가 ECR에 접근할 권한이 필요했지. EKS는 조금 달라. EKS라는 시스템 전체가 아니라, 실제로 ECR에서 이미지를 다운로드하는 주체는 누구일까?”

“이미지를 다운로드하는 주체…?”

루나의 질문은 솔라의 관점을 바꾸었다. EKS라는 거대한 개념이 아니라, 그 안에서 실제로 일하는 누군가를 상상해야 했다. 이미지를 내려받아 컨테이너로 실행하는 건 EKS 클러스터 안에서 일하는 ‘워커 노드(Worker Node)‘들이다.

“아, 실제로 이미지를 가져오는 건 EKS 클러스터를 구성하는 서버, 그러니까 워커 노드들이구나.”

“맞아. 그럼 문제는 이렇게 바뀌지. ‘어떻게 워커 노드가 ECR에 접근할 권한을 가질까?’”

그 순간, 솔라는 이전 장에서 보았던 ‘IAM 역할(Role)‘이라는 개념을 떠올렸다. CodeBuild가 임시 토큰을 발급받을 수 있었던 건, ‘CodeBuild 서비스’에 할당된 IAM 역할 덕분이었다.

“설마… 워커 노드 자체에 ECR에서 이미지를 읽어올 수 있는 권한이 미리 부여되어 있는 거야?”

“빙고.”

루나는 짧게 답하며 미소 지었다.

“EKS 클러스터를 만들 때, 워커 노드들이 사용할 IAM 역할을 지정해. 그리고 그 역할에는 ‘ECR 저장소에서 이미지를 읽을 수 있는(ECR Read-Only) 권한’이 포함되어 있지. 그래서 워커 노드들은 별도의 로그인 절차 없이도, 자신의 신분(IAM 역할)만으로 ECR에 접근해서 필요한 이미지를 가져올 수 있는 거야. deployment.yaml 파일은 그저 ‘어떤 창고에 있는 어떤 물건을 가져와’라고 지시하는 주문서일 뿐이고, 물건을 가져오는 배달원(워커 노드)은 이미 그 창고에 들어갈 수 있는 출입증을 항상 목에 걸고 있는 셈이지.”

솔라는 눈을 감고 전체 흐름을 머릿속으로 다시 그렸다. 안개처럼 뿌옇던 마지막 구간이 선명하게 연결되었다. deployment.yamlimage 필드는 마법의 명령어가 아니었다. 그것은 전체 시스템의 신뢰 관계 위에 세워진, 가장 확실하고 명료한 ‘좌표’였다. ECR에 안전하게 보관된 배포 단위를 정확히 지목하는 마지막 조립 지시. 이제야 모든 과정이 하나의 그림으로 합쳐졌다.

솔라는 자리에서 일어나 책상 옆 작은 화이트보드로 향했다. 더 이상 질문은 없었다. 대신, 그녀는 직접 그림을 그려보기 시작했다.

  1. 가장 왼쪽에 개발자의 노트북을 그리고, git push 화살표를 그렸다.
  2. 화살표는 CodePipeline이라는 긴 레일로 이어졌다.
  3. 레일의 첫 번째 역은 CodeBuild였다. 솔라는 역 아래에 작은 박스를 그리고 그 안에 buildspec.yml이라고 적었다. 박스에서는 다시 세 개의 작은 화살표가 뻗어 나왔다.
    • pre_build: IAM 역할로 ECR 임시 토큰 획득
    • build: Dockerfile 레시피로 소스 코드를 이미지로 포장
    • post_build: 포장된 이미지를 ECR로 push
  4. 다음 역은 ECR이라는 이름의 견고한 창고였다. 방금 CodeBuild가 보낸 이미지 상자가 그 안에 그려졌다. 상자에는 <ACCOUNT_ID>.../m-6th-t24-backend:latest라는 꼬리표가 붙어 있었다.
  5. 그리고 마침내 마지막 역, EKS 클러스터가 그려졌다. 클러스터 안에는 여러 개의 워커 노드가 있었다. 솔라는 그중 하나에 ‘ECR 읽기 권한’이라고 적힌 목걸이를 그려 넣었다.
  6. 마지막으로, EKS 클러스터를 향해 deployment.yaml 파일에서 뻗어 나온 화살표가 그려졌다. 화살표 끝에는 창고의 이미지 꼬리표와 똑같은 주소가 적혀 있었다. 그 지시에 따라, 목걸이를 건 워커 노드가 ECR 창고에서 정확한 이미지 상자를 가져와 실행시키는 모습으로 그림은 완성되었다.

화이트보드에는 소스 코드 한 줄이 EKS 위에서 살아 움직이는 서비스가 되기까지의 전체 여정이 담긴, 솔라 자신만의 ‘배포 단위 조립도’가 펼쳐져 있었다.

“이거였구나.”

솔라는 완성된 그림을 보며 나지막이 말했다. 흩어져 있던 도구의 이름들이 아니라, 각자의 역할과 책임을 가지고 다음 주자에게 결과물을 전달하는 거대한 릴레이. 이제 그녀는 ‘빌드 성공’이라는 초록불이 이 기나긴 여정의 어디쯤을 비추는 신호등인지, 그리고 그 불빛이 다음 주자를 어디로 안내하는지 명확하게 설명할 수 있게 되었다.