알뜰폰 요금제 모요(MOYO), 모두의 요금제

개발 환경 줄 서지 마세요 : MSA에서 1인 1환경 만들기

서청운2026.01.10
개발 환경 줄 서지 마세요 : MSA에서 1인 1환경 만들기

들어가며

안녕하세요. 모요의 DevOps 엔지니어 서청운이에요.

“알파 환경 테스트를 위해 잠시 알파 환경 배포를 점유할게요.”

“혹시 스테이징 환경에 제가 배포해도 될까요?”

개발 환경의 부족 문제는 많은 조직에서 공통적으로 겪고 있는 문제에요. 많은 팀들이 테스트를 위해 동시에 한 서비스에 대해 변경 사항을 개발 환경에 적용하려고 하다보니 이런 문제가 생기곤 하죠. git branch(깃 브랜치) 전략에 따라 다른 유형의 문제가 생기기도 하는데, 환경이 점유되어 뒤의 사람이 배포를 못하는 문제가 생기거나, 한 환경에 여러 배포가 계속 진행되는 브랜치 전략의 경우 버그가 발생하면 누구의 변경 사항이 문제를 일으켰는지 파악하는 데 몇 시간을 소비하기도 하죠. 결국 브랜치 전략의 한계라기보다 개발 환경의 구조적인 한계라고 보는게 맞을 것 같아요.

이런 문제는 QA 일정에 딜레이가 생기기도 하고 온전한 테스트 기간을 확보할 수 없기 때문에 릴리스가 지연되거나 프로덕션 배포에 대한 확신이 없는 상태에서 제품을 출시하게 되기도 하죠.

모요에서도 많은 스쿼드들이 빠르게 새로운 기능과 상품을 출시하며 통신 시장의 문제를 해결하고 있는데요. 저희도 역시 위와 같은 문제를 겪었고 단순한 일시적인 해결책이 아닌 장기적으로, 그리고 근본적으로 문제를 해결해나가고 싶었어요.

새로운 임시 개발 환경을 찾아서

기존 개발 환경 배포 방식은 모두가 하나의 개발 환경을 공유하며 경쟁해야 했어요. 새로운 해결책은 모든 개발자에게 공유 개발 환경 안에서 자신만의 임시 개발 환경을 제공하는 아이디어였어요.

이 임시 개발 환경은 dev1,dev2… 처럼 전체 스택을 수십 번 복제본을 만들어내는 방식은 아니에요. 그런 방식은 비용과 복잡성 측면에서 큰 단점이 될 수 있죠. 대신 더 우아하고 효율적인 방식으로 처리할 수 있도록 했어요. 서비스 메시의 복잡한 라우팅이 가능하다는 장점을 잘 활용하면 동일한 기본 인프라를 공유하면서도 각 개발자의 변경 사항을 다른 개발자들로부터 분리할 수 있을 것 같다는 아이디어가 있었고, 그 아이디어로부터 설계는 시작되었답니다.

PR 생성 시 브랜치 이름 기반 임시 환경 자동 생성 구조

예를 들어 개발자 A가 어떤 기능을 개발 중이라고 가정해 봐요. PR(풀 리퀘스트)를 만들면 그 브랜치의 이름을 딴 임시 환경이 생성되고, 개발 임시 환경이 자동으로 생성되는 구조에요. 특정 헤더가 포함된 테스트 요청을 개발, 스테이징 환경의 클러스터로 보내면, 요청은 그 임시 환경의 서비스로 라우팅돼요. 다른 모든 종속성의 경우 기본 개발 환경에서 실행 중인 안정적인 기준 서비스로 라우팅돼요. 즉, 개발자 A는 다른 개발자 B의 작업에 영향받지 않고 PR을 테스트할 수 있고, 그 반대도 마찬가지예요.

아래와 같은 예시를 볼까요? 개발 환경에서 테스트가 필요해 alpha.moyoplan.com 으로 접근했을 때 다음과 같이 [web-frontend] → [core] → [hello-world] 순서로 마이크로 서비스 간 통신이 진행된다고 가정해요.

