Eureka Server 적용

박세건·2024년 9월 20일
0

기술 실습

목록 보기
11/18
post-thumbnail

Eureka 서버란?

MSA 구조로 운영을 진행하고 컨테이너 및 쿠번네티스와 같은 추가 서비스를 운영하게 되면 서버의 IP 변경되거나 관련 정보가 변경될 수가 있가 이러한 마이크로 서비스들을 발견하기 쉽게 주소록 역할을 수행하는 서비스라고 생각하면 쉽다.

Eureka Server 프로젝트 생성

start.spring.io에 들어가서 Eureka Server로 운영할 프로젝트를 생성한다.

  • 의존성 : Eureka Server 만 추가한다.

Eureka Server 등록

application.yml 과 Application.java 파일로 간단하게 Eureka Server를 세팅할 수 있다.

  • Application.java 설정
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {

	public static void main(String[] args) {
		SpringApplication.run(EurekaApplication.class, args);
	}

}

@EnableEurekaServer 어노테이션을 통해 Spring Boot 애플리케이션이 Eureka 서버로 설정되어 다른 서비스들이 등록할 수 있는 중앙 서버 역할을 하게 됩니다. 이를 통해 마이크로서비스 아키텍처에서 서비스 간의 통신을 쉽게 관리할 수 있습니다.

  • application.yml 설정
spring:
  application:
    name: eureka  # 애플리케이션의 이름을 'eureka'로 설정합니다.

server:
  port: 8761  # 서버가 실행될 포트 번호를 8761로 설정합니다.

eureka:
  client:
    register-with-eureka: false  # 이 서버가 Eureka 서버로서 동작하므로 다른 Eureka 서버에 등록하지 않도록 설정합니다.
    fetch-registry: false  # 다른 Eureka 서버로부터 서비스 등록 정보를 가져오지 않도록 설정합니다.
    # false로 설정하는 이유는 이 프로젝트 자체가 유레카 서버이기 때문에 다른 유레카 서버로부터 정보를 등록하거나 가져올 필요가 없다
    service-url:
      defaultZone: http://j11a604.p.ssafy.io:8761/eureka # Eureka 클라이언트들이 Eureka서버와 통신할때의 주소, Eureka 서버의 URL을 입력해야 합니다.

  server:
    wait-time-in-ms-when-sync-empty: 5  # Eureka 서버가 동기화할 때, 레지스트리가 비어 있을 경우 대기 시간을 5ms로 설정합니다.

management:
  endpoints:
    web:
      exposure:
        include: "*"  # 모든 관리 엔드포인트를 웹에서 노출하도록 설정합니다.
  • register-with-eureka: false
    • 이 속성으로 현재의 서버가 Eureka 서버임을 나타낼 수 있습니다.
  • fetch-registry: false
    • 다른 유레카 서버에서 정보를 가져올 필요가 없으니 false 처리
  • service-url.defaultZone:
    • 다른 마이크로서비스들 처럼 유레카 서버를 이용할 Client들에게 Eureka 서버에 등록 하기위해 사용할 주소
  • server.wait-time-in-ms-when-sync-empty:
    • 다른 Eureka 서버와 동기화를 시도할때 만약 유레카 서버의 레지스트리(저장된 서버들)이 비어있다면 5ms 시간동안 대기한다

      즉, Eureka 서버가 빈 레지스트리 상태일 때 동기화 시도를 얼마나 자주 할지를 결정하는 중요한 파라미터입니다. 이 값을 조정하여 시스템의 성능과 효율성을 최적화할 수 있습니다.


정상적으로 유레카 대시보드에 접속 성공!
Eureka의 대시보드 접속 주소는localhost:8761 입니다


마이크로서비스(Eureka Client) 등록

Gateway를 유테카 서버에 연결시켰던 방식과 동일하게 진행한다.

의존성 설정

	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
dependencyManagement {
	imports {
		mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
	}
}
ext {
	set('springCloudVersion', "2023.0.3")
}

위 세가지 관련 의존성을 추가해주면 application.yml 에서 eureka 명령어를 사용할 수 있게된다.

application.yml 수정

이전에 Gateway에서 진행했던 코드를 사용한다.

