개인→팀→엔터프라이즈 3단계 진화 과정 - 컨테이너 기술 실습
# 터미널 2개 사용 권장
터미널 1: Docker 명령어 실행용
터미널 2: 가이드 확인, 메모 작성용
# macOS에서 터미널 분할
Command + D (세로 분할)
Command + Shift + D (가로 분할)
docker ps -a로 상태 확인docker stop <container_name>으로 정리# 이미지 관련
docker pull postgres:15 # 이미지 다운로드
docker images # 로컬 이미지 목록
docker rmi <image_name> # 이미지 삭제
# 컨테이너 관련
docker run # 컨테이너 실행
docker ps # 실행 중인 컨테이너 목록
docker ps -a # 모든 컨테이너 목록
docker stop <container_name> # 컨테이너 중지
docker rm <container_name> # 컨테이너 삭제
# 유용한 옵션들
-d # 백그라운드 실행
-p 8000:8000 # 포트 매핑 (호스트:컨테이너)
--name my-app # 컨테이너 이름 지정
-e POSTGRES_PASSWORD=pass # 환경변수 설정
# 2025-10-02 11:00 시작
$ brew install --cask docker
# 또는 https://docker.com/products/docker-desktop 에서 다운로드
# 설치 확인
$ docker --version
Docker version 20.10.12, build e91ed57
# ⚠️ 중요: Docker Desktop 앱 실행 필요
$ open -a Docker
# 또는 Applications에서 Docker Desktop 실행
# 상단 메뉴바에 Docker 아이콘이 나타날 때까지 대기 (약 30초)
# Docker daemon 실행 확인
$ docker run hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.
# 만약 "Cannot connect to the Docker daemon" 에러가 나면:
# 1. Docker Desktop이 실행되었는지 확인
# 2. 상단 메뉴바에 Docker 아이콘 확인
# 3. 30초 정도 대기 후 재시도
# PostgreSQL 컨테이너 실행
$ docker run -d \
--name my-postgres \
-e POSTGRES_PASSWORD=mypassword \
-e POSTGRES_DB=testdb \
-p 5432:5432 \
postgres:15
# 실행 확인
$ docker ps
CONTAINER ID IMAGE COMMAND STATUS
abc123def456 postgres:15 "docker-entrypoint.s…" Up 2 minutes
# ⚠️ 중요: 페이징 모드 문제 해결
# 테이블 목록 조회 시 페이징 모드로 인해 멈출 수 있음
# 해결책: \pset pager off 옵션 사용
# 테스트 테이블 생성
$ docker exec -it my-postgres psql -U postgres -d testdb -c "CREATE TABLE users (id SERIAL PRIMARY KEY, name VARCHAR(50), email VARCHAR(100));"
CREATE TABLE
# 테스트 데이터 삽입
$ docker exec -it my-postgres psql -U postgres -d testdb -c "INSERT INTO users (name, email) VALUES ('테스트유저', 'test@example.com'), ('관리자', 'admin@example.com');"
INSERT 0 2
# 데이터 조회 (페이징 끄고)
$ docker exec -it my-postgres psql -U postgres -d testdb -c "\pset pager off" -c "SELECT * FROM users;"
Pager usage is off.
id | name | email
----+------------+-------------------
1 | 테스트유저 | test@example.com
2 | 관리자 | admin@example.com
(2 rows)
# 테이블 목록 확인 (페이징 끄고)
$ docker exec -it my-postgres psql -U postgres -d testdb -c "\pset pager off" -c "\dt"
Pager usage is off.
List of relations
Schema | Name | Type | Owner
--------+-------+-------+----------
public | users | table | postgres
(1 row)
testdb# 프롬프트에서 멈춤-c "\pset pager off" 옵션으로 페이징 비활성화\q + Enter 또는 Ctrl+Dq 누른 후 \qPostgreSQL과 연결되는 간단한 웹 API를 만들어서 Docker 컨테이너로 실행해보겠습니다. 실제 개발에서 자주 사용하는 패턴입니다.
# 프로젝트 디렉토리 생성
$ mkdir docker-test && cd docker-test
# 간단한 FastAPI 앱 작성
$ cat > main.py << 'EOF'
from fastapi import FastAPI
import psycopg2
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello Docker!"}
@app.get("/db-test")
def test_db():
try:
conn = psycopg2.connect(
host="host.docker.internal",
database="testdb",
user="postgres",
password="mypassword"
)
return {"db_status": "connected"}
except Exception as e:
return {"db_status": "error", "detail": str(e)}
EOF
# requirements.txt 작성
$ cat > requirements.txt << 'EOF'
fastapi==0.104.1
uvicorn==0.24.0
psycopg2-binary==2.9.9
EOF
# Dockerfile 작성
$ cat > Dockerfile << 'EOF'
FROM python:3.11-slim
WORKDIR /app
# 시스템 의존성 설치
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Python 의존성 설치
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 앱 코드 복사
COPY . .
# 포트 노출
EXPOSE 8000
# 앱 실행
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
EOF
/ → "Hello Docker!" 메시지 반환/db-test → PostgreSQL 연결 테스트왜 버전을 고정하나? 팀원들이 모두 같은 버전을 사용하도록
# 이미지 빌드
$ docker build -t my-fastapi .
[+] Building 45.2s (9/9) FINISHED
=> [1/5] FROM docker.io/library/python:3.11-slim
=> [2/5] WORKDIR /app
=> [3/5] COPY requirements.txt .
=> [4/5] RUN pip install -r requirements.txt
=> [5/5] COPY . .
=> exporting to image
# 컨테이너 실행
$ docker run -d -p 8000:8000 --name my-app my-fastapi
# 테스트
$ curl http://localhost:8000/
{"message":"Hello Docker!"}
$ curl http://localhost:8000/db-test
{"db_status":"connected"}
# 예약된 기본 파일명들 (Docker가 자동으로 찾음)
docker-compose.yml # 서비스 정의 파일 (필수)
docker-compose.yaml # yml과 동일 (선택)
.env # 환경변수 파일 (자동 로드)
Dockerfile # 이미지 빌드 파일
# 서비스명은 자유롭게 설정 가능
services:
postgres: # 이름 자유 (db, database, pg 등 가능)
redis: # 이름 자유 (cache, memory 등 가능)
backend: # 이름 자유 (api, app, web 등 가능)
git clone <프로젝트> # 1. 프로젝트 복제
cd <프로젝트> # 2. 디렉토리 이동
docker-compose up -d # 3. 전체 환경 실행 (끝!)
docker-compose.yml - 서비스 정의 ✅Dockerfile - 이미지 빌드 설정 ✅requirements.txt - 의존성 목록 ✅init.sql - 초기 데이터베이스 스키마 ✅.env - 환경변수 (또는 .env.example) ✅# 기존 컨테이너 정리
$ docker stop my-postgres my-app
$ docker rm my-postgres my-app
# docker-compose.yml 작성
$ cat > docker-compose.yml << 'EOF'
version: '3.8'
services:
postgres:
image: postgres:15
environment:
POSTGRES_DB: edustack
POSTGRES_USER: team
POSTGRES_PASSWORD: teampass123
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U team"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
backend:
build: .
ports:
- "8000:8000"
environment:
- DATABASE_URL=postgresql://team:teampass123@postgres:5432/edustack
- REDIS_URL=redis://redis:6379
volumes:
- .:/app
depends_on:
postgres:
condition: service_healthy
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
volumes:
postgres_data:
redis_data:
EOF
postgres_data:/var/lib/postgresql/data → 데이터 영속성./init.sql:/docker-entrypoint-initdb.d/init.sql → 초기 데이터 자동 생성# .env 파일 (팀 공유용)
$ cat > .env << 'EOF'
# Database
DB_HOST=postgres
DB_NAME=edustack
DB_USER=team
DB_PASSWORD=teampass123
# Redis
REDIS_HOST=redis
REDIS_PORT=6379
# App
DEBUG=true
LOG_LEVEL=info
EOF
# 초기 데이터베이스 스키마
$ cat > init.sql << 'EOF'
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (name, email) VALUES
('팀장', 'leader@team.com'),
('개발자1', 'dev1@team.com'),
('개발자2', 'dev2@team.com');
EOF
.env.example 파일을 Git에 커밋.env 파일을 복사해서 사용신입 개발자가 git clone → docker-compose up
두 명령어만으로 완전한 개발 환경을 구축할 수 있습니다!
# 전체 스택 실행
$ docker-compose up -d
Creating network "docker-test_default" with the default driver
Creating volume "docker-test_postgres_data" with local driver
Creating volume "docker-test_redis_data" with local driver
Creating docker-test_postgres_1 ... done
Creating docker-test_redis_1 ... done
Creating docker-test_backend_1 ... done
# 서비스 상태 확인
$ docker-compose ps
Name Command State Ports
---------------------------------------------------------------------------------
docker-test_backend_1 uvicorn main:app --host 0 ... Up 0.0.0.0:8000->8000/tcp
docker-test_postgres_1 docker-entrypoint.sh postgres Up 0.0.0.0:5432->5432/tcp
docker-test_redis_1 docker-entrypoint.sh redis ... Up 0.0.0.0:6379->6379/tcp
# 데이터베이스 테스트
$ docker-compose exec postgres psql -U team -d edustack -c "\pset pager off" -c "SELECT * FROM users;"
Pager usage is off.
id | name | email | created_at
----+----------+----------------+----------------------------
1 | 팀장 | leader@team.com | 2025-10-03 06:07:47.747639
2 | 개발자1 | dev1@team.com | 2025-10-03 06:07:47.747639
3 | 개발자2 | dev2@team.com | 2025-10-03 06:07:47.747639
# API 테스트 - 첫 번째 시도
$ curl http://localhost:8000/db-test
{"db_status":"error","detail":"password authentication failed for user \"postgres\""}
# 🚨 문제 발견! FastAPI 앱이 아직 개인 개발 설정을 사용하고 있음
docker-compose는 정상 실행되지만 API가 데이터베이스에 연결되지 않는 경우가 있습니다!
conn = psycopg2.connect(
host="host.docker.internal",
database="testdb",
user="postgres",
password="mypassword"
)
conn = psycopg2.connect(
host="postgres", # 서비스명 사용
database="edustack",
user="team",
password="teampass123"
)
# FastAPI 코드 수정 후 컨테이너 재시작
$ docker-compose restart backend
# 다시 API 테스트
$ curl http://localhost:8000/
{"message":"Hello Docker!"}
$ curl http://localhost:8000/db-test
{"db_status":"connected"}
$ git clone https://github.com/team/project
$ cd project
$ docker-compose up -d
# → 모든 팀원과 동일한 환경 완성!
$ git pull
$ docker-compose down
$ docker-compose up -d --build
# → 최신 변경사항 반영
팀 프로젝트에서 docker-compose로 잘 작동하던 것이 왜 EKS로 가야 할까요? 실제 운영 환경에서 발생하는 문제들을 해결하기 위해서입니다.
EKS는 "여러 대의 서버를 하나처럼 관리해주는 시스템"입니다. docker-compose가 한 서버에서 여러 컨테이너를 관리한다면, EKS는 여러 서버에서 수백 개의 컨테이너를 관리합니다.
AWS 완전 관리, API 서버, etcd, 스케줄러
EC2 인스턴스, 실제 컨테이너 실행
VPC, 서브넷, 로드밸런서
IAM 역할, 보안 그룹
EKS는 편리하지만 비용이 발생합니다. 실제 프로젝트에서는 비용 대비 효과를 신중히 고려해야 합니다.
# 1. eksctl 설치 (macOS)
$ brew tap weaveworks/tap
$ brew install weaveworks/tap/eksctl
# eksctl 직접 설치 (Homebrew 없는 경우)
$ curl --silent --location "https://github.com/weaveworks/eksctl/releases/latest/download/eksctl_$(uname -s)_amd64.tar.gz" | tar xz -C /tmp
$ sudo mv /tmp/eksctl /usr/local/bin
# 2. kubectl 설치 (macOS)
$ brew install kubectl
# kubectl 직접 설치
$ curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/darwin/amd64/kubectl"
$ chmod +x kubectl
$ sudo mv kubectl /usr/local/bin/
# 3. AWS CLI 설치
$ brew install awscli
# 또는
$ pip3 install awscli
# AWS CLI 설정
$ aws configure
AWS Access Key ID [None]: YOUR_ACCESS_KEY
AWS Secret Access Key [None]: YOUR_SECRET_KEY
Default region name [None]: ap-northeast-2
Default output format [None]: json
# eksctl 설치 확인
$ eksctl version
0.147.0
# kubectl 설치 확인
$ kubectl version --client
Client Version: v1.22.5
# AWS CLI 및 권한 확인
$ aws sts get-caller-identity
{
"UserId": "AROAVP3ECGNLQBTNMJEH4:████████████████████",
"Account": "████████████",
"Arn": "arn:aws:sts::████████████:assumed-role/AWSReservedSSO_emart-architect-admin-sso-ps_████████████████/████████████████████"
}
# IAM 권한 확인
$ aws iam list-attached-role-policies --role-name AWSReservedSSO_emart-architect-admin-sso-ps_████████████████
{
"AttachedPolicies": [
{
"PolicyName": "AdministratorAccess",
"PolicyArn": "arn:aws:iam::aws:policy/AdministratorAccess"
}
]
}
# 기존 VPC 확인
$ aws ec2 describe-vpcs --region ap-northeast-2
{
"Vpcs": [
{
"VpcId": "vpc-█████████████████",
"State": "available",
"CidrBlock": "10.224.48.0/20",
"Tags": [
{
"Key": "Name",
"Value": "prd-emart-vpc"
}
]
}
]
}
# 🧪 실습용 ECR 생성 (테스트 목적)
$ aws ecr create-repository \
--repository-name shr-dev-edustack-backend-ecr \
--image-scanning-configuration scanOnPush=true \
--tags '[
{"Key":"ServiceName","Value":"edustack"},
{"Key":"Environment","Value":"dev"},
{"Key":"Owner","Value":"████"},
{"Key":"Email","Value":"██████████████@emart.com"},
{"Key":"Purpose","Value":"ai-education-platform"}
]'
# ✅ 실제 회사에서는 기존 ECR 사용
# 예시: ████████████.dkr.ecr.ap-northeast-2.amazonaws.com/company-shared-ecr
# ECR 로그인 (임시 토큰 방식)
$ aws ecr get-login-password --region ap-northeast-2 --profile shared-service | \
docker login --username AWS --password-stdin \
████████████.dkr.ecr.ap-northeast-2.amazonaws.com
Login Succeeded
# 이미지 태그 및 푸시
$ docker tag my-fastapi:latest \
████████████.dkr.ecr.ap-northeast-2.amazonaws.com/shr-dev-edustack-backend-ecr:latest
$ docker push \
████████████.dkr.ecr.ap-northeast-2.amazonaws.com/shr-dev-edustack-backend-ecr:latest
The push refers to repository [████████████.dkr.ecr.ap-northeast-2.amazonaws.com/shr-dev-edustack-backend-ecr]
6044865a98da: Pushed
6ea2fa38e70e: Pushed
3b63a3e4f2b3: Pushed
1feeb1efc936: Pushed
41cfedeb33b0: Pushed
5c1c2aa63cc6: Pushed
5fc964a1adf1: Pushed
1e09695463df: Pushed
090e9c58f474: Pushed
latest: digest: sha256:548cbb13c49f786733492b80f0928615efb1009c9fd4807a042e3bbcd9667bf3 size: 2204
# 업로드된 이미지 확인
$ aws ecr list-images --repository-name shr-dev-edustack-backend-ecr
{
"imageIds": [
{
"imageDigest": "sha256:548cbb13c49f786733492b80f0928615efb1009c9fd4807a042e3bbcd9667bf3",
"imageTag": "latest"
}
]
}
지금까지 한 대의 서버에서 docker-compose로 실행하던 것을 여러 대의 서버에서 자동으로 관리되도록 확장합니다.
EKS는 단순히 서버 하나를 만드는 게 아니라 전체 인프라를 구축하기 때문입니다. 네트워크, 보안, 로드밸런서, 모니터링 등 운영에 필요한 모든 것을 자동으로 설정합니다.
먼저 우리가 구축할 EduStack EKS 아키텍처를 살펴보겠습니다.
# eksctl로 거버넌스 준수 클러스터 생성
$ eksctl create cluster \
--name shr-prd-edustack-cluster-eks \
--region ap-northeast-2 \
--vpc-private-subnets subnet-xxx,subnet-yyy \
--tags ServiceName=edustack,Environment=prd,Owner=emart
# 노드 그룹 추가
$ eksctl create nodegroup \
--cluster shr-prd-edustack-cluster-eks \
--name workers \
--node-type t3.medium \
--nodes 2 \
--nodes-min 1 \
--nodes-max 4
# 노드 상태 확인
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
ip-███████████.ap-northeast-2.compute.internal Ready <none> 4m5s v1.32.9-eks-113cf36
ip-██████████.ap-northeast-2.compute.internal Ready <none> 4m4s v1.32.9-eks-113cf36
# 클러스터 정보 확인
$ kubectl cluster-info
Kubernetes control plane is running at https://████████████████████████████████.gr7.ap-northeast-2.eks.amazonaws.com
CoreDNS is running at https://████████████████████████████████.gr7.ap-northeast-2.eks.amazonaws.com/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
# 네임스페이스 확인
$ kubectl get namespaces
NAME STATUS AGE
default Active 10m
kube-node-lease Active 10m
kube-public Active 10m
kube-system Active 10m
# EduStack 애플리케이션 배포 상태
$ kubectl get pods -n edustack
NAME READY STATUS RESTARTS AGE
edustack-backend-██████████ 1/1 Running 0 7s
edustack-backend-██████████ 1/1 Running 0 6s
postgres-█████████████ 1/1 Running 0 59s
# 서비스 및 LoadBalancer 확인
$ kubectl get svc -n edustack
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
edustack-backend-service LoadBalancer 10.100.239.15 ████████████████████████████████████████████████████████████████████████████████████ 80:32695/TCP 10m
postgres ClusterIP 10.100.177.197 <none> 5432/TCP 73s
# 애플리케이션 접근 테스트
$ curl http://████████████████████████████████████████████████████████████████████████████████████/
{"message":"EduStack API","version":"1.0.0","docs":"/docs"}
docker buildx build --platform linux/amd64 사용Mac (ARM64)에서 빌드한 Docker 이미지를 EKS (x86_64) 노드에서 실행할 때 발생하는 아키텍처 불일치 문제
# ❌ 문제 상황: 기본 빌드 (ARM64)
$ docker build -t edustack/backend:v1 .
$ docker push $ECR_REPO:v1
# EKS 배포 후 Pod 상태 확인
$ kubectl get pods -n edustack
NAME READY STATUS RESTARTS AGE
edustack-backend-xxx-yyy 0/1 CrashLoopBackOff 3 2m
# Pod 로그 확인
$ kubectl logs edustack-backend-xxx-yyy -n edustack
exec /usr/local/bin/uvicorn: exec format error
# ✅ 해결책: 플랫폼 지정 빌드 (x86_64)
$ docker buildx create --use
$ docker buildx build --platform linux/amd64 \
-t edustack/backend:v2-amd64 \
--push \
-t $ECR_REPO:v2-amd64 .
# 배포 매니페스트 업데이트
$ kubectl set image deployment/edustack-backend \
backend=$ECR_REPO:v2-amd64 -n edustack
# 정상 배포 확인
$ kubectl get pods -n edustack
NAME READY STATUS RESTARTS AGE
edustack-backend-abc-def 1/1 Running 0 30s
edustack-backend-ghi-jkl 1/1 Running 0 25s
--platform linux/amd64 옵션으로 테스트이제 앞서 개발한 EduStack 프로젝트를 EKS에 배포해보겠습니다!
# EduStack 전체 스택 배포
$ kubectl apply -f infrastructure/k8s/
# 배포 상태 확인
$ kubectl get pods -n edustack
NAME READY STATUS RESTARTS AGE
edustack-backend-7d4b8c8f9d-abc12 1/1 Running 0 2m
edustack-backend-7d4b8c8f9d-def34 1/1 Running 0 2m
edustack-postgres-6b8d9c7f8e-xyz89 1/1 Running 0 3m
edustack-redis-5c7d8e9f0a-qrs12 1/1 Running 0 3m
# 서비스 확인
$ kubectl get svc -n edustack
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
edustack-backend-service LoadBalancer 10.100.123.45 a1b2c3d4e5f6-123456789.ap-northeast-2.elb.amazonaws.com 80:32000/TCP 3m
postgres ClusterIP 10.100.234.56 5432/TCP 3m
redis ClusterIP 10.100.345.67 6379/TCP 3m
# EduStack API 테스트
$ curl http://a1b2c3d4e5f6-123456789.ap-northeast-2.elb.amazonaws.com/
{"message":"Welcome to EduStack AI Education Platform","version":"1.0.0","environment":"dev"}
$ curl http://a1b2c3d4e5f6-123456789.ap-northeast-2.elb.amazonaws.com/health
{"status":"healthy","database":"connected","redis":"connected","timestamp":"2025-10-02T13:40:00Z"}
# 로그 확인
$ kubectl logs -n edustack deployment/edustack-backend
INFO: Started server process [1]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO: EduStack AI Education Platform initialized
INFO: Database connection established
INFO: Redis connection established
# 스케일링 테스트
$ kubectl scale deployment edustack-backend --replicas=3 -n edustack
deployment.apps/edustack-backend scaled
$ kubectl get pods -n edustack
NAME READY STATUS RESTARTS AGE
edustack-backend-7d4b8c8f9d-abc12 1/1 Running 0 5m
edustack-backend-7d4b8c8f9d-def34 1/1 Running 0 5m
edustack-backend-7d4b8c8f9d-ghi56 1/1 Running 0 30s
# EduStack 프로젝트 디렉토리로 이동
$ cd ~/Develop/edustack
# Kubernetes 매니페스트 디렉토리 생성 (프로젝트 구조 준수)
$ mkdir -p infrastructure/k8s
$ cat > infrastructure/k8s/namespace.yaml << 'EOF'
apiVersion: v1
kind: Namespace
metadata:
name: edustack
labels:
name: edustack
environment: prd
project: edustack
EOF
$ cat > infrastructure/k8s/backend-deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: edustack-backend
namespace: edustack
labels:
app: edustack-backend
component: backend
spec:
replicas: 2
selector:
matchLabels:
app: edustack-backend
template:
metadata:
labels:
app: edustack-backend
component: backend
spec:
containers:
- name: backend
image: ████████████.dkr.ecr.ap-northeast-2.amazonaws.com/shr-dev-edustack-backend-ecr:latest
ports:
- containerPort: 8000
env:
- name: DATABASE_URL
value: "postgresql://edustack:edustack123@postgres:5432/edustack"
- name: REDIS_URL
value: "redis://redis:6379"
EOF
$ cat > infrastructure/k8s/backend-service.yaml << 'EOF'
apiVersion: v1
kind: Service
metadata:
name: edustack-backend-service
namespace: edustack
spec:
selector:
app: edustack-backend
ports:
- port: 80
targetPort: 8000
type: LoadBalancer
EOF
# PostgreSQL 배포 생성
$ cat > infrastructure/k8s/postgres-deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
namespace: edustack
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: "edustack"
- name: POSTGRES_USER
value: "edustack"
- name: POSTGRES_PASSWORD
value: "edustack123"
---
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: edustack
spec:
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
type: ClusterIP
EOF
# EduStack Backend Deployment
$ cat > infrastructure/k8s/backend-deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: edustack-backend
namespace: edustack
labels:
app: edustack-backend
component: api
environment: dev
spec:
replicas: 2
selector:
matchLabels:
app: edustack-backend
template:
metadata:
labels:
app: edustack-backend
component: api
spec:
containers:
- name: backend
image: 123456789.dkr.ecr.ap-northeast-2.amazonaws.com/shr-dev-edustack-backend-ecr:latest
ports:
- containerPort: 8000
env:
- name: DATABASE_URL
value: "postgresql://edustack:edustack123@postgres:5432/edustack"
- name: REDIS_URL
value: "redis://redis:6379"
- name: ENVIRONMENT
value: "dev"
- name: DEBUG
value: "true"
- name: PROJECT_NAME
value: "EduStack AI Education Platform"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
EOF
# EduStack Backend Service
$ cat > infrastructure/k8s/backend-service.yaml << 'EOF'
apiVersion: v1
kind: Service
metadata:
name: edustack-backend-service
namespace: edustack
labels:
app: edustack-backend
spec:
selector:
app: edustack-backend
ports:
- name: http
protocol: TCP
port: 80
targetPort: 8000
type: LoadBalancer
EOF
$ cat > infrastructure/k8s/postgres-deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: edustack-postgres
namespace: edustack
spec:
replicas: 1
selector:
matchLabels:
app: edustack-postgres
template:
metadata:
labels:
app: edustack-postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
env:
- name: POSTGRES_DB
value: "edustack"
- name: POSTGRES_USER
value: "edustack"
- name: POSTGRES_PASSWORD
value: "edustack123"
ports:
- containerPort: 5432
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-storage
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: edustack
spec:
selector:
app: edustack-postgres
ports:
- port: 5432
targetPort: 5432
EOF
$ cat > infrastructure/k8s/redis-deployment.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: edustack-redis
namespace: edustack
spec:
replicas: 1
selector:
matchLabels:
app: edustack-redis
template:
metadata:
labels:
app: edustack-redis
spec:
containers:
- name: redis
image: redis:7-alpine
ports:
- containerPort: 6379
command: ["redis-server", "--appendonly", "yes"]
volumeMounts:
- name: redis-storage
mountPath: /data
volumes:
- name: redis-storage
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: edustack
spec:
selector:
app: edustack-redis
ports:
- port: 6379
targetPort: 6379
EOF
# 1. 로컬 개발
docker build -t my-app .
# 2. ECR 푸시
docker tag my-app:latest $ECR_REPO:v1.0.0
docker push $ECR_REPO:v1.0.0
# 3. EKS 배포
kubectl set image deployment/backend-deployment \
backend=$ECR_REPO:v1.0.0 -n edustack
# 클러스터 상태 확인
kubectl get nodes
kubectl top pods -n edustack
# 로그 수집
kubectl logs -f deployment/backend-deployment \
-n edustack
# 스케일링
kubectl scale deployment backend-deployment \
--replicas=5 -n edustack
EKS 배포는 성공했지만 LoadBalancer 주소가 문제였습니다:
https://████████████████████████████████.gr7.ap-northeast-2.eks.amazonaws.com
처음에는 기존 CloudFront에 EKS ALB를 새 Origin으로 추가하는 방식을 계획했습니다.
# 1차 계획
기존: https://sac-serviceinfra.emart.com/aiseminar/ → S3 (정적)
신규: https://sac-serviceinfra.emart.com/aiseminar-api/ → EKS ALB (동적)
CloudFront 배포:
├── Origin 1: S3 (기존)
├── Origin 2: EKS ALB (신규 추가)
├── Behavior 1: /aiseminar/* → S3
├── Behavior 2: /aiseminar-api/* → ALB
├── WAF: 기존 규칙 적용
└── SSL: 기존 인증서 활용
CloudFront Origin 분석 중 기존 API Gateway가 이미 연결되어 있음을 발견!
# 기존 CloudFront Origins 확인
$ aws cloudfront get-distribution-config --id ██████████████
{
"Origins": [
{"DomainName": "sac-prd-comm-contents-s3.s3.amazonaws.com"},
{"DomainName": "shr-prd-ai-seminar-materials-s3.s3.amazonaws.com"},
{"DomainName": "██████████.execute-api.ap-northeast-2.amazonaws.com"} ← 기존 API Gateway!
]
}
# API Gateway 상세 정보
$ aws apigateway get-rest-api --rest-api-id ██████████
{
"name": "sac-prd-aws-account-alert-cfn",
"createdDate": "2022-05-16T23:30:57+09:00"
}
정적 사이트에서 API를 호출할 때 브라우저 제약사항 발견:
경로 기반 분리 + CloudFront Origin 확장 방식으로 최종 결정
# 기존 서비스 (변경 없음)
정적 학습 자료: https://sac-serviceinfra.emart.com/aiseminar/
# 신규 API 서비스
동적 API: https://sac-serviceinfra.emart.com/api/edustack/
CloudFront Behavior 설정:
우선순위 0: /api/edustack/* → EKS ALB Origin (no-cache)
우선순위 1: /aiseminar/* → S3 Origin (기존, medium-cache)
주요 엔드포인트:
├── /api/edustack/ → API 정보
├── /api/edustack/docs → API 문서
├── /api/edustack/health → 상태 확인
├── /api/edustack/contents → 컨텐츠 API
└── /api/edustack/analytics → 분석 API
# CloudFront 설정 변경 (AWS 콘솔에서)
1. CloudFront 배포 ██████████████ 선택
2. Origins 탭에서 EKS ALB Origin 추가
- Origin Domain: ████████████████████████████████.gr7.ap-northeast-2.eks.amazonaws.com
- Protocol: HTTP Only
- Port: 80
3. Behaviors 탭에서 /api/edustack/* 규칙 추가
- Origin: EKS ALB
- Cache Policy: CachingDisabled
4. 배포 업데이트 (5-10분 소요)
# 테스트
$ curl https://sac-serviceinfra.emart.com/api/edustack/health
{"status":"healthy","timestamp":"2025-10-03T16:42:00Z"}
# JavaScript에서 API 호출 (CORS 문제 없음)
fetch('https://sac-serviceinfra.emart.com/api/edustack/contents')
.then(response => response.json())
.then(data => console.log(data));
https://████████████████████████████████.gr7.ap-northeast-2.eks.amazonaws.com
https://sac-serviceinfra.emart.com/api/edustack/
사용자 → https://sac-serviceinfra.emart.com/api/edustack/ → CloudFront → EKS ALB (내부)
결과: 못생긴 EKS 도메인은 완전히 숨겨지고, 깔끔한 통합 도메인으로 완전 대체!
이론적 최적해 → 실무적 최적해로의 진화 과정을 함께 거쳐가며, 단순한 기술 구현이 아닌 비즈니스 제약사항을 고려한 현실적 의사결정을 지원하는 것이 AI Assistant와의 협업에서 얻는 진정한 가치입니다.
실습 완료 후 반드시 리소스를 정리해야 불필요한 비용이 발생하지 않습니다.