[LG CNS AM Inspire Camp 1기] 2차 미니프로젝트 회고록

정성엽·2025년 4월 5일
0

LG CNS AM Inspire 1기

목록 보기
64/70
post-thumbnail

INTRO

2차 프로젝트가 끝나고 3일이 지난 이제서야 회고록을 써보려고 한다 😅

프로젝트 기간 동안 많은 트러블 슈팅이 있었고, 이를 해결하기 위해 정말 많은 시간과 노력을 쏟았다.

짧은 기간이었지만 MSA 아키텍처와 클라우드 환경에서의 서비스 구축을 직접 경험하면서 의미 있는 기술적 도전을 할 수 있었다.

이제부터 필자가 프로젝트에서 어떤 파트를 담당했고, 어떤 작업을 했는지 소개해보려고 한다!


1. 어떤 역할을 맡았는지?

본 프로젝트의 주제는 1차 프로젝트에서 진행했던 다른 팀의 결과물에 MSA를 적용하는 것이었다.

여기서 필자는 AWS 클라우드 아키텍처 및 CI/CD 파이프라인 구축 이라는 DevOps 파트를 맡게 되었다.

사실 AWS와 CI/CD 모두 이번 수업에서 처음 접했던 내용이긴 했지만, 한번 해보면 좋은 경험이 될 것 같아 자진해서 지원해봤다.

하지만, 생각만큼 쉽지는 않았다..


2. CI/CD 구축

우선 CI/CD를 구축하기 위해 AWS EC2에 Jenkins를 직접 설치하는 방법을 사용했다.

수업 시간 중에, 강사님께서 Jenkins의 CI/CD 작업은 많은 프로젝트와 연결되어있고, 프로젝트 규모에 따라 자원을 많이 사용할 수 있기 때문에 별도로 관리하는게 좋다는 말씀해주셨기 때문이다.

💡 파이프라인 동작

백엔드 파트에서 도메인별로 나눈 프로젝트에는 이와 같은 파일 구조를 가지도록 설계했다.

이후, 파이프라인은 다음과 같은 순서로 동작하도록 설계해봤다.

동작 과정을 살펴보면, Git Clone 으로 가져온 프로젝트 파일을 사용해서 Docker Image Build 를 진행하기 때문에 프로젝트 내부에 Dockerfile 이 필요했고, 각 프로젝트마다 파이프라인이 다를 수 있기 때문에 별도의 Jenkinsfile 을 작성해서 추가하는 방법을 사용했다.

💡 문제점

CI/CD 파이프라인은 Git Webhook을 사용해서 동작한다.

따라서, 각 프로젝트마다 Git Webhook을 추가해주는데 여기서 Jenkins에서 기본적으로 제공하는 Webhook을 사용하면 상세한 설정이 불가하다.

한가지 예를 들자면, API Gateway Repo에 파이프라인을 작성해두고 AWS Cloud 환경에서 Eureka에 API Gateway가 등록되는 테스트를 진행하고 있었다.

이 과정에서 다른 팀원이 Develop 브랜치에 푸쉬를 하는 상황이 있었는데 파이프라인이 동작해서 재배포가 되는 과정이 꽤 있었다.

그래서 우리는 Generic Webhook Trigger 라는 별도의 플러그인을 사용해서 반자동화를 사용하는 방법을 택했다.

단순히 Develop 브랜치에 Push나 PR이 발생하는 모든 경우에 파이프라인이 동작하는것이 아닌, Deploy 라벨을 추가한 경우에만 파이프라인이 동작되도록 프로젝트를 구성하고 진행했다.

물론, 브랜치를 세세하게 나누고 특정 브랜치에 대해서는 완전 자동화를 사용하는 방법도 있겠으나 프로젝트 Rule을 세세하게 정할 시간이 별로 없었기 때문에 이러한 방식으로 해결해봤다.

💡 아쉬운점

본 프로젝트를 진행하면서 Jenkinsfile을 실제로 하나씩 모두 작성해서 프로젝트에 넣어줬는데, Groovy로 Jenkinsfile의 구조를 잡아주고 Github Scan을 사용하면 코드의 중복을 최소화해서 작업할 수 있다고 한다.

Groovy 언어를 사용해본적이 없어서 당시에는 적용해볼 엄두가 나지 않았으나, 이런 부분을 적용하지 못한게 아쉬워서 시간남으면 혼자 해보려고 한다


