spring mircoservices-01
프로젝트 구조
Google Cloud Platform & Google Kubernetes Engine
Jenkins, Kubernetes 를 배포하기 위해서 Google Cloud Platform 을 사용한다.
GCP 내에서 VM 을 생성하여 Debian/Ubuntu 내에서 작업이 이루어진다.
깃허브 주소
서비스 당 하나의 repo 로 분리해서 등록하였다.
VM 인스턴스 생성
asia-northeast3 (Seoul)
을 선택하고 VM 을 하나 생성한다. default 로는 Debian
으로 설치된다. 해당 VM 에 설치된 서버에 프로젝트를 배포하는 것이 목표이다.
- Cloud NAT 생성
- VPC Network > Firewall > TCP 8080 포트 개방
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 어쩌고 저쩌고
- gcp 에 접근하기 위한 credential
- keys 로 이동해서 JSON 타입의 key 를 생성 (파일로 다운로드 됨)
- 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 cluster
로 GKE 클러스터
를 생성한다. 여기서 설정하는 몇몇 설정, 특히 네트워크 관련 설정은 클러스터 생성 후 변경이 불가능하니 주의가 필요하다.
private 으로 설정해서 외부로 IP 가 노출되도록 설정한 인스턴스 이외에는 접근할 수 없도록 한다. 그럼 2가지 경고 메시지를 볼 수 있다.
- (1) 클러스터를 생성하고 나서
Cloud NAT
를 통해 아웃바운드 인터넷 연결을 활성화 시켜라 - (2) 컨트롤 플레인이 허용한 네트워크를 활성화 시켜라
나처럼 초보자는 여기서 헤매기 딱 좋은 키워드가 있다.
VPC
NAT
Subnet
CIDR
(참고)
관련해서 구글이 제공하는 도움말 링크 모음은 다음과 같다.
쿠버네티스 클러스터 관련해서 핵심만 요약한 영상 자료는 다음을 참고했다.
쿠버네티스 클러스터 생성창에서 뭔가 중요한 사항을 알려준다. 더 이상 kube-dns
를 사용하지 않고 Cloud DNS
를 사용한다는 알림이다.
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
github 로 push 와 동시에 build, 그리고 jenkins 에 kubernetes 플러그인을 통하여 GKE 로 배포까지 완료되도록 설정한다. 브랜치는 */main 뿐 아니라 여러개를 설정할 수 있다.
다만, 이러한 파이프라인이 작동하기 위해서는 github repo 에 webhook 을 설정해야 한다.
위에서 secret 은 Jenkins 의 설정에서 생성한 token 이다.
젠킨스에서 파이프라인 구성 후에 Build now (지금 빌드) 를 누르면 github 에서 정보를 받아온다. 한번 빌드 후에는 github 로 push 와 동시에 CD/CI 과정이 진행된다.
여기까지 중요한 포인트만 정리해보자.
- Serverless 서비스를 위한 플랫폼으로 GCP 를 사용한다.
- GCP 에서 제공하는 GKE 를 사용한다.
- Dockerhub 가 아닌 구글이 제공하는 Artifact Registry 를 사용한다.
- Cluster 를 생성한다.
- Cloud NAT 를 생성하여 클러스터 내에서 서비스-to-서비스 통신이 원활하게 이루어지도록 한다.
- 젠킨스를 띄울 VM 을 생성한다.
- 젠킨스를 설치하고 각종 환경변수, 인증서를 등록한다.
- 젠킨스에서 파이프라인을 구성한다.
- github, gke 의 인증을 위해서 위에서 등록한 인증 정보를 사용한다.
- github 에 push 하면 젠킨스를 통해 빌드 후 k8s 로 배포가 완료된다.
보안 설정
보안에 관련된 부분은 검색해보면 너무나도 방법이 다양하다. 단순화하여 큰 그림을 보는 것이 목적이므로 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 token
에 SCOPE_internal
를 가지고 있는 요청만 허용한다.
참고
OAuth2
(+소셜로그인) 관련 토이 프로젝트
중간점검
몇 번의 시행착오 끝에 모든 서비스를 GKE 에 등록하고, postman 으로 order-service, payment-service, product-service 응답이 정상적으로 이루어지는 것을 확인했다. 중요한 비즈니스 로직은 없기에 생략한다.
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.key
와certificate.crt
를gcp
에 등록한다.
TCP 통신 -> HTTPS 통신
변경을 위해 LoadBalancer
가 아닌 Ingress
로 변경하고, 인증서 정보를 받아올 수 있도록 YAML 파일을 수정해야 한다.
gcp 도움말을 쭉 읽어보며 필요한 작업을 정리해본다.
- 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
관련된 도움말이 여러 갈래로 나눠진 상태로 자기 주장이 강해 정확히 어떤 걸 적용시켜야 할지가 어지러웠다.
- google 이 관리하는 SSL 인증서 등록
FrontConfig
를 통한 HTTPS 설정BackendConfig
를 통한 HTTPS 설정- 인그레스를 사용한 HTTPS 부하 분산에서 여러 SSL 인증서 사용
- 외부 애플리케이션 부하 분산기용 인그레스 구성
- 부하 분산기와 애플리케이션 간 HTTPS(TLS)
- 클라이언트와 부하 분산기 간 HTTPS(TLS) 설정
- GKE 네트워크 개요
위 문서를 참고하여 외부 부하 분산을 위한 인그레스 작동 방법 을 참고한다. 인그레스 서비스가 부하분산기 역할을 하되, 해당 인그레스 서비스에 유효한 SSL 인증서가 존재하면 된다. 그럼 결국 yaml 파일을 작성해서
Deployment
NodePort
ServiceIngress
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.key
와 certificate.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 파일을 작성했다.
하지만! 여전히 오류는 그대로다.. 설정이 부족하다고 하는데 어떤 설정이 부족한지 설명을 해주지 않는다.
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 {
}