Post

spring mircoservices-01

프로젝트 구조

msa-msa-1 drawio

Google Cloud Platform & Google Kubernetes Engine
Jenkins, Kubernetes 를 배포하기 위해서 Google Cloud Platform 을 사용한다.
GCP 내에서 VM 을 생성하여 Debian/Ubuntu 내에서 작업이 이루어진다.

깃허브 주소

서비스 당 하나의 repo 로 분리해서 등록하였다.

  1. config-server
  2. product-service
  3. payment-service
  4. order-service
  5. cloud-gateway

VM 인스턴스 생성

asia-northeast3 (Seoul) 을 선택하고 VM 을 하나 생성한다. default 로는 Debian 으로 설치된다. 해당 VM 에 설치된 서버에 프로젝트를 배포하는 것이 목표이다.

  • Cloud NAT 생성
  • VPC Network > Firewall > TCP 8080 포트 개방

image

image

jenkins 에 접근하는 포트인 8080 을 열어서 ${VM IP 주소}:8080 으로의 접속을 허용한다.

Jenkins 설치

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1.
sudo wget -O /usr/share/keyrings/jenkins-keyring.asc \
  https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key
# 2.
echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] \
  https://pkg.jenkins.io/debian-stable binary/ | sudo tee \
  /etc/apt/sources.list.d/jenkins.list > /dev/null
sudo apt-get update
sudo apt-get install jenkins

# git, kubectl
sudo apt-get install git
sudo apt-get install kubectl

Jenkins 설정

github, gcp 와 연결하기 위해서 token 을 등록해야한다. Crendentials 로 이동한다.

  • repo 를 관리할 수 있는 권한을 가진 토큰
  • gcp 에서 IAM 으로 이동, service account 생성
    • keys 로 이동해서 JSON 타입의 key 를 생성 (파일로 다운로드 됨)
      • gcp 에 접근하기 위한 credential
        • secret file
      • google 계정을 인증하기 위한 credential 총 2개 생성
        • google account 어쩌고 저쩌고
  • jenkins 에서 plugin 설치
    • Google Kubernetes Engine 설치

Java 설치

커뮤니티 기반 빌드인 AdoptOpenJDK 가 많이 사용되는데, 최근 Eclipse Adoptium 으로 이전했다. docker-hub 에 등록된 openjdk 를 들어가보면 더 이상 관리되지 않으며 Azul, Temurin 등 다른 jdk 사용을 권장하고 있다.

Debian/Ubuntu 기준으로 설치는 아래 명령어를 순서대로 입력하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
# 1.
sudo apt install -y wget apt-transport-https
# 2.
sudo mkdir -p /etc/apt/keyrings
# 3.
sudo wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | sudo tee /etc/apt/keyrings/adoptium.asc
# 4.
echo "deb [signed-by=/etc/apt/keyrings/adoptium.asc] https://packages.adoptium.net/artifactory/deb $(awk -F= '/^VERSION_CODENAME/{print$2}' /etc/os-release) main" | sudo tee /etc/apt/sources.list.d/adoptium.list
# 5.
sudo apt update
# 6.
sudo apt install temurin-17-jdk

Kubernetes Cluster 생성

Private clusterGKE 클러스터를 생성한다. 여기서 설정하는 몇몇 설정, 특히 네트워크 관련 설정은 클러스터 생성 후 변경이 불가능하니 주의가 필요하다.

image

private 으로 설정해서 외부로 IP 가 노출되도록 설정한 인스턴스 이외에는 접근할 수 없도록 한다. 그럼 2가지 경고 메시지를 볼 수 있다.

  • (1) 클러스터를 생성하고 나서 Cloud NAT 를 통해 아웃바운드 인터넷 연결을 활성화 시켜라
  • (2) 컨트롤 플레인이 허용한 네트워크를 활성화 시켜라

나처럼 초보자는 여기서 헤매기 딱 좋은 키워드가 있다.

관련해서 구글이 제공하는 도움말 링크 모음은 다음과 같다.

쿠버네티스 클러스터 관련해서 핵심만 요약한 영상 자료는 다음을 참고했다.

