Express 서버를 Docker로 빌드하고 EKS에 배포하기
인생은 실전.
이론으로만 알고 있는 것들을 체화하기 위해 “Express 서버를 Docker로 빌드해 AWS EKS(Elastic Kubernetes Service)에 올리는” 전체 과정을 직접 경험해 보았다.
이 글은 Docker, AWS ECR, Helm Chart, EKS를 연동해 실제 서비스 환경으로 배포했던 과정을 정리한 기록이다.
1. 배포를 위해 알아야 할 개념들
🐳 1. Docker
Docker는 애플리케이션을 가볍고 어디서든 똑같이 실행할 수 있게 해주느 컨테이너 기술의 사실상 표준이다.
과거에는 "내 컴퓨터에서는 잘 됐는데, 서버에 올리니 안 되네요?"라는 말이 자주 들렸다. 서버마다 설치된 라이브러리 버전, 운영체제, 설정 등이 모두 달랐기 때문이다.
Docker는 이러한 문제를 해결하기 위해, 애플리케이션과 그 실행에 필요한 모든 환경(라이브러리, 종속성, 설정 파일 등)을 하나의 '이미지(Image)'로 묶어버린다. 이 이미지만 있으면 어디서든 동일한 환경의 '컨테이너(Container)'를 실행할 수 있다.
즉, 내 로컬 PC에서 만든 Docker 이미지를 운영 서버(EKS)로 그대로 가져가 실행해도 "환경이 달라서 발생하는 문제"가 원천적으로 사라진다.
- Dockerfile: 이미지를 만드는 방법을 정의한 레시피 파일이다. (FROM node:20, COPY . ., RUN npm install 등 단계별 명령어를 기록한다.)
- Image: 애플리케이션과 실행 환경을 하나로 묶어놓은, 실행 가능한 패키지이다. (읽기 전용)
- Container: 이미지를 실제로 메모리에 올려 실행한 인스턴스이다. 하나의 이미지로 여러 개의 컨테이너를 만들 수 있다.
🐙 2. Kubernetes
Kubernetes(쿠버네티스, K8s)는 수많은 컨테이너를 자동으로 배포, 스케일링, 관리 및 복구해주는 시스템이다. 이를 '컨테이너 오케스트레이션'이라고 부른다.
컨테이너가 한두 개일 때는 docker run 명령어로 직접 관리할 수 있지만, 서비스 규모가 커져 컨테이너가 수십, 수백 개가 되면 이야기가 달라진다. "어떤 컨테이너가 죽었는지", "트래픽이 몰릴 때 자동으로 개수를 늘릴지", "업데이트는 어떻게 중단 없이 할지" 등을 사람이 직접 관리하는 것은 불가능에 가깝다.
쿠버네티스는 바로 이 지휘자(오케스트레이터) 역할을 수행한다.
- Pod: 쿠버네티스에서 배포할 수 있는 가장 작은 단위이다. 보통 하나의 컨테이너(경우에 따라 여러 개)를 감싸는 주머니와 같다.
- Deployment: Pod을 몇 개나 띄울지, 어떻게 업데이트할지(롤링 업데이트 등)와 같은 배포 전략을 정의한다. Pod이 죽으면 자동으로 다시 살리는 역할도 한다.
- Service: 여러 개의 동일한 Pod에 안정적으로 접근할 수 있도록 고유한 네트워크 주소(엔드포인트)를 부여하는 역할을 한다.
- ConfigMap / Secret: 애플리케이션의 설정값이나 DB 비밀번호 같은 민감 정보를 코드와 분리하여 관리하게 해준다.
즉, 쿠버네티스는 컨테이너를 실행하는 것을 넘어, 컨테이너가 안정적으로 잘 돌아가도록 관리하는 데 초점을 맞춘 시스템이다.
3. Helm Chart — 쿠버네티스 설정을 패키징하는 도구
Helm은 쿠버네티스용 패키지 매니저이다. npm이 Node.js의 패키지를 관리하고, apt나 brew가 시스템 패키지를 관리하듯, Helm은 쿠버네티스 애플리케이션 배포 단위를 관리한다.
단순한 Express 서버 하나를 쿠버네티스에 배포하려 해도, 위에서 언급한 Deployment, Service, ConfigMap 등 수많은 .yaml 설정 파일이 필요하다. 이걸 매번 kubectl apply -f ... 명령어로 하나씩 적용하는 것은 매우 번거로울 뿐 아니라 실수하기도 쉽다.
Helm은 이 모든 .yaml 파일들을 하나의 차트(Chart)라는 패키지로 묶어 관리하게 해준다.
장점
- 템플릿화:
values.yaml파일에 환경별(개발/스테이징/운영) 설정값을 변수처럼 정의하고, 차트를 재사용하여 손쉽게 배포할 수 있다. - 간편한 배포 및 관리:
helm install,helm upgrade등 간단한 명령어로 복잡한 애플리케이션을 배포하고 업그레이드할 수 있다. - 버전 관리 및 롤백: 배포 버전을 관리하고, 문제가 생겼을 때
helm rollback명령어로 이전 버전으로 쉽게 되돌릴 수 있다.
한마디로 Helm은 복잡한 쿠버네티스 설정 파일들을 템플릿화하여 편하게 관리해주는 도구이다.
4. AWS ECR — 안전한 이미지 저장소
ECR(Elastic Container Registry)은 AWS에서 제공하는 완전 관리형 Docker 컨테이너 레지스트리이다. Docker 이미지를 빌드했다면, 쿠버네티스 클러스터(EKS)가 그 이미지를 가져올 수 있는 중앙 저장소가 필요한데, ECR이 바로 그 역할을 한다.
"AWS 환경에서 사용하는 안전하고 빠른 Docker Hub"라고 생각하면 쉽다.
ECR 사용 흐름:
- 로컬 환경에서 Docker 이미지를 빌드한다. (
docker build ...) - AWS CLI를 통해 ECR에 로그인하여 인증 받는다.
- 빌드한 이미지에 ECR 주소로 태그를 지정하고 푸시한다. (
docker push {ECR_URL}) - EKS(쿠버네티스)에서는 이 ECR 이미지 주소를 참조하여 Pod을 생성할 때 이미지를 풀(Pull) 받는다.
이 과정을 통해 개발자의 PC에서 만들어진 Docker 이미지가 EKS 클러스터에 안전하게 전달된다.
5. AWS EKS — 관리형 Kubernetes 서비스
EKS(Elastic Kubernetes Service)는 AWS가 제공하는 관리형 쿠버네티스 서비스이다.
쿠버네티스는 매우 강력하지만, '컨트롤 플레인'이라고 불리는 마스터 노드들을 직접 설치하고, 관리하고, 안정적으로 유지하는 것은 매우 복잡하고 어려운 일일 것이다.
EKS를 사용하면 이 컨트롤 플레인의 설치와 관리를 모두 AWS가 대신 해준다. 사용자는 그저 kubectl과 같은 도구를 이용해 EKS 클러스터에 애플리케이션을 배포하기만 하면 된다.
장점:
- 컨트롤 플레인 관리 불필요: 마스터 노드의 고가용성, 패치, 보안 등을 신경 쓸 필요가 없다.
- AWS 서비스와 완벽한 통합: IAM(권한), VPC(네트워크), 로드 밸런서, CloudWatch(모니터링) 등 다른 AWS 서비스와 긴밀하게 연동된다.
- 자동화된 확장 및 관리: 워커 노드(실제 컨테이너가 실행되는 서버)의 오토스케일링, 롤링 업데이트 등을 손쉽게 지원한다.
결국 EKS는 AWS 위에서 가장 쉽고 안정적으로 쿠버네티스를 사용하는 방법이라고 할 수 있다.
🤔 잠깐, EKS와 EC2는 뭐가 다른거지?
많이 헷갈리는 개념이다. "어차피 둘 다 AWS에서 서버 돌리는 거 아니야?" 라는 생각이 든다. 어떤 의미로는 맞다. 하지만 추상화의 수준과 관리의 초점이 완전히 다르다.
| 구분 | EC2 (Elastic Compute Cloud) | EKS (Elastic Kubernetes Service) |
|---|---|---|
| 핵심 단위 | 가상 머신 (Virtual Machine) | 컨테이너 (Container) |
| 추상화 수준 | 인프라(IaaS): OS가 설치된 빈 컴퓨터 한 대를 빌리는 것과 같다. | 컨테이너 플랫폼(PaaS에 가까움): 컨테이너들을 잘 운영하기 위한 환경 자체를 빌리는 것이다. |
| 관리 책임 | OS, 런타임, 보안 패치, 애플리케이션 등 모든 것을 직접 관리해야 한다. | 컨테이너의 배포와 생명주기에만 집중하면 된다. (OS, K8s 컨트롤 플레인 관리는 AWS가 담당) |
| 확장성 | Auto Scaling Group을 통해 VM 단위로 스케일링한다. | Pod Autoscaler(HPA) 등을 통해 컨테이너(Pod) 단위로 훨씬 빠르고 유연하게 스케일링한다. |
| 적합한 용도 | 전통적인 애플리케이션, OS 레벨의 완전한 제어가 필요한 경우 | 마이크로서비스 아키텍처(MSA), 컨테이너 기반의 현대적인 애플리케이션 |
나도 많이 헷갈리기 때문에 이해가 쉽게 비유하자면,
- EC2는 빈 주방을 임대하는 것과 같다고 볼 수 있다. 어떤 화구를 놓고, 어떤 조리도구를 배치하고, 요리사 동선을 어떻게 짤지 모두 직접 결정하고 세팅해야 한다.
- EKS는 최신식 프렌차이즈 주방 시스템을 도입하는 것과 같다. 이미 검증된 레시피(Dockerfile), 효율적인 주방 동선과 역할 분담(Kubernetes), 그리고 전체 운영을 지휘하는 매니저(Control Plane)가 갖춰져 있어, 우리는 요리(애플리케이션 개발)에만 집중하면 된다.
Express 서버, EKS에 배포하기
그럼 이제 실제 Express로 개발된 서버를 EKS에 배포해 보려고 한다.
1. API 서버 준비
먼저, 배포할 간단한 Express 서버를 준비한다. 중요한 점은 쿠버네티스가 서비스의 상태를 확인할 수 있도록 헬스 체크(Health Check) 엔드포인트를 추가하는 것이다.
- 서버 포트는
8000번으로 설정했다. GET /health요청 시,200 OK상태 코드와 함께 "alive" 메시지를 응답하도록 구현했다.
이 /health 엔드포인트는 잠시 후 Helm 차트에서 컨테이너가 정상적으로 살아있는지(livenessProbe), 요청을 받을 준비가 되었는지(readinessProbe) 확인하는 데 사용된다.
2. Docker 이미지 빌드
이제 Express 서버를 컨테이너 환경에서 실행할 수 있도록 Dockerfile을 작성하고 이미지를 빌드한다.
# Dockerfile 예시
FROM node:20-slim
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 8000
CMD [ "node", "server.js" ].dockerignore 파일에 node_modules, logs, .env 등 불필요하거나 민감한 파일들을 추가하여 이미지에 포함되지 않도록 하는 것을 잊지 말자.
다음 명령어로 이미지를 빌드한다. 이미지 태그에는 나중에 푸시할 AWS ECR 저장소 주소를 미리 포함하는 것이 편리하다.
# docker build -t {ECR_저장소_URL}/{이미지_이름}:{태그} .
docker build -t {AWS_ACCOUNT_ID}.dkr.ecr.{AWS_REGION}.amazonaws.com/{IMAGE_NAME}:latest .{AWS_ACCOUNT_ID}: 사용자의 12자리 AWS 계정 ID{AWS_REGION}: ECR 리포지토리가 있는 AWS 리전 (예: ap-northeast-2){IMAGE_NAME}: ECR에 저장할 이미지 이름 (예: my-express-app)
빌드가 완료되면 로컬에서 이미지가 잘 실행되는지 간단히 테스트 해보자.
# 빌드된 이미지 확인
docker images | grep {IMAGE_NAME}
# 컨테이너 실행 테스트 (로컬 8000번 포트와 컨테이너 8000번 포트 연결)
docker run -p 8000:8000 {AWS_ACCOUNT_ID}.dkr.ecr.{AWS_REGION}.amazonaws.com/{IMAGE_NAME}:latest3. ECR 로그인 & 이미지 푸시
로컬에서 만든 Docker 이미지를 EKS 클러스터가 가져갈 수 있도록 AWS ECR(Elastic Container Registry)에 업로드해야 한다. 먼저 AWS CLI를 통해 ECR에 로그인한다.
aws ecr get-login-password --region {AWS_REGION} | \
docker login --username AWS --password-stdin {AWS_ACCOUNT_ID}.dkr.ecr.{AWS_REGION}.amazonaws.com이 명령어는 ECR에 접근할 수 있는 임시 인증 토큰을 발급받아 Docker 클라이언트를 인증하는 과정이다. --password-stdin 옵션을 사용하면 토큰이 터미널에 직접 노출되지 않아 더 안전하다.
로그인이 성공했다면, docker push 명령어로 이미지를 ECR에 업로드하면 된다.
docker push {AWS_ACCOUNT_ID}.dkr.ecr.{AWS_REGION}.amazonaws.com/{IMAGE_NAME}:latest4. Helm 차트 준비
이제 쿠버네티스에 배포할 리소스들을 정의할 차례이다. Helm을 사용하면 Deployment, Service 등 복잡한 YAML 설정들을 하나의 패키지로 묶어 체계적으로 관리할 수 있다.
values.yaml 파일에 배포에 필요한 주요 설정값들을 정의한다.
# values.yaml 예시
replicaCount: 1 # 몇 개의 Pod을 띄울 것인가
image:
repository: {AWS_ACCOUNT_ID}.dkr.ecr.{AWS_REGION}.amazonaws.com/{IMAGE_NAME}
tag: latest
pullPolicy: Always # 항상 ECR에서 최신 이미지를 받아오도록 설정
service:
type: ClusterIP
port: 80
containerPort: 8000 # Express 서버가 사용하는 포트
# 컨테이너가 살아있는지 검사 (문제가 생기면 컨테이너 재시작)
livenessProbe:
httpGet:
path: /health # 1번 단계에서 만든 헬스 체크 경로
port: 8000
initialDelaySeconds: 10 # 컨테이너 시작 후 10초 뒤부터 검사
periodSeconds: 10 # 10초마다 반복
# 컨테이너가 트래픽을 받을 준비가 되었는지 검사 (준비될 때까지 서비스에 연결 안 됨)
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5EKS 배포 및 확인
모든 준비가 끝났다. Helm 명령어를 사용해 EKS 클러스터에 애플리케이션을 배포하자.
# helm upgrade --install {릴리스_이름} {차트_경로} -n {네임스페이스}
helm upgrade --install my-app ./charts/my-app -n production- --install: 해당 이름의 릴리스가 없다면 새로 설치하고, 이미 있다면 업데이트를 진행한다. 이 옵션 덕분에 최초 배포와 업데이트를 동일한 명령어로 수행할 수 있다.
{릴리스_이름}: 배포를 식별하는 고유한 이름이다.{차트_경로}: Helm 차트가 위치한 로컬 디렉토리 경로이다.-n {네임스페이스}: 배포할 쿠버네티스 네임스페이스를 지정한다.
배포가 완료되면 kubectl 명령어를 통해 Pod과 Service가 정상적으로 생성되었는지, 로그는 잘 출력되는지 확인한다.
# production 네임스페이스의 Pod 목록 확인
kubectl get pods -n production
# Service 확인
kubectl get svc -n production
# 특정 Pod의 로그 확인
kubectl logs -n production {POD_NAME}Pod의 상태가 Running으로 표시되고 로그에 에러가 없다면 성공적으로 배포 완료! 🎉
3. 트러블슈팅: "Pod이 Running 상태가 아닐 때"
배포 명령어를 실행하고 kubectl get pods를 입력했을 때, 내가 기대하는 Running 상태 대신 다른 낯선 상태를 마주했다. 당황하지 말자.
이럴 때 문제의 원인을 파악하는 가장 첫 번째이자 가장 강력한 명령어는 바로 kubectl describe 이다.
kubectl describe pod {POD_NAME} -n {NAMESPACE}이 명령어는 Pod의 상세한 정보와 함께, 생성부터 현재까지 발생한 이벤트(Events) 목록을 시간순으로 보여준다. 대부분의 문제 원인은 이 이벤트 로그 마지막 몇 줄에 단서가 있다.
내가 겪은 이슈와 더불어, 많이 발생하는 Pod의 문제 상태와 해결 방법을 알아보자.
1. ImagePullBackOff
- 상태 의미: "이미지를 가져오려 했지만 계속 실패하고 있음"
ECR이라는 창고에서 Docker 이미지라는 물건을 가져와야 하는데, 창고 주소가 틀렸거나 출입증(권한)이 없어 못 들어가는 상황과 같다.
kubectl describe pod를 실행해보면 이벤트 로그에 Failed to pull image 또는 ErrImagePull과 같은 메시지가 보일 것이다.
주요 원인 및 해결 방법:
-
이미지 주소 또는 태그 오타:
Helm values.yaml이나deployment.yaml에 정의한 이미지repository주소나tag가 실제 ECR에 있는 것과 다른지 확인하자. -
ECR 접근 권한 부족: EKS 워커 노드(EC2)의 IAM Role에
AmazonEC2ContainerRegistryReadOnly정책이 연결되어 있는지 확인해야 한다. -
이미지가 ECR에 없음: Docker 이미지를 빌드만 하고 ECR에
push하지 않았는지, 혹은 다른 태그로 푸시했는지 ECR 콘솔에서 확인한다.
2. CrashLoopBackOff
- 상태 의미: "컨테이너를 실행했지만, 시작하자마자 계속 비정상 종료돼서 무한 재시작 중임!"
이 상태는 이미지 자체는 잘 가져왔지만, 컨테이너 내부에서 실행되는 애플리케이션에 문제가 있다는 신호이다.
주요 원인 및 해결 방법:
-
애플리케이션 코드 에러: 가장 먼저
kubectl logs {POD_NAME}명령어로 로그를 확인한다. 로그에 출력된 에러 메시지를 보고 코드의 버그를 수정해야 한다. Pod이 너무 빨리 재시작해서 로그를 보기 어렵다면--previous플래그를 사용해 이전에 종료된 컨테이너의 로그를 볼 수 있다. -
잘못된 환경 변수 또는 설정 파일:
ConfigMap이나Secret으로 전달한 환경 변수(DB 주소 등)가 잘못되어 애플리케이션이 시작되지 못하는 경우이다. 설정값을 다시 확인해야 한다. -
메모리 부족 (OOMKilled): Pod에 할당된 메모리보다 앱이 더 많은 메모리를 사용해서 강제 종료되는 경우이다.
kubectl describe pod이벤트 로그에OOMKilled메시지가 있는지 확인하고, 메모리 할당량을 늘리거나 코드에서 메모리 누수를 해결해야 한다. -
CPU 아키텍처 불일치: M1/M2 Mac(ARM64) 사용자들이 흔히 겪는 문제이다.
내가 실제로 겪은 문제인데, 로컬 환경인 M1/M2 Mac(ARM64)에서 빌드한 Docker 이미지를 AMD/Intel(AMD64) CPU를 사용하는 EKS 워커 노드에서 실행하려고 할 때 발생한다. CPU 명령어 세트가 달라 컨테이너가 즉시 종료된다.
이 경우 kubectl logs로는 특별한 에러가 보이지 않을 수 있다. 하지만 kubectl describe pod {POD_NAME} 명령어로 이벤트를 확인하면, exec format error 라는 단서를 찾을 수 있다.
docker 이미지를 빌드할 때 --platform 옵션을 사용해 실행될 환경의 아키텍처를 명시적으로 지정해야 한다.
# EKS 노드 환경인 linux/amd64용 이미지로 강제 빌드
docker build --platform linux/amd64 -t {이미지_이름}:{태그} .3. Pending
- 상태 의미: "Pod을 실행할 준비는 끝났는데, 어느 노드에 배치해야 할지 몰라 대기 중."
이 상태는 Pod 자체의 문제라기보다는 클러스터의 리소스가 부족할 때 주로 발생한다.
주요 원인 및 해결 방법:
- 클러스터 리소스 부족 (CPU, Memory):
kubectl describe pod이벤트 로그를 보면 "Insufficient cpu" 또는 "Insufficient memory"와 같은 명확한 메시지를 찾을 수 있다. Pod의 리소스 요청량을 줄이거나, 클러스터 오토스케일러(Cluster Autoscaler)를 통해 워커 노드를 증설해야 한다.
직접 부딪혀보니 보이는 것들
솔직히 말해, 이번 배포 프로세스를 처음부터 끝까지 직접 경험하며 생각보다 많은 지점에서 헤맸다. 머리로 아는 것과 직접 경험으로 부딪히며 해결하는 것의 차이를 다시 한번 실감하는 시간이었다.
특히 클라우드 환경에서는 작은 실수가 예상치 못한 비용으로 이어질 수 있다는 생각에, AWS 청구 대시보드를 수시로 확인하며 조심스럽게 진행해야 했다.💰
이번 과정을 통해 가장 크게 느낀 점은, 견고한 인프라를 설계하고 트러블슈팅을 하기 위해서는 결국 네트워크(VPC, Subnet, Ingress 등)에 대한 깊은 이해가 필수적이라는 것이다. 컨테이너와 쿠버네티스에 집중하다가도, 결국 모든 길은 네트워크로 통함을 실감했다.
공부하면 할수록 알아야 할 것이 산더미처럼 불어나는 기분이다. 하지만 하나씩 문제를 해결하고, 비로소 Running 상태의 Pod을 마주했을 때의 기쁨이 이 모든 어려움을 잊게 해주는 것 같다. 바로 이 점이 엔지니어링의 묘미가 아닐까 싶다.