Search

MSA라는 시류에 편승하기(2)

게시일
2023. 3. 20.
생성 일시
2023/03/20 17:55
이번 글에서는 앱의 초기 설계, 그리고 컨테이너와 쿠버네티스를 사용하는 로컬 개발 환경을 구성하는 과정에서 있었던 결정과 문제들에 대해 다룬다. 여러 새로운 도구들을 다룰텐데 설치법 같은 일반적인 내용들은 링크로 갈음하고, 이보다는 내가 겪었던 문제들 위주로 설명한다.
overengineered-blog-app이란 이름으로 레포를 만들어 작업을 진행 중이며, 글을 쓰는 시점에서 헤드 커밋은 6bb20a0이다.

쿠버네티스 & 로컬 개발 환경

Minikube

미니큐브는 경량 쿠버네티스 구현체이며 주요 쿠버네티스를 사용하는 로컬 개발환경을 구성하는 데 사용된다.
일반적인 설치 방법에 대해선 이 포스트에 잘 설명되어 있다.
다양한 OS와 드라이버를 지원한다. - 목록
나는 실리콘 맥을 사용 중이다. VirtualBox나 Hyperkit는 사용이 제한되어 Docker 기반으로 미니큐브를 구성했다.
minikube config set driver docker
미니큐브의 IP를 맥의 DNS에 등록하면 로컬 개발시에도 도메인으로 접속이 가능하다 (이러 형태로 많이 사용하는 것 같다)
1.
minikube ip로 미니큐브의 IP를 확인한다.
2.
/etc/hosts 파일에 엔트리를 추가한다.
Ingress와 함께 사용
Ingress 사용을 위해선 미니큐브에 addon 설치가 필요하다. 이 포스트에 잘 설명되어 있다.
docker driver + Ingress 구성시 DNS 인식 실패 에러
이렇게 미니큐브에서 Ingress를 사용할 수 있도록 addon을 설치하고 쿠버네티스 Ingress 매니페스트에도 /etc/hosts에 추가한 네임 서버를 host로 정의했지만 접속이 되지 않는 문제가 있었다.
7332 여기에서 이 이슈에 대한 여러 논의를 찾을 수 있었다.
제시된 해결법은,
minikune tunnel을 사용하거나
docker-mac-net-connect 이걸 설치하여 사용하는 것이다. 나는 이걸로 해결했다.
배포시 도커 이미지를 pull 하지 못하는 경우가 있다. Error: ErrImagePull
배포를 수행한 환경, 터미널이 미니큐브가 실행되고 있는 도커 환경과 같지 않아서 발생하는 문제이다.
eval $(minikube docker-env) 커맨드를 실행하면 미니큐브의 도커 환경에 대한 환경변수를 지정해준다.
이후에 다시 필요한 이미지들을 빌드하고 쿠버네티스 배포를 수행하면 된다.
혹은 호스트 환경에 빌드 된 이미지들을 미니큐브의 도커 환경 내부로 직접 가져올 수도 있다.
minikube image load <image name>

Telepresence