쿠버네티스 클러스터 생성창에서 뭔가 중요한 사항을 알려준다. 더 이상 kube-dns 를 사용하지 않고 Cloud DNS 를 사용한다는 알림이다.

image


Jenkins 파이프라인 구성

  • k8s 배포를 위한 YAML 파일
  • Jenkinsfile

2가지를 준비해야 한다.

스프링이 제공하는 spring-cloud-config-server 를 배포해보자.

deployment.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# @format
---
## Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: config-server-app
spec:
  selector:
    matchLabels:
      app: config-server-app
  template:
    metadata:
      labels:
        app: config-server-app
    spec:
      containers:
        - name: config-server-app
          image: IMAGE_URL/config-server
          imagePullPolicy: Always
          ports:
            - containerPort: 9296
          resources:
            requests:
              memory: "100Mi"
              cpu: "250m"
            limits:
              memory: "200Mi"
              cpu: "500m"

---
## Service

apiVersion: v1
kind: Service
metadata:
  name: config-server-svc
spec:
  selector:
    app: config-server-app
  ports:
    - port: 80
      targetPort: 9296

Jenkins 에 등록한 환경변수를 호출한다. 환경변수는 다음과 같다.

  • Artifact Registry 정보
  • 프로젝트 정보
  • Zone 정보
  • Jenkins 에 등록한 github credentials
  • GCP 에 로그인하기 위한 credentials

image

github 로 push 와 동시에 build, 그리고 jenkins 에 kubernetes 플러그인을 통하여 GKE 로 배포까지 완료되도록 설정한다. 브랜치는 */main 뿐 아니라 여러개를 설정할 수 있다.

다만, 이러한 파이프라인이 작동하기 위해서는 github repo 에 webhook 을 설정해야 한다.

image

위에서 secret 은 Jenkins 의 설정에서 생성한 token 이다.

젠킨스에서 파이프라인 구성 후에 Build now (지금 빌드) 를 누르면 github 에서 정보를 받아온다. 한번 빌드 후에는 github 로 push 와 동시에 CD/CI 과정이 진행된다.

여기까지 중요한 포인트만 정리해보자.

  1. Serverless 서비스를 위한 플랫폼으로 GCP 를 사용한다.
  2. GCP 에서 제공하는 GKE 를 사용한다.
    1. Dockerhub 가 아닌 구글이 제공하는 Artifact Registry 를 사용한다.
    2. Cluster 를 생성한다.
    3. Cloud NAT 를 생성하여 클러스터 내에서 서비스-to-서비스 통신이 원활하게 이루어지도록 한다.
  3. 젠킨스를 띄울 VM 을 생성한다.
    1. 젠킨스를 설치하고 각종 환경변수, 인증서를 등록한다.
    2. 젠킨스에서 파이프라인을 구성한다.
      1. github, gke 의 인증을 위해서 위에서 등록한 인증 정보를 사용한다.
  4. github 에 push 하면 젠킨스를 통해 빌드 후 k8s 로 배포가 완료된다.

보안 설정

msa-security_simple drawio

보안에 관련된 부분은 검색해보면 너무나도 방법이 다양하다. 단순화하여 큰 그림을 보는 것이 목적이므로 Okta 에 인증, 인가를 위임했다. 모든 api 호출의 수문장 역할을 맡는 cloud-gateway 에서 사용자가 토큰을 가지고 있는지, 가지고 있다면 어떤 권한을 가진 유저인지 판단한다.

OAuth2 + OpenID Connect 으로 특정 인가서버에 요청을 보내서 보안 토큰을 받아올 수도 있고, 혹은 스프링 부트 자체로 인가 서버를 구축할 수도 있다. 하지만, 여러 사람들의 글을 참고 + 직접 소셜 로그인 등을 구현해보니 외부에 인가 서버를 두고 필요 시에 /oauth2/login/code/${인가서버} 로 요청을 보내서 토큰을 받아오는 것이 더 합리적인 선택이라고 생각했다.

결국 access token, refresh token 을 받아오는 것이 중요하다. 보안을 위해서 rsa 방식으로 생성된 jwt 를 사용자에게 쿠키를 통해서 전달한다. (보안을 위해 httpSecure 옵션을 부여) 해당 사용자는 access token 을 쿠키에 저장한 뒤 다시 gateway 로 요청을 보낸다. gateway 에서 다시 다른 서비스로 요청을 보내면, rest template 설정에 등록한 interceptor 가 해당 요청을 가로채서 검사한다.

