diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..9a3f5ae --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,122 @@ +name: Deploy to EC2 + +on: + push: + branches: [ main ] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Docker Build + run: docker build -t eightyage . + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_KEY }} + aws-region: ap-northeast-2 + + - name: ECR Login + run: | + aws ecr get-login-password | \ + docker login --username AWS --password-stdin ${{ secrets.ECR_URI }} + + - name: Docker Images Check + run: docker images + + - name: ECR Push + run: | + docker tag eightyage:latest ${{ secrets.ECR_URI }} + docker push ${{ secrets.ECR_URI }} + + - name: Deploy on EC2 + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.EC2_HOST }} + username: ubuntu + key: ${{ secrets.EC2_SSH_KEY }} + envs: ECR_URI,DB_URL,DB_USER,DB_PASSWORD,JWT_SECRET_KEY,AWS_ACCESS_KEY,AWS_SECRET_KEY,REDIS_HOST + + script: | + export ECR_URI=${{ secrets.ECR_URI }} + export DB_URL=${{ secrets.DB_URL }} + export DB_USER=${{ secrets.DB_USER }} + export DB_PASSWORD=${{ secrets.DB_PASSWORD }} + export JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} + export AWS_ACCESS_KEY=${{ secrets.AWS_ACCESS_KEY }} + export AWS_SECRET_KEY=${{ secrets.AWS_SECRET_KEY }} + export REDIS_HOST=${{ secrets.REDIS_HOST }} + + docker ps -q --filter ancestor=$ECR_URI | xargs -r docker stop + docker ps -aq --filter ancestor=$ECR_URI | xargs -r docker rm + + aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin $ECR_URI + docker pull $ECR_URI + docker run -d -p 8080:8080 \ + -e DB_URL=$DB_URL \ + -e DB_USER=$DB_USER \ + -e DB_PASSWORD=$DB_PASSWORD \ + -e JWT_SECRET_KEY=$JWT_SECRET_KEY \ + -e AWS_ACCESS_KEY=$AWS_ACCESS_KEY \ + -e AWS_SECRET_KEY=$AWS_SECRET_KEY \ + -e REDIS_HOST=$REDIS_HOST \ + $ECR_URI + + - name: Health Check + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.EC2_HOST }} + username: ubuntu + key: ${{ secrets.EC2_SSH_KEY }} + script: | + for i in {1..10}; do + echo "⏳ Health check attempt $i..." + if curl -f http://localhost:8080/actuator/health; then + echo "✅ Health check succeeded!" + exit 0 + fi + sleep 5 + done + echo "❌ Health check failed after multiple attempts" + exit 1 + + - name: Notify Slack - 배포 성공 + if: success() + run: | + curl -X POST -H 'Content-type: application/json' \ + --data '{ + "text": "✅ *배포 성공!* 🎉", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*✅ 배포 성공했습니다!*\n\n*브랜치:* `${{ github.ref_name }}`\n" + } + } + ] + }' \ + ${{ secrets.SLACK_WEBHOOK_URL }} + + - name: Notify Slack - 배포 실패 + if: failure() + run: | + curl -X POST -H 'Content-type: application/json' \ + --data '{ + "text": "❌ *배포 실패!* 🔥", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*❌ 배포 실패했습니다!*\n\n*브랜치:* `${{ github.ref_name }}`\n" + } + } + ] + }' \ + ${{ secrets.SLACK_WEBHOOK_URL }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2b62ead --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM gradle:8.6-jdk17 AS build +WORKDIR /app +COPY . . +RUN gradle clean build -x test + +FROM eclipse-temurin:17-jdk-alpine +WORKDIR /app + +COPY --from=build /app/build/libs/*.jar app.jar +EXPOSE 8080 + +ENTRYPOINT ["java", "-Dspring.profiles.active=prod", "-jar", "app.jar"] \ No newline at end of file diff --git a/build.gradle b/build.gradle index 7bfe89d..6d882b4 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,8 @@ dependencies { implementation 'org.redisson:redisson:3.23.5' testImplementation 'org.mockito:mockito-inline:5.2.0' + + implementation 'org.springframework.boot:spring-boot-starter-actuator' } tasks.named('test') { diff --git a/src/main/java/com/example/eightyage/global/config/RedissonConfig.java b/src/main/java/com/example/eightyage/global/config/RedissonConfig.java index 4586fb3..655646c 100644 --- a/src/main/java/com/example/eightyage/global/config/RedissonConfig.java +++ b/src/main/java/com/example/eightyage/global/config/RedissonConfig.java @@ -5,15 +5,19 @@ import org.redisson.config.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.beans.factory.annotation.Value; @Configuration public class RedissonConfig { + @Value("${spring.data.redis.host}") + private String redisHost; + @Bean public RedissonClient redisson() { Config config = new Config(); config.useSingleServer() - .setAddress("redis://localhost:6379"); - return Redisson.create(); + .setAddress("redis://" + redisHost + ":6379"); + return Redisson.create(config); } } diff --git a/src/main/java/com/example/eightyage/global/config/SecurityConfig.java b/src/main/java/com/example/eightyage/global/config/SecurityConfig.java index 2fe9756..50ee441 100644 --- a/src/main/java/com/example/eightyage/global/config/SecurityConfig.java +++ b/src/main/java/com/example/eightyage/global/config/SecurityConfig.java @@ -42,6 +42,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .rememberMe(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .requestMatchers(request -> request.getRequestURI().startsWith("/api/v1/auth")).permitAll() + .requestMatchers("/actuator/**").permitAll() .anyRequest().authenticated() ) .build(); diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 0000000..3006731 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,50 @@ +server: + port: 8080 + servlet: + context-path: / + encoding: + charset: UTF-8 + enabled: true + force: true + session: + timeout: 1800 + +spring: + application: + name: eightyage + + data: + redis: + host: ${SPRING_DATA_REDIS_HOST} + port: 6379 + + datasource: + url: ${DB_URL} + username: ${DB_USER} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + show_sql: false + format_sql: true + use_sql_comments: false + dialect: org.hibernate.dialect.MySQLDialect + +jwt: + secret: + key: ${JWT_SECRET_KEY} + +management: + endpoints: + web: + exposure: + include: health,info + endpoint: + health: + show-details: always + security: + enabled: false \ No newline at end of file