개발 환경 web-frontend → core → hello-world 마이크로 서비스 통신 구조

위와 같은 구조는 이전에 개발 환경에서 테스트를 할 때는 환경을 점유하거나 배포 히스토리를 따라갈 때 애를 먹었었던 구조였어요. 앞으로는 다음과 같은 flow로 임시 환경 테스트가 가능해지도록 했어요.

Header 기반 임시 환경 라우팅 - feature-A 환경으로 트래픽 전달

alpha.moyoplan.com 으로 접속할 때 특정한 Header 값에 임시 환경 이름을 넣게 되면 임시 환경으로 배포한 서비스로 라우팅되고, 이를 통해 독립적인 테스트가 가능해져요.

위의 그림은 web-frontend 서비스에서 PR을 올려 alpha 환경의 feature-A 임시 환경으로 배포를 하고, 동일하게 core 서비스에서도 feature-A 임시 환경으로 배포를 한 상황이에요. (프론트와 백엔드 모두 변경사항을 함께 테스트하고 싶을 때)

처음 트래픽을 받는 인그레스 게이트웨이에서 web-frontend 서비스로 트래픽을 보낼 때, Header 값 기반의 라우팅(Header-based-routing)을 통해 임시 환경 값에 해당하는 임시 환경이 존재한다면 그 환경으로 트래픽을 보내고 그렇지 않다면 기본 개발 환경(baseline)의 서비스로 트래픽을 보내게 해요.

위의 경우는, web-frontend 서비스에 feature-A 환경이 있어 해당 환경으로 트래픽을 보낸 거예요.

web-frontend 서비스에서도 core 서비스로 트래픽을 보내야 할 때, core에도 해당 임시 환경이 있으므로 feature-A 환경으로 트래픽을 보낼 거예요. core에서 hello-world 서비스로 요청을 보낼 때는, feature-A 환경이 없어서 baseline인 hello-world 서비스로 트래픽을 보낸 모습이에요.

baseline은 hello-world 서비스의 경우와 같이 임시 환경이 없는 서비스들이 트래픽을 받게 되며, 앞서 보여드린 구조처럼 main에 머지되어 프로덕션 배포 시 함께 이 baseline의 서비스들이 배포되게 하여 코드가 최신화가 유지되도록 했어요.

이제 남은 건 구현. 재료는 서비스 메시와 Header Propagation

위에서 제시한 아이디어의 핵심 구현사항을 정리하면 다음과 같아요.

  1. 한 서비스에서 PR(풀 리퀘스트)를 만들면 그 브랜치의 이름을 딴 개발 임시 환경이 생성돼요.
  2. 게이트웨이와 서비스, 서비스와 서비스 간의 통신에서 특정한 헤더값이 임시 환경의 이름이고, 그 임시 환경이 존재한다면 해당 서비스로 라우팅하고 그렇지 않으면 기본 개발 환경으로 라우팅해요.
  3. PR이 닫히거나 머지되면 개발 임시 환경이 자동으로 정리되어요.

이제 남은 건 이 사항들을 어떻게 구현할 것인가 일 텐데, 1번 구현은 CI/CD 파이프라인에서 helm과 argoCD를 잘 조합하면 어렵지 않게 구현할 수 있을 것 같아요. 특히 helm 구성본의 복제본을 만들어 임시 환경을 생성하면 그 서비스의 configmap, secrets, 리소스 request 및 limit 등의 정보도 함께 복제되니 관리에도 큰 이점을 가져올 수 있어요.

2번 구현은 꽤 난이도가 있어 보이기도 하네요. MSA 환경에서 각 마이크로 서비스 간의 통신에서 복잡한 라우팅을 구현하려면 서비스 메시가 꼭 필요해요. 다행히 모요의 인프라는 올해 상반기, ECS에서 EKS로 마이그레이션을 진행하며 쿠버네티스와 서비스 메시 중 하나인 istio를 함께 도입했기에 추가적인 도입 절차는 불필요했어요.

