Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# docker-compose.prod.yml 용 환경변수 템플릿
# 실제 값은 .env 로 복사해서 채우고, .env 는 절대 커밋하지 말 것

# Docker Hub 사용자명 (앱 이미지: <DOCKER_USERNAME>/docker-springboot:latest)
DOCKER_USERNAME=your-dockerhub-username

# MySQL
MYSQL_ROOT_PASSWORD=change-me-root-password
DB_NAME=course_register
DB_USERNAME=course
DB_PASSWORD=change-me-app-password
46 changes: 9 additions & 37 deletions .github/workflows/cd-test.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
name: CD Test Pipeline
name: CD Test Pipeline (build & push image)

# test/** 브랜치 push 시 커밋 SHA 태그로 이미지를 push 한다.
# 서버 배포는 하지 않는다 (Terraform + user-data 담당).
on:
push:
branches:
- 'test/**'

jobs:
build-and-deploy-test:
build-and-push-test:
runs-on: ubuntu-latest

steps:
Expand All @@ -22,7 +24,7 @@ jobs:
- name: Grant permission for gradlew
run: chmod +x gradlew

- name: Clean and Build JAR
- name: Build JAR
run: |
./gradlew clean
./gradlew bootJar
Expand All @@ -33,37 +35,7 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Build Docker image (tag = commit SHA)
run: docker build -t ${{ secrets.DOCKER_USERNAME }}/docker-springboot:${{ github.sha }} .

- name: Push Docker image (tag = commit SHA)
run: docker push ${{ secrets.DOCKER_USERNAME }}/docker-springboot:${{ github.sha }}

- name: Deploy to EC2 (test)
uses: appleboy/[email protected]
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script: |
set -e

export DOCKER_USERNAME=${{ secrets.DOCKER_USERNAME }}
export IMAGE_TAG=${{ github.sha }}

# DB 환경변수 (현재 prod compose를 재사용한다는 전제)
export EC2_PRIVATE_IP=${{ secrets.EC2_PRIVATE_IP }}
export SPRING_PROFILES_ACTIVE=prod
export DB_HOST=${{ secrets.DB_HOST }}
export DB_PORT=${{ secrets.DB_PORT }}
export DB_NAME=${{ secrets.DB_NAME }}
export DB_USERNAME=${{ secrets.DB_USERNAME }}
export DB_PASSWORD=${{ secrets.DB_PASSWORD }}

echo "[INFO] Deploying test build with IMAGE_TAG=$IMAGE_TAG"

# app만 최신 SHA 태그로 pull & restart
sudo -E docker compose -f docker-compose.prod.yml pull app
sudo -E docker compose -f docker-compose.prod.yml up -d --no-deps app

sudo docker image prune -f
- name: Build & push image (tag = commit SHA)
run: |
docker build -t ${{ secrets.DOCKER_USERNAME }}/docker-springboot:${{ github.sha }} .
docker push ${{ secrets.DOCKER_USERNAME }}/docker-springboot:${{ github.sha }}
40 changes: 10 additions & 30 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
name: CD pipeline
name: CD pipeline (build & push image)

# develop에 머지되면 최신 이미지를 Docker Hub에 push 한다.
# 서버 배포는 Terraform(user-data가 :latest 를 pull)이 담당하므로 여기서는 하지 않는다.
on:
push:
branches:
- develop

jobs:
build-and-deploy:
build-and-push:
runs-on: ubuntu-latest

steps:
Expand All @@ -16,12 +19,12 @@ jobs:
with:
java-version: '17'
distribution: 'temurin'
cache: gradle

- name: Grant permission for gradlew
run: chmod +x gradlew


- name: Clean and Build JAR
- name: Build JAR
run: |
./gradlew clean
./gradlew bootJar
Expand All @@ -32,31 +35,8 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}

- name: Build Docker image
run: docker build -t ${{ secrets.DOCKER_USERNAME }}/docker-springboot .
- name: Build Docker image (:latest)
run: docker build -t ${{ secrets.DOCKER_USERNAME }}/docker-springboot:latest .