3. AWS Cloud Architecture

프로젝트를 진행하면서 가장 많이 했던 고민이 아키텍처에 대한 고민인 것 같다.

물론, 최종적으로 사용한 결과물이 완벽하진 않지만 어떤 내용을 고민했는지 공유해보려고 한다.

우선, 프로젝트의 초기 아키텍처를 살펴보면 다음과 같다.

💡 초기 아키텍처

위 사진과 같이 ECS에 서비스를 여러개 띄우고 ALB 하나가 정의된 Fargate에 대해서 로드 밸런싱을 모두 진행하는 구조로 설계했다.

하지만, 우리가 프로젝트에 MSA를 적용하는 과정에서 Spring Cloud API Gateway, Config Service, Eureka 등등을 사용하는데 이러한 서비스는 Outer Architecture에 속한다고 볼 수 있다.

실제로 사용자에게 서비스를 제공하기 위한 비즈니스 로직이 처리되는 부분이 아니기 때문이다.

중간에 강사님께 피드백을 받는 시간이 종종 있었는데, 이러한 Outer Architecture는 고가용성을 확보하기 위해 EC2에 배포한다고 피드백을 주셨다.

또한, 필자의 경우도 이미 Spring Cloud API Gateway를 사용하는 구조에서 ALB를 사용해 로드밸런싱을 하는게 과연 맞는 선택일까 하는 생각도 가지고 있었다.

그래서 구조를 다시 변경하기 시작했다..!

(이미 파이프라인까지 모두 만들어둔 상태였으나, 피드백을 받고 전체적으로 수정하기로 결정했다..........)

💡 최종 아키텍처

이미 Spring Cloud API Gateway를 사용하고 있으니 모든 요청을 API Gateway로 받도록 수정했다.

이후, 여러개의 서비스 인스턴스를 유레카에 등록했다면 라운드로빈 방식으로 라우팅을 진행하지만, 여기서 우리는 상대적으로 많은 트래픽이 몰릴 수 있다고 생각한 User-Service와 Track-Service에 ALB를 사용해서 로드 밸런싱과 오케스트레이션을 진행하도록 구조를 변경했다.

이렇게 구조를 작성한다면 User-Service와 Track-Service는 Eureka의 서비스 디스커버리 기능을 포기하게 된다.

조금 더 자세히 설명하자면 다음과 같다.

우선 우리는 Config 서버에서 API Gateway에 대한 라우팅 정보를 받아와서 동작하도록 설계했다.

그 라우팅 정보는 다음과 같다.

Sample Code