istio의 VirtualService를 다음과 같이 작성하면 의도하려 했던 라우팅을 구현할 수 있어요.

1apiVersion: networking.istio.io/v1alpha3
2kind: VirtualService
3metadata:
4 name: productpage-route
5spec:
6 http:
7 - name: 'feature-a-route'
8 match:
9 - headers:
10 x-moyo-ephemeral-route:
11 exact: feature-a
12 route:
13 - destination:
14 host: productpage
15 subset: feature-a
16 - name: 'feature-b-route'
17 match:
18 - headers:
19 x-moyo-ephemeral-route:
20 exact: feature-b
21 route:
22 - destination:
23 host: productpage
24 subset: feature-b
25 - name: 'productpage-default-route'
26 route:
27 - destination:
28 host: productpage
1apiVersion: networking.istio.io/v1alpha3
2kind: VirtualService
3metadata:
4 name: productpage-route
5spec:
6 http:
7 - name: 'feature-a-route'
8 match:
9 - headers:
10 x-moyo-ephemeral-route:
11 exact: feature-a
12 route:
13 - destination:
14 host: productpage
15 subset: feature-a
16 - name: 'feature-b-route'
17 match:
18 - headers:
19 x-moyo-ephemeral-route:
20 exact: feature-b
21 route:
22 - destination:
23 host: productpage
24 subset: feature-b
25 - name: 'productpage-default-route'
26 route:
27 - destination:
28 host: productpage

feature-a-route , feature-b-route 라우팅을 거치면서 특정 임시 환경에 해당하는 헤더값이 존재하면 그 임시 환경의 서비스로 라우팅되고 하나도 매칭되지 않으면 기본 개발 환경으로 라우팅되도록 할 수 있었어요.

이런 형식으로 임시 환경의 라우팅을 생성하는 helm 차트를 만들고, CI/CD 파이프라인에서는 Github Actions 등의 도구를 사용해서 임시 환경 생성 시 임시 환경 서비스와 해당 라우팅이 생성되도록 해 성공적으로 자동화를 구현할 수 있었어요.

하지만 여기서 아직 중요한 구현 필수사항이 남아있어요. 바로 헤더를 전파하는 부분인데요. Istio는 애플리케이션이 인바운드와 아웃바운드 트래픽과 같은 네트워킹을 책임지는 Pod 사이드카로 Envoy 프록시를 사용해요. 그러나 여전히 서비스 애플리케이션 레이어에 속하는 한 가지 책임이 있어요. 바로 헤더 전파예요.

Envoy 프록시는 애플리케이션으로 전송하는 요청과 애플리케이션이 응답하는 요청을 상호 연관시킬 수 없으므로, 헤더는 Istio에 의해 자동으로 전파될 수 없어요. 대부분의 경우 헤더 기반 라우팅은 애플리케이션 개발자가 헤더 전달을 구현해야 하죠.

Istio 공식 문서에는 다음과 같이 설명하고 있어요.

애플리케이션 대신 이스티오가 헤더를 전파할 수 없는 이유는 무엇인가?

이스티오 사이드카는 연관된 애플리케이션 인스턴스의 인바운드 및 아웃바운드 요청을 모두 처리하지만, 아웃바운드 요청을 이를 유발한 인바운드 요청과 암묵적으로 연관시키는 방법이 없습니다. 이러한 상관관계를 달성할 수 있는 유일한 방법은 애플리케이션이 인바운드 요청의 관련 정보(예: 헤더)를 아웃바운드 요청으로 전파하는 것입니다. 헤더 전파는 클라이언트 라이브러리를 통하거나 수동으로 수행되어야 합니다. 자세한 내용은 'Istio를 사용한 분산 추적을 위해 필요한 것은 무엇인가?'에서 확인할 수 있습니다. (번역)

즉, 각 마이크로서비스 간 통신에서 헤더를 전파시키기 위해서는 통신 라이브러리를 공통화해서 다른 마이크로 서비스를 호출할 때 헤더에 직접 넣어주는 종류의 작업이 필요해요. 그 작업 방식에 대해 2가지 방법으로 고민을 했는데요.

  1. http 통신 라이브러리 공통화
  2. eBPF 기반 분산 추적 솔루션 사용