- name: Push to Docker Hub
run: docker push ${{ secrets.DOCKER_USERNAME }}/docker-springboot

- name: Deploy to EC2
uses: appleboy/[email protected]
with:
host: ${{ secrets.EC2_HOST }}
username: ${{ secrets.EC2_USER }}
key: ${{ secrets.EC2_PRIVATE_KEY }}
script: |
export DOCKER_USERNAME=${{ secrets.DOCKER_USERNAME }}
export EC2_PRIVATE_IP=${{ secrets.EC2_PRIVATE_IP }}
export SPRING_PROFILES_ACTIVE=prod
export DB_HOST=${{ secrets.DB_HOST }}
export DB_PORT=${{ secrets.DB_PORT }}
export DB_NAME=${{ secrets.DB_NAME }}
export DB_USERNAME=${{ secrets.DB_USERNAME }}
export DB_PASSWORD=${{ secrets.DB_PASSWORD }}

# spring boot 이미지 최신화 및 업데이트
sudo -E docker-compose -f docker-compose.prod.yml pull app
sudo -E docker-compose -f docker-compose.prod.yml up -d --no-deps app

# 필요 없는 이미지 정리
sudo docker image prune -f
run: docker push ${{ secrets.DOCKER_USERNAME }}/docker-springboot:latest
16 changes: 15 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,18 @@ out/
src/main/resources/application.yml
src/main/resources/application-test.yml

docker-compose.yml
docker-compose.yml

### secrets / env ###
.env
!.env.example