spring:
  config:
    activate:
      on-profile: "routes"
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/members/**
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/admin/**
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/auth/**
        - id: track-service
          uri: lb://track-service
          predicates:
            - Path=/tracks/**
        - id: recommendation-service
          uri: lb://recommendation-service
          predicates:
            - Path=/music/**

여기서 user-service와 track-service가 ALB를 사용하도록 하기 위해, prod 환경에서는 다음과 같은 라우팅 정보를 가져오도록 한다.

Sample Code

spring:
  config:
    activate:
      on-profile: "routes-prod"
  cloud:
    gateway:
      routes:
        - id: user-service
	      # 실제 ALB URI 입력
          uri: http://user-service-alb.region.elb.amazonaws.com 
          predicates:
            - Path=/members/**
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/admin/**
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/auth/**
        - id: track-service
          # 실제 ALB URI 입력
          uri: http://track-service-alb.region.elb.amazonaws.com 
          predicates:
            - Path=/tracks/**
        - id: recommendation-service
          uri: lb://recommendation-service
          predicates:
            - Path=/music/**
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOrigins: "*"
            allow-credentials: false
            allowedHeaders:
              - x-requested-with
              - authorization
              - content-type
              - credential
              - X-AUTH-TOKEN
              - X-CSRF-TOKEN
            allowedMethods:
              - POST
              - GET
              - PUT
              - OPTIONS
              - DELETE

이렇게되면 User-Service와 Track-Service는 Eureka에 등록할 필요가 없고, ALB가 대신 ECS 서비스의 여러 태스크 간 로드 밸런싱을 담당하게 된다.

만약, 서비스 자체적인 트래픽에 대해서 로드밸런싱이 필요하다면 API Gateway 앞쪽에 ALB를 하나 더 추가하는 방법도 있을 것이다.

💡 문제점

가장 큰 문제점은 비용이다.

만약, 실무에서 Cloud Architecture를 구성해야한다면 불필요한 부분은 최대한 제거해서 아키텍처를 구성하는게 맞다.

실제 트래픽에 대한 테스트를 진행한게 아니기 때문에 위처럼 구조를 작성하는게 오히려 불필요한 관리를 더하는 일이 될 수 있으며, Fargate의 경우 메모리 설정을 서비스에 Fit하게 맞춘게 아니라면 메모리를 사용하지 않아도 그대로 비용이 청구된다.

실제로 비용청구 내용을 보니 메모리는 10%만 사용하면서 전체 비용을 청구하는 모습을 볼 수 있었다..

다른 팀의 경우 EC2에 도커를 사용해서 프로젝트를 관리하는 구조를 보여주기도 했는데, 비용과 관리를 생각하면 이번 프로젝트에서는 그러한 구조가 훨씬 더 적절하다는 생각이 들긴한다.

따라서, 본 프로젝트에서는 최대한 여러가지 방법을 시도해봤다는데에 의의를 두려고 한다 👊


4. EC2 (Eureka) & ECS

위에서 최종 아키텍처를 보면 알겠지만, EC2에 Eureka 프로젝트를 도커로 띄워서 사용하고 있다.

이때, 하나의 EC2에는 하나의 서비스만 띄우는 구조를 채택했기 때문에 각 EC2에 Spring 프로젝트를 띄우는 경우 Docker의 Host Network를 사용해서 포트를 뚫어줬다.

이런 구조를 사용해서 Spring Cloud의 Eureka, API Gateway, Config Server를 각 EC2에 띄웠다.

그리고 각 서비스는 ECS에서 클러스터를 도메인별로 분리해서, 단순히 컨테이너만 띄우는 Fargate를 사용했다.

그런데 우선 첫번째로 Eureka에 서비스가 등록되지 않는 문제가 발생했다.

로그를 살펴봤을 때, Config Server에서 설정 정보를 패치했음에도 자꾸 localhost에서 정보를 가져오려고 시도했기 때문에 발생한 문제였다.

그래서 bootstrap에 Eureka를 먼저 받도록 설정하여 해결했다.

💡 API Gateway 호출 문제

EC2에 Spring Cloud API Gateway를 띄우고, Gateway를 통해 ECS의 서비스를 호출하려고 시도해보니 호출이 안되는 문제를 발견했다.

로그를 살펴보니 API Gateway에도 아무런 로그가 찍히지 않는 모습을 볼 수 있었다.

보안정책으로 Gateway 포트를 열어놨음에도 요청이 안들어오는 문제였다.

사실 이 문제 때문에 엄청 고생했다.

관련 포스팅을 겨우겨우 찾아 Github Repo에 작성된 코드를 살펴보니 Global CORS 설정이 되어있는 모습을 볼 수 있었다.

그래서 본 프로젝트에도 API Gateway 설정 파일에 GlobalCORS 설정을 추가해서 문제를 해결할 수 있었다.

아무래도 로그가 찍히지 않았다는 것은 Gateway가 CORS 프리플라이트 요청 단계에서 차단되었다는 것을 의미한다고 생각한다.

Postman으로 요청을 넣었음에도 요청이 도달하지 않는다는 것은 API Gateway가 CORS 헤더를 확인하도록 설정되어 있었기 때문이다.

따라서, 이 부분을 모두 허용함으로써 해결할 수 있었다..!

💡 ECS 서비스 호출 문제

위 설정을 마치고 API Gateway의 로그를 보니 요청은 정상적으로 들어온다.

그런데 이제는 요청에서 404가 떠버리는 모습을 볼 수 있었다.

라우팅 테이블은 제대로 등록되어있다고 계속해서 로그가 찍히는데, 해당 라우트 정보로 요청을 진행해도 404가 뜨는 상황이었다.

필자는 이 문제가 Eureka에서 서비스 인스턴스의 정보를 제대로 받아오지 못해서 발생한다고 생각했다.

그래서 Eureka에 등록된 서비스 정보를 찾아보기 시작했다.

위 사진은 EC2로 옮기기 전에 Eureka에 API Gateway가 등록되는 모습이다. (관련 사진을 안찍어놔서 없다..)

보다시피 169. 으로 시작되는 IP가 등록되는 모습을 볼 수 있다.

이 정보는 우리가 띄운 ECS Fargate 컨테이너의 주소 이다.

즉, 우리의 실제 서빙하고 있는 서비스의 Private IP가 아니라 컨테이너에서 사용하는 주소이므로 위 정보를 다시 내려받는다고해도 API Gateway는 정상적으로 서비스에 라우팅을 할 수 없다.

Eureka의 경우 /eureka/apps 로 들어가면 실제 등록된 서비스 인스턴스의 정보를 더 상세하게 확인할 수 있다.

살펴보니 IP Address도 위에서 언급한 Fargate 컨테이너 주소로 등록되어있는 모습을 볼 수 있었다.

따라서, 별도의 코드를 작성해서 Spring 프로젝트가 구동될 때, AWS Private IP를 Eureka에 등록하도록 설정을 변경했다.

로그를 찍도록 코드를 작성했는데, 실제 Private IP로 등록되는 모습을 볼 수 있다.

위 사진이 바로 /eureka/apps 정보에서 확인할 수 있는 서비스 인스턴스의 정보인데, ipAddr 관련 정보가 원하는대로 서비스의 Private IP가 등록된 모습을 볼 수 있었다.

이후, 서비스를 호출하면 제대로 호출이 된다!

💡 문제점

이렇게 문제점을 해결해보면서 느낀 것은 AWS Cloud 환경에서 Spring Cloud를 적용하는게 꽤 까다롭다는 것을 느꼈다.

실제로 AWS에서도 Gateway 서비스를 제공하고 있기 때문에, 환경에 맞는 서비스를 사용하는게 더 낫다는 생각도 들었다.

다음 프로젝트 때, 기회가 된다면 AWS Gateway를 사용해서 플랫폼 친화적으로 아키텍처를 구성해보고 싶다.


5. 그 외의 트러블 슈팅...

사실 이것말고도 엄청 많은 문제를 겪었다.

아마 한 스텝을 나가는데 3시간씩 걸린것 같다...

💡 RabbitMQ

예를들어 RabbitMQ를 EC2에 설치하는 과정에서 4.0-management Docker 이미지를 가져와서 설치했는데, 이건 찾아보니까 linux/amd64 환경은 지원하지 않는다.

EC2의 환경을 linux/arm64 로 변경하여 RabbitMQ를 직접 설치할 수 있었다.

💡 Mac M3

필자의 노트북은 맥북 M3칩을 사용하는데, CPU를 M1/M2/M3로 사용하는 환경에서는 도커 이미지를 빌드할 때, 무조건 플랫폼을 명시해줘야 한다.

맥에서 빌드하는 이미지를 빌드하는 환경과 AWS Cloud 인스턴스 환경이 맞지 않는 경우가 대부분이기 때문이다.

따라서, CI/CD를 작성할 때 platform을 명시해주자..!

◉ 커맨드
docker build --platform linux/amd64 -t mini2/api-gateway .


OUTRO

이번 프로젝트는 AWS, Jenkins 등을 활용한 CI/CD 파이프라인 구축부터 클라우드 아키텍처 설계, 배포까지 전적으로 맡아서 진행한 값진 경험이었다.

처음 해보는 것들이 많아 수많은 삽질과 밤샘을 했지만, 그만큼 배움의 깊이도 남달랐다 진짜로..

특히 마지막에 제대로 API Call이 되는 순간은 너무 감격스러웠다.

비록 과정은 고통스러웠지만, 문제를 하나씩 해결해 나가는 과정에서 MSA 아키텍처와 클라우드 환경에 대한 이해도가 크게 높아졌다.

짧은 기간 동안 정말 많은 기술을 경험하고 성장할 수 있어서 힘들었지만 그만큼 보람찼던 프로젝트였다.

이 경험이 앞으로의 개발에서 큰 자산이 될 것이라 확신한다 👊

프로젝트 Github

📖 참고

트러블 슈팅 (feat. Eureka AWS 배포하기)
[Jenkins CI/CD] 4. WebHook을 이용한 자동 배포
[CI/CD] Jenkins에서 특정 브랜치에 대한 GitHub Webhook만 처리

profile
코린이

0개의 댓글