1번 방법은 현재 모요 백엔드 챕터에서 사용하는 Spring 기반에서 http 통신 라이브러리를 공통 규격화시키려는 니즈가 있었고, 해당 작업과 함께 작업한다면 큰 부담이 없어 보였어요. 다만 프론트 챕터에서 사용하는 Next.js 서버에서도 추가 작업이 필요하고, 백엔드 챕터에서 미래에 Go나 Python 같은 다른 언어를 사용하고자 할 경우에 또 다른 허들이 되기도 하고 장기적으로 관리 포인트가 너무 많을 것 같다는 단점이 있었어요.

2번 방법은 애플리케이션에 코드를 변경할 필요 없이 즉각적인 추적 기능을 사용할 수 있어 장점이 있어 보였어요. OpenTelemetry의 baggage 같은 기능을 사용해 헤더를 전파시킬 수 있고, eBPF 기술이다 보니 특정 언어나 프레임워크에 제한받지도 않아 해당 측면에서도 큰 장점이 있어요. 헤더 전파를 넘어서 eBPF 기반의 옵저버빌리티 측면에서 확장시킬 수 있는 가능성도 상당히 많아지겠고요.

서비스 간의 attribute가 전파되는 모습 - DataDog Baggage서비스 간의 attribute가 전파되는 모습

다만 도입하려는 목적성을 넘어선 오버 엔지니어링이라는 생각이 들었고, 상용 환경에서는 사용하지 않을 목적이라 개발 환경에서만 사용한다면 개발-상용 인프라 환경의 차이가 커져 안정성 검증 및 디버깅 측면에서 좋지 않을 확률이 크다 생각했어요.

두 가지 방법 모두 장단점이 존재해 고민이 깊어지던 도중, 저희는 새로운 구현 방식을 발견하게 됐어요.

데이터독 분산 추적 라이브러리를 사용해보자

1번과 2번 방법에서 고민을 하던 도중, 2가지 방법의 장점을 모두 담고 있는 새로운 아이디어를 생각해냈어요. 바로 데이터독 분산 추적 라이브러리를 통한 구현이죠. 현재 모요는 데이터독의 APM 트레이스 연동을 위해 모든 서비스들이 데이터독 분산 추적 라이브러리(dd-trace-java, dd-trace-js 등)를 모두 공통적으로 사용 중이었어요. 이 라이브러리를 잘만 사용할 수 있다면 추가적인 코드 작업 없이도 분산 추적의 컨텍스트 전파 기능을 사용해 특정 헤더값이 한 요청에서 그대로 쭉 전파되도록 할 수 있을 것 같았어요.

가능성을 확인하고 바로 연구에 돌입했어요. 공식 문서에서도 잘 확인되지 않는 내용이라 코드를 까보고 이슈를 따라가는 등의 시간은 걸렸지만, 결론은 간단하게 baggage를 사용해 원하는 컨텍스트를 전파시킬 수 있었어요. java 라이브러리의 경우 다음과 같이 환경 변수를 추가해주면 특정 헤더값을 한 요청 내에서 전파시킬 수 있어요.

1 env:
2 - name: DD_TRACE_HEADER_BAGGAGE
3 value: x-moyo-ephemeral-route:headerSpanName
1 env:
2 - name: DD_TRACE_HEADER_BAGGAGE
3 value: x-moyo-ephemeral-route:headerSpanName

해당 환경 변수를 사용하면 x-moyo-ephemeral-route의 헤더값이 전파가 되는 걸 확인할 수 있었어요.

1curl -H "x-moyo-ephemeral-route: feature-a" pod-a
1curl -H "x-moyo-ephemeral-route: feature-a" pod-a