쿠버네티스를 사용한 로컬 개발 시, 변경된 코드를 적용하기까지의 사이클이 굉장히 길어질 것으로 예상된다. 혹시 아래와 같이 간소화할 수 있는 방법이 있는지 찾아보았다.
as-is: 앱 빌드 → 도커 이미지 빌드 → 클러스터에 재배포 → 동작 확인
to-be: 앱 빌드 → 동작 확인
로컬 개발을 편하게 해주는 몇가지 도구들이 있다. - 목록
이들 중 다수는 도커 이미지 빌드 과정을 요구했고 그렇지 않은 건 (아마) Telepresence 뿐이었다.
현재는 2버전이 최신이며 1버전과는 제공되는 API가 완전히 달라졌으므로 레퍼런스를 찾을 때 주의가 필요하다 (한글로 공유되고 있는 포스트들은 1버전 기준이 많다).
제공하는 편의기능들
모든 서비스들을 호스트 환경에서 접근 할 수 있게 프록시 해준다. 직접 kubectl로 포트 포워딩을 하지 않아도 된다.
클러스터 내의 일부 서비스를 인터셉트 하여 로컬에서 실행 중인 서버로 대치할 수 있다. 해당 서버로 들어오는/나가는 통신 모두 쿠버네티스 클러스터 환경과 연결된다.
ConfigMap과 Secret는 쿠버네티스 오브젝트로 클러스터에 위치하며, 배포된 서비스에서 참조된다. 이때, volume을 사용하기도 한다. Telepresence에 의해 인터셉트 된 서비스도 이것들에 접근할 수 있을까?
관련된 몇가지 기능을 제공한다. (스포일러: 나는 이 기능들을 적극적으로 쓰지는 않기로 결정했다.)
—-env-file: 배포에 적용된 환경 변수를 모두 추출하여 호스트 OS에 로컬 파일로 제공한다.
--mount: 쿠버네티스 상의 볼륨을 로컬 파일시스템에서 접근 가능하게 만들 수 있다.
macOS 환경에서 사용시 몇가지 플러그인 설치가 추가로 요구된다. (가이드)
macfuse가 처음 실행되면 ‘System Extension Blocked’ 에러창과 함께 추가적인 보안 권한을 요구할 것이다. macOS Ventura에선 시스템 설정 > 개인정보 보호 및 보안 화면의 하단에 관련 설정이 있다.
맥을 안전모드로 부팅하고 ‘시동 보안 유틸리티’에 대한 추가 설정도 해야한다. 이 포스트에 잘 설명되어 있다. 이후 몇번 더 재부팅이 필요할 수도 있다.
필요한 모든 준비 작업을 마치고 해당 기능을 사용해봤다. 기대와 달리 로컬 환경에서 구동 중인 앱이 마법 같이 쿠버네티스 상의 ConfigMap, Secret, volume을 참조 할 수 있게 해주는 것은 아니었다.
--env-file은 환경변수를 추출하여 파일로 제공할 뿐이고, --mount는 볼륨을 로컬 파일 시스템으로 열어줄 뿐이다. 이후에 로컬 구동 중인 서버가 해당 파일들을 참조하도록 하는 설정은 직접 해야한다.
구성 과정에 손도 많이 가고 그닥 편하지도 않기에 그냥 loca.env 같은 걸 만들어 로컬 앱에서 사용하는 전통적인 방식을 사용하기로 했다.
이외의 문제점
sudo 권한을 자주 요구한다.
알 수 없는 이유로 정상작동 하지 않을 때가 종종 있다.
인터셉트 요청시 랜덤하게 타임아웃 발생. 다시 시도하면 또 된다.
필요로하는 agent, plugin 등이 없다는 에러가 발생한다. Telepresence를 재연결 하거나 Minikube 환경을 다시 만들면 해결되었다.
이거 보다 나은 걸 아직 찾진 못해서 계속 쓸 예정이다.

Kustomize

커스터마이즈는 쿠버네티스 매니페스트에 대한 공통 정의(base)와 개발, 운영과 같은 환경별 정의(overlays)를 작성하여 재사용 할 수 있게 해준다.
이 분야에서는 Helm도 많이 사용되는 것 같으나, 러닝 커브가 높아보였고 그나마 최근에 MSA를 배울 때 읽은 책에서 다뤘던 도구가 커스터마이즈라 이걸 쓰기로 했다.
configMap/secretGenerator 기능을 제공한다. (사용된 곳)
쿠버네티스의 기본 동작과 달리 값/파일 변경을 감지하여 자동으로 재배포 해준다.
커스터마이즈는 구성에서 참조할 리소스를 반드시 하위 디렉토리에 포함시켜야 하는 제약이 있다.
이 이슈를 보면 많은 불평과 이를 위해 제시된 추가 옵션이 있음을 확인 할 수 있다.
--load-restrictor (구버전에선 --load_restrictor)
하지만 이 옵션을 사용하진 않기로 했다.
현재 Kustomize의 디자인에 부합하면서 안정적으로 제공되는 기능이라고 보기 조금 어려운 것 같다.
kubectl에서 바로 적용이 불가하여 반드시 build 후 apply 해야만 한다.
원래는 프로젝트의 루트에 config-repo라는 디렉토리를 만들고 여기에서 여러 서비스들의 모든 설정값이나 시크릿을 관리할 계획이었다. (이것들은 쿠버네티스 외에 도커 컴포즈나 로컬 서버에서도 사용될 것을 상정했다.)
그러나 위와 같은 커스터마이즈의 제약으로 kustomize 하위로 config-repo 디렉토리를 위치시켰다.
덕분에 docker-compose에서는 path가 장황해졌다.
더 좋은 방법이 있으면 좋겠다.

어플리케이션 개발

실제 비지니스 로직을 개발하기 이전이며 서비스간에 임의의 통신을 주고 받는 형태로만 구성했다.

기본적인 서비스간 통신

서비스 이름이나 포트 설정을 바꾸지 않아도 쿠버네티스와 도커 컴포즈 모두에서 동작 가능하도록 했다.
일단은 gRPC가 아닌 HTTP 웹 요청으로 구성했다.

Aggregator / API Composition 패턴

