Spring Cloud 프로젝트에서는 클라우드 기반 분산 시스템에서 필요로하는 일반적은 패턴들을 빠르게 구축할 수 있도록 도구를 제공해준다. 대표적으로는 다음의 기능을 제공한다. (언급된 것 외에도 여러가지 기능을 제공한다.)
- 구성파일 관리 : Spring Cloud Config
- 서비스 디스커버리: Spring Cloud Zookeeper
- 써킷 브레이커: Spring Cloud Circuit Breaker
- 지능형 라우팅: Spring Cloud Gateway
마이크로서비스 애플리케이션이 클라우드 네이티브하게 구성되려면 12 Factor 를 지켜줘야 하는데, 여기서 환경별로 달라질 애플리케이션의 구성파일을 Spring Cloud Config Server
로 관리하며, 스케일 아웃이 자유로운 마이크로서비스 환경이라면 서비스 디스커버리는 필수기 때문에 Spring Cloud Zookeeper
를 사용하면 Zookeeper를 서비스 레지스트리로 쉽게 사용할 수 있다.
Spring Cloud Kubernetes란?
앞서 말했듯이 마이크로서비스 환경을 구성하려면 애플리케이션 구성파일을 외부에서 관리해야 하며, 서비스 디스커버리를 위한 서비스 레지스트리는 필수이다. 그런데 만약 쿠버네티스 환경에서 마이크로서비스 환경을 구성한다고 하자. 쿠버네티스에서는 기본적으로 Service
라는 쿠버네티스 리소스를 통해 클러스터 내 서비스 디스커버리 기능을 제공하고 있다. 또한, 쿠버네티스를 할 줄 안다면 쿠버네티스에서는 ConfigMap
으로 애플리케이션 구성파일을 관리하는 것으로 알고 있을 것이다.
그런데 쿠버네티스 환경에서 Spring Cloud를 사용하여 마이크로서비스 환경을 구축하기 위해 Zookeeper와 Config Server를 도입하는 것은 불필요하다고 생각할 수 있다. 쿠버네티스에서 기본적으로 제공해주는 Service, ConfigMap을 활용하여 Spring Cloud를 사용하는 것이 좋은데, 이를 가능하게 하는 것이 Spring Cloud Kubernetes이다.
주요 기능
Spring Cloud Kubernetes의 주요 기능은 다음과 같다.
- 부트 시점에서 ConfigMap / Secret에 저장된 application.yaml 로딩 (PropertySource)
- 쿠버네티스 Service를 활용한 서비스 디스커버리 & 클라이언트 사이드 로드밸런싱
Dependency 설정
gradle 기준으로 디펜던시 설정은 다음과 같이 하면 된다.
plugins {
id("io.spring.dependency-management") version "1.1.0"
}
val springCloudVersion = "2022.0.3" # Spring Boot Version에 따라 다름.
dependencyManagement {
imports {
mavenBom("org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}")
}
}
dependenties {
// spring-cloud
implementation("org.springframework.cloud:spring-cloud-starter-bootstrap")
// 2020.0.x 이상
implementation("org.springframework.cloud:spring-cloud-starter-kubernetes-client-all")
// Hoxton 이하
implementation("org.springframework.cloud:spring-cloud-starter-kubernetes-all")
}
- spring cloud docs 에 들어가면 부트 버전에 따른 스프링 클라우드 버전 표가 있는데 자신의 부트 버전에 맞는 버전을 사용하면 된다.
- 참고로 Hoxton 이하 버전과 2020.0.x 이상 버전은 사용해야 할 디펜던시가 다르다. (가급적 최신버전을 사용하자.)
PropertySource
PropertySource는 스프링에서 사용하는 프로퍼티를 저장하는 곳으로 생각하면 되는데, Spring Boot를 기준으로 보통 resources 디렉터리에 두는 application.yaml
, application.properties
파일으로 이해하면 된다. 하지만 Spring Cloud에서는 외부 PropertySource 들을 둘 수도 있다.
Spring Cloud Kuberenetes를 사용하면 application.yaml
파일을 ConfigMap에 저장한 후 쿠버네티스를 외부 PropertySource로 두어서 부트가 뜰 때 ConfigMap에 저장된 프로퍼티를 사용할 수 있다.
일단 실습에 앞서 ConfigMap이 프로퍼티에 반영하는지 확인하기 위해 spring-boot-actuator
디펜던시도 추가하자.
dependenties {
implementation("org.springframework.boot:spring-boot-starter-actuator")
}
bootstrap.yaml
일단 resources 디렉터리 하위에 bootstrap.yaml을 만들어 다음과 같이 설정하자.
server:
port: 9000
spring:
application:
name: todo-api
cloud:
kubernetes:
config:
enabled: true
sources:
- name: todo-api
secrets:
enable-api: true
sources:
- name: todo-api
management:
endpoint:
configprops:
show-values: always
endpoints:
web:
exposure:
include: health, configprops
spring.cloud.kubernetes.config.enabled
가 true인 경우, 쿠버네티스를 외부 PropertySource로 사용한다.- 외부 propertySource는 ConfigMap, Secret 모두 사용 가능하며 여러 프로퍼티를 동시에 사용할 수도 있다. 여기서는 애플리케이션과 같은 네임스페이스에 있는
todo-api
ConfigMap과todo-api
Secret에 있는 프로퍼티를 가져온다. management
에서는 액츄에이터 설정인데/actuator/configprops
로 요청하면 애플리케이션에 설정된 프로퍼티들을 모두 볼 수 있다.
ConfigMap & Secret
그 후 애플리케이션 프로퍼티를 저장하기 위해 ConfigMap과 Secret을 생성하자. ConfigMap은 일반적인 데이터를 담는 용도로 사용하며, Secret은 비밀번호 등 민감한 정보를 저장하기 위해 사용한다. 예시로, spring-boot-jpa
를 사용하는 애플리케이션에서 데이터소스를 ConfigMap으로 저장할 수 있다. ConfigMap, Secret을 생성할 때 bootstrap.yaml
에서 구성한 이름으로 ConfigMap과 Secret을 생성하자. 네임스페이스도 애플리케이션과 같은 네임스페이스로 생성하자.
apiVersion: v1
kind: ConfigMap
metadata:
name: todo-api
namespace: todo
data:
application.yaml: |
spring:
datasource:
url: jdbc:mysql://mysql.middleware:3306/todolist?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8&serverTimezone=UTC
username: todouser
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
show-sql: true
generate-ddl: false
database: mysql
database-platform: org.hibernate.dialect.MySQLDialect
hibernate:
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
apiVersion: v1
kind: Secret
metadata:
name: todo-api
namespace: todo
stringData:
application.yaml: |
spring:
datasource:
password: toboBeer1!
배포
ConfigMap, Secret을 모두 생성 후에 Spring 애플리케이션을 쿠버네티스에 배포해보자. 그런데 배포 전에 필요한 것이 있다. 애플리케이션 내에서 ConfigMap, Secret을 조회할 수 있어야 정상 작동하기 때문이다. 그러려면 애플리케이션에 권한을 줘야 하는데 쿠버네티스에서는 RBAC로 권한을 부여할 수 있다. 여기서 필요한 것은 파드 단위로 사용할 수 있는 계정인 ServiceAccount와 권한을 설정하는 Role(ClusterRole), 계정에 Role을 부여하는 RoleBinding(ClusterRoleBinding)이 있다.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: todo
name: spring-boot-reader
rules:
- apiGroups: [""]
resources: ["pods", "configmaps", "secrets", "services", "endpoints"]
verbs: ["get", "watch", "list"]
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: spring-boot-role
namespace: todo
subjects:
- kind: ServiceAccount
name: todo
apiGroup: ""
roleRef:
kind: Role
name: spring-boot-reader
apiGroup: rbac.authorization.k8s.io
apiVersion: v1
kind: ServiceAccount
metadata:
name: todo
namespace: todo
ServiceAccount, Role, RoleBinding을 생성했다면 Deployment에서 파드에 사용할 ServiceAccount를 부여하자. 예를 들면 다음과 같다.
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: api
tier: backend
name: api
namespace: todo
spec:
replicas: 1
selector:
matchLabels:
app: api
tier: backend
template:
metadata:
labels:
app: api
tier: backend
spec:
serviceAccount: todo # 추가!
containers:
- name: api
image: beer1/todo-server-kotlin:0.1.0
imagePullPolicy: Always
env:
- name: SPRING_PROFILES_ACTIVE
value: kubernetes
resources:
requests:
cpu: 300m
memory: 500Mi
limits:
cpu: 300m
memory: 500Mi
ports:
- name: http
protocol: TCP
containerPort: 9000
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 9000
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 9000
initialDelaySeconds: 120
periodSeconds: 10
배포 후 로그를 보면 ConfigMap과 Secret을 읽었다는 것을 확인할 수 있다. (ConfigMapPropertySourceLocator
, SecretPropertySourceLocator
)
$ kubectl logs api-5c745d7684-lpnd2
2023-09-23T06:05:44.952Z INFO 1 --- [ restartedMain] .e.DevToolsPropertyDefaultsPostProcessor : Devtools property defaults active! Set 'spring.devtools.add-properties' to 'false' to disable
2023-09-23T06:05:49.450Z INFO 1 --- [ restartedMain] o.s.c.k.client.KubernetesClientUtils : Created API client in the cluster.
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.1.3)
2023-09-23T06:05:54.740Z WARN 1 --- [ restartedMain] s.c.k.c.c.ConfigMapPropertySourceLocator : path support is deprecated and will be removed in a future release. Please use spring.config.import
2023-09-23T06:05:54.742Z INFO 1 --- [ restartedMain] b.c.PropertySourceBootstrapConfiguration : Located property source: [BootstrapPropertySource {name='bootstrapProperties-configmap.todo-api.todo'}]
2023-09-23T06:05:54.746Z WARN 1 --- [ restartedMain] o.s.c.k.c.c.SecretsPropertySourceLocator : path support is deprecated and will be removed in a future release. Please use spring.config.import
2023-09-23T06:05:54.948Z INFO 1 --- [ restartedMain] b.c.PropertySourceBootstrapConfiguration : Located property source: [BootstrapPropertySource {name='bootstrapProperties-secret.todo-api.todo'}]
2023-09-23T06:05:55.056Z INFO 1 --- [ restartedMain] c.tutorial.todo.TodoListApplicationKt : The following 1 profile is active: "kubernetes"
2023-09-23T06:06:09.156Z INFO 1 --- [ restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-09-23T06:06:10.343Z INFO 1 --- [ restartedMain] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 1077 ms. Found 1 JPA repository interfaces.
...
적용된 프로퍼티 확인
적용된 프로퍼티를 확인하는 방법은 일단 DB가 잘 연결되어있는지 확인하는 등의 방식이 있겠지만 Actuator를 활용하는 방법도 있다. 위에서 management를 설정했기 때문에 애플리케이션에는 /actuator/configprops
엔드포인트가 열려있을 것이다. kubectl에서 제공하는 포트포워드 기능을 활용하여 파드로 직접 /actuator/configprops
요청을 해보자.
먼저 포트포워드 설정을 한다.
# 왼쪽 포트: 호스트 포트, 오른쪽 포트: 파드 컨테이너 포트
$ kubectl port-forward pods/api-5c745d7684-lpnd2 9000:9000
Forwarding from 127.0.0.1:9000 -> 9000
Forwarding from [::1]:9000 -> 9000
그 다음 새 터미털 창을 켜서 /actuator/configprops
요청을 하자. 응답이 JSON 형식으로 출력되기 때문에 jq를 사용하여 예쁘게 출력해보자. 없으면 설치하자.
$ brew install jq
$ curl http://localhost:9000/actuator/configprops | jq . | grep DataSourceProperties -C 40
...
"spring.datasource-org.springframework.boot.autoconfigure.jdbc.DataSourceProperties": {
"prefix": "spring.datasource",
"properties": {
"password": "toboBeer1!",
"embeddedDatabaseConnection": "H2",
"driverClassName": "com.mysql.cj.jdbc.Driver",
"generateUniqueName": true,
"xa": {
"properties": {}
},
"url": "jdbc:mysql://mysql.middleware:3306/todolist?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8&serverTimezone=UTC",
"username": "todouser"
},
"inputs": {
"password": {
"value": "toboBeer1!",
"origin": "\"spring.datasource.password\" from property source \"bootstrapProperties-secret.todo-api.todo\""
},
"embeddedDatabaseConnection": {},
"driverClassName": {
"value": "com.mysql.cj.jdbc.Driver",
"origin": "\"spring.datasource.driver-class-name\" from property source \"bootstrapProperties-configmap.todo-api.todo\""
},
"generateUniqueName": {},
"xa": {
"properties": {}
},
"url": {
"value": "jdbc:mysql://mysql.middleware:3306/todolist?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8&serverTimezone=UTC",
"origin": "\"spring.datasource.url\" from property source \"bootstrapProperties-configmap.todo-api.todo\""
},
"username": {
"value": "todouser",
"origin": "\"spring.datasource.username\" from property source \"bootstrapProperties-configmap.todo-api.todo\""
}
}
},
...
데이터소스 프로퍼티가 ConfigMap과 Secret에 잘 찍히는 것을 확인하였다.
댓글