트레이스와 로그 컨텍스트가 잘 연동되어 있을 때 엔보이 액세스 로그의 헤더 패턴을 사용하면 디버깅을 더 쉽고 효율적으로 할 수 있어요. 마이크로 서비스 간 통신에서 인바운드, 아웃바운드로 오고가는 요청에서 헤더값을 발라내 확인하는 방식으로요. 저희는 데이터독으로 로그, 트레이스 등의 정보들이 연동되어 있는 상태여서 헤더값이 전파되는 것을 로그로 쉽게 확인해 빠르게 디버깅을 진행할 수 있었어요.

1# istiod 설정
2meshConfig:
3 accessLogFile: /dev/stdout
4 # x-moyo-ephemeral-route 헤더값 확인
5 accessLogFormat: "[%START_TIME%] %RESPONSE_CODE% %REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %REQ(x-datadog-trace-id)% %ROUTE_NAME% %PROTOCOL%
6 %RESPONSE_FLAGS% %DURATION%ms(Req %REQUEST_DURATION%ms, Res %RESPONSE_DURATION%ms) %REQ(:AUTHORITY)% %UPSTREAM_HOST_NAME%(%UPSTREAM_HOST%) %REQ(USER-AGENT)% %REQ(X-FORWARDED-FOR)% %REQ(X-REQUEST-ID)% %REQ(x-moyo-ephemeral-route)%\n"
1# istiod 설정
2meshConfig:
3 accessLogFile: /dev/stdout
4 # x-moyo-ephemeral-route 헤더값 확인
5 accessLogFormat: "[%START_TIME%] %RESPONSE_CODE% %REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %REQ(x-datadog-trace-id)% %ROUTE_NAME% %PROTOCOL%
6 %RESPONSE_FLAGS% %DURATION%ms(Req %REQUEST_DURATION%ms, Res %RESPONSE_DURATION%ms) %REQ(:AUTHORITY)% %UPSTREAM_HOST_NAME%(%UPSTREAM_HOST%) %REQ(USER-AGENT)% %REQ(X-FORWARDED-FOR)% %REQ(X-REQUEST-ID)% %REQ(x-moyo-ephemeral-route)%\n"

Envoy 액세스 로그에서 x-moyo-ephemeral-route 헤더 확인

동작 원리는 무엇일까?

분산 추적 라이브러리만 사용했을 뿐인데, 헤더 전파에는 어떤 원리로 동작하는 걸까요? 앞서서 OpenTelemetry의 Baggage 기능을 사용해 한 요청에서 헤더를 전파할 계획이라고 말씀드렸는데요. OpenTelemetry에서 Baggage는 컨텍스트(context)와 함께 존재하는 컨텍스트 정보에요. Baggage는 키-값 저장소로, 원하는 데이터를 컨텍스트와 함께 전파할 수 있게 해줘요.

따라서 Baggage는 여러 서비스 및 프로세스를 거치는 동안 "추가적인 정보" (예: 사용자 ID, 계정번호, 제품ID 등)를 전달하고 해당 값들을 스팬(span), 로그(log), 메트릭(metrics) 등에 활용할 수 있게 설계되어 있어요. 즉, 단순히 trace ID, span ID만 전파하는 것이 아니라, 특정한 메타데이터를 서비스 호출 경로 전체에 걸쳐 유지할 수 있게 하는 기능이에요.

아래 예시와 같이 여러 서비스(svc1, svc2, svc3)가 연결되어 있으며, svc3가 svc1이 수신한 정보를 가져오도록 하려는 경우를 예를 들면,

Baggage를 통한 서비스 간 컨텍스트 전파 - svc1, svc2, svc3https://www.cncf.io/blog/2024/05/28/is-testing-in-production-even-possible/

사용자 정의 헤더를 직접 정의해 사용하고 이를 이어지는 통신 전체에 전달할 수 있지만 좋은 방법은 아니에요. 헤더를 전달하기 위해 각 서비스의 코드를 수정해야 하기 때문이죠.