eureka:
  instance:
    prefer-ip-address: true # IP주소를 통한 등록으로 선택
  client:
    register-with-eureka: true #자신의 서버를 등록
    fetch-registry: true # 유레카 서버의 다른 서버의 정보를 가져올지
    service-url:
      defaultZone: http://j11a604.p.ssafy.io:8761/eureka

코드 추가

Gateway, Eureka Server 배포

관련 설정을 마무리했다면 실제로 EC2에 배포 시켜서 정상적으로 Eureka Server 대시보드가 작동되는지 확인해보자

Dockerfile 작성

다른 마이크로서비스들의 Dockerfile에는 노출시키면 안되는 정보들 때문에 환경변수를 사용해서 CI/CD를 진행했지만 Gateway나 Eureka Server의 경우는 DB와 연결되는 부분이 없기에 따로 환경변수를 사용해서 진행하지 않는다.

# Eclipse Temurin 17 JDK 이미지를 기반으로 합니다.
FROM eclipse-temurin:17-jdk

# 작업 디렉토리를 /app으로 설정합니다.
WORKDIR /app

# 빌드된 JAR 파일을 /app 디렉토리에 복사합니다.
COPY build/libs/gateway-0.0.1-SNAPSHOT.jar app.jar

# 컨테이너가 사용하는 포트 80 노출합니다.
EXPOSE 80

# 컨테이너가 시작될 때 실행할 명령을 설정합니다.
ENTRYPOINT ["java", "-jar", "app.jar"]

# 컨테이너에 /data 디렉토리를 볼륨으로 설정합니다.
VOLUME ["/data"]
  • Gateway와 Eureka Server 둘다 포트번호와 jar파일 이름만 수정해서 생성한다.

CI/CD Pipeline Script 작성

Jenkins에서 Item을 만들어주고 Pipeline 계열로 지정한다.
마이크로서비스와 동일하게 진행해주고 Gitlab에서 Webhook과정도 동일하게 진행하여 연결해준다.

Dockerfile과 동일하게 환경변수에 대한 설정이 필요없기에 환경변수 설정을 제거하고 작성한다.

pipeline {
    agent any



    stages {
        stage('Repository clone') {
            steps {
                sh 'pwd'
                git branch: 'gateway', credentialsId: 'qkrtprjs', url: 'https://lab.ssafy.com/s11-fintech-finance-sub1/S11P21A604.git'
            }
            post {
                failure {
                  echo 'Repository clone failure !'
                }
                success {
                  echo 'Repository clone success !'
                }
            }
        }
         stage('Build image') {
            steps {
                dir('gateway') {
                    
                    sh 'chmod +x ./gradlew'
                    sh './gradlew build'
                    sh 'pwd'
                    sh 'docker build -t qkrtprjs/gateway .'
                }
                echo 'Build image...'
            }
            post {
                failure {
                    echo 'Build image failure !'
                    script {
                        def Author_ID = sh(script: "git show -s --pretty=%an", returnStdout: true).trim()
                        def Author_Name = sh(script: "git show -s --pretty=%ae", returnStdout: true).trim()
                        mattermostSend (color: 'danger', 
                        message: "도커 이미지 빌드 실패: ${env.JOB_NAME} #${env.BUILD_NUMBER} by ${Author_ID}(${Author_Name})\n(<${env.BUILD_URL}|Details>)", 
                        endpoint: 'https://meeting.ssafy.com/hooks/bb6j17ansjnambc9cjddf8gw7o', 
                        channel: 'CICD'
                        ) 
                    }
                }
                success {
                    echo 'Build image success !'
                    script {
                        def Author_ID = sh(script: "git show -s --pretty=%an", returnStdout: true).trim()
                        def Author_Name = sh(script: "git show -s --pretty=%ae", returnStdout: true).trim()
                        mattermostSend (color: 'good', 
                        message: "도커 이미지 빌드 성공: ${env.JOB_NAME} #${env.BUILD_NUMBER} by ${Author_ID}(${Author_Name})\n(<${env.BUILD_URL}|Details>)", 
                        endpoint: 'https://meeting.ssafy.com/hooks/bb6j17ansjnambc9cjddf8gw7o', 
                        channel: 'CICD'
                        )
                    }  
                }
            }
        }

        stage('Remove Previous image') {
            steps {
                script {
                    try {
                        sh 'docker stop gateway'
                        sh 'docker rm gateway'
                    } catch (e) {
                        echo 'fail to stop and remove container'
                    }
                }
            }
            post {
                failure {
                  echo 'Remove Previous image failure !'
                }
                success {
                  echo 'Remove Previous image success !'
                }
            }
        }
        stage('Run New image') {
            steps {
                sh 'docker run --name gateway -d -p 80:80 qkrtprjs/gateway'
                echo 'Run New member image'
            }
            post {
                failure {
                    echo 'Run New image failure !'
                    script {
                        def Author_ID = sh(script: "git show -s --pretty=%an", returnStdout: true).trim()
                        def Author_Name = sh(script: "git show -s --pretty=%ae", returnStdout: true).trim()
                        mattermostSend (color: 'danger', 
                        message: "서비스 배포 실패: ${env.JOB_NAME} #${env.BUILD_NUMBER} by ${Author_ID}(${Author_Name})\n(<${env.BUILD_URL}|Details>)", 
                        endpoint: 'https://meeting.ssafy.com/hooks/bb6j17ansjnambc9cjddf8gw7o', 
                        channel: 'CICD'
                        ) 
                    }
                }
                success {
                    echo 'Run New image success !'
                    script {
                        def Author_ID = sh(script: "git show -s --pretty=%an", returnStdout: true).trim()
                        def Author_Name = sh(script: "git show -s --pretty=%ae", returnStdout: true).trim()
                        mattermostSend (color: 'good', 
                        message: "서비스 배포 성공: ${env.JOB_NAME} #${env.BUILD_NUMBER} by ${Author_ID}(${Author_Name})\n(<${env.BUILD_URL}|Details>)", 
                        endpoint: 'https://meeting.ssafy.com/hooks/bb6j17ansjnambc9cjddf8gw7o', 
                        channel: 'CICD'
                        )
                    }
                }
            }
        }
    }
}
  • 서비스 명과 포트번호에 대한 정보를 주의하며 gateway와 Eureka Server 스크립트를 작성한다.

