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
145 changes: 145 additions & 0 deletions .github/workflows/cd-prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
name: CD-PROD

on:
workflow_run:
workflows: [ "CI-PROD" ]
types: [ completed ]
workflow_dispatch:

concurrency:
group: cd-prod
cancel-in-progress: false

permissions:
contents: read
id-token: write

env:
AWS_REGION: ${{ vars.AWS_REGION }}
ECR_REGISTRY: ${{ vars.ECR_REGISTRY }}
ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
ECS_CLUSTER: ${{ vars.ECS_CLUSTER }}
ECS_SERVICE: ${{ vars.ECS_SERVICE }}
TASK_FAMILY: ${{ vars.TASK_FAMILY }}
CONTAINER_NAME: ${{ vars.CONTAINER_NAME }}

jobs:
deploy:
if: >
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main'
)
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}

- name: Resolve image tag
id: tag
run: |
if [ "${{ github.event_name }}" = "workflow_run" ]; then
IMAGE_TAG="${{ github.event.workflow_run.head_sha }}"
else
IMAGE_TAG="${{ github.sha }}"
fi
IMAGE_TAG="${IMAGE_TAG:0:7}"
echo "image_tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT"

- name: Verify image exists in ECR
run: |
aws ecr describe-images \
--repository-name "${ECR_REPOSITORY}" \
--image-ids imageTag="${{ steps.tag.outputs.image_tag }}" \
> /dev/null

- name: Get current task definition
run: |
aws ecs describe-task-definition \
--task-definition "${TASK_FAMILY}" \
--query 'taskDefinition' > taskdef.json

- name: Render new task definition
env:
IMAGE_URI: ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ steps.tag.outputs.image_tag }}
run: |
jq \
--arg CN "${CONTAINER_NAME}" \
--arg IMG "${IMAGE_URI}" \
--arg SPRING_PROFILES_ACTIVE "prod" \
--arg SERVER_PORT "${{ vars.SERVER_PORT }}" \
--arg POSTGRES_HOST "${{ vars.POSTGRES_HOST }}" \
--arg POSTGRES_PORT "${{ vars.POSTGRES_PORT }}" \
--arg POSTGRES_DB "${{ vars.POSTGRES_DB }}" \
--arg POSTGRES_USER "${{ vars.POSTGRES_USER }}" \
--arg POSTGRES_PASSWORD "${{ secrets.POSTGRES_PASSWORD }}" \
--arg REDIS_HOST "${{ vars.REDIS_HOST }}" \
--arg REDIS_PORT "${{ vars.REDIS_PORT }}" \
--arg KAFKA_BROKERS "${{ vars.KAFKA_BROKERS }}" \
--arg EUREKA_ENABLED "${{ vars.EUREKA_ENABLED }}" \
--arg EUREKA_HOST "${{ vars.EUREKA_HOST }}" \
--arg EUREKA_PORT "${{ vars.EUREKA_PORT }}" \
--arg JWT_SECRET "${{ secrets.JWT_SECRET }}" \
--arg INTERNAL_AUTH_SECRET "${{ secrets.INTERNAL_AUTH_SECRET }}" \
'
.containerDefinitions |= map(
if .name == $CN then
.image = $IMG
| .environment = [
{"name":"SPRING_PROFILES_ACTIVE","value":$SPRING_PROFILES_ACTIVE},
{"name":"SERVER_PORT","value":$SERVER_PORT},
{"name":"POSTGRES_HOST","value":$POSTGRES_HOST},
{"name":"POSTGRES_PORT","value":$POSTGRES_PORT},
{"name":"POSTGRES_DB","value":$POSTGRES_DB},
{"name":"POSTGRES_USER","value":$POSTGRES_USER},
{"name":"POSTGRES_PASSWORD","value":$POSTGRES_PASSWORD},
{"name":"REDIS_HOST","value":$REDIS_HOST},
{"name":"REDIS_PORT","value":$REDIS_PORT},
{"name":"KAFKA_BROKERS","value":$KAFKA_BROKERS},
{"name":"EUREKA_ENABLED","value":$EUREKA_ENABLED},
{"name":"EUREKA_HOST","value":$EUREKA_HOST},
{"name":"EUREKA_PORT","value":$EUREKA_PORT},
{"name":"JWT_SECRET","value":$JWT_SECRET},
{"name":"INTERNAL_AUTH_SECRET","value":$INTERNAL_AUTH_SECRET},
{"name":"TZ","value":"Asia/Seoul"}
]
else .
end
)
| del(
.taskDefinitionArn, .revision, .status,
.requiresAttributes, .compatibilities,
.registeredAt, .registeredBy
)
' taskdef.json > taskdef.new.json

- name: Register new task definition revision
id: register
run: |
ARN=$(aws ecs register-task-definition \
--cli-input-json file://taskdef.new.json \
--query 'taskDefinition.taskDefinitionArn' \
--output text)
echo "task_def_arn=${ARN}" >> "$GITHUB_OUTPUT"

- name: Deploy to ECS service
run: |
aws ecs update-service \
--cluster "${ECS_CLUSTER}" \
--service "${ECS_SERVICE}" \
--task-definition "${{ steps.register.outputs.task_def_arn }}" \
--force-new-deployment

- name: Wait for stable
run: |
aws ecs wait services-stable \
--cluster "${ECS_CLUSTER}" \
--services "${ECS_SERVICE}"
57 changes: 57 additions & 0 deletions .github/workflows/ci-prod.yml
Original file line number Diff line number Diff line change
@@ -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 }}
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
HELP.md
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

Expand Down Expand Up @@ -62,6 +61,7 @@ Temporary Items
*.ctxt
.mtj.tmp/
*.jar
!gradle/wrapper/gradle-wrapper.jar
*.war
*.nar
*.ear
Expand Down Expand Up @@ -105,4 +105,4 @@ $RECYCLE.BIN/

### Docker ###
*.tar
docker-compose.override.yml
docker-compose.override.yml
21 changes: 13 additions & 8 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
9 changes: 9 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -90,3 +94,8 @@ bootJar {
dependsOn asciidoctor
from("${asciidoctor.outputDir}") { into 'static/docs' }
}

// plain jar ์ƒ์„ฑ ๋ฐฉ์ง€
jar {
enabled = false
}
Binary file added gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ protected boolean shouldNotFilter(HttpServletRequest request) {
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health", "/actuator/prometheus").permitAll()
.requestMatchers("/api/v1/products/**", "/api/v1/admin/products/**").permitAll()
.requestMatchers("/internal/**").permitAll()
.anyRequest().authenticated()
Expand Down
59 changes: 59 additions & 0 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String> kafkaTemplate;

@Autowired
private StockSchedulerService stockSchedulerService;

Expand Down
Loading