### Terraform ###
**/.terraform/*
*.tfstate
*.tfstate.*
*.tfvars
!*.tfvars.example
crash.log
override.tf
override.tf.json
102 changes: 102 additions & 0 deletions DEPLOY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# 🚀 배포 가이드 (단일 EC2 + Docker + Terraform)

> 리전: **ap-northeast-2 (서울)** / 구조: 단일 EC2 안에 `app` + `mysql` + `redis` 컨테이너
> 운영 방식: **테스트할 때만 `terraform apply`로 올리고, 끝나면 `terraform destroy`로 내림** (임시 환경)
>
> 최종 목표: 단일 서버로 배포하되 scale-out(다중 서버) 성능 차이를 측정할 수 있게 만드는 것.
> 이 문서는 첫 배포 기준 체크리스트다.

---

## 아키텍처

```
EC2 (t3.medium, Ubuntu, ap-northeast-2)
┌──────────────────────────────────────────┐
:80 app (Spring Boot)
├── mysql (컨테이너, EBS 볼륨)
└── redis (컨테이너)
└──────────────────────────────────────────┘
Terraform로 프로비저닝 · user-data로 부트스트랩
```

---

## Phase 0 — 사전 준비 (코드 + 계정)

배포 3단계 전에 반드시 끝내야 하는 준비.

### 코드
- [ ] `docker-compose.prod.yml`에 **MySQL 컨테이너 추가** + `app`이 `DB_HOST=mysql`로 연결
- [ ] **앱 이미지 재빌드 & Docker Hub push** — ⚠️ 최신 리팩터링 반영본으로 (기존 이미지는 옛 코드)
```bash
./gradlew clean bootJar
docker build -t <DOCKER_USERNAME>/docker-springboot:latest .
docker push <DOCKER_USERNAME>/docker-springboot:latest
```
- [ ] **시드 데이터** — ⚠️ 과목/회원이 없으면 부하테스트에서 신청할 대상이 없음
→ `DataInitializer`(CommandLineRunner)로 과목 + 테스트 회원 N명 생성 (BCrypt 해시 처리)
- [ ] `.env` 작성 (DB 비번 등) + `.gitignore` 확인 (커밋 금지)

### 계정 / 도구
- [ ] AWS 계정, **IAM 사용자 + 액세스키** 발급 (Terraform용, 최소권한)
- [ ] 로컬에 **AWS CLI** 설치 + `aws configure` (region: `ap-northeast-2`)
- [ ] 로컬에 **Terraform** 설치
- [ ] **SSH 키페어** 생성 (`ssh-keygen -t ed25519`)

---

## Phase 1 — 프로비저닝 (Terraform)

코드로 AWS 리소스를 정의하고 `apply`.

- [ ] Terraform 프로젝트 구조
- `provider.tf` — AWS provider, `region = "ap-northeast-2"`
- `main.tf` — 리소스 정의
- `variables.tf` / `terraform.tfvars`(**gitignore**) — 변수·비밀값
- `outputs.tf` — 완료 후 퍼블릭 IP 출력
- [ ] 리소스
- 기본 VPC/서브넷 (`data`로 참조, 새로 안 만듦)
- **Security Group**: 22(SSH, 내 IP만), 80(HTTP, 전체)
- **Key Pair** (Phase 0 공개키 등록)
- **EC2**: `t3.medium`, Ubuntu 22.04 (AMI는 `data`로 최신 조회)
- (선택) **Elastic IP** — 재시작 시 IP 고정 (안 쓰면 생략)
- [ ] `terraform init` → `plan` → `apply` → 퍼블릭 IP 획득

---

## Phase 2 — 부트스트랩 (user-data)

EC2가 부팅하며 자동 실행하는 스크립트.

- [ ] `user-data.sh`
- Docker + docker-compose plugin 설치
- 앱 파일 배치: `docker-compose.prod.yml`, `.env` (git clone 또는 scp)
- `.env` 비밀값 주입 (학습 단계는 scp, 이후 SSM Parameter Store 권장)
- [ ] lua 등 리소스는 jar(classpath)에 포함되는지 확인 → 포함되면 별도 전송 불필요

---

## Phase 3 — 배포 & 검증

- [ ] `docker compose -f docker-compose.prod.yml up -d`
- [ ] MySQL 스키마: `ddl-auto: update`가 테이블 자동 생성 → 시드로 과목 데이터 확인
- [ ] **스모크 테스트**: `http://<IP>` 접속 → 로그인 → 수강신청 플로우 1회 (리팩터링 후 첫 실동작 검증)
- [ ] 로그 확인: `docker compose logs -f`

---

## Phase 4 — 운영 (올리기/내리기)

- [ ] 내릴 때: `terraform destroy` → 과금 리소스 0
- [ ] ⚠️ destroy 시 MySQL 컨테이너 데이터도 삭제됨 → 부하테스트는 매번 깨끗한 상태가 좋고, 시더가 다시 채움
- [ ] 다시 올릴 때: `terraform apply`

---

## 나중에 (scale-out 실험 단계)

- [ ] 세션 외부화: `spring-session-data-redis` (다중 인스턴스 로그인 유지)
- [ ] nginx + app replica 다중화, 이후 EC2 다중 + ALB
- [ ] 스케줄러 중복 실행 문제 발견 → `ShedLock`으로 단일 인스턴스 실행 보장
- [ ] 단일 vs 다중 서버 부하테스트(k6) 비교
28 changes: 26 additions & 2 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,25 @@
services:
mysql:
image: mysql:8.0
container_name: mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
volumes:
- mysql-data:/var/lib/mysql
healthcheck:
test: [ "CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-p${MYSQL_ROOT_PASSWORD}" ]
interval: 5s
timeout: 5s
retries: 20
networks:
- app-network

redis:
image: redis:7-alpine
container_name: redis
Expand All @@ -22,19 +43,22 @@ services:
- "80:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
DB_HOST: ${DB_HOST}
DB_PORT: ${DB_PORT}
DB_HOST: mysql
DB_PORT: 3306
DB_NAME: ${DB_NAME}
DB_USERNAME: ${DB_USERNAME}
DB_PASSWORD: ${DB_PASSWORD}
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
networks:
- app-network

volumes:
mysql-data:
redis-data:

networks:
Expand Down
Loading
Loading