정상적으로 빌드와 배포가 진행되는 것을 확인

다음으로 gateway를 통해서 같은 API 요청(gateway의 포트번호가 80이기에 제거하고 요청)

정상적으로 같은 결과를 응답하는 것을 확인

생각해보니 마이크로서비스에 Eureka 설정을 해주지 않았는데 작동이된다?

당연히 Gateway 설정에서 라우팅을 통해서 연결해주었기 때문에 받아온 요청을 다시 설정해둔 uri로 전달해주는 것이기에 정상적으로 작동하는 것이다.
그렇다면 다른 마이크로서비스들도 Eureka 서버에 등록해야하는 이유는?

Eureka에 등록했을때의 장점

  1. 자동 서비스 디스커버리

    • Eureka 미사용: 서비스 A가 서비스 B에 요청하려면 서비스 B의 URL을 코드에 하드코딩해야 합니다. 서비스 B의 위치가 바뀌면, 서비스 A의 코드를 수정해야 합니다.

    • Eureka 사용: 서비스 A는 lb://B_SERVICE와 같이 서비스 이름만 알고 있으면 됩니다. Eureka가 서비스 B의 현재 위치를 자동으로 찾아서 요청을 전달합니다. 서비스 B의 위치가 바뀌어도 서비스 A의 코드는 수정할 필요가 없습니다.

  2. 로드 밸런싱

    • Eureka 미사용: 각 서비스 인스턴스의 URL을 직접 관리해야 하며, 특정 인스턴스에 부하가 몰릴 수 있습니다.

    • Eureka 사용: Eureka는 여러 인스턴스를 등록하고, Gateway는 lb://B_SERVICE를 통해 요청을 보낼 때 자동으로 로드 밸런싱을 수행합니다. 이는 트래픽을 고르게 분산시켜 성능을 향상시킵니다.

  3. 서비스 상태 관리

    • Eureka 미사용: 서비스 A가 서비스 B에 요청할 때, 서비스 B가 다운되면 A는 오류를 발생시키고, 수동으로 처리해야 합니다.

    • Eureka 사용: Eureka는 각 서비스의 상태를 모니터링합니다. 서비스 B가 다운되면, Eureka는 이를 감지하고 서비스 A의 요청이 다운된 인스턴스로 가지 않도록 합니다. 따라서 사용자에게 더 나은 경험을 제공합니다.

  4. 동적 인프라 구성

    • Eureka 미사용: 새로운 인스턴스를 추가하면, 모든 소비자 서비스의 코드를 업데이트해야 합니다.

    • Eureka 사용: 새로운 인스턴스를 Eureka에 등록하면, 기존 서비스는 이를 자동으로 인식하고 사용할 수 있습니다. 이는 운영을 간소화하고, 새로운 인스턴스 추가가 용이합니다.

  5. 서비스 버전 관리

    • Eureka 미사용: 각 버전의 URL을 관리해야 하며, 새로운 버전을 추가할 때마다 기존 코드를 수정해야 합니다.

    • Eureka 사용: 서비스 버전을 다르게 등록할 수 있습니다. 예를 들어, SERVICE_V1, SERVICE_V2로 등록하면, 클라이언트는 필요에 따라 특정 버전을 호출할 수 있습니다.