토큰을 검증하는 방법은 많겠지만, Okta 인가서버internal 이라는 scope 를 하나 생성했다. 즉, 인가서버에 등록된 회원만이 internal 이라는 scope 를 가지고 있다. 따라서 access tokenSCOPE_internal 를 가지고 있는 요청만 허용한다.

참고

OAuth2 (+소셜로그인) 관련 토이 프로젝트


중간점검

몇 번의 시행착오 끝에 모든 서비스를 GKE 에 등록하고, postman 으로 order-service, payment-service, product-service 응답이 정상적으로 이루어지는 것을 확인했다. 중요한 비즈니스 로직은 없기에 생략한다.

SCR-20231028-fbjk

HTTPS 연결 삽질

Chrome 브라우저 정책이 Lax 로 변경됨에 따라, 인증 및 인가 정보를 담고 있는 jwt 형식의 access_token 등을 쿠키로 전달하기 위해선 프론트, 백엔드 모두 https 통신이 필요하다.

가비아에서 도메인을 구매 후, HTTPS 연결을 위한 시도, 결국 실패했지만 나중을 위해 기록으로 남겨둔다. (HTTP 도메인 연결은 DNS 에 레코드만 추가하면 바로 적용된다.)

SSL 발급

HTTP 통신 중 오가는 패킷들을 두꺼운 보안으로 감싼다고 생각하자. SSL(Secure Socket Layer) 이라고 하는 인증서를 발급받는다. 실제 상용 서비스에서는 비용을 지불하고 SSL 을 발급받겠지만, 개인 학습용이므로 CertBot 을 이용해서 무료로 90일 마다 갱신해야하는 인증서를 발급받았다. CertBot 에서 성공적으로 SSL 을 발급받으면 아래와 같은 파일을 얻을 수 있다.

1
2
3
├── ca_bundle.crt
├── certificate.crt
└── private.key
  • private.keycertificate.crtgcp 에 등록한다.

TCP 통신 -> HTTPS 통신 변경을 위해 LoadBalancer 가 아닌 Ingress 로 변경하고, 인증서 정보를 받아올 수 있도록 YAML 파일을 수정해야 한다.

gcp 도움말을 쭉 읽어보며 필요한 작업을 정리해본다.

image

  • SSL 인증서
    • 구글이 관리하도록 한다.
      • 구글에서 직접 구매한 도메인이 아니면 등록이 안되는 것 같다.
    • 혹은 private.key, certificate.crt 파일을 직접 업로드한다.
  • 고정 ip 를 생성해서 Ingress 서비스에 할당한다.
    • 해당 고정 ip 를 가비아에서 구매한 도메인 DNS 관리 > A 타입에 등록한다.
  • yaml 파일에
    • certificate
    • ingress
    • 관련 설정을 추가한다.

SSL 인증서 생성

Certificate Manage 에 등록했더니, 제대로 동작하지 않았다. Provisioning 에서 Active 상태로 안넘어갔다. 도움말을 통해, 차선책으로 secret 을 생성했다.

VM 에 접속한 뒤, private.key, certificate.crt 파일을 업로드했다.

1
2
3
kubectl create secret tls msa-secret \
--cert CERT_FILE \
--key PRIVATE_KEY_FILE 

kubectl get secret 을 통해 생성된 것 확인

1
2
3
4
NAME            TYPE                DATA   AGE
credentials     Opaque              2      2d21h
msa-secret      kubernetes.io/tls   2      9s
my-tls-secret   kubernetes.io/tls   2      2d21h

관련된 도움말이 여러 갈래로 나눠진 상태로 자기 주장이 강해 정확히 어떤 걸 적용시켜야 할지가 어지러웠다.

위 문서를 참고하여 외부 부하 분산을 위한 인그레스 작동 방법 을 참고한다. 인그레스 서비스가 부하분산기 역할을 하되, 해당 인그레스 서비스에 유효한 SSL 인증서가 존재하면 된다. 그럼 결국 yaml 파일을 작성해서

  • Deployment
  • NodePort Service
  • Ingress Service
    • 인증서 설정
    • HTTPS 관련 설정

