diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..145bb8d --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,100 @@ +name: Deploy to Amazon Linux EC2 (compose-prod) + +on: + push: + branches: [ main ] # main 브랜치 푸시 시 배포 + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + # 1) 코드 체크아웃 + - name: Checkout + uses: actions/checkout@v4 + + # 2) JDK 설치 (Spring 빌드) + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + # 3) Gradle 빌드 (테스트 생략 예시) + - name: Build Spring Boot + run: ./gradlew clean build -x test + + # 4) EC2로 파일 전송 + # - compose 파일: docker-compose-prod.yml + # - backendProject 폴더 안의 Dockerfile + # - CI에서 생성한 JAR (backendProject/build/libs/*.jar) + # - (있다면) .env + - name: Upload artifacts to EC2 + uses: appleboy/scp-action@v0.1.7 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + source: | + docker-compose-prod.yml + backendProject/Dockerfile + backendProject/build/libs/*.jar + .env + target: ${{ secrets.EC2_APP_DIR }} + + # 5) EC2에서 배포 (Amazon Linux) + - name: Deploy on EC2 + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.EC2_HOST }} + username: ${{ secrets.EC2_USER }} + key: ${{ secrets.EC2_SSH_KEY }} + script: | + set -e + APP_DIR="${{ secrets.EC2_APP_DIR }}" + cd "$APP_DIR" + + # Docker/compose 설치(없을 때) + if ! command -v docker >/dev/null 2>&1; then + if command -v dnf >/dev/null 2>&1; then + sudo dnf -y update + sudo dnf -y install docker + else + sudo yum -y update + sudo yum -y install docker + fi + sudo systemctl enable docker + sudo systemctl start docker + sudo usermod -aG docker $USER || true + fi + if ! docker compose version >/dev/null 2>&1; then + if command -v dnf >/dev/null 2>&1; then + sudo dnf -y install docker-compose-plugin + else + sudo yum -y install docker-compose-plugin || true + fi + fi + + # 기존 컨테이너 중지 후 재빌드/재기동 + sudo docker compose -f docker-compose-prod.yml down || true + sudo docker compose -f docker-compose-prod.yml build --no-cache + sudo docker compose -f docker-compose-prod.yml up -d + + # (옵션) 안 쓰는 이미지 정리 + sudo docker image prune -f + + # 6) (선택) CloudFront 캐시 무효화 + - name: Configure AWS credentials + if: ${{ secrets.CF_DISTRIBUTION_ID != '' }} + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Invalidate CloudFront + if: ${{ secrets.CF_DISTRIBUTION_ID != '' }} + run: | + aws cloudfront create-invalidation \ + --distribution-id "${{ secrets.CF_DISTRIBUTION_ID }}" \ + --paths "/*" diff --git a/backendProject/src/main/java/likelion/mlb/backendProject/global/health/HealthController.java b/backendProject/src/main/java/likelion/mlb/backendProject/global/health/HealthController.java new file mode 100644 index 0000000..c0d4b5b --- /dev/null +++ b/backendProject/src/main/java/likelion/mlb/backendProject/global/health/HealthController.java @@ -0,0 +1,13 @@ +package likelion.mlb.backendProject.global.health; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HealthController { + @GetMapping("/health") + public ResponseEntity healthCheck() { + return ResponseEntity.ok("ok"); + } +} diff --git a/backendProject/src/main/java/likelion/mlb/backendProject/global/security/config/SecurityConfig.java b/backendProject/src/main/java/likelion/mlb/backendProject/global/security/config/SecurityConfig.java index a0b5a41..d6c0b9e 100644 --- a/backendProject/src/main/java/likelion/mlb/backendProject/global/security/config/SecurityConfig.java +++ b/backendProject/src/main/java/likelion/mlb/backendProject/global/security/config/SecurityConfig.java @@ -42,6 +42,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/login.html", "/login-success.html").permitAll() .requestMatchers("/api/**", "/oauth2/**", "/login.html").permitAll() .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() + .requestMatchers("/health").permitAll() .requestMatchers("/ws/**").permitAll() .requestMatchers( "/swagger-ui/**", diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml new file mode 100644 index 0000000..ecf3712 --- /dev/null +++ b/docker-compose-prod.yml @@ -0,0 +1,139 @@ +version: '3.8' + +services: + + api1: + build: + context: ./backendProject + container_name: mlb-api-1 + ports: + - "8080:8080" + env_file: + - .env + depends_on: + mlb-redis1: + condition: service_healthy + mlb-elasticsearch: + condition: service_healthy + mlb-kafka: + condition: service_healthy + networks: + - backend + + api2: + build: + context: ./backendProject + container_name: mlb-api-2 + ports: + - "8081:8080" + env_file: + - .env + depends_on: + mlb-redis1: + condition: service_healthy + mlb-elasticsearch: + condition: service_healthy + mlb-kafka: + condition: service_healthy + networks: + - backend + + # Redis - 3 인스턴스 + mlb-redis1: + image: redis:7.2 + container_name: mlb-redis1 + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 10 + networks: + - backend + + mlb-redis2: + image: redis:7.2 + container_name: mlb-redis2 + ports: + - "6380:6379" + networks: + - backend + + mlb-redis3: + image: redis:7.2 + container_name: mlb-redis3 + ports: + - "6381:6379" + networks: + - backend + + # Elasticsearch + mlb-elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.13.4 + container_name: mlb-elasticsearch + environment: + - node.name=es01 + - discovery.type=single-node + - xpack.security.enabled=false + - xpack.security.transport.ssl.enabled=false + - ES_JAVA_OPTS=-Xms512m -Xmx512m + ports: + - "9200:9200" + - "9300:9300" + volumes: + - es_data:/usr/share/elasticsearch/data + healthcheck: + # TCP 열림 확인(의존 패키지 없이 동작) + test: ["CMD-SHELL", "bash -lc 'echo > /dev/tcp/127.0.0.1/9200'"] + interval: 10s + timeout: 3s + retries: 30 + networks: + - backend + + # Zookeeper (for Kafka) + mlb-zookeeper: + image: confluentinc/cp-zookeeper:7.4.1 + container_name: mlb-zookeeper + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + ports: + - "2181:2181" + healthcheck: + test: ["CMD-SHELL", "bash -lc 'echo > /dev/tcp/127.0.0.1/2181'"] + interval: 10s + timeout: 3s + retries: 30 + networks: + - backend + + # Kafka + mlb-kafka: + image: confluentinc/cp-kafka:7.4.1 + container_name: mlb-kafka + depends_on: + - mlb-zookeeper + ports: + - "9092:9092" + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: mlb-zookeeper:2181 + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://mlb-kafka:9092 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + healthcheck: + test: ["CMD-SHELL", "bash -lc 'echo > /dev/tcp/127.0.0.1/9092'"] + interval: 10s + timeout: 3s + retries: 30 + networks: + - backend + +volumes: + es_data: + +networks: + backend: + driver: bridge