diff --git a/.github/workflows/docker-ci-cd.yml b/.github/workflows/docker-ci-cd.yml new file mode 100644 index 00000000..0fbb5d0d --- /dev/null +++ b/.github/workflows/docker-ci-cd.yml @@ -0,0 +1,103 @@ +name: CI CD + +on: + push: + branches: + - Ci/#91-cicd-gitaction + - main + # todo: 추후 main으로 수정할 것 + +jobs: + CI: + environment: oneul-tanda + runs-on: ubuntu-latest + + strategy: + matrix: + service: + - eureka-server + - gateway-service + - user-service + - flight-service + - reservation-service + - queue-service + - payment-service + + steps: + - name: Checkout source + uses: actions/checkout@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Build Docker Image + run: | + docker build -t ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.service }}:latest ./${{ matrix.service }} + + - name: Push Docker Image + run: | + docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ matrix.service }}:latest + +# 추후 분리가 필요할 수 있음 + CD: + needs: CI + environment: oneul-tanda + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: set up .env + run: | + cat < .env + JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }} + USER_DB_ROOT_PASSWORD=${{ secrets.USER_DB_ROOT_PASSWORD }} + USER_DB_ID=${{ secrets.USER_DB_ID }} + USER_DB_PASSWORD=${{ secrets.USER_DB_PASSWORD }} + RESERVATION_DB_ROOT_PASSWORD=${{ secrets.RESERVATION_DB_ROOT_PASSWORD }} + RESERVATION_DB_ID=${{ secrets.RESERVATION_DB_ID }} + RESERVATION_DB_PASSWORD=${{ secrets.RESERVATION_DB_PASSWORD }} + FLIGHT_DB_ID=${{ secrets.FLIGHT_DB_ID }} + FLIGHT_DB_PASSWORD=${{ secrets.FLIGHT_DB_PASSWORD }} + AMADEUS_CLIENT_ID=${{ secrets.AMADEUS_CLIENT_ID }} + AMADEUS_CLIENT_SECRET=${{ secrets.AMADEUS_CLIENT_SECRET }} + GF_SECURITY_ADMIN_USER=${{ secrets.GF_SECURITY_ADMIN_USER }} + GF_SECURITY_ADMIN_PASSWORD=${{ secrets.GF_SECURITY_ADMIN_PASSWORD }} + PAYMENT_DB_USERNAME=${{ secrets.PAYMENT_DB_USERNAME }} + PAYMENT_DB_ROOT_PASSWORD=${{ secrets.PAYMENT_DB_ROOT_PASSWORD }} + PAYMENT_DB_PASSWORD=${{ secrets.PAYMENT_DB_PASSWORD }} + IMPORT_API_KEY=${{ secrets.IMPORT_API_KEY }} + IMPORT_API_SECRET_KEY=${{ secrets.IMPORT_API_SECRET_KEY }} + EOF + + - name: Set up SSH + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H ${{ secrets.INSTANCE_IP }} >> ~/.ssh/known_hosts + + - name: Make sure remote directory exists + run: | + ssh ${{ secrets.SSH_USER }}@${{ secrets.INSTANCE_IP }} "mkdir -p /home/${{ secrets.SSH_USER }}/app" + + - name: Send docker-compose files and .env and init-scripts + run: | + scp ./docker-compose-server.yml ${{ secrets.SSH_USER }}@${{ secrets.INSTANCE_IP }}:/home/ubuntu/app/docker-compose.yml + scp ./.env ${{ secrets.SSH_USER }}@${{ secrets.INSTANCE_IP }}:/home/ubuntu/app/ + scp ./prometheus.yml ${{ secrets.SSH_USER }}@${{ secrets.INSTANCE_IP }}:/home/ubuntu/app/ + scp -r ./init-scripts ${{ secrets.SSH_USER }}@${{ secrets.INSTANCE_IP }}:/home/ubuntu/app/ + scp -r ./grafana ${{ secrets.SSH_USER }}@${{ secrets.INSTANCE_IP }}:/home/ubuntu/app/ + + - name: Run docker compose on server + run: | + ssh -tt ${{ secrets.SSH_USER }}@${{ secrets.INSTANCE_IP }} " + cd /home/${{ secrets.SSH_USER }}/app && + docker compose pull && + docker compose down && + docker compose up -d + " \ No newline at end of file diff --git a/api-http-test/integration-server.http b/api-http-test/integration-server.http new file mode 100644 index 00000000..79fd012b --- /dev/null +++ b/api-http-test/integration-server.http @@ -0,0 +1,100 @@ +# 전체 시나리오 +# 회원가입 → 로그인 → 특가 항공권 예매 요청 → 예약 대기열 서비스: 좌석 선점 처리 → 예약 대기열 서비스: 좌석 선점 성공 → +# 예약 대기열 서비스 Kafka prodcuer: 예약 선점 성공 이벤트 발행 → 예약 서비스 Kafka Consumer: 예약 임시 생성 → +# 예약 서비스: 예약 확정 (탑승객 정보 입력 + 결제 처리) + + +### 유저 생성 - internal +POST http://43.203.66.111:19091/api/v1/users/signup +Content-Type: application/json + +{ + "username": "tester", + "password": "12345678a*", + "nickname": "테스터", + "email": "test@test.com", + "contact": "010-1111-2222" +} + + +### 로그인 +POST http://43.203.66.111:19091/api/v1/users/login +Content-Type: application/json + +{ + "username": "tester", + "password": "12345678a*" +} +> {% + client.global.set("access_token", response.headers.valueOf("Authorization")) +%} + +### 항공편 조회 및 저장 +POST http://43.203.66.111:19091/api/v1/amadeus/flights/fetch-and-save?departureAirportCode=LGW&arrivalAirportCode=KWE&departureDate=2025-05-03T19:45:25&requiredSeats=2 +Authorization: Bearer {{access_token}} + + +### 특가 항공권 예매 요청 +POST http://43.203.66.111:19091/api/v1/queue +Content-Type: application/json +Authorization: Bearer {{access_token}} + +{ + "flightId" : "f83230fd-366c-4d32-875a-3f444b6c00fa", + "seatCount" : 2 +} + + +### 예약 대기열 서비스 Kafka prodcuer: 예약 선점 성공 이벤트 발행 + +### 예약 서비스 Kafka Consumer: 예약 임시 생성 + + + +### 예약 확정 V2 +PUT http://43.203.66.111:19091/api/v1/reservations/confirm +Content-Type: application/json +Authorization: Bearer {{access_token}} + +{ + "flightId": "f83230fd-366c-4d32-875a-3f444b6c00fa", + "passengers": [ + { + "name": "홍길동1", + "birth": "1990-01-01", + "gender": "MALE", + "passportNumber": "P12345678" + }, + { + "name": "홍길동", + "birth": "1995-06-10", + "gender": "FEMALE", + "passportNumber": "P87654321" + } + ] +} + + +### 예약 확정 V2, 결제 재시도 +PUT http://43.203.66.111:19091/api/v1/reservations/confirm +Content-Type: application/json +Authorization: Bearer {{access_token}} + +{ + "flightId": "57944490-33fb-4bb1-9254-1eae30a1a25c" +} + + + + +### 예약 단일 조회 +GET http://43.203.66.111:19091/api/v1/reservations/4dbd8623-d076-4132-ae1b-c795da8972f1 +Content-Type: application/json +Authorization: Bearer {{access_token}} + + +## 예약 목록 조회 +### 예약 목록 조회 - Default +GET http://43.203.66.111:19091/api/v1/reservations +Content-Type: application/json +Authorization: Bearer {{access_token}} diff --git a/docker-compose-server.yml b/docker-compose-server.yml new file mode 100644 index 00000000..52e102fd --- /dev/null +++ b/docker-compose-server.yml @@ -0,0 +1,240 @@ +services: + eureka-server: + image: duckori/eureka-server:latest + container_name: oneultanda-eureka-server + ports: + - "19090:19090" + + gateway: + image: duckori/gateway-service:latest + ports: + - "19091:19091" + env_file: + - .env + environment: + - JWT_SECRET_KEY=${JWT_SECRET_KEY} + depends_on: + - eureka-server + - kafka + - redis + + user-service: + image: duckori/user-service:latest + container_name: oneultanda-user-service + volumes: + - ./user-service:/app/user-service + ports: + - "19092:19092" + env_file: + - .env + environment: + - JWT_SECRET_KEY=${JWT_SECRET_KEY} + depends_on: + - user-db + - eureka-server + - redis + + user-db: + image: postgres:16.3 + container_name: oneultanda-user-db + environment: + - POSTGRES_USER=${USER_DB_ID} + - POSTGRES_DB=user_db + - POSTGRES_PASSWORD=${USER_DB_PASSWORD} + ports: + - "5432:5432" + volumes: + - user-postgres-data:/var/lib/postgresql/data + + flight-service: + image: duckori/flight-service:latest + container_name: oneultanda-flight-service + volumes: + - ./flight-service:/app/flight-service + ports: + - "19094:19094" + env_file: + - .env + environment: + - AMADEUS_CLIENT_ID=${AMADEUS_CLIENT_ID} + - AMADEUS_CLIENT_SECRET=${AMADEUS_CLIENT_SECRET} + depends_on: + - flight-db + - redis + - eureka-server + + flight-db: + image: postgres:16.3 + container_name: oneultanda-flight-db + environment: + - POSTGRES_USER=${FLIGHT_DB_ID} + - POSTGRES_DB=flight_db + - POSTGRES_PASSWORD=${FLIGHT_DB_PASSWORD} + ports: + - "5433:5432" + volumes: + - flight-postgres-data:/var/lib/postgresql/data + - ./init-scripts:/docker-entrypoint-initdb.d + + reservation-service: + image: duckori/reservation-service:latest + container_name: oneultanda-reservation-service + volumes: + - ./reservation-service:/app/reservation-service + ports: + - "19095:19095" + env_file: + - .env + depends_on: + - reservation-db + - kafka + - eureka-server + + reservation-db: + image: postgres:16.3 + container_name: oneultanda-reservation-db + environment: + - POSTGRES_USER=${RESERVATION_DB_ID} + - POSTGRES_DB=reservation_db + - POSTGRES_PASSWORD=${RESERVATION_DB_PASSWORD} + ports: + - "5434:5432" + volumes: + - reservation-postgres-data:/var/lib/postgresql/data + + queue-service: + image: duckori/queue-service:latest + container_name: oneultanda-queue-service + volumes: + - ./queue-service:/app/queue-service + ports: + - "19096:19096" + env_file: + - .env + depends_on: + - redis + - kafka + - eureka-server + + redis: + image: redis:latest + container_name: oneultanda-redis + ports: + - "6379:6379" + restart: always + + payment-service: + image: duckori/payment-service:latest + container_name: oneultanda-payment-service + volumes: + - ./payment-service:/app/payment-service + ports: + - "19093:19093" + env_file: + - .env + depends_on: + - payments-db + - eureka-server + + payments-db: + image: postgres:16.3 + container_name: oneultanda-payments-db + environment: + - POSTGRES_USER=${PAYMENT_DB_USERNAME} + - POSTGRES_DB=payments_db + - POSTGRES_PASSWORD=${PAYMENT_DB_PASSWORD} + ports: + - "5435:5432" + volumes: + - payments-postgres-data:/var/lib/postgresql/data + + + zookeeper: + image: wurstmeister/zookeeper:latest + platform: linux/amd64 + ports: + - "2181:2181" + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + + kafka: + image: wurstmeister/kafka:latest + platform: linux/amd64 + ports: + - "9092:9092" + environment: + KAFKA_ADVERTISED_LISTENERS: INSIDE://kafka:29092,OUTSIDE://localhost:9092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT + KAFKA_LISTENERS: INSIDE://0.0.0.0:29092,OUTSIDE://0.0.0.0:9092 + KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + depends_on: + - zookeeper + + kafka-ui: + image: provectuslabs/kafka-ui:latest + platform: linux/amd64 + ports: + - "8080:8080" + environment: + KAFKA_CLUSTERS_0_NAME: local + KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:29092 + KAFKA_CLUSTERS_0_ZOOKEEPER: zookeeper:2181 + KAFKA_CLUSTERS_0_READONLY: "false" + depends_on: + - kafka + + prometheus-init: + image: alpine + container_name: prometheus-init + command: ["sh", "-c", "mkdir -p /prometheus/data && chown -R 65534:65534 /prometheus"] + volumes: + - prometheus_data:/prometheus + restart: "no" + + prometheus: + image: prom/prometheus:latest + container_name: prometheus + ports: + - "9090:9090" + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + depends_on: + - prometheus-init + + grafana-init: + image: alpine + container_name: grafana-init + command: ["sh", "-c", "chown -R 472:472 /var/lib/grafana"] + volumes: + - grafana-storage:/var/lib/grafana + restart: "no" + + grafana: + image: grafana/grafana:latest + container_name: grafana + ports: + - "3000:3000" + volumes: + - grafana-storage:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + environment: + - GF_SECURITY_ADMIN_USER=${GF_SECURITY_ADMIN_USER} + - GF_SECURITY_ADMIN_PASSWORD=${GF_SECURITY_ADMIN_PASSWORD} + depends_on: + - grafana-init + - prometheus + +volumes: + user-postgres-data: + reservation-postgres-data: + flight-postgres-data: + payments-postgres-data: + prometheus_data: + grafana-storage: diff --git a/flight-service/src/main/java/com/oneul_tanda/flight_service/infrastructure/config/CacheConfig.java b/flight-service/src/main/java/com/oneul_tanda/flight_service/infrastructure/config/CacheConfig.java index 5f3353bf..75702fc9 100644 --- a/flight-service/src/main/java/com/oneul_tanda/flight_service/infrastructure/config/CacheConfig.java +++ b/flight-service/src/main/java/com/oneul_tanda/flight_service/infrastructure/config/CacheConfig.java @@ -25,7 +25,7 @@ public class CacheConfig { @Bean RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory, ObjectMapper objectMapper) { - // 기본 직렬화기 (다른 캐시용) + // 기본 직렬화기 GenericJackson2JsonRedisSerializer genericSerializer = new GenericJackson2JsonRedisSerializer( objectMapper); @@ -62,7 +62,7 @@ RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory, ObjectM // flightOffers 캐시 설정 (1분 TTL) RedisCacheConfiguration flightOffersConfig = RedisCacheConfiguration.defaultCacheConfig() - .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(flightSerializer)) + .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericSerializer)) .disableCachingNullValues() .entryTtl(Duration.ofMinutes(1)); @@ -90,4 +90,4 @@ public RedisTemplate redisTemplate( return template; } -} +} \ No newline at end of file diff --git a/gateway-service/src/test/http/gateway.http b/gateway-service/src/test/http/gateway.http index eb660d74..933888d3 100644 --- a/gateway-service/src/test/http/gateway.http +++ b/gateway-service/src/test/http/gateway.http @@ -1,4 +1,5 @@ ### 유저 생성 - internal +# todo: 중복 유저 관련해서는 500에러가 나온다 수정 필요 POST http://localhost:19091/api/v1/users/signup Content-Type: application/json diff --git a/payment-service/build.gradle b/payment-service/build.gradle index 17731c74..2c40dbf2 100644 --- a/payment-service/build.gradle +++ b/payment-service/build.gradle @@ -36,8 +36,10 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' // eureka implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' - // mysql - runtimeOnly 'com.mysql:mysql-connector-j' +// // mysql +// runtimeOnly 'com.mysql:mysql-connector-j' + // postsql + runtimeOnly 'org.postgresql:postgresql' // test testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/payment-service/src/main/resources/application.yml b/payment-service/src/main/resources/application.yml index d5cf3686..67543d79 100644 --- a/payment-service/src/main/resources/application.yml +++ b/payment-service/src/main/resources/application.yml @@ -5,11 +5,18 @@ spring: application: name: payment-service +# datasource: +# url: jdbc:mysql://payments-db:3306/payments_db +# username: ${PAYMENT_DB_USERNAME} +# password: ${PAYMENT_DB_PASSWORD} +# driver-class-name: com.mysql.cj.jdbc.Driver + datasource: - url: jdbc:mysql://payments-db:3306/payments_db + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://payments-db:5432/payments_db username: ${PAYMENT_DB_USERNAME} password: ${PAYMENT_DB_PASSWORD} - driver-class-name: com.mysql.cj.jdbc.Driver + jpa: hibernate: diff --git a/prometheus.yml b/prometheus.yml index 157e2dbc..71219ce0 100644 --- a/prometheus.yml +++ b/prometheus.yml @@ -5,4 +5,4 @@ scrape_configs: - job_name: 'spring-boot' metrics_path: '/actuator/prometheus' static_configs: - - targets: ['host.docker.internal:19095', 'host.docker.internal:19096'] + - targets: ['flight-service:19094', 'reservation-service:19095', 'queue-service:19096'] diff --git a/reservation-service/build.gradle b/reservation-service/build.gradle index adc0e93d..0168f3af 100644 --- a/reservation-service/build.gradle +++ b/reservation-service/build.gradle @@ -38,7 +38,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-redis' testImplementation 'org.springframework.kafka:spring-kafka-test' compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' + // postsql + runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' runtimeOnly 'io.micrometer:micrometer-registry-prometheus' diff --git a/reservation-service/src/main/java/com/oneul_tanda/reservation_service/reservation/application/service/ReservationServiceImpl.java b/reservation-service/src/main/java/com/oneul_tanda/reservation_service/reservation/application/service/ReservationServiceImpl.java index e61ad4db..5aa7bf1f 100644 --- a/reservation-service/src/main/java/com/oneul_tanda/reservation_service/reservation/application/service/ReservationServiceImpl.java +++ b/reservation-service/src/main/java/com/oneul_tanda/reservation_service/reservation/application/service/ReservationServiceImpl.java @@ -243,7 +243,7 @@ public ConfirmReservationResponseDto confirmReservationV2(ConfirmReservationComm GetFlightInfo flightInfo = flightClient.getFlight(flightId); List ticketList = createTicketsFromHoldData(userId, flightInfo, passengers); Reservation reservation = Reservation.createReservation(userId, ticketList); - + reservationRepository.save(reservation); // 4. 결제 요청 @@ -253,7 +253,6 @@ public ConfirmReservationResponseDto confirmReservationV2(ConfirmReservationComm } - // 5. 결제 성공 -> 예약 확정 reservation.confirmReservation(); reservationRepository.save(reservation); diff --git a/reservation-service/src/main/resources/application.yml b/reservation-service/src/main/resources/application.yml index 6aacc188..aa606d0f 100644 --- a/reservation-service/src/main/resources/application.yml +++ b/reservation-service/src/main/resources/application.yml @@ -2,24 +2,27 @@ spring: application: name: reservation-service +# datasource: +# url: jdbc:mysql://reservation-db:3306/reservation_db +# username: ${RESERVATION_DB_ID} +# password: ${RESERVATION_DB_PASSWORD} +# driver-class-name: com.mysql.cj.jdbc.Driver + datasource: - url: jdbc:mysql://reservation-db:3306/reservation_db + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://reservation-db:5432/reservation_db username: ${RESERVATION_DB_ID} password: ${RESERVATION_DB_PASSWORD} - driver-class-name: com.mysql.cj.jdbc.Driver jpa: - database: mysql hibernate: - ddl-auto: create + ddl-auto: update properties: + jdbc: + time_zone: UTC hibernate: format_sql: true - use_sql_comments: true - spring.jpa.open-in-view: false - default_batch_fetch_size: 10 - default_schema: reservation_db - dialect: org.hibernate.dialect.MySQL8Dialect + show_sql: true data: redis: diff --git a/user-service/build.gradle b/user-service/build.gradle index ad66bb98..10974750 100644 --- a/user-service/build.gradle +++ b/user-service/build.gradle @@ -33,7 +33,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' compileOnly 'org.projectlombok:lombok' - runtimeOnly 'com.mysql:mysql-connector-j' +// runtimeOnly 'com.mysql:mysql-connector-j' + // postsql + runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/user-service/src/main/java/com/oneultanda/userservice/infrastructure/entity/UserJpaEntity.java b/user-service/src/main/java/com/oneultanda/userservice/infrastructure/entity/UserJpaEntity.java index 29019b55..d9982e0d 100644 --- a/user-service/src/main/java/com/oneultanda/userservice/infrastructure/entity/UserJpaEntity.java +++ b/user-service/src/main/java/com/oneultanda/userservice/infrastructure/entity/UserJpaEntity.java @@ -20,7 +20,7 @@ public class UserJpaEntity extends BaseTimeEntity { @Id @GeneratedValue @UuidGenerator - @Column(columnDefinition = "BINARY(16)") + @Column(columnDefinition = "uuid") private UUID id; @Column(nullable = false, unique = true) diff --git a/user-service/src/main/resources/application.yml b/user-service/src/main/resources/application.yml index ec680096..08e1d9d5 100644 --- a/user-service/src/main/resources/application.yml +++ b/user-service/src/main/resources/application.yml @@ -2,11 +2,17 @@ spring: application: name: user-service +# datasource: +# url: jdbc:mysql://oneultanda-user-db:3306/user_db +# username: ${USER_DB_ID} +# password: ${USER_DB_PASSWORD} +# driver-class-name: com.mysql.cj.jdbc.Driver + datasource: - url: jdbc:mysql://oneultanda-user-db:3306/user_db + driver-class-name: org.postgresql.Driver + url: jdbc:postgresql://user-db:5432/user_db username: ${USER_DB_ID} password: ${USER_DB_PASSWORD} - driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: