EKS 배포 가이드: NestJS, NextJS, PostgreSQL
AWS EKS를 사용하여 NestJS 백엔드, NextJS 프론트엔드, PostgreSQL 데이터베이스를 배포하는 것은 일반적인 클라우드 네이티브 아키텍처입니다. 이는 여러 구성 요소와 단계를 포함하며, Kubernetes의 이점을 활용하여 확장성, 복원력 및 관리 용이성을 제공합니다.
다음은 배포를 위한 주요 구성 요소와 단계입니다.
0. 필수 도구 설치 및 AWS CLI 설정
EKS 클러스터 배포 및 관리를 위해 다음 도구들이 로컬 환경에 설치되어 있어야 합니다. 설치되어 있지 않다면 Homebrew(macOS 기준)를 사용하여 설치합니다.
aws-cli: AWS 리소스 관리 (ECR, IAM 등)brew install awscli
aws configure # Access Key, Secret Key, Default region (예: ap-northeast-1), Default output format (json) 설정kubectl: Kubernetes 클러스터 제어brew install kubectleksctl: EKS 클러스터 생성 및 관리brew install eksctldocker: 애플리케이션 컨테이너화# Docker Desktop 설치 및 실행 확인helm: Kubernetes 패키지 관리자 (Load Balancer Controller 배포에 사용)brew install helm
1. 아키텍처 개요
| 구성 요소 | 기술/서비스 | 역할 |
|---|---|---|
| 컨테이너 오케스트레이션 | AWS EKS (Kubernetes) | NestJS, NextJS 애플리케이션 컨테이너를 관리, 배포, 확장 |
| 백엔드 애플리케이션 | NestJS | REST API 또는 GraphQL 서비스 제공 |
| 프론트엔드 애플리케이션 | NextJS | 사용자 인터페이스 제공 (SSR/SSG/ISR 포함) |
| 데이터베이스 | PostgreSQL | 애플리케이션 데이터 저장 및 관리 |
| 컨테이너 이미지 저장소 | AWS ECR (Elastic Container Registry) | NestJS 및 NextJS Docker 이미지 저장 |
| 데이터베이스 서비스 | AWS RDS for PostgreSQL (권장) 또는 EKS 내부의 컨테이너화된 PostgreSQL | 영구적이고 관리되는 데이터베이스 인스턴스 제공 |
| 로드 밸런싱 및 접속 | AWS ALB/NLB (Application/Network Load Balancer), Kubernetes Service, Ingress | 외부 트래픽을 EKS의 서비스로 라우팅 |
2. 배포 단계별 상세 가이드
단계 1: 프로젝트 컨테이너화 및 이미지 저장 (ECR)
애플리케이션을 EKS에 배포하려면 먼저 Docker 이미지로 컨테이너화하고, AWS ECR(Elastic Container Registry)에 저장해야 합니다.
-
Dockerfile작성-
백엔드 (
backend-vivid-ai) 멀티-스테이지 빌드를 사용하여 최종 이미지 크기를 최소화하고 효율성을 높입니다.backend-vivid-ai/Dockerfile파일을 생성합니다.# ---- 1. Builder 스테이지: 앱 빌드 ----
FROM node:22-alpine AS builder
# 작업 디렉토리 설정
WORKDIR /usr/src/app
# package.json과 package-lock.json을 먼저 복사하여 의존성 캐싱 활용
COPY package*.json ./
# 의존성 설치
RUN npm install
# 소스 코드 전체 복사
COPY . .
# 애플리케이션 빌드
RUN npm run build
# 프로덕션용 의존성만 남기기 (개발용 의존성 제거)
RUN npm prune --production
# ---- 2. Runner 스테이지: 최종 이미지 생성 ----
FROM node:22-alpine
# 작업 디렉토리 설정
WORKDIR /usr/src/app
# Builder 스테이지에서 프로덕션용 의존성만 복사
COPY --from=builder /usr/src/app/node_modules ./node_modules
# Builder 스테이지에서 빌드된 결과물만 복사
COPY --from=builder /usr/src/app/dist ./dist
# 애플리케이션이 3000번 포트를 사용하므로 외부에 노출
EXPOSE 3000
# 컨테이너 시작 시 실행될 명령어
CMD ["node", "dist/main"] -
프론트엔드 (
frontend-vivid-ai) Next.js의output: 'standalone'기능을 활용하여 이미지 크기를 최적화합니다. 먼저frontend-vivid-ai/next.config.ts파일을 수정합니다.import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
output: 'standalone',
};
export default nextConfig;이후
frontend-vivid-ai/Dockerfile파일을 생성합니다.# ---- 1. Builder 스테이지: 앱 빌드 ----
FROM node:22-alpine AS builder
# 작업 디렉토리 설정
WORKDIR /app
# 빌드 인수를 선언하고 환경 변수로 설정합니다.
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
# package.json과 package-lock.json을 먼저 복사
COPY package*.json ./
# 의존성 설치
RUN npm install
# 소스 코드 전체 복사
COPY . .
# 애플리케이션 빌드 (standalone output 생성)
RUN npm run build
# ---- 2. Runner 스테이지: 최종 이미지 생성 ----
FROM node:22-alpine
# 작업 디렉토리 설정
WORKDIR /app
# 프로덕션 환경 변수 설정
ENV NODE_ENV=production
# Builder 스테이지에서 생성된 standalone 폴더 내용 복사
COPY --from=builder /app/.next/standalone ./
# 정적 에셋을 위해 public 폴더 복사
COPY --from=builder /app/public ./public
# 최적화된 이미지, 폰트 등을 위해 .next/static 폴더 복사
COPY --from=builder /app/.next/static ./.next/static
# Next.js 기본 포트인 3000번 노출
EXPOSE 3000
# standalone 모드에서 서버를 실행하는 명령어
CMD ["node", "server.js"]
-
-
ECR 리포지토리 생성 및 Docker 이미지 푸시
-
ECR 리포지토리 생성
ap-northeast-1리전에 백엔드와 프론트엔드용 ECR 리포지토리를 생성합니다.aws ecr create-repository --repository-name backend-vivid-ai --region ap-northeast-1
aws ecr create-repository --repository-name frontend-vivid-ai --region ap-northeast-1 -
Docker 로그인 ECR에 이미지를 푸시하기 위해 Docker 클라이언트를 ECR에 로그인합니다.
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 194722419257.dkr.ecr.ap-northeast-1.amazonaws.com -
Docker 이미지 빌드 및 푸시 트러블슈팅:
no match for platform in manifest: not found(아키텍처 불일치) 로컬 개발 환경(예: Apple Silicon Mac)과 EKS 워커 노드(AMD64)의 아키텍처가 다를 경우 발생하는 오류입니다.docker build시--platform linux/amd64옵션을 명시하여 워커 노드 아키텍처에 맞는 이미지를 빌드해야 합니다.트러블슈팅:
403 Forbidden(ECR 로그인 만료) Docker 로그인 토큰이 만료되었을 경우 발생합니다.aws ecr get-login-password명령어를 다시 실행하여 로그인 토큰을 갱신해야 합니다.백엔드 이미지 빌드 및 푸시:
cd backend-vivid-ai
docker build --platform linux/amd64 -t backend-vivid-ai .
&& docker tag backend-vivid-ai:latest 194722419257.dkr.ecr.ap-northeast-1.amazonaws.com/backend-vivid-ai:latest
&& docker push 194722419257.dkr.ecr.ap-northeast-1.amazonaws.com/backend-vivid-ai:latest
cd ..프론트엔드 이미지 빌드 및 푸시:
cd frontend-vivid-ai
docker build --platform linux/amd64 \
--build-arg NEXT_PUBLIC_API_URL=http://k8s-default-autoprom-179b96464f-1356167102.ap-northeast-1.elb.amazonaws.com/api \
-t frontend-vivid-ai .
&& docker tag frontend-vivid-ai:latest 194722419257.dkr.ecr.ap-northeast-1.amazonaws.com/frontend-vivid-ai:latest
&& docker push 194722419257.dkr.ecr.ap-northeast-1.amazonaws.com/frontend-vivid-ai:latest
cd ..
-
단계 2: PostgreSQL 데이터베이스 설정 (RDS 권장)
EKS 환경에서 영구적인 데이터베이스를 운영하는 것은 복잡하며, 백업, 복구, 확장성, 보안 관점에서 AWS RDS for PostgreSQL을 사용하는 것이 일반적이고 권장됩니다.
-
AWS RDS 인스턴스 생성: (기존 내용 유지)
- AWS RDS 콘솔에 접속합니다. (도쿄 리전으로 자동 선택)
- "데이터베이스 생성" 버튼을 클릭합니다.
- **"표준 생성"**과 엔진 유형으로 **"PostgreSQL"**을 선택합니다.
- 템플릿 섹션에서 비용 절감을 위해 **"프리 티어"**를 선택합니다. (프리 티어 사용이 불가능한 경우, 가장 작은 사양으로 진행)
- 설정 섹션에서 다음을 입력합니다.
- DB 인스턴스 식별자:
vivid-ai-db와 같이 식별하기 쉬운 이름을 입력합니다. - 마스터 사용자 이름:
postgres또는 원하는 사용자 이름을 입력합니다. - 마스터 암호: 데이터베이스에 접속할 암호를 입력하고, 반드시 이 암호를 안전한 곳에 기록해 둡니다. Kubernetes Secret 설정 시 필요합니다.
- DB 인스턴스 식별자:
- 연결 섹션에서 다음을 설정합니다.
- VPC: 나중에 EKS 클러스터를 생성할 VPC를 선택해야 합니다. 지금은 기본(default) VPC를 선택해도 괜찮습니다.
- 퍼블릭 액세스: "아니요" 를 선택하여 데이터베이스가 외부 인터넷에 노출되지 않도록 합니다.
- "데이터베이스 생성" 버튼을 눌러 생성을 시작합니다. (생성까지 몇 분 소요)
-
데이터베이스 엔드포인트 선택: (기존 내용 유지) RDS 인스턴스 생성 후, 두 가지 엔드포인트가 보일 수 있습니다. 백엔드 애플리케이션은 쓰기/읽기 작업을 모두 수행해야 하므로, Writer 엔드포인트를 사용해야 합니다.
- Writer 엔드포인트 (쓰기/읽기 가능):
vivid-ai-db-dev.cluster-c5ockyckitzu.ap-northeast-1.rds.amazonaws.com(예시) - 이 엔드포인트를 사용합니다. - Reader 엔드포인트 (읽기 전용):
vivid-ai-db-dev.cluster-ro-c5ockyckitzu.ap-northeast-1.rds.amazonaws.com(예시) - 읽기 전용 복제본에 사용됩니다.
- Writer 엔드포인트 (쓰기/읽기 가능):
-
연결 정보 관리: 데이터베이스 연결 문자열 (호스트, 포트, 사용자, 비밀번호)은 Kubernetes의 Secret 객체를 사용하여 안전하게 관리합니다.
- SSL 연결: 백엔드 애플리케이션은 RDS에 SSL을 사용하여 연결합니다.
app.module.ts는ssl: { rejectUnauthorized: false }로 TypeORM을 구성하며,POSTGRES_SSL_ENABLED환경 변수 (true로 설정)에 의해 제어됩니다.
- SSL 연결: 백엔드 애플리케이션은 RDS에 SSL을 사용하여 연결합니다.
단계 3: EKS 클러스터 설정
-
VPC 및 서브넷 준비: EKS 클러스터에 필요한 VPC, 퍼블릭/프라이빗 서브넷, 인터넷 게이트웨이, NAT 게이트웨이를 준비합니다. (eksctl이 대부분 자동 처리)
-
EKS 클러스터 생성:
eksctl명령어를 사용하여 EKS 클러스터를 생성합니다.vivid-ai-cluster-dev라는 이름으로ap-northeast-1리전에t3.medium인스턴스 타입의 워커 노드 2개를 생성합니다.eksctl create cluster \
--name vivid-ai-cluster-dev \
--region ap-northeast-1 \
--node-type t3.medium \
--nodes 2 \
--nodes-min 1 \
--nodes-max 3- 트러블슈팅:
AccessDeniedException(IAM 권한 부족)eksctl을 실행하는 IAM 사용자에게 EKS 클러스터 생성에 필요한 권한이 없을 때 발생합니다.eksctl은 EKS뿐만 아니라 CloudFormation, VPC, EC2, IAM 등 다양한 AWS 리소스를 생성하므로 광범위한 권한이 필요합니다. 해결:eks-deploy-vivid-ai사용자에게 다음 AWS 관리형 정책과 인라인 정책을 추가합니다.- 관리형 정책:
AWSCloudFormationFullAccess,AmazonVPCFullAccess,AmazonEC2FullAccess,IAMFullAccess - 인라인 정책 (EKS 관련):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "eks:*",
"Resource": "*"
}
]
}
- 관리형 정책:
- 트러블슈팅:
-
워커 노드 구성: 애플리케이션을 실행할 EC2 인스턴스 기반의 노드 그룹이 클러스터 생성 시 함께 구성됩니다.
- 트러블슈팅: AWS 콘솔에서 EKS 클러스터 접근 불가
클러스터 생성 후 AWS 콘솔에서 EKS 클러스터에 접근하려 할 때
현재 IAM 보안 주체가 이 클러스터에 있는 Kubernetes 객체에 액세스할 수 없습니다.메시지가 나타날 수 있습니다. 이는 클러스터를 생성한 IAM 사용자(eks-deploy-vivid-ai)만이 초기에 관리자 권한을 가지기 때문입니다. 해결: AWS 콘솔에 로그인한 사용자에게도 클러스터 관리자 권한을 부여해야 합니다.kubectl edit configmap aws-auth -n kube-system명령어를 사용하여mapUsers:섹션에 콘솔 로그인 사용자의 ARN을system:masters그룹으로 추가합니다.# kubectl edit configmap aws-auth -n kube-system 실행 후
data:
mapRoles: |
# ... 기존 내용 ...
mapUsers: |
- userarn: arn:aws:iam::YOUR_ACCOUNT_ID:user/YOUR_CONSOLE_LOGIN_USERNAME
username: YOUR_CONSOLE_LOGIN_USERNAME
groups:
- system:masters
- 트러블슈팅: AWS 콘솔에서 EKS 클러스터 접근 불가
클러스터 생성 후 AWS 콘솔에서 EKS 클러스터에 접근하려 할 때
단계 4: Kubernetes Manifest 작성 및 배포
NestJS와 NextJS 애플리케이션을 EKS에 배포하기 위한 YAML 파일을 작성하고 클러스터에 적용합니다.
-
Secret (
secret.yaml): RDS 연결 정보, 환경 변수 등을 포함하는 **Secret**을 생성합니다. 이 파일은 민감한 정보를 포함하므로.gitignore에 추가하여 Git에 추적되지 않도록 합니다.# secret.yaml (프로젝트 루트에 생성)
apiVersion: v1
kind: Secret
metadata:
name: vivid-ai-secret
type: Opaque data:
아래 값들은 Base64로 인코딩된 값이어야 합니다.
echo -n 'YOUR_VALUE' | base64
DB_HOST: ZGItYXV0b3Byb21ha2VyLWFpLXBvc3RncmVzLXJkcy5jNW9ja3lja2l0enUuYXAtbm9ydGhlYXN0LTEucmRzLmFtYXpvbmF3cy5jb20= DB_PORT: NTQzMg== # 5432 DB_USERNAME: cG9zdGdyZXM= DB_PASSWORD: YOUR_DATABASE_PASSWORD_BASE64 # 실제 DB 비밀번호를 Base64 인코딩하여 채워주세요。 DB_DATABASE: cG9zdGdyZXM= # postgres JWT_SECRET: c3VwZXItc2VjcmV0LWp3dC1rZXktdGhhdC1pcy1sb25nLWFuZC1jb21wbGV4LWZvci1kZXZlbG9wbWVudC1lbnZpcm9ubWVudA== # 업데이트된 JWT_SECRET
```bash
# secret.yaml 파일에 실제 값 채운 후 적용
kubectl apply -f secret.yaml
-
Deployment (
backend-deployment-dev.yaml,frontend-deployment-dev.yaml):-
백엔드 Deployment (
backend-deployment-dev.yaml)# backend-deployment-dev.yaml (프로젝트 루트에 생성)
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend-deployment-dev
spec:
replicas: 2
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend-container
image: 194722419257.dkr.ecr.ap-northeast-1.amazonaws.com/backend-vivid-ai:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV # 환경 구분 (development, production)
value: development
- name: CORS_ORIGIN # 프론트엔드 ALB DNS
value: http://k8s-default-autoprom-179b96464f-1356167102.ap-northeast-1.elb.amazonaws.com
- name: SECURE_COOKIES # 쿠키 secure 속성 제어 (true/false)
value: "false"
- name: POSTGRES_SSL_ENABLED # PostgreSQL SSL 연결 제어 (true/false)
value: "true"
- name: POSTGRES_HOST
valueFrom:
secretKeyRef:
name: vivid-ai-secret
key: DB_HOST
- name: POSTGRES_PORT
valueFrom:
secretKeyRef:
name: vivid-ai-secret
key: DB_PORT
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: vivid-ai-secret
key: DB_USERNAME
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: vivid-ai-secret
key: DB_PASSWORD
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: vivid-ai-secret
key: DB_DATABASE
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: vivid-ai-secret
key: JWT_SECRET -
프론트엔드 Deployment (
frontend-deployment-dev.yaml)# frontend-deployment-dev.yaml (프로젝트 루트에 생성)
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend-deployment-dev
spec:
replicas: 2
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend-container
image: 194722419257.dkr.ecr.ap-northeast-1.amazonaws.com/frontend-vivid-ai:latest
ports:
- containerPort: 3000
env:
- name: NEXT_PUBLIC_API_URL # 프론트엔드에서 백엔드 API 호출 시 사용
value: http://k8s-default-autoprom-179b96464f-1356167102.ap-northeast-1.elb.amazonaws.com/api
# Deployment 적용
kubectl apply -f backend-deployment-dev.yaml
kubectl apply -f frontend-deployment-dev.yaml- 트러블슈팅:
ImagePullBackOff또는ErrImagePull(ECR 권한 부족 또는 아키텍처 불일치) 워커 노드가 ECR에서 이미지를 가져오지 못할 때 발생합니다. 주로 워커 노드의 IAM 역할에 ECR 읽기 권한(AmazonEC2ContainerRegistryReadOnly)이 없거나, 이미지 아키텍처가 워커 노드와 맞지 않을 때 발생합니다。 해결:- 워커 노드의 IAM 역할 이름 확인 (예:
eksctl-vivid-ai-cluster-dev-NodeInstanceRole-XXXXXX)eksctl get nodegroup --cluster vivid-ai-cluster-dev --region ap-northeast-1 -o json - 정책 연결
aws iam attach-role-policy --role-name YOUR_NODE_INSTANCE_ROLE_NAME --policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly --platform linux/amd64옵션을 사용하여 이미지를 다시 빌드하고 ECR에 푸시합니다. (단계 1 참조)- 기존 Pod들을 삭제하여 Kubernetes가 새 이미지로 Pod를 다시 생성하도록 합니다。
kubectl delete pod -l app=backend
kubectl delete pod -l app=frontend
- 워커 노드의 IAM 역할 이름 확인 (예:
-
-
Service (
backend-service-dev.yaml,frontend-service-dev.yaml):- 백엔드 Service (
backend-service-dev.yaml)# backend-service-dev.yaml (프로젝트 루트에 생성)
apiVersion: v1
kind: Service
metadata:
name: backend-service
- 백엔드 Service (
spec: selector: app: backend ports:
- protocol: TCP port: 3000 targetPort: 3000 type: ClusterIP # 클러스터 내부에서만 접근 가능
* **프론트엔드 Service (`frontend-service-dev.yaml`)**
Ingress를 통해 외부 접근을 관리할 것이므로 `ClusterIP` 타입으로 변경합니다.
```yaml
# frontend-service-dev.yaml (프로젝트 루트에 생성)
apiVersion: v1
kind: Service
metadata:
name: frontend-service
spec:
selector:
app: frontend
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: ClusterIP # 클러스터 내부에서만 접근 가능
# Service 적용
kubectl apply -f backend-service-dev.yaml
kubectl apply -f frontend-service-dev.yaml
-
Ingress (
ingress-dev.yaml): 외부 트래픽을 처리하고 라우팅하기 위해 **Ingress**를 생성합니다. AWS 환경에서는 AWS Load Balancer Controller를 설치하여 Ingress 객체가 자동으로 AWS ALB를 프로비저닝하도록 구성합니다.-
AWS Load Balancer Controller 설치
- IAM 정책 생성: Load Balancer Controller가 AWS 리소스(ALB 등)를 관리할 수 있도록 IAM 정책을 생성합니다.
curl -o iam_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.7.1/docs/install/iam_policy.json
aws iam create-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json - IAM OIDC 공급자 연결:
eksctl을 사용하여 클러스터에 IAM OIDC 공급자를 연결합니다.eksctl utils associate-iam-oidc-provider --region=ap-northeast-1 --cluster=vivid-ai-cluster-dev --approve - IAM 서비스 계정 생성: 위 정책을 사용할 Kubernetes 서비스 계정을 생성하고, 이 서비스 계정에 IAM 역할을 연결합니다.
eksctl create iamserviceaccount \
--cluster=vivid-ai-cluster-dev \
--namespace=kube-system \
--name=aws-load-balancer-controller \
--attach-policy-arn=arn:aws:iam::YOUR_ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy \
--override-existing-serviceaccounts \
--approve \
--region=ap-northeast-1 - Helm 설치: Helm이 설치되어 있지 않다면 설치합니다.
brew install helm - 컨트롤러 배포: Helm을 사용하여 Load Balancer Controller를 클러스터에 배포합니다.
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName=vivid-ai-cluster-dev \
--set serviceAccount.create=false \
--set serviceAccount.name=aws-load-balancer-controller \
--set image.repository=public.ecr.aws/eks/aws-load-balancer-controller \
--set region=ap-northeast-1
- IAM 정책 생성: Load Balancer Controller가 AWS 리소스(ALB 등)를 관리할 수 있도록 IAM 정책을 생성합니다.
-
Ingress 리소스 (
ingress-dev.yaml) 임시 도메인 사용을 위해 경로 기반 라우팅을 설정합니다.# ingress-dev.yaml (프로젝트 루트에 생성)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: autopromomer-ai-ingress
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
alb.ingress.kubernetes.io/healthcheck-path-backend-service-dev: /api/health # 백엔드 헬스 체크 경로
alb.ingress.kubernetes.io/healthcheck-path-frontend-service-dev: /health # 프론트엔드 헬스 체크 경로
-
spec: rules:
- http:
paths:
- path: /api pathType: Prefix backend: service: name: backend-service-dev port: number: 3000
- path: / pathType: Prefix backend: service: name: frontend-service port: number: 80
```bash
# Ingress 적용
kubectl apply -f ingress-dev.yaml
-
트러블슈팅:
403 Forbidden(ALB Controller 권한 부족) Load Balancer Controller가elasticloadbalancing:DescribeListenerAttributes와 같은 AWS API를 호출할 권한이 없을 때 발생합니다。 해결:AWSLoadBalancerControllerIAMPolicy정책에 누락된 권한을 추가하고, 정책 버전을 업데이트합니다。- 정책 문서에
elasticloadbalancing:DescribeListenerAttributes액션을 추가한iam_policy_updated.json파일을 생성합니다. aws iam create-policy-version --policy-arn arn:aws:iam::YOUR_ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy_updated.json --set-as-default명령어로 새 정책 버전을 기본으로 설정합니다.- 기존 Ingress를 삭제하고 재적용하여 컨트롤러가 업데이트된 정책을 사용하도록 합니다.
- 정책 문서에
-
트러블슈팅: Ingress 리소스가 삭제되지 않고 멈춤 (Finalizer 문제)
kubectl delete ingress명령 후 Ingress 리소스가Terminating상태로 멈춰있을 때 발생합니다. 이는 Ingress 리소스에 설정된 Finalizer 때문에 컨트롤러가 관련 AWS 리소스 정리를 기다리다가 멈춘 경우입니다。 해결:kubectl patch ingress autopromomer-ai-ingress -p '{"metadata":{"finalizers":null}}' --type=merge명령어로 Ingress 리소스의finalizers를 강제로 제거합니다. (ALB를 수동으로 삭제한 후 진행하는 것이 안전합니다.)
단계 5: 배포 및 확인
-
Manifest 적용:
kubectl apply -f [manifest 파일]명령어로 Secret, Deployment, Service, Ingress 순서대로 배포합니다. -
배포 상태 확인:
kubectl get pods,kubectl get deployments,kubectl get svc,kubectl get ing명령어로 모든 리소스가 정상적으로 실행 중인지 확인합니다. -
접속 테스트: Ingress가 제공하는 엔드포인트(ALB DNS)를 통해 애플리케이션에 접속합니다.
# Ingress의 ADDRESS 확인
kubectl get ingress autopromomer-ai-ingress출력된
ADDRESS(예:k8s-default-autoprom-179b96464f-1356167102.ap-northeast-1.elb.amazonaws.com)를 사용하여 접속합니다.- 프론트엔드 접근 URL:
http://<INGRESS_ALB_DNS_NAME>/ - 백엔드 API 접근 URL:
http://<INGRESS_ALB_DNS_NAME>/api
- 프론트엔드 접근 URL:
3. 핵심 고려 사항
- 보안 (Security): RDS와 EKS 간의 통신은 프라이빗 서브넷을 통해 이루어져야 하며, 보안 그룹을 통해 최소한의 접근만 허용해야 합니다. DB 비밀번호는 반드시 Kubernetes Secret으로 관리해야 합니다.
- 지속적 배포 (CD): 배포 과정을 자동화하기 위해 AWS CodePipeline/CodeBuild 또는 GitLab CI/GitHub Actions/ArgoCD 같은 CI/CD 도구를 구축하는 것이 좋습니다. 새로운 이미지 빌드 시 EKS의 Deployment를 자동으로 업데이트할 수 있습니다.
- 비용 (Cost): EKS는 관리형 서비스 비용과 워커 노드의 EC2 비용이 발생합니다. NextJS와 NestJS의 트래픽 패턴과 리소스 요구 사항을 고려하여 노드 크기와 오토 스케일링을 설정해야 합니다.
- 영구 저장소 (Persistent Storage): PostgreSQL을 EKS 내부에서 실행하는 경우 **PersistentVolume (PV)**과 **PersistentVolumeClaim (PVC)**이 필요하며, AWS에서는 EBS 기반의 StorageClass를 사용해야 합니다. 하지만 RDS 사용을 강력히 권장합니다.
4. 환경 변수 관리 및 인증
- 환경 구분:
NODE_ENV환경 변수를 사용하여production,development,local환경을 명확하게 구분합니다.production: 실제 운영 서버development: EKS 개발 서버 (현재 배포된 서버)local: 로컬 개발 환경
- 백엔드 환경 변수 (
backend-deployment-dev.yaml):NODE_ENV:development(EKS 개발 서버용).CORS_ORIGIN: ALB DNS 이름으로 설정됩니다 (예:http://<ALB_DNS>).SECURE_COOKIES:false(현재 HTTP 접속용; HTTPS 시true로 설정).POSTGRES_SSL_ENABLED:true(RDS SSL 연결용).JWT_SECRET: 토큰 서명/검증을 위한 길고 복잡한 문자열.
- 프론트엔드 환경 변수 (
.env.production/ Docker build-arg):NEXT_PUBLIC_API_URL:/api접두사를 포함한 ALB DNS 이름으로 설정됩니다 (예:http://<ALB_DNS>/api).
- 인증 (JWT & 쿠키):
- JWT 시크릿:
JWT_SECRET환경 변수에 의해 관리됩니다. - 쿠키 구성 (
auth.controller.ts):httpOnly: truesecure:SECURE_COOKIES환경 변수에 따라 조건부로 설정됩니다.sameSite:secure속성에 따라 조건부로 설정됩니다 (secure 시none, 비secure 시lax) - 크로스-사이트 컨텍스트에서 브라우저의 쿠키 거부를 방지합니다.
- JWT 시크릿:
7. 프라이빗 RDS 데이터베이스 접속 (SSH 터널링)
보안을 위해 RDS 데이터베이스는 프라이빗 서브넷에 배포되어 인터넷에서 직접 접근할 수 없습니다. 데이터베이스를 GUI 도구(예: PgAdmin)로 관리해야 할 경우, VPC 내부에 있는 **배스천 호스트(Bastion Host)**를 통해 우회하여 접속해야 합니다.
단계 1: 배스천 호스트 EC2 인스턴스 생성
- AWS EC2 콘솔에서 **"인스턴스 시작"**을 클릭합니다.
- 이름:
vivid-ai-bastion-dev - AMI:
Amazon Linux 2023 AMI - 인스턴스 유형:
t2.micro(프리 티어) - 키 페어: **"새 키 페어 생성"**을 통해
.pem형식의 키 페어(vivid-ai-bastion-key.pem)를 생성하고, 다운로드하여 안전하게 보관합니다. - 네트워크 설정:
- VPC: EKS 클러스터와 동일한 VPC(
vpc-05d26f998a53b5067)를 선택합니다. - 서브넷: 이름에 **
Public**이 포함된 서브넷을 선택합니다. - 퍼블릭 IP 자동 할당: "활성화(Enable)"
- 방화벽(보안 그룹): "보안 그룹 생성"을 선택하고, 인바운드 규칙으로 유형:
SSH, 소스:내 IP를 설정합니다. 보안 그룹 이름은vivid-ai-bastion-sg로 지정합니다.
- VPC: EKS 클러스터와 동일한 VPC(
- **"인스턴스 시작"**을 클릭합니다.
단계 2: 보안 그룹 및 RDS 설정 변경
- RDS 보안 그룹 수정 (
vivid-ai-development-rds-sg):- **"인바운드 규칙 편집"**으로 이동합니다.
- 기존의
PostgreSQL규칙(소스: "내 IP" 또는0.0.0.0/0)을 삭제합니다. - **"규칙 추가"**를 클릭하고 유형:
PostgreSQL, 소스: 위에서 생성한 배스천의 보안 그룹(vivid-ai-bastion-sg)을 선택합니다.
- RDS 퍼블릭 액세스 비활성화:
- RDS 인스턴스 "수정" 페이지로 이동합니다.
- "연결" 섹션에서 **"퍼블릭 액세스"**를 **"아니요(No)"**로 변경하고 즉시 적용합니다.
단계 3: GUI 도구(PgAdmin)에 SSH 터널 설정
- Connection 탭:
- Hostname/address: RDS 엔드포인트 주소
- Port:
5432 - Maintenance database:
postgres - Username/Password: RDS 마스터 사용자 정보
- SSH Tunnel 탭:
- Use SSH tunneling?: "Yes"
- Tunnel host: 배스천 호스트의 퍼블릭 IPv4 주소
- Tunnel port:
22 - Username:
ec2-user - Authentication: "Identity file" 선택 후, 다운로드한
.pem키 파일 지정
- Parameters 탭:
- SSL mode:
Require
- SSL mode:
- 설정을 저장하고 접속합니다.
8. 문제 해결 노트
-
Ingress
rewrite-target미작동:rewrite-target이 적용되지 않을 경우, 백엔드를 글로벌 접두사로 구성하고 Ingress에서 어노테이션을 제거합니다. -
Ingress 삭제 지연: Ingress가
Terminating상태로 멈출 경우, AWS 콘솔에서 연결된 ALB를 수동으로 삭제한 후kubectl patch ingress autopromomer-ai-ingress -p '{"metadata":{"finalizers":null}}' --type=merge명령으로 Kubernetes 리소스의finalizers를 강제로 제거합니다. -
no pg_hba.conf entry... no encryption:POSTGRES_SSL_ENABLED=true및app.module.ts에ssl: { rejectUnauthorized: false }가 활성화되어 있는지 확인합니다. -
401 Unauthorized(쿠키 미전송):auth.controller.ts의SECURE_COOKIES및sameSite설정을 HTTP/HTTPS 접속 환경에 맞게 확인합니다. -
403 Forbidden(ECR 로그인 만료): Docker 로그인 토큰이 만료되었을 경우 발생합니다.aws ecr get-login-password명령어를 다시 실행하여 로그인 토큰을 갱신해야 합니다. -
no match for platform in manifest: not found(아키텍처 불일치):docker build시--platform linux/amd64옵션을 명시하여 워커 노드 아키텍처에 맞는 이미지를 빌드해야 합니다. -
ImagePullBackOff또는ErrImagePull(ECR 권한 부족): 워커 노드의 IAM 역할에AmazonEC2ContainerRegistryReadOnly정책이 연결되어 있는지 확인합니다. -
VPC CIDR 충돌로 인한 EKS 클러스터 재생성:
-
원인: 기존 EKS 클러스터의 VPC CIDR(
192.168.0.0/16)이 로컬 네트워크 대역과 충돌하여 네트워크 문제가 발생했습니다. -
해결:
eksctl delete cluster로 기존 클러스터를 삭제하고,10.12.0.0/16CIDR을 사용하는 새로운 VPC 내에eksctl create cluster로vivid-ai-cluster-development클러스터를 재생성했습니다. -
주의: 클러스터 삭제 시 Ingress Controller가 프로비저닝한 ALB (
finalizer문제)로 인해 삭제가 지연될 수 있습니다. 이때는 AWS 콘솔에서 ALB를 수동으로 삭제한 후 클러스터 삭제를 재시도해야 합니다.
-
-
GitHub Actions
ResourceNotFoundException(EKS 클러스터 이름 변경):-
원인: EKS 클러스터 이름이
vivid-ai-cluster-dev에서vivid-ai-cluster-development로 변경되면서 GitHub Actions 워크플로우 내의 클러스터 이름 설정이 불일치했습니다. -
해결:
backend-vivid-ai및frontend-vivid-ai각각의.github/workflows/deploy.yml파일에서EKS_CLUSTER_NAME환경 변수를 새로운 클러스터 이름으로 업데이트했습니다.
-
-
GitHub Actions
AccessDeniedException(EKS IAM 정책 누락):-
원인: 배포 IAM 역할(
GitHubActions-vivid-ai-dev-role)에 새로운 EKS 클러스터에 대한eks:DescribeCluster등 필수 권한이 없었습니다.aws-iam-authenticator버전 업데이트 등으로 인해eks:DescribeCluster외 추가 권한이 필요해졌습니다. -
해결:
EKS-Deploy-User-PolicyIAM 정책에eks:DescribeNodegroup,eks:ListClusters,eks:ListNodegroups액션을vivid-ai-cluster-development클러스터 ARN에 대한 리소스로 추가했습니다.
-
-
GitHub Actions
You must be logged in to the server(aws-auth ConfigMap):-
원인: EKS 클러스터의
aws-authConfigMap에 GitHub Actions에서 사용하는 IAM 역할(GitHubActions-vivid-ai-dev-role)이system:masters그룹으로 매핑되어 있지 않았습니다. -
해결:
kubectl patch configmap aws-auth -n kube-system명령을 사용하여aws-authConfigMap에GitHubActions-vivid-ai-dev-role역할을system:masters그룹으로 매핑하는userarn및rolearn항목을 추가했습니다. (동시에 AWS 콘솔 로그인 사용자의 ARN도system:masters로 추가하여 콘솔 접근 권한 확보)
-
-
백엔드
502 Bad Gateway(RDS/Redis/SQS/S3 연결 문제 종합):-
원인: 새로 생성된 VPC 및 EKS 클러스터 환경에서 RDS, ElastiCache Redis, SQS, S3 연결 정보 및 보안 그룹 설정이 최신 상태로 업데이트되지 않아 발생하는 총체적 문제였습니다.
-
해결:
-
RDS: 새로운 EKS 워커 노드 보안 그룹(
sg-084a140a753d6a7d0)을 RDS 보안 그룹(vivid-ai-development-rds-sg)의 인바운드 규칙에 추가하여5432포트 접근을 허용했습니다. -
ElastiCache Redis: 새로운 ElastiCache Redis 클러스터(
vivid-ai-redis-dev)를 생성하고, EKS 워커 노드 보안 그룹을 Redis 보안 그룹으로 설정했습니다. Redis 클라이언트의connectTimeout을 10초로 늘리고, "전송 중 암호화"가 활성화되어 있는 점을 감안하여 Redis 연결 URL을rediss://스키마 기반으로 변경했습니다 (app.module.ts수정). -
SQS: 테스트용 SQS 큐(
vivid-ai-computation-queue-dev)를 생성하고,backend-deployment-dev.yaml에 해당 큐의 URL과 이름을 설정했습니다.generation.module.ts의SQS_COMPUTATION_QUEUE_URL의존성 활성화 코드를 다시 적용했습니다. -
S3:
vivid-ai-generated-devS3 버킷을 생성하고,backend-deployment-dev.yaml에 버킷 이름을 설정했습니다. -
환경 변수:
backend-deployment-dev.yaml이 .env 파일과 동기화되도록 필수 환경 변수를 추가/수정했습니다. -
API 버전 관리: 백엔드 API에
/v1접두사를 도입했으며, Ingress 헬스 체크 경로도/api/v1로 변경했습니다.
-
-
-
백엔드 Pod
0/1 Ready(Startup Probe 실패):-
원인: 애플리케이션 시작 시간이
startupProbe설정보다 길거나, 시작 과정 중 특정 모듈(초기에는 Swagger 설정, 이후 Redis 연결 문제)에서 블로킹되어 헬스 체크에 응답하지 못했습니다. -
해결:
-
main.ts에서app.listen()호출 전후에 상세 로그를 추가하여 블로킹 지점을 파악했습니다. -
Swagger 설정이 시작을 지연시키는 것을 확인하고,
main.ts에서 Swagger를 임시 주석 처리했습니다. (나중에 Redis 연결 문제가 해결된 후 Swagger는 다시 활성화) -
Redis 연결 문제가 해결된 후,
startupProbe의failureThreshold를 60으로 늘려 애플리케이션이 완전히 시작될 시간을 충분히 제공하여 이 문제를 최종적으로 해결했습니다. (프로브는 다시 활성화)
-
-
9. 배포 자동화 (CI/CD with GitHub Actions)
development 환경에 대한 배포는 GitHub Actions를 통해 자동화됩니다. 각 서브모듈(backend-vivid-ai, frontend-vivid-ai) 리포지토리의 development 브랜치에 코드가 푸시되면, 해당 애플리케이션의 빌드, ECR 푸시, EKS 배포가 자동으로 실행됩니다.
9.1. 워크플로우 파일 위치
- 백엔드:
backend-vivid-ai/.github/workflows/deploy.yml - 프론트엔드:
frontend-vivid-ai/.github/workflows/deploy.yml
9.2. 인증 방식 (OIDC)
- GitHub Actions는 AWS와 안전하게 통신하기 위해 **OIDC(OpenID Connect)**를 사용합니다. 이를 통해 장기 자격 증명(Access Key) 없이 임시 역할을 수임하여 보안을 강화합니다.
GitHubActionsOidcRole이라는 IAM 역할이 사용되며, 이 역할의 신뢰 정책은 각 서브모듈 리포지토리의development브랜치에서 오는 요청만 허용하도록 제한되어 있습니다.
9.3. GitHub Secrets
- 각 서브모듈 리포지토리의 Settings > Secrets and variables > Actions에 다음 Secret들이 설정되어 있어야 합니다.
AWS_IAM_ROLE_ARN_DEV: 워크플로우가 수임할 IAM 역할의 ARN.NEXT_PUBLIC_API_URL_DEV(프론트엔드 전용): 개발 서버 ALB의 API URL.
9.4. 배포 프로세스
- 트리거:
development브랜치에 코드가 푸시됩니다. - AWS 인증:
aws-actions/configure-aws-credentials액션이 OIDC를 통해 IAM 역할을 수임하여 임시 자격 증명을 얻습니다. - ECR 로그인:
aws-actions/amazon-ecr-login액션이 ECR에 로그인합니다. - Docker 이미지 빌드 & 푸시:
- 애플리케이션을 Docker 이미지로 빌드합니다. (
--platform linux/amd64사용) - 이미지에 Git 커밋 해시(
github.sha)와latest태그를 붙여 ECR에 푸시합니다.
- 애플리케이션을 Docker 이미지로 빌드합니다. (
- EKS 클러스터 연결:
aws-actions/amazon-eks-cluster액션이kubeconfig를 설정하여kubectl이 클러스터와 통신할 수 있도록 합니다. - 배포:
kubectl set image명령을 사용하여 EKS의 Deployment가 새로운 이미지 태그를 사용하도록 업데이트합니다.- 이 명령은 Kubernetes의 롤링 업데이트를 트리거하여, 중단 없이 새로운 버전의 애플리케이션으로 점진적으로 교체합니다.