-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 배포 환경 구성 (Flyway / Redis / InfluxDB / docker-compose / CI-CD) #322
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| name: CI | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: [ develop, main ] | ||
|
|
||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v3 | ||
|
|
||
| - name: Set up JDK 17 | ||
| uses: actions/setup-java@v3 | ||
| with: | ||
| java-version: '17' | ||
| distribution: 'temurin' | ||
| cache: gradle | ||
|
|
||
| - name: Grant execute permission for gradlew | ||
| run: chmod +x gradlew | ||
|
|
||
| - name: Run tests | ||
| run: ./gradlew test | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| name: Deploy to EC2 | ||
|
|
||
| on: | ||
| push: | ||
| branches: [ main ] | ||
|
|
||
| jobs: | ||
| deploy: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v3 | ||
|
|
||
| - name: Set up JDK 17 | ||
| uses: actions/setup-java@v3 | ||
| with: | ||
| java-version: '17' | ||
| distribution: 'temurin' | ||
| cache: gradle | ||
|
|
||
| - name: Grant execute permission for gradlew | ||
| run: chmod +x gradlew | ||
|
|
||
| - name: Build with Gradle | ||
| run: ./gradlew clean build -x test | ||
|
|
||
| - name: Copy jar and Dockerfile to EC2 | ||
| uses: appleboy/scp-action@v0.1.7 | ||
| with: | ||
| host: ${{ secrets.EC2_HOST }} | ||
| username: ${{ secrets.EC2_USERNAME }} | ||
| key: ${{ secrets.EC2_SSH_KEY }} | ||
| source: "build/libs/*.jar,Dockerfile" | ||
| target: "/home/${{ secrets.EC2_USERNAME }}/app" | ||
|
|
||
| - name: Deploy on EC2 | ||
| uses: appleboy/ssh-action@v1.0.0 | ||
| with: | ||
| host: ${{ secrets.EC2_HOST }} | ||
| username: ${{ secrets.EC2_USERNAME }} | ||
| key: ${{ secrets.EC2_SSH_KEY }} | ||
| script: | | ||
| cd /home/${{ secrets.EC2_USERNAME }}/app | ||
|
|
||
| docker stop hard-click-app || true | ||
| docker rm hard-click-app || true | ||
| docker rmi hard-click-app || true | ||
|
|
||
| docker build -t hard-click-app . | ||
|
|
||
| docker run -d \ | ||
| --name hard-click-app \ | ||
| --restart unless-stopped \ | ||
| --network host \ | ||
| -e DB_URL="${{ secrets.DB_URL }}" \ | ||
| -e DB_USERNAME="${{ secrets.DB_USERNAME }}" \ | ||
| -e DB_PASSWORD="${{ secrets.DB_PASSWORD }}" \ | ||
| -e JWT_SECRET="${{ secrets.JWT_SECRET }}" \ | ||
| -e JWT_ACCESS_EXPIRATION=1800000 \ | ||
| -e JWT_REFRESH_EXPIRATION=1209600000 \ | ||
| -e MAIL_USERNAME="${{ secrets.MAIL_USERNAME }}" \ | ||
| -e MAIL_PASSWORD="${{ secrets.MAIL_PASSWORD }}" \ | ||
| -e REDIS_HOST=localhost \ | ||
| -e REDIS_PORT=6379 \ | ||
| -e REDIS_PASSWORD="${{ secrets.REDIS_PASSWORD }}" \ | ||
| -e INFLUX_URL=http://localhost:8086 \ | ||
| -e INFLUX_TOKEN="${{ secrets.INFLUX_TOKEN }}" \ | ||
| -e INFLUX_ORG=hardclick \ | ||
| -e INFLUX_BUCKET=hardclick-metrics \ | ||
| hard-click-app | ||
|
|
||
| echo "=== Deploy complete ===" | ||
| docker ps | grep hard-click-app | ||
|
Comment on lines
+73
to
+74
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 배포 성공 판정이 너무 이릅니다.
🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,4 @@ | ||||||||||||||||||||||||
| FROM eclipse-temurin:17-jre-alpine | ||||||||||||||||||||||||
| WORKDIR /app | ||||||||||||||||||||||||
| COPY build/libs/*.jar app.jar | ||||||||||||||||||||||||
| ENTRYPOINT ["java", "-jar", "app.jar"] | ||||||||||||||||||||||||
|
Comment on lines
+1
to
+4
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 컨테이너를 root로 실행하고 있습니다. 현재 이미지가 기본 사용자(root)로 애플리케이션을 실행합니다. 애플리케이션 취약점이 발생했을 때 컨테이너 내부 권한이 과도하므로, 전용 비특권 사용자를 만든 뒤 예시 수정 FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
+RUN addgroup -S app && adduser -S app -G app
COPY build/libs/*.jar app.jar
+RUN chown -R app:app /app
+USER app
ENTRYPOINT ["java", "-jar", "app.jar"]📝 Committable suggestion
Suggested change
🧰 Tools🪛 Trivy (0.69.3)[error] 1-1: Image user should not be 'root' Specify at least 1 USER command in Dockerfile with non-root user as argument Rule: DS-0002 (IaC/Dockerfile) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| services: | ||
| mysql: | ||
| image: mysql:8.0 | ||
| container_name: hard-click-mysql | ||
| restart: unless-stopped | ||
| environment: | ||
| MYSQL_DATABASE: Hard-Click | ||
| MYSQL_USER: Hard-Click | ||
| MYSQL_PASSWORD: Hard-Click | ||
| MYSQL_ROOT_PASSWORD: Hard-Click | ||
|
Comment on lines
+6
to
+10
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 로컬 Compose 자격 증명을 코드에 하드코딩하지 마세요.
🤖 Prompt for AI Agents |
||
| ports: | ||
| - "3306:3306" | ||
| volumes: | ||
| - mysql-data:/var/lib/mysql | ||
| command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci | ||
|
|
||
| redis: | ||
| image: redis:7 | ||
| container_name: hardclick-redis | ||
| ports: | ||
| - "6379:6379" | ||
| volumes: | ||
| - redis-data:/data | ||
|
|
||
| influxdb: | ||
| image: influxdb:2 | ||
| container_name: hardclick-influxdb | ||
| ports: | ||
| - "8086:8086" | ||
| volumes: | ||
| - influxdb-data:/var/lib/influxdb2 | ||
|
|
||
| volumes: | ||
| mysql-data: | ||
| redis-data: | ||
| influxdb-data: | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package com.wanted.backend.domain.learning_activity.application.service; | ||
|
|
||
| import com.wanted.backend.domain.learning_activity.application.port.VideoCatalogPort; | ||
| import com.wanted.backend.domain.learning_activity.domain.model.VideoAccessInfo; | ||
| import com.wanted.backend.domain.learning_activity.domain.model.VideoProgress; | ||
| import com.wanted.backend.domain.learning_activity.domain.repository.VideoProgressRepository; | ||
| import com.wanted.backend.global.exception.BusinessException; | ||
| import com.wanted.backend.global.exception.ErrorCode; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| @Transactional(readOnly = true) | ||
| public class PlayableVideoProgressReader { | ||
|
|
||
| private final VideoCatalogPort videoCatalogPort; | ||
| private final VideoAccessService videoAccessService; | ||
| private final VideoProgressRepository videoProgressRepository; | ||
|
|
||
| public PlayableVideoProgress get(Long memberId, Long videoId) { | ||
| VideoAccessInfo accessInfo = videoCatalogPort.findByVideoId(videoId) | ||
| .orElseThrow(() -> new BusinessException(ErrorCode.VIDEO_NOT_FOUND)); | ||
|
|
||
| videoAccessService.validatePlayable(memberId, accessInfo); | ||
|
|
||
| VideoProgress progress = videoProgressRepository | ||
| .findByMemberIdAndVideoId(memberId, videoId) | ||
| .orElse(VideoProgress.empty(memberId, accessInfo.courseId(), videoId)); | ||
|
|
||
| return new PlayableVideoProgress(accessInfo, progress); | ||
| } | ||
|
|
||
| public record PlayableVideoProgress( | ||
| VideoAccessInfo accessInfo, | ||
| VideoProgress progress | ||
| ) { | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,8 +5,7 @@ public enum PaymentStatus { | |
| REFUNDED, | ||
| READY, | ||
| FAILED, | ||
| CANCELED, | ||
| CANCELLED; | ||
| CANCELED; | ||
|
|
||
| public static PaymentStatus from(String value) { | ||
| return PaymentStatus.valueOf(value); | ||
|
Comment on lines
+8
to
11
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 기존
🤖 Prompt for AI Agents |
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,28 @@ | ||||||||||||||||
| package com.wanted.backend.global.config; | ||||||||||||||||
|
|
||||||||||||||||
| import com.influxdb.client.InfluxDBClient; | ||||||||||||||||
| import com.influxdb.client.InfluxDBClientFactory; | ||||||||||||||||
| import org.springframework.beans.factory.annotation.Value; | ||||||||||||||||
| import org.springframework.context.annotation.Bean; | ||||||||||||||||
| import org.springframework.context.annotation.Configuration; | ||||||||||||||||
|
|
||||||||||||||||
| @Configuration | ||||||||||||||||
| public class InfluxDbConfig { | ||||||||||||||||
|
|
||||||||||||||||
| @Value("${influx.url}") | ||||||||||||||||
| private String url; | ||||||||||||||||
|
|
||||||||||||||||
| @Value("${influx.token}") | ||||||||||||||||
| private String token; | ||||||||||||||||
|
|
||||||||||||||||
| @Value("${influx.org}") | ||||||||||||||||
| private String org; | ||||||||||||||||
|
|
||||||||||||||||
| @Value("${influx.bucket}") | ||||||||||||||||
| private String bucket; | ||||||||||||||||
|
|
||||||||||||||||
| @Bean | ||||||||||||||||
| public InfluxDBClient influxDBClient() { | ||||||||||||||||
| return InfluxDBClientFactory.create(url, token.toCharArray(), org, bucket); | ||||||||||||||||
|
Comment on lines
+24
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. InfluxDB 클라이언트가 항상 생성돼서 테스트/로컬 기동을 깨뜨릴 수 있습니다. 지금은 수정 예시 import com.influxdb.client.InfluxDBClient;
import com.influxdb.client.InfluxDBClientFactory;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
`@Configuration`
public class InfluxDbConfig {
@@
`@Bean`
+ `@ConditionalOnProperty`(prefix = "influx", name = "token")
public InfluxDBClient influxDBClient() {
return InfluxDBClientFactory.create(url, token.toCharArray(), org, bucket);
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package com.wanted.backend.global.config; | ||
|
|
||
| import org.springframework.cache.annotation.EnableCaching; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.data.redis.cache.RedisCacheConfiguration; | ||
| import org.springframework.data.redis.cache.RedisCacheManager; | ||
| import org.springframework.data.redis.connection.RedisConnectionFactory; | ||
| import org.springframework.data.redis.core.StringRedisTemplate; | ||
| import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; | ||
| import org.springframework.data.redis.serializer.RedisSerializationContext; | ||
| import org.springframework.data.redis.serializer.StringRedisSerializer; | ||
|
|
||
| import java.time.Duration; | ||
|
|
||
| @Configuration | ||
| @EnableCaching | ||
| public class RedisConfig { | ||
|
|
||
| @Bean | ||
| public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { | ||
| RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() | ||
| .entryTtl(Duration.ofMinutes(10)) | ||
| .serializeKeysWith( | ||
| RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) | ||
| .serializeValuesWith( | ||
| RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); | ||
|
|
||
| return RedisCacheManager.builder(connectionFactory) | ||
| .cacheDefaults(config) | ||
| .build(); | ||
| } | ||
|
|
||
| @Bean | ||
| public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) { | ||
| return new StringRedisTemplate(connectionFactory); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
push트리거가 빠져 있어서 CI 목표를 완전히 못 채웁니다.현재는 PR에서만 테스트가 돌고,
develop/main직접 푸시는 이 워크플로우를 타지 않습니다. 링크된 목표가push/PR양쪽 자동 실행이라면push도 같이 등록해야 합니다.예시 수정
on: pull_request: branches: [ develop, main ] + push: + branches: [ develop, main ]📝 Committable suggestion
🤖 Prompt for AI Agents