1. 배경
기존 진행하던 쿠버네티스 프로젝트가 마무리되고, 신규 프로젝트를 맡게 되면서 어떤 언어와 프레임워크를 이용해서 개발을 진행할지 고민을 했습니다. 본인은 Python을 가장 많이 쓰고 익숙하지만, 프로젝트가 완료 됐을 때 내부 회사에서만 사용하는 것이 아닌, 외부에 PaaS(K8S, 컨테이너) 형태로 배포할 수 있기 때문에, 코드를 볼 수 있는 인터프리터 언어라는 점 때문에 배제되었습니다. 그렇기 때문에 컴파일 언어를 사용해야 한다는 점과, 향후 MSA 구조를 도입할 예정이었기 때문에 이를 고려하여 언어 후보를 Golang과 Spring Cloud로 결정하였습니다.
본인은 Kubernets를 주로 해왔고, 계속 하고 싶은 맘이 있었기 때문에 이전부터 Golang에 대해 공부하고 싶은 마음이 있었습니다. (Kubernetes도 Golang으로 개발되었고, 관련된 많은 설루션이 Golang으로 개발된다). 하지만 프로젝트의 기간이 많지 않아 시간이 촉박하여, 커뮤니티와 레퍼런스가 잘 구성되어 있는 부분을 중요하게 생각하였고, (생각 보다 Spring Cloud는 참조할 수 있는 문서가 많지 않다) 또한 일반 개발자들의 경우 대부분이 Golang보다 Spring에 익숙하기 때문에 향후 새롭게 프로젝트에 참여하게 되는 팀원에 대한 부담을 줄이고자 Spring Cloud로 선택하게 되었습니다.
본인은 Spring 기반의 프로젝트 또한 처음이기 때문에 진행하며 공부한 내용, Spring Cloud 구조부터 CI/CD 등 기록하고 공유하려고 합니다.
1.1 Spring Cloud?
Spring Cloud는 Spring과 무엇이 다를까? 라는 생각이 들 수 있습니다. 일반적으로 Spring boot를 이용하여 애플리케이션을 개발할 때, Spring MVC 패턴을 이용하고, Hibernate와 같은 JPA를 이용하여 데이터의 통신이 이루어지도록 개발을 합니다. 하지만 개발의 규모가 커지고, 코드가 복잡해지면 개발과 유지보수에 불편함을 겪게 됩니다. 또한 특정 기능 오류로 인한 전체 애플리케이션 영향, 리소스의 효율적 관리 한계 등 이러한 이유 때문에 최근 들어 기능 별로 애플리케이션을 분리하여 독립성을 높이고, 모듈 간의 의존성을 줄여 유지보수의 편의성을 높이는 MSA(MicroServiceArchitecture) 구조가 주목받게 됩니다.
Spring Cloud의 MSA 구조
그러면 Spring Boot의 Application을 MSA 구조로 변환하려고 하고자 할 때 간단하게 다음과 같은 의문이 생길 수 있습니다.
- 각 기능별로 Application을 만들면 많은 Config나 Property는 어떻게 관리하지?(DB정보,ENV 등)
- 각 모듈 간의 통신이 필요한데 분리되면 Function이나 Class Import가 안되는데 어떻게 사용하지?
- 어플리케이션이 분리되면 Client가 각 모듈을 어떻게 찾아가지?
- Spring Cloud는 이러한 MSA 구조의 애플리케이션을 개발하기 위하여 필요한 기능을 제공하는 프레임워크입니다. 이를 이용하여 보다 편리하게 Spring 기반의 MSA 구조를 개발할 수 있습니다.
1.2 Spring Cloud On Kubernetes?
MSA 구조의 어플리케이션을 개발했다면 배포를 어디에, 어떻게 할까 생각해 봐야 됩니다. 일반적으로 MSA 애플리케이션의 경우 장점을 완전히 활용하기 위해서 대부분은 Kubernetes에 배포합니다. 이는 컨테이너의 특징과 쿠버네티스의 기능이 MSA 구조와 잘 맞기 때문인데, 예를 들어 특정 모듈의 부하가 높아져 리소스가 더 필요할 때, MSA가 아닌 Monolothic의 경우 전체 애플리케이션(또는 서버)을 Scale Up/Out 해야 되지만, MSA 구조의 경우 부하가 생긴 애플리케이션(컨테이너)만 Scale Up을 하기 때문에 리소스 관리 측면에서 효율적이고, 컨테이너의 경량화된 특성 때문에 속도면에서 이점이 있습니다(이는 무중단 HA 구성에 유리합니다). 이러한 이유 때문에 Spring Cloud로 개발된 애플리케이션 또한 Kubernetes에 배포하고자 하였고, Spring Cloud의 완전한 기능을 Kubernetes에서 사용하기 위한 프레임워크입니다.
2. 개발/배포 환경 구성도
언어와 프레임워크를 정하고 본격적으로 개발하기에 앞서 개발 환경을 구축합니다. 사실 배포 환경은 Kubernetes로 정했기 때문에 간단합니다. (개발 후 Helm 차트를 만들어 Kubernetes에 배포한다.) 이를 바탕으로 개발 환경 또한 Minikube와 같은 단일 클러스터를 만들고, 컨테이너에서 개발하고 CI/CD를 구축하여 배포하는 식으로 개발 환경을 구성하려고 했습니다. 하지만 해당 구조의 경우 개발자들이 쿠버네티스에 대한 배경지식이 있어야 되서 한계가 있었고, 이러한 이유로 개발 환경은 로컬에서 Spring Cloud를 이용하여 개발하는 구조로 변경하였습니다.
전체적인 개발 환경 구조는 다음과 같습니다. 각각의 모듈(MicroService)을 다른 포트로 실행시킵니다. 이를 Netflx Eureka에 등록합니다. Eureka는 Service Discover의 역할로 이를 통해 각 모듈은 Application Name으로 통신할 수 있습니다. Client(FrontEnd)는 SpringCloudGateway(이하 SCG)를 통해 Backend에 접근하며, URL 기반으로 SCG에 설정된 룰을 기반으로 분기되어 EndPoint에 도달하게 됩니다. 그 외에 설정을 관리하기 위한 Config Server
나, MicroService의 로그를 관리하기 위한 Distributed tracing
을 추가할 수 있습니다.
개발 환경 구조
배포환경의 경우 다음과 같습니다. 개발 환경과 동일한 구조에서 모두 Kubernetes의 리소스로 변환합니다. MicroService는 각각의 Deployment로, ConfigServer의 설정 정보는 ConfigMap이나 Secret으로 변환합니다. SCG의 경우 SpringCloud On Kubernetes에서 쿠버네티스의 설치할 수 있도록 CRD를 제공하기 때문에 이를 이용하여 구성합니다. 최종적으로 SCG에 Ingress룰만 설정하여 도메인을 통해 Client가 접근할 수 있도록 설정하였습니다. 현재 구조에서 개발 환경과 크게 다른 점은 Eureka의 설치 유무입니다. 이는 Kubernets에서 자체적으로 ServiceDiscovery기능을 제공하기 때문인데, Service 명 만으로 엔드포인트를 찾을 수 있습니다. 예를 들어 auth라는 Deployment와 이를 연결하는 auth라는 Service를 만들게 되면 auth.[namespace].svc.cluster.local:[port]
와 같이 DNS를 통해 찾아갈 수 있고, 같은 네임스페이스에 있는 애플리케이션들은 auth:[port]
로 통신할 수 있습니다. 그렇기 때문에 로컬 환경과 다르게 Eureka가 필요하지 않습니다.
하지만 쿠버네티스 서비스 디스커버리 기능을 이용하여 구성할 경우 코드레벨에서 쿠버네티스 관련 코드가 작성되게 됩니다. 이는 개발자가 쿠버네티스 관련 코드를 작성하는 것을 지양하는 과정에서 적합하지 않습니다. 이는 향후 Spring Cloud 서비스 디스커버리를 도입하여 변경합니다.
3. 로컬 개발환경 구성
위의 구성도를 바탕으로 샘플코드를 만들어 실행합니다. 샘플 코드의 경우 Employee, Department 이름의 두 개의 서비스와, Eureka, SCG(Spring Cloud Gateway)로 이루어집니다. 전체 프로젝트 코드 구조는 다음과 같습니다.
하나의 디렉터리 내부에 모든 모듈을 모으고 관리합니다. postgres
의 경우 docker로 실행시킬 때 데이터를 저장하기 위한 볼륨 파일입니다. 이러한 구조를 Git에 올려 코드를 관리합니다. 현재 테스트 코드의 경우 2개의 모듈이 존재하여 이러한 구조에서도 코드 관리가 어려울 게 없습니다. 실제 개발을 진행하는 경우 모듈이 훨씬 많아져 이러한 구조와 같이 하나의 레포에서 모든 서비스를 관리하는 부분에서 어려움을 겪을 수 있습니다. 다만 모듈마다 레포를 나누는 것 또한 복잡성이 높아지기 때문에 환경에 따라 적합한 관리 방법을 도입해야 합니다.
3-1. Eureka 구성
Eureka의 경우 로컬 개발 환경에서 MS(MicroService) 간 통신을 위해 서비스 디스커버리 역할을 합니다. 이를 위해 Eureka Server 의존성을 추가합니다.
이후 main 클래스에서 EnableEureakServer
어노테이션을 추가하여 유레카 서버로 구동합니다.
이후 실행 후 8761 포트로 접근 시 유레카가 실행된 것을 확인할 수 있습니다.
3-2 MicroService 서비스 구성
MS의 경우 간단하게 CRUD 기능을 지원하는 API를 만듭니다. 현재 테스트 단계에서는 두 개 서비스 모두 코드와 구성이 동일합니다. 초기 Dependency 추가는 다음과 같습니다. Eureka 서버에 등록하기 위한 spring-cloud-starter-netflix-eureka-client
, OpenFeign 사용을 위한 spring-cloud-starter-openfeign
를 추가합니다.
이후 테스트 통신을 위하여 Controller를 만들고 API를 한 개 추가합니다.
이후 설정파일에 Eureka와 Postgres 설정을 추가합니다. 각각의 모듈은 개별적인 포트를 사용하기 때문에 포트의 번호는 다르게 설정해줘야 됩니다. Postgres 접속 정보와 같은 설정은 향후 ConfigServer를 통해 관리하도록 변경할 예정입니다.
#application.yaml
server:
port: 8081
spring:
application:
name: employee
datasource:
driver-class-name: org.postgresql.Driver
url: "jdbc:postgresql://localhost:5432/myapp"
username: postgres
password: my_password
jpa:
show-sql: true
hibernate:
ddl-auto: create
format_sql: true
jdbc:
lob:
non_contextual_creation: true
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
3-3 SCG 구성
Spring Cloud Gateway를 사용하기 위해 Depency에 다음과 같이 추가합니다. SCG를 사용하기 위한 spring-cloud-starter-gateway
이후 Application.yaml에 유레카 서버 정보와 함께 Routes 룰을 설정합니다.
server:
port: 8079
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: employee
predicates:
- Path=/msa/employee/**
uri: lb://employee
filters:
- RewritePath=/msa/employee/(?<segment>.*), /$\{segment}
- id: department
predicates:
- Path=/msa/department/**
uri: lb://department
filters:
- RewritePath=/msa/department/(?<segment>.*), /$\{segment}
Routes 룰의 예시는 다음과 같습니다. /msa/employee
/ 로 들어오는 request는 Application.name에 employee인 모듈로 연결되고, 도착하면 /msa/employee/
경로를 지우고 나머지 경로를 전송합니다.
3-4 로컬 환경 테스트
위의 과정을 통해 샘플 코드를 작성하고, 실행하면 유레카에서 다음과 같이 모든 서비스가 등록되고 UP
상태인 걸 확인할 수 있습니다.
이후 SCG 테스트를 위하여 포스트맨을 통해 샘플 코드로 Request를 전송합니다. Department 서비스로 Request를 전송하는 것이 아닌 SCG를 통해 접근합니다. 위에서 설정한 Routes 룰에 따라 Prefix로 /msa/department
를 추가하고 Controller에서 작성한 /test
경로를 통해 전송하면 다음과 같이 정상적으로 접근이 가능한 것을 확인 할 수 있습니다.
다음과 같이 테스트를 통해 로컬 환경에서의 기본 구성은 완료가 되었습니다.
4. 배포 환경 구성
위의 로컬 환경을 바탕으로 쿠버네티스에 배포하기 위하여 환경을 구성합니다. 현재 테스트 단계에서는 Helm이 아닌 개별적인 리소스를 만드는 과정을 테스트하고 이후에 차트를 생성하여 CI/CD를 구축합니다. 또한 서비스 디스커버리의 경우 쿠버네티스 네이티브 디스커버리를 사용합니다.(향후 테스트 후에 Spring Cloud ServiceDiscovery 도입)
4-1. PostgreSql 설정
DB 구성을 위한 Postgres Statefulset을 쿠버네티스에 설치합니다. 현재 테스트 단계에서 제가 샘플로 만든 Manifest이기 때문에 실제 환경에서는 공식 헬름을 사용하는 것을 권장합니다.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgresql-db
spec:
serviceName: postgresql-db-service
selector:
matchLabels:
app: postgresql-db
replicas: 1
template:
metadata:
labels:
app: postgresql-db
spec:
containers:
- name: postgresql-db
image: postgres:latest
volumeMounts:
- name: postgresql-db-disk
mountPath: /data
env:
- name: POSTGRES_PASSWORD
value: mypassword
- name: PGDATA
value: /data/pgdata
volumeClaimTemplates:
- metadata:
name: postgresql-db-disk
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: nfs-client
resources:
requests:
storage: 10Gi
위의 매니페스트를 기반으로 생성한 Statsfulset을 연결하는 Headless 서비스를 생성합니다.
apiVersion: v1
kind: Service
metadata:
labels:
app: postgresql-db
name: postgresql-db
spec:
ports:
- port: 5432
protocol: TCP
targetPort: 5432
selector:
app: postgresql-db
clusterIP: None
type: ClusterIP
다음과 같이 정상적으로 headless 서비스를 통해 Postgres에 접근할 수 있는지 확인합니다. 옵션 중 -h postgresql-db는
서비스 명으로 dns를 통해 접근합니다. ID/PW는 statefulset 설치 시 env로 설정한 계정 정보입니다.
정상적으로 로그인이 되었다면 이후 생성할 모듈에서도 접근이 가능합니다.
4-2. Department, Employee 생성
각 모듈을 쿠버네티스에 배포하기 위해 로컬과 설정이 다른 부분이 있습니다.
- DB 접속 정보: 쿠버네티스에 설치한 DB의 접속정보로 변경합니다.
- Eureka Client: 쿠버네티스 네이티브 서비스 디스커버리를 사용하기 때문에 Eureka 설정을 하지 않습니다.
- 포트 번호: 각 컨테이너로 독립적으로 실행되기 때문에 관리를 위해 8082 포트로 통일합니다.
#application-k8s.yaml
server:
port: 8082
spring:
application:
name: employee
datasource:
driver-class-name: org.postgresql.Driver
url: "jdbc:postgresql://postgresql-db:5432/myapp"
username: postgres
password: megazone00!
jpa:
show-sql: true
hibernate:
ddl-auto: create
format_sql: true
jdbc:
lob:
non_contextual_creation: true
Spring boot의 프로파일을 분리하여 로컬/배포 환경을 각각 다르게 실행합니다. 이를 바탕으로 도커파일을 만들어 이미지를 생성합니다. 도커파일의 경우 gradle로 빌드된 jar 파일을 복사한 후 실행시키는 정도의 내용입니다.
FROM openjdk:17
ENV APP_FILE employee-0.0.1-SNAPSHOT.jar
ENV APP_HOME /workflow
EXPOSE 8083
COPY build/libs/$APP_FILE $APP_HOME/
WORKDIR $APP_HOME
ENTRYPOINT ["java","-jar","-Dspring.profiles.active=k8s","employee-0.0.1-SNAPSHOT.jar"]
docker build/push
커맨드를 통해 Private Repo에 이미지를 업로드합니다.
해당 이미지를 바탕으로 쿠버네티스 매니페스트를 생성하고 배포합니다.
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: department
name: department
spec:
replicas: 1
selector:
matchLabels:
app: department
strategy: {}
template:
metadata:
labels:
app: department
spec:
containers:
- image: 077728726991.dkr.ecr.us-east-2.amazonaws.com/department:latest
imagePullPolicy: Always
name: department
ports:
- containerPort: 8082
imagePullSecrets:
- name: regcred
---
# Service
apiVersion: v1
kind: Service
metadata:
labels:
app: department
name: department
spec:
ports:
- port: 8082
protocol: TCP
targetPort: 8082
selector:
app: department
type: ClusterIP
전체 코드 구조는 다음과 같이 서비스마다 Dockerfile과 매니페스트를 저장하는 k8s 폴더가 추가되고, 또한 프로파일을 분리하기 위해 설정파일 또한 추가됩니다.
이후 쿠버네티스에 배포한 후 로그를 확인하면 정상적으로 배포되었고 프로파일 또한 k8s로 실행된 것을 확인할 수 있습니다. 동일한 방법으로 employee 또한 배포합니다.
4-3 SCG 설치
Vmware에서 개발한 SCG Operator를 설치하기 위해 다음 공식 문서를 참조합니다. 우선 Kubernetes Installer를 다운합니다. 이후 오퍼레이터 설치를 위한 이미지를 프라이빗 레포에 업로드해야 됩니다. 저는 ECR을 사용하고, upload 하는 script가 docker cli를 사용하기 때문에 docker가 설치되어 있어야 됩니다.
# docker login to ecr
aws ecr get-login-password --region us-east-2 |
docker login --username AWS --password-stdin 077728726991.dkr.ecr.us-east-2.amazonaws.com
이후 이미지가 업로드될 repo 주소화 함께 Script를 실행합니다.
스크립트가 정상적으로 실행되면 다음 3개의 이미지가 업로드되어야 됩니다. gateway
, scg-operator
, spring-gateway
이후 Kubernetes에서 ECR 레포에 접근하기 위한 Secret을 생성합니다.
$ kubectl create secret docker-registry spring-cloud-gateway-image-pull-secret -n ${installation_namespace} \
--docker-server=${registry} \
--docker-username=${username} \
--docker-password=${password}
secret/spring-cloud-gateway-image-pull-secret created
이후 helm/scg-image-values.yaml
파일을 생성하여 image가 저장된 repo와 secret을 입력합니다.
scg-operator:
image: "077728726991.dkr.ecr.us-east-2.amazonaws.com/scg-operator:2.1.0"
registryCredentialsSecret: regcred
gateway:
image: "077728726991.dkr.ecr.us-east-2.amazonaws.com/gateway:2.1.0"
registryCredentialsSecret: regcred
생성한 values
파일을 바탕으로 스크립트를 실행합니다.
./scripts/install-spring-cloud-gateway.sh --namespace my_namespace_name
정상적으로 생성되었다면 다음과 같이 scg-operator가 설치된 것을 확인할 수 있습니다.
오퍼레이터가 설치되면 다음과 같이 3개의 CRD가 설치되며 각각의 역할은 다음과 같습니다.
- SpringCloudGateway : 독립적인 Gateway로 CRD에 따라 Statefulset이 생성됩니다.
- SpringCloudGatewayRouteConfigs: 게이트웨이의 라우트룰로 Application.yaml에서 구성한 컨피그를 설정합니다.
- SpringCloudGateWayRouteConfigs : 위의 두 개의 CRD를 연결합니다.
로컬 환경에서 구성한 Gateway를 쿠버네티스에 설치하기 위하여 CRD를 생성합니다.
SCG 설치가 완료되면 위에서 배포한 Employee, Department를 SCG를 통해서 통신할 수 있도록 CRD를 생성해 줘야 됩니다.
apiVersion: "tanzu.vmware.com/v1"
kind: SpringCloudGateway
metadata:
name: scg-example-gateway
---
apiVersion: "tanzu.vmware.com/v1"
kind: SpringCloudGatewayRouteConfig
metadata:
name: scg-example-gateway-routes
spec:
service:
name: department
port: 8082
routes:
- predicates:
- Path=/msa/department/**
filters:
- StripPrefix=2
---
apiVersion: "tanzu.vmware.com/v1"
kind: SpringCloudGatewayMapping
metadata:
name: scg-example-gateway-mapping
spec:
gatewayRef:
name: scg-example-gateway
routeConfigRef:
name: scg-example-gateway-routes
다음과 같이 위의 매니페스트를 기반으로 게이트웨이, 라우팅룰, 매핑을 생성합니다. 이후 정상적으로 생성이 되면, 다음과 같이 스프링 게이트웨이를 통해 연결되는 것을 확인할 수 있습니다.
저는 게이트웨이를 Ingress와 연결하여 도메인을 통해 접근할 수 있도록 설정하였습니다.
이를 통해 Spring Cloud On Kubernetes 프레임워크를 통해 간단한 애플리케이션을 배포하는 과정을 테스트하였습니다. 향후에는 CI/CD와 모니터링 및 로깅 등 고도화 작업에 대해 테스트한 후 정리해 포스팅하겠습니다.
'Spring Cloud' 카테고리의 다른 글
Spring Cloud를 통한 MSA 도입기 #2 - CI/CD (1) | 2023.10.13 |
---|