개별적인 서비스들이 모두 최종적으로 노출될 API를 제공하는 방식은 서비스간 의존성이 중구난방이 되거나 관리에 어려움이 생길 것 같았다.
인터널 한 서비스, 이를 조합하여 최종 API를 제공하는 서비스. 이렇게 2개의 티어를 분리해야겠다는 생각이 들었다.
다행히 많이 쓰는 패턴인 것 같고 Aggregator 혹은 API Composition 패턴이란 이름으로 불리는 것 같다.
overengineered-blog-app에선 blog-aggregator라는 이름의 aggregator 역할을 하는 서비스를 만들고 여기에서만 사용자 API(GraphQL)을 제공하도록 구성했다.
현재는 이 aggregator 하나에서만 모든 API를 제공하는 꼴이 되었지만 만약 앱의 컨텍스트가 넓어진다면 다른 aggregator가 추가 될 여지도 있다. (user, order-aggregator 등)

Spring Boot + GraphQL

Kotlin + WebFlux + Spring Security가 함께 사용되며, 리액티브 코드를 작성하는 데 Mono/Flux 보다는 코틀린 coroutines를 선호하려고 한다.
GraphQL API를 구현하는 데는 Netflix DGS를 사용했다.
Mono 대신 코틀린 코루틴을 사용해도 아무 문제 없이 작동한다.
codegen 기능이 있다.
코틀린을 사용하는 프로젝트에서 사용시 데이터 패쳐에 대한 보일러 플레이트를 제공해야할 dgs-codegen-generated-examples 폴더가 비어있는 것을 확인 할 수 있다.
코틀린은 지원하지 않는 기능이며, 실험적인 기능으로 나중에 제거될 수도 있다고 한다. (질답)
Spring Security
아직은 실제 인증/인가 기능 구현 이전이지만, 임의로 최소한의 어드민에 대한 접근제어를 구성해놓았다. (코드)
DGS, WebFlux, Kotlin Coroutines을 함께 사용하는 환경에선 일부 API들이 제한되고 다른 것으로 대체된다.
1.
DGS 데이터 패쳐에서 @Secured를 사용할 수 없다.
공식 문서와 예시에서는 사용 가능하다고 나와있으나, 이는 Spring MVC에서 유효하다.
@PreAuthorize는 쓸 수 있다.
2.
Spring Security의 config 클래스에는 @EnableReactiveMethodSecurity가 필요하다.
리액티브를 활성화 했으므로 @PreAuthorize로 어노테이트 된 메서드는 기본적으로 Mono/Flux를 리턴해야 한다.
@EnableReactiveMethodSecurity(useAuthorizationManager = false)로 설정하면 Mono/Flux를 코루틴으로 대체 가능했다.
왜 이 설정에서만 동작하는지 정확한 맥락은 파악이 안되지만 관련 이슈들이 있고 논의가 진행되고 있다. 12821 12080
리액티브 WebClient가 DNS를 인식하지 못한다?
현재는 임의의 HTTP 요청으로 서비스간 통신을 테스트하고 있다.
WebFlux 프로젝트에서 권장되는 WebClient로 타 서비스의 네임서버를 URL로 지정하여 요청시(e.g. http://post-service) 계속 실패했다. 근데 RestTemplate으로 대체하면 정상 작동한다?
이렇게 설정하니 해결되었다.
SO에서 이 답변의 도움을 받아 해결했다.
netty의 동작 때문인 것 같으나 근본적인 원인이 무엇이고 해결법이 이것 뿐인지는 아직 파악 중이다. (관련 문서)
실리콘 맥에서 발생하는 Unable to load io.netty.resolver.dns.macos.MacOSDnsServerAddressStreamProvider, fallback to system defaults. 경고와 직접적인 관련이 있는 이슈는 아닌 것 같다.
처음엔 이게 원인인가 싶어 이 이슈에 제시된 해결법에 따라 조치했으나 최초의 문제가 해결되지는 않았다.
아직 굉장히 간단한 구성이라 앞으로 바뀔 부분이 많을지도 모른다.

마치며

프로젝트를 시작하고 생각 이상으로 많은 문제들을 겪고 있다. 사용되는 툴이 다양하고 조합 또한 니치 해지면서 흔치 않은 문제들이 자주 발생하는 것 같다. 해결이 쉽지도 않았다. 이중엔 생산적인 문제들이 거의 없었지만 이런 환경에서는 어쩔 수 없는 것 같다.
전체적인 개발환경 구성 및 설계 단계, 비지니스 로직 개발. 이렇게 2단계로 나눠 개발을 계속할 계획이다. 현재는 첫번째 단계에 집중하고 있으며, 이 글에서 많은 내용을 다루었음에도 불구하고 아직도 아래와 같은 작업들이 남아있다.
gRPC를 통한 서비스간 통신 구성
DB, MQ 등 외부 리소스 구성
DB와 ORM 등을 사용한 기본적인 persist 방식 구성
이후에도 해야 할 작업이 많으므로 글은 이만 줄이겠다.