Eureka 서버를 사용하면 서비스 간의 결합도가 낮아지고, 관리가 용이해지며, 시스템의 유연성과 확장성을 높일 수 있습니다. 이는 마이크로서비스 아키텍처의 장점을 최대한 활용하는 데 큰 도움이 됩니다.


각각의 마이크로서비스의 application.yml 파일에 eureka 설정을 완료한 후에 lb://${서비스명} 명령어로 코드를 수정하고 다시 테스트 진행

API 테스트

서버를 띄우는 것이 진행되었다면, 본격으로 Gateway를 통해서 API가 정상적으로 작동되는지를 확인해보자

특정한 마이크로서비스의 API를 POSTMAN을 통해서 실행했을때 게이트웨이 주소를 통해서 데이터를 응답받는 것을 확인


[트러블슈팅] 서비스 이름으로 Gateway 라우팅 에러🤐

마이크로 서비스 이름으로 Gateway 라우팅을 설정하고 유레카 서버에 Gateway와 해당 마이크로 서비스의 등록을 완료하였지만 Gateway에서 해당되는 마이크로 서비스로 요청을 전달하지 못하는 문제가 발생하였다.

에러 내용

2024-09-26T04:41:57.473Z ERROR 1 --- [gateway] [or-http-epoll-2] a.w.r.e.AbstractErrorWebExceptionHandler : [312cf383-7]  500 Server Error for HTTP GET "/api/notification/hello"

java.net.UnknownHostException: Failed to resolve '1451b0530aa3' [A(1)]
        at io.netty.resolver.dns.DnsResolveContext.finishResolve(DnsResolveContext.java:1151) ~[netty-resolver-dns-4.1.113.Final.jar!/:4.1.113.Final]
        Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
        *__checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
        *__checkpoint ⇢ HTTP GET "/api/notification/hello" [ExceptionHandlingWebHandler]
        - id: notificationRoute
          #          uri: http://j11a604.p.ssafy.io:8083
          uri: lb://NOTIFICATION
          predicates:
            - Path=/api/notification/**

위 코드를 사용해서 uri로 접속하지 않고 마이크로서비스의 이름으로 해당 마이크로서비스에 접근할 수 있도록 설정하였다.


위와 같이 유레카 서버에 정상적으로 저장이 된것을 확인하였지만 여전히 Gateway로 응답을 보내도 500대 에러가 발생

오랜시간 구글링한 결과...
eureka.instance.hostname 값이 localhost로 지정되어있어야 정상적으로 작동이 되지만, 윈도우에선는 hostname의 기본값이 default로 입력이되고 맥에서는 기본값이 localhost로 제공되는 것을 확인하였다.
그렇다면 ec2에 제공되는 퍼블릭IP 주소로 해당값을 수정해 보도록 했다.
해당 글을 확인한뒤 코드를 수정

eureka:
  instance:
    hostname: j11a604.p.ssafy.io

정상적으로 Gateway(80번 포트)로 접근했을때 마이크로서비스에 접근해서 해당되는 응답값을 보여주는 것을 확인할 수 있었다.


생각해볼 것

  1. Spring Cloud Gateway를 사용해서 요청을 몰아서 받고 적절하게 서버에 나눠주는 방식으로 진행하며 트래픽의 과부하를 막는다고 한다.
  • 몰아서 받는거 자체가 트래픽 과부하가 아닌가?
  • Gateway를 통해서만 요청을 보낼 수 있도록 다른 마이크로서비스들의 접근을 막아야할까?
profile
멋있는 사람 - 일단 하자

0개의 댓글