diff --git a/.github/workflows/ci-prod.yml b/.github/workflows/ci-prod.yml new file mode 100644 index 0000000..c220937 --- /dev/null +++ b/.github/workflows/ci-prod.yml @@ -0,0 +1,57 @@ +name: CI-PROD + +on: + pull_request: + branches: [ main, dev ] + push: + branches: [ main, dev ] + +permissions: + contents: read # 내 코드를 읽을 수 있게(코드이동) 접근 권한을 준다. + id-token: write # OIDC 토큰을 발급할 수 있게 한다. + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout # 내 코드 이동 깃허브 컴퓨터로 + uses: actions/checkout@v4 + + - name: Set up JDK 17 # jdk 환경 설치 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + + - name: Run tests # 테스트 실행 + run: ./gradlew test --no-daemon + + - name: Generate image tag + run: echo "IMAGE_TAG=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_ENV + + - name: Configure AWS credentials (OIDC) # OIDC 기반으로 역할 자격증명 + if: github.event_name == 'push' + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ secrets.AWS_ROLE_ARN }} + aws-region: ${{ vars.AWS_REGION }} + + - name: Login to Amazon ECR # 위에서 자격증명하고 ecr 로그인 + if: github.event_name == 'push' + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build Docker image # 이미지 빌드해서 + run: | + docker build --build-arg SERVER_PORT=${{ vars.SERVER_PORT }} -t ${{ vars.ECR_REGISTRY }}/${{ vars.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} . + + - name: Push Docker image to ECR # 이미지 ecr로 업로드 + if: github.event_name == 'push' + run: | + docker push ${{ vars.ECR_REGISTRY }}/${{ vars.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} diff --git a/.gitignore b/.gitignore index 479b760..2bd5fad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ HELP.md .gradle build/ -!gradle/wrapper/gradle-wrapper.jar !**/src/main/**/build/ !**/src/test/**/build/ @@ -62,6 +61,7 @@ Temporary Items *.ctxt .mtj.tmp/ *.jar +!gradle/wrapper/gradle-wrapper.jar *.war *.nar *.ear @@ -105,4 +105,4 @@ $RECYCLE.BIN/ ### Docker ### *.tar -docker-compose.override.yml \ No newline at end of file +docker-compose.override.yml diff --git a/Dockerfile b/Dockerfile index 271db62..7b6892e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,20 @@ -FROM eclipse-temurin:17-jre +FROM eclipse-temurin:17-jdk AS builder WORKDIR /app -# 1. non-root 전용 유저 및 그룹 생성 (appuser) -RUN groupadd -r appuser && useradd -r -g appuser appuser +COPY gradlew . +COPY gradle gradle +COPY build.gradle settings.gradle ./ +COPY src src + +RUN chmod +x ./gradlew +RUN ./gradlew bootJar -x test -x asciidoctor --no-daemon -# 2. 호스트(로컬 PC)에서 이미 빌드된 jar 파일을 복사 (파일 소유권 자동 지정) -COPY --chown=appuser:appuser build/libs/*-SNAPSHOT.jar app.jar +FROM eclipse-temurin:17-jre +WORKDIR /app -# 3. 컨테이너 실행 권한을 appuser로 전환 -USER appuser +COPY --from=builder /app/build/libs/*.jar app.jar -EXPOSE 19900 +ARG SERVER_PORT=19900 +EXPOSE ${SERVER_PORT} ENTRYPOINT ["java", "-jar", "/app/app.jar"] diff --git a/build.gradle b/build.gradle index c9927d5..5dd906b 100644 --- a/build.gradle +++ b/build.gradle @@ -66,6 +66,10 @@ dependencies { testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.springframework.security:spring-security-test' + + runtimeOnly 'io.micrometer:micrometer-registry-prometheus' + + testImplementation 'org.testcontainers:testcontainers' } dependencyManagement { @@ -90,3 +94,8 @@ bootJar { dependsOn asciidoctor from("${asciidoctor.outputDir}") { into 'static/docs' } } + +// plain jar 생성 방지 +jar { + enabled = false +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..d997cfc Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 0000000..62316ee --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,59 @@ +server: + port: ${SERVER_PORT:19900} + +spring: + application: + name: inventory-service + + datasource: + url: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + username: ${POSTGRES_USER} + password: ${POSTGRES_PASSWORD} + driver-class-name: org.postgresql.Driver + + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT:6379} + + kafka: + bootstrap-servers: ${KAFKA_BROKERS} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.apache.kafka.common.serialization.StringSerializer + consumer: + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.apache.kafka.common.serialization.StringDeserializer + group-id: ${spring.application.name}-group + + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + default_schema: inventory_service + format_sql: true + show-sql: true + + cloud: + inetutils: + ignored-interfaces: [ "lo", "docker.*", "veth.*" ] + preferred-networks: [ "10.0" ] + use-only-site-local-interfaces: true + +eureka: + client: + enabled: ${EUREKA_ENABLED:false} + service-url: + defaultZone: http://${EUREKA_HOST}:${EUREKA_PORT}/eureka/ + instance: + prefer-ip-address: true + +management: + endpoints: + web: + exposure: + include: health,info,prometheus + endpoint: + health: + show-details: always diff --git a/src/test/java/com/michelet/inventory/application/StockSchedulerServiceTest.java b/src/test/java/com/michelet/inventory/application/StockSchedulerServiceTest.java index ae4b668..01be473 100644 --- a/src/test/java/com/michelet/inventory/application/StockSchedulerServiceTest.java +++ b/src/test/java/com/michelet/inventory/application/StockSchedulerServiceTest.java @@ -9,14 +9,38 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.kafka.core.KafkaTemplate; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; @SpringBootTest @ActiveProfiles("test") @Transactional +@Testcontainers class StockSchedulerServiceTest { + // 테스트 실행 시 가짜 미니 Redis 컨테이너 띄우기 (인프라 환경과 동일한 버전) + @Container + static GenericContainer redis = new GenericContainer<>("redis:7-alpine") + .withExposedPorts(6379); + + // 띄워진 가짜 Redis의 동적 IP와 포트를 스프링 환경변수에 주입 + @DynamicPropertySource + static void redisProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.redis.host", redis::getHost); + registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379)); + } + + // Kafka 연결 에러(로그 도배 및 실패)를 막기 위해 가짜 템플릿 주입 + @MockitoBean + private KafkaTemplate kafkaTemplate; + @Autowired private StockSchedulerService stockSchedulerService;