위 과정에 살만 붙이면 된다고 생각했다.

인그레스 서비스 생성

  • 클라이언트와 부하 분산기 간 HTTPS(TLS) 설정 (예제)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress-2
spec:
  tls:
    - secretName: SECRET_NAME
  rules:
    - http:
        paths:
          - path: /*
            pathType: ImplementationSpecific
            backend:
              service:
                name: SERVICE_NAME
                port:
                  number: 60000
  • 사용자 관리 인증서 생성하기

구글 관리형 인증서는 다음과 같이 생성하면 된다. (예제)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  apiVersion: networking.gke.io/v1
  kind: ManagedCertificate
  metadata:
    name: FIRST_CERT_NAME
  spec:
    domains:
      - FIRST_DOMAIN
  ---
  apiVersion: networking.gke.io/v1
  kind: ManagedCertificate
  metadata:
    name: SECOND_CERT_NAME
  spec:
    domains:
      - SECOND_DOMAIN

하지만! 가비아 도메인 + CertBot SSL 조합으로는 여러번 시도해도 계속 실패였다. 따라서 위에서 언급한 것 처럼, k8s secret 을 직접 생성했다.

CertBot 에서 발급받은 private.keycertificate.crt 를 VM 으로 전송한 다음, 만들었지만 도움말에 따라서 사용자 관리 인증서 도 만들었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1.
openssl genrsa -out test-ingress-1.key 2048
# 2.
openssl req -new -key test-ingress-1.key -out test-ingress-1.csr \
    -subj "/CN=jjcode.pe.kr"
# 3.
openssl x509 -req -days 365 -in test-ingress-1.csr -signkey test-ingress-1.key \
    -out test-ingress-1.crt
# 4. 결과
# 결론적으로 CertBot 에서 제공한 것과 동일한 파일이 생긴다.
# 
-rw-r--r-- 1 jeongjinkim1992 jeongjinkim1992 2301 Oct 29 13:56 certificate.crt
-rw-r--r-- 1 jeongjinkim1992 jeongjinkim1992 1702 Oct 29 08:03 private.key
-rw-r--r-- 1 jeongjinkim1992 jeongjinkim1992 1001 Nov  1 12:59 test-ingress-1.crt
-rw-r--r-- 1 jeongjinkim1992 jeongjinkim1992  895 Nov  1 12:59 test-ingress-1.csr
-rw------- 1 jeongjinkim1992 jeongjinkim1992 1679 Nov  1 12:59 test-ingress-1.key
  • 인그레스에 대한 인증서 지정 (예제)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-mc-ingress
spec:
  tls:
  - secretName: FIRST_SECRET_NAME
  - secretName: SECOND_SECRET_NAME
  rules:
  - host: FIRST_DOMAIN
    http:
      paths:
      - pathType: ImplementationSpecific
        backend:
          service:
            name: my-mc-service
            port:
              number: 60001
  - host: SECOND_DOMAIN
    http:
      paths:
      - pathType: ImplementationSpecific
        backend:
          service:
            name: my-mc-service
            port:
              number: 60002

결론적으로 아래 yml 파일을 작성했다.

하지만! 여전히 오류는 그대로다.. 설정이 부족하다고 하는데 어떤 설정이 부족한지 설명을 해주지 않는다.

image

HTTPS 통신이 된다면

  • Spring Cloud Gateway 모듈은 webflux 모듈을 사용하기 때문에 HttpServletResponse 가 아닌 ServerResponse 를 사용한다.
  • 이 때, 응답 헤더에 쿠키를 추가하기 위해서는 ResponseCookie 를 사용하면 된다고 한다.
  • 크롬 브라우저 정책이 Lax 로 바뀜에 따라서 백엔드 서버에서 httpSecure 옵션을 주고 인증 및 인가 정보를 쿠키에 담아서 보내면 된다.

느낀점

  • @Deprecated 된 라이브러리를 만났을 때
    • 영문으로 된 공식문서, 스택오버 플로우에 질문 올리기, 관련 문서 찾기 능력이 상승했다.
    • 디버깅 과정을 거치며 어느 지점에 어떤 문제가 발생하는 지 파악하는 능력을 길렀다.
  • 서비스 인스턴스 간 통신에 장애가 생겼을 때
    • zipkin 와 같은 통합 모니터링 툴의 중요성을 깨달았다.
  • 도커, 쿠버네티스, GCP 같은 생소한 개념을 접했을 때
    • 다른 사람의 성공 사례를 통해서 큰 윤곽을 그리고, 디테일은 공식 문서와 관련 영상으로 채운다.
    • 온갖 에러를 한줄 씩 해결하면서 개념을 익혔다.
    • 에러 로그를 보며 필요한 부분, 검색의 키워드를 뽑는 능력을 길렀다.
  • 가독성 좋은 코드의 중요성
    • 메서드 이름, 변수 이름을 제대로 짓는 것의 중요성을 깨달았다.
    • 여러 서비스가 점점 늘어나며 Bean 이름이 중복되거나, 변수 이름이 중복되는 등
    • 한눈에 의미를 파악하지 못하면 그 만큼 시간과 비용이 소모된다.
  • 설정 클래스, 사용 클래스 분리
    • 서비스나 로직의 설정을 담당하는 클래스, 해당 클래스를 호출해서 사용하는 클래스를 명확히 구분해야한다.
    • 관련 하위 기능, 혹은 상위 기능을 추가해야 하는 경우
    • 프록시 패턴, 어댑터 패턴, 팩토리 패턴 등 더 복잡한 로직을 동원해야 한다.
  • 리눅스, 네트워크는 꼭 필요하다
    • 로컬 환경에서만 돌아가는 앱은 의미가 없다.
    • 서버, 서버리스 환경에 모두 익숙해지기 위해서는 리눅스 + 네트워크에 관한 지식과 경험이 필요하다는 것을 절실히 체감했다.
  • 토큰에 관한 이해
    • RSA 방식
    • SSH 인증서
    • 다른 서비스 업체 간 인증
    • 을 위해서 RSA 로 암호화된 토큰을 발급하고, 검증하는 인증, 인가 과정에 대해서 자세히 알 수 있었다.

숙제

  • 실제 서비스라고 가정하고, 비즈니스 로직 추가
  • Okta 인증 관련한 로직 다듬기
  • 구글, 카카오 로그인 추가
  • HTTPS 연동

추가

OpenFeign 의 장점 및 단점

출처

장점

  • 인터페이스와 어노테이션 기반으로 간단한 외부 API 통신 가능
  • Spring MVC 어노테이션 지원
  • Eureka, CircuitBreaker, LoadBalancer 등 Spring Cloud 기술과 통합이 쉬움

단점

  • HTTP2 를 지원하지 않아 별도의 설정 필요
  • 어플리케이션 실행 초반에 초기화 관련 에러 발생 가능성 ⬆️
  • 테스트 도구를 제공하지 않아서 별도의 설정 필요
    • @DataJpaTest 와 유사하게 어노테이션을 직접 작성하여 해결 가능

OpenFeign 부분만 테스트 하기 위한 클래스 생성

1
2
3
4
5
6
7
8
9
10
// FeignTest 를 위한 클래스 생성
@ImportAutoConfiguration({ 
	OpenFeignConfig.class, 
	CustomPropertiesConfig.class, 
	FeignAutoConfiguration.class, 
	HttpMessageConvertersAutoConfiguration.class, 
	}) 
class FeignTestContext {

}

해당 설정 파일을 적용시켜 커스텀 어노테이션 작성

1
2
3
4
5
6
7
8
9
10
11
@SpringBootTest(
	classes = {FeignTestContext.class},
	properties = {
		"spring.profiles.active=local"
	}
)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface FeginTest {

}

출처

  1. https://s-core.co.kr/insight/view/spring-cloud-gateway-%EA%B8%B0%EB%B0%98%EC%9D%98-api-%EA%B2%8C%EC%9D%B4%ED%8A%B8%EC%9B%A8%EC%9D%B4-%EA%B5%AC%EC%B6%95/
This post is licensed under CC BY 4.0 by the author.