서비스별 API 분할 운영 전략
서비스가 복잡해지고 프로덕트들이 늘어감에 따라 필요한 정보를 전달해주는 API가 비대해지는 것은 필연적인 일입니다. 코드에서 의존성 결합도가 높아지면 테스트가 쉽지 않고 많은 사이드 이펙트를 동반하는 것처럼 API도 여러 프로덕트의 기능들의 결합도가 높다면 예상치 못한 오류나 리소스 문제를 야기할 수 있습니다. 이래 저래 고민하던 찰나 우연히 넷플릭스의 GraphQL Federation에 대한 연재 글을 미디엄에서 보고 영감을 얻어 현재 케어닥의 상황에 맞게 설계해보았습니다.
넷플릭스의 GraphQL Federation 연재 글
https://netflixtechblog.com/how-netflix-scales-its-api-with-graphql-federation-part-1-ae3557c187e2
https://netflixtechblog.com/how-netflix-scales-its-api-with-graphql-federation-part-2-bbe71aaec44a
기존 구조는 하나의 GKE 클러스터 각 서비스를 파드에 배포해 리소스를 공유하며 운영 중이었습니다. Cloud DNS에서 GCLB(Google Cloud Load Balancer)에 Traefik(Edge router)를 백엔드로 붙여 쿠버네티스 CRD(Custom Resource Defination)으로 서비스에 연결하고 있었습니다. 구성 당시 GCLB에서 각 파드로 보낼 수 있는 방법이 없어 Traefik으로 로드밸런싱을 한 것이죠. (다른 방법이 있었을 수도...) 각 서비스 프로젝트 저장소에서 각 서비스로 gitops를 통해 배포해 인프라 수정 없이 서브 도메인에 서비스를 올릴 수 있었습니다. 하지만 클러스터 리소스를 공유하기 때문에 리소스 이슈(매우 큰 CSV 파일을 만든다던지 웹 트래픽이 증가할 경우)가 발생할 경우 전체 서비스에 영향이 가게 되고 또 SSL이 클러스터 내부 Cert Manager에서 발행된 cert 파일을 쿠버네티스 secret으로 관리되고 관리형 클러스터인지라 마스터 노드 업그레이드나 스펙 변경 시 클러스터를 변경해야 하는 이슈가 있을 경우 통째로 교체를 했을 경우 SSL에 대한 검증을 할 수 없는 단점이 있었습니다.
NEG(Network Endpoint Group)이 beta를 끝내고 GA가 되자마자 인프라 설계를 다시 한 TO-BE입니다.
GCLB의 백엔드로 NEG를 붙일 경우 Cloud Function, Cloud Run, GKE 등을 url map형태로 연결할 수 있습니다. 사이드 이펙트를 최소화하기 위해 트래픽이 많은 웹서비스들과 리소스에 영향을 줄 수 있는 유틸리티 파드들(File export, scheduling 등)은 Cloud Run으로 빼내고 NEG에 연결했습니다. GKE로 직접 관리하는 클러스터에는 API 파드들만 모아놓고 Traefik으로 Edge routing을 하도록 했습니다. 각 서비스별로 API를 파드에 배포해 Traefik의 미들웨어 기능으로 보안을 강화하고 메이저 버전이 업데이트되는 API라던지 테스트가 필요한 API 등을 유연하게 올려 운영이 가능해졌습니다. Traefik의 기능을 이용해 인프라 레벨의 A/B테스트도 가능하며 SSL이 GCLB에서 관리형으로 제공되기 때문에 관리형 클러스터인 API클러스터 교체를 blue-green 전략으로 사이드 이펙트에 대한 걱정 없이 무중단으로 교체 할 수 있게 됩니다.
인프라의 리소스는 서로 영향이 없어야 하지만 각 서비스별로 공유해야 하는 코드는 많아야 생산성 향상이나 서비스 구축에 있어서 이득을 가져갈 수 있는것은 누구라도 공감할 니즈일 것입니다. 디자인 시스템을 구축할 때 적용하려 했던 monorepo를 이번 기회에 본격적으로 적용해 보았습니다.
https://brunch.co.kr/@fifthsage/6
https://github.com/lerna/lerna
모든 저장소를 하나로 모을 수도 있겠지만 저장소가 너무 많이 비대해지고 많은 것들을 하나로 묶었을 때의 부작용들을 새각 해 큰 관심사별로 묶고 메인테이너를 지정해서 운영하는 것이 낫겠다는 판단을 했습니다. 크게 나눈 3가지 관심사는 다음과 같습니다
클라이언트인 웹과 앱의 경우 UI를 기준으로, API의 경우에는 Entity와 Service 모듈을 기준으로 서비스들이 구축되고 웹과 앱은 플랫폼이 다르기 때문에 공통으로 묶는 이점 별로 없어 크게 3개의 package 프로젝트로 나누었습니다. Web과 App은 각 javascript와 react를 기반으로 하기 때문에 hook이나 utility 등을 공유할 수 있고 API의 경우 graphQL을 사용하고 있고 클라이언트에서는 code gen을 해서 typing을 생성하지만 직접 붙는 API가 아닐 경우(예를 들면 kafka에 메시지를 날리고 클라이언트에서 받는 경우)에는 package로 typing을 공유할 수 있어서 NPM private을 사용해 각 package 프로젝트들 사이에 코드 공유가 가능하도록 구성했습니다. 다른 챕터가 만든 모듈이 좋아 보인다면 publishing 요청 후 사용하는 것도 가능한 것이죠. 이렇게 공통 모듈이 쌓이게 되면 생산성에 가속화가 될 수 있다는 사실!
각 monorepo의 package들은 Cloud Run과 Federation이 되어 있는 API 클러스터 파드에 대응되어 배포가 되고 코드를 공유하기 때문에 마이크로 서비스의 구성이 쉬워지고 생산성이 향상되는 이득을 가질 수 있습니다. API는 아니지만 Entity에 대한 접근이나 서비스 로직 처리가 필요한 경우 관련 package를 이용해 Cloud Function이나 Cloud Rund으로 배포를 하고 NEG에서 url map을 설정해주면 되고 (terraform으로 자동화가 가능해 보입니다) API의 경우 각 서비스별로 쿠버네티스 매니페스트에 ingress route를 설정해 주면 배포와 동시에 Taefik에서 동적 연결이 가능하기 때문에 개발자는 package들을 이용해 서비스를 구성해 배포만 하면 되는 것이죠. Cloud Function과 Cloud Run의 경우 제공되는 CLI로 간단하게 배포되며 각 배포 버전 별 weight조절도 클릭 한번으로 가능하기 때문에 점진적 적용이나 롤백이 용이하며 트래픽에 따라 horizontal, vertical 스케일링이 매니지드로 제공되기 때문에 웹 서비스를 제공하기에 매우 적합해 보입니다. API의 경우 기존에 있던 gitops를 확장해 별다른 변경 없이 배포가 됩니다. (모든 배포자동화는 이미 구성이 완료 되어 있기 때문에 신경 쓸 필요도 없습니다). 결과적으로 이 후에 웹, 앱, API와 관계 없이 어떤 서비스의 확장에도 대응이 가능하며 특히 API는 이전 버전과의 호환성이라던지 이슈가 생겼을 때 전체 서비스가 마비되는 상황에서 해방될 수 있습니다.
최근 React Native도 monorepo에 대한 대응이 메인 커밋에 올라오는 등 작년 디자인 시스템에 적용할 때보다 더욱 많아졌다는 것을 느꼈습니다. 스타트업이 아니더라도 코드 공유를 통한 생산성 향상과 적절한 인프라의 구성은 개발자라면 누구나 공감하는 이슈일 거라 생각합니다. hoisting에 대한 이해도와 node module의 기본 구조만 알면 적당한 삽질이면 구성이 가능하다고 생각됩니다(4명의 시니어 개발자가 삽질을...) 하지만 동료들의 도움이 아니라면 혼자 해내기엔 시간도 오래 걸리고 태스크를 함께 병행하며 하기엔 더더욱 어려웠을 것입니다. 저의 의견에 공감해주시고 적극적으로 구성에 함께한 8명의 제품개발 스쿼드 동료들에게 감사하다는 말을 전하고 싶습니다.