헤더를 전파하기 위해 이미 추적 라이브러리를 사용하고 있다면, 가장 좋은 방법은 Baggage 헤더를 사용하는 거예요. 이는 서비스 코드를 수정할 필요 없이 체인 전체에 걸쳐 전달되는 헤더이기 때문이죠.

Baggage 헤더를 사용한 서비스 코드 수정 없이 전파

따라서 저희는 이미 사용 중인 분산 추적 라이브러리의 Baggage 헤더를 사용해 서비스의 코드 수정 없이도 특정한 헤더 값을 한 요청에서 전파할 수 있게 된 거예요.

잘 사용해주세요!

이제 모든 구현은 끝났고 개발자들에게 좋은 사용성과 편리성을 제공해 문제를 해결하는 일만 남았어요. 프론트와 백엔드 구분 없이, 다음과 같은 플로우로 개발 임시 환경을 생성해 배포하도록 했어요.

PR에서 개발·스테이징 환경 선택 후 배포 플로우

먼저 PR에서 개발, 스테이징 환경을 선택해 배포할 수 있도록 했어요.

크롬 익스텐션으로 임시 환경 이름 설정 후 웹 테스트

그 다음으로 크롬 익스텐션을 사용해 임시 환경 이름을 셋팅하고, 웹에서 테스트를 진행할 수 있게 했어요. 이 익스텐션도 저희가 만든 익스텐션으로, 사용자가 설정한 임시 환경 값으로 헤더를 설정해 웹페이지 Request Header에 공통적으로 커스텀 헤더를 추가하도록 했어요. ModHeader 같은 툴과 비슷한 기능이에요.

ModHeader와 유사한 커스텀 헤더 추가 익스텐션

사용성 측면에선 비개발자분들도 더 쉽게 사용할 수 있도록 개발을 진행 중이에요. 더불어 임시 환경으로 제대로 라우팅이 되었는지도 더 쉽게 디버깅할 수 있도록 대시보드를 만들거나 하는 계획도 있어요.

이제 시작일뿐

궁극적으로 개발 환경의 병목 현상을 해결하는 것은 단순한 도구의 문제를 넘어 개발 조직 문화의 인식을 바꾸는 것으로 완성된다고 생각해요. 모요의 플랫폼 팀은 스쿼드에 소속된 개발자들이 더 빠르게 움직일 수 있도록 지원하는 셀프 서비스 도구를 제공하는 플랫폼 엔지니어링 사고방식을 가지고 나아가려 해요.

여러 피처 개발이 혼합되어 있는 개발 환경 대신, 모든 개발자들에게 각각의 피처 개발이 필요할 때마다 PR마다 독립된 개발 환경을 사용할 수 있는 생태계를 구축했고, 더 고도화해 나가려고 하는데요. 개발 환경 배포가 막혀 병목이 생기지 않고, 예기치 못한 버그로 디버깅에 더 많은 시간을 쓰지 않고, 마이크로서비스의 핵심 가치를 실현하는 길로 갈 수 있을 것이라 기대해요. 궁극적으로는 더 작은 코드 변경 사항을 독립적이고 신속하게 개발, 테스트, 프로덕션에 배포하는 문화를 가져가려고 해요.

특히 다양한 AI 도구의 지원으로 개발자들이 코드를 3배 더 빠르게 작성하고 배포할 때, QA 프로세스도 그 속도를 따라잡아야만 해요. 기존처럼 병합 후 단일 스테이징 서버에서 QA를 진행하는 구조는 절대 이 정도의 속도를 위해 설계된 방식이 아니기 때문이에요. 자동으로 빠르게 생성되고 안전하게 격리된 환경을 보장하는 임시 개발 환경을 통한 개발 문화는 빠르게 성장하는 스타트업이 병목을 줄이고 더 안전하고 빠른 배포를 향해 가는 첫 시작점이라고 생각해요.

앞으로 더 다양한 모요의 개발 이야기들을 자주 알려드리려 해요. 많은 관심 부탁드리며 모요의 개발 문화에도 관심이 생기셨다면 언제든 커피챗으로 연락주시거나 채용 공고를 확인해주시길 바라요. 감사해요.