AWS EKS에 로그 트레이싱 구축(2)-kubernetes에 Jaeger 설치
지난 글에서 AWS EKS로 구축된 쿠버네티스 클러스터에 EFK 스택을 구성하였다.
이번 글에서는 AWS EKS로 구축된 쿠버네티스 클러스터에 Jaeger를 설치하여 분산 환경 로그 트레이싱을 구축해보도록 하겠다.
Jaeger는 EC2 인스턴스에 별도로 구성하는것이 아닌, 쿠버네티스 클러스터 내에 구성할 것이다.
이 글의 실습을 진행하기 전에 다음과 같은 실습환경이 준비되어 있어야 한다.
-º AWS EKS 구축(AWS EKS를 활용한 쿠버네티스 클러스터 구축 참고)
- AWS EKS에 스프링부트 서비스 배포(AWS EKS에서 CodePipeline을 활용하여 스프링부트 서비스 배포하기 참고)
- AWS EKS에 EFK 스택 구성(AWS EKS에 로그 트레이싱 환경 구축하기(1) - EFK 스택 구성하기 참고)
또한 아래 글은 Jaeger에 대한 이론적인 내용만 참고하도록 하자.
- Jaeger란 무엇인가?(Jaeger를 활용한 분산 환경 서비스 로그 트레이싱 참고)
이 글의 진행 순서는 다음과 같이 진행된다.
1. 분산환경 로그 트레이싱 구조
2. 스프링부트 서비스 수정하기
3. 쿠버네티스 환경에 Jaeger 설치하기
4. 테스트
1. 분산환경 로그 트레이싱 구조
지난번에 이어 이번 글에서 구축하는 로그 트레이싱의 구조를 보면 다음과 같다(한번 더 설명하겠다).
우선 지난번 글에서 실습한 내용은 AWS EKS를 구축한 후, 프라이빗 서브넷에는 EC2 인스턴스로 구성된 워커 노드 그룹을 생성했다. 그리고 각각의 EC2 인스턴스에 Fluentd를 데몬셋 형태로 띄워서 로그 정보를 수집해서 Elasticsearch로 전송하게 되면 사용자는 Kibana UI를 통해서 수집된 로그 정보를 확인했다. Kibana는 로드밸런서 형태로 퍼블릭 서브넷에 띄워서 외부에서 접속 가능하도록 하였다.(AWS EKS에서 로그 트레이싱 구축하기(1) - EFK 스택 구성하기 참고)
이번 글에서는 Jaeger는 Jaeger Collector와 Jaeger Query를 Pod로 띄운 후, 스프링부트 서비스에 Jaeger Agent를 Sidecar 패턴으로 주입시킬 것이다. 그 후 Jaeger Agent가 스프링부트 서비스의 로그 트레이싱 정보를 Jaeger Collector로 보내면 Jaeger Collector는 트레이싱 정보를 저장소(Elasticsearch)로 보낼 것이다. 그리고 사용자가 Jaeger Web UI로 트레이싱 정보를 확인할 때 Jaeger Query가 호출되어 저장소(Elasticsearch)에 저장된 트레이싱 정보를 Jaeger Web UI에 보내줄 것이다.
2. 스프링부트 서비스 수정하기
Jaeger를 설치하기 전에 스프링부트 서비스를 수정하자.
스프링부트 서비스에서 수정할 부분은 디플로이먼트 매니페스트 파일, build.gradle, application.yaml파일이다.
우선 첫번째로 디플로이먼트 매니페스트 파일을 수정해보자.
수정할 내용은 metadata 밑에 annotations를 추가하여 "sidecar.jaegertracing.io/inject": "true"를 추가하는 내용이다.
해당 문구를 추가하는 이유는, 디플로이먼트로 배포되는 스프링부트 서비스 Pod에 Jaeger Agent를 Sidecar 패턴으로 주입시킬 것이기 때문이다. 이렇게 하면 스프링부트에서 로그 트레이싱 정보를 Jaeger Agent로 전송할 때, 호스트가 같기 때문에 localhost로 전송해도 전달이 된다.
annotations:
"sidecar.jaegertracing.io/inject": "true"
deployment.yaml :
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloud-l3-project-deployment
annotations:
"sidecar.jaegertracing.io/inject": "true"
spec:
replicas: 1
selector:
matchLabels:
app: cloud-l3-project
template:
metadata:
labels:
app: cloud-l3-project
spec:
containers:
- name: cloud-l3-project
image: AWS_ECR_URI #buildspec.yml에서 ECR의 URI:TAG 로 치환해줌
ports:
- containerPort: 8080
imagePullPolicy: Always
env:
- name: DATE
value: 'DATE_STRING'
다음으로 build.gradle 파일을 수정하자.
build.gradle은 dependencies에 opentracing-spring-jaeger-web-starter 를 추가해 주면 된다.
opentracing-spring-jaeger-web-starter를 추가해줘야, 스프링부트 서비스에서 Jaeger Agent로 로그 트레이싱 정보를 보내줄 수 있다.
build.gradle :
dependencies {
...
compile group: 'io.opentracing.contrib', name: 'opentracing-spring-jaeger-web-starter', version: '3.1.2' // jaeger
...
}
다음으로 application.yml 파일을 수정하자.
application.yml에는 opentracing.jaeger 밑에 jaeger ui 화면에서 확인할 서비스 명을 임의로 넣고, udp-sender에는 로그 트레이싱 정보를 보내줄 jaeger agent 주소를 입력한다. 그런데 아까 위에서 jaeger agent는 Sidecar 패턴을 사용해서 스프링부트 서비스 Pod에 주입될 것이기 때문에 host는 localhost로 입력하고 port정보는 Jaeger Agent의 udp port인 6831을 입력한다.
application.yml :
server:
port: 8080
spring:
application:
name: cafe-service
opentracing:
jaeger:
service-name: cafe-svc # jaeger service name
udp-sender:
host: localhost #simplest-agent.observability.svc.cluster.local # jaeger server ip
port: 6831 # jaeger udp port
3. 쿠버네티스 환경에 Jaeger 설치하기
다음으로 쿠버네티스 환경에 Jaeger를 설치해보자.
Jaeger 설치는 https://www.jaegertracing.io/docs/1.18/operator/ 글을 참고해서 진행하였다.
우선 Jaeger설치를 도와줄 jaeger-operator를 위한 리소스정보가 담긴 매니페스트 파일을 쿠버네티스 클러스터에 적용시킨다.
$ kubectl create namespace observability
$ kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/crds/jaegertracing.io_jaegers_crd.yaml
$ kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/service_account.yaml
$ kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/role.yaml
$ kubectl create -n observability -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/role_binding.yaml
위 내용을 간단하게 설명하자면, observability라는 네임스페이스를 만든 후, jaeger-operator를 위한 ServiceAccount를 만든 후, jaeger-operator에게 필요한 role을 생성한 다음 role-binding으로 role을 부여했다.
다음으로 jaeger-operator의 메니페스트 파일을 다운받는다.
$ curl -LJ https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/operator.yaml >> operator.yaml
그리고 해당 파일을 수정하는데, "-name : WATCH_NAMESPACE" 밑에 있는 valueFrom 정보를 모두 삭제해준다.
해당 내용을 삭제해줘야 스프링부트 서비스 디플로이먼트로 배포된 pod에 Jaeger Agent가 Sidecar 패턴으로 정상적으로 주입된다.
operator.yaml :
apiVersion: apps/v1
kind: Deployment
metadata:
name: jaeger-operator
spec:
replicas: 1
selector:
matchLabels:
name: jaeger-operator
template:
metadata:
labels:
name: jaeger-operator
spec:
serviceAccountName: jaeger-operator
containers:
- name: jaeger-operator
image: jaegertracing/jaeger-operator:1.18.1
ports:
- containerPort: 8383
name: http-metrics
- containerPort: 8686
name: cr-metrics
args: ["start"]
imagePullPolicy: Always
env:
- name: WATCH_NAMESPACE
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: OPERATOR_NAME
value: "jaeger-operator"
수정이 완료되었으면 operator.yaml을 적용하고 정상적으로 리소스가 생성되는지 확인해보자.
$ kubectl apply -f operator.yaml -n observability
$ kubectl get pod --all-namespaces
NAMESPACE NAME READY STATUS RESTARTS AGE
default cloud-l3-project-deployment-b5d4b5b9d-flst2 1/1 Running 0 8s
kube-logging es-cluster-0 1/1 Running 0 4h16m
kube-logging es-cluster-1 1/1 Running 0 4h16m
kube-logging es-cluster-2 1/1 Running 0 4h15m
kube-logging fluentd-82td4 1/1 Running 0 3h52m
kube-logging fluentd-bwcg6 1/1 Running 0 3h52m
kube-logging kibana-866c457776-d5mdh 1/1 Running 0 90m
kube-system aws-node-9r5q5 1/1 Running 0 7h43m
kube-system aws-node-jk7jh 1/1 Running 0 7h43m
kube-system coredns-7dd7f84d9-8q86c 1/1 Running 0 8h
kube-system coredns-7dd7f84d9-ptd6t 1/1 Running 0 8h
kube-system kube-proxy-rcnrj 1/1 Running 0 7h43m
kube-system kube-proxy-vn6v4 1/1 Running 0 7h43m
observability jaeger-operator-c886b9cfb-djqhh 1/1 Running 0 3m35s
다음으로 jaeger 매니페스트 파일을 작성해보자.
jaeger.yaml :
apiVersion: jaegertracing.io/v1
kind: Jaeger
metadata:
name: jaeger
spec:
strategy: production
collector:
maxReplicas: 5
resources:
limits:
cpu: 100m
memory: 128Mi
storage:
type: elasticsearch
options:
es:
server-urls: http://elasticsearch.kube-logging.svc:9200
위 내용을 간단히 설명하자면, resources에는 Jaeger가 사용할 리소스 정보를 작성하고, storage에는 elasticsearch를 입력하고, 이전 글(AWS EKS에서 로그 트레이싱 구축하기(1) - EFK 스택 구성하기 참고)에서 생성했던 elasticsearch url 정보를 작성했다(http://[서비스명].[네임스페이스명].svc:[포트번호]).
위와 같이 작성하면 Jaeger Agent에서 Jaeger Collector로 로그 트레이싱 정보를 보내게 되면, Jaeger Collector는 Elasticsearch에 트레이싱 정보를 저장하게 된다. 그리고 Jaeger UI를 통해 트레이싱 정보를 조회하면, Jaeger Query가 Elasticsearch에서 정보를 조회해서 UI로 트레이싱 정보를 노출시킨다.
작성이 완료되었으면 해당 파일을 쿠버네티스 클러스터에 적용시킨다.
$ kubectl apply -f jaeger.yaml -n observability
그리고 다음엔 cluster-role과 cluster-role-binding을 적용시키자.
왜냐하면 스프링부트 서비스의 네임스페이스가 jaeger의 네임스페이스와 다르기 때문에 jaeger operator에서 적용했던 role과 role-binding은 쓸모가 없고(role은 동일한 네임스페이스인 경우만 유효하다), 클러스터 전체에 적용하는 cluster-role과 cluster-role-binding을 해주지 않으면, sidecar패턴이 정상적으로 주입되지 않는다(물론 순서도 중요하다. jaeger까지 모두 적용된 후 cluster-role과 cluster-role-binding을 적용해줘야 한다)
바로 cluster-role과 cluster-role-binding을 적용해주자.
$ kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/cluster_role.yaml
$ kubectl create -f https://raw.githubusercontent.com/jaegertracing/jaeger-operator/master/deploy/cluster_role_binding.yaml
이제 jaeger-agent가 sidecar패턴으로 잘 적용되었는지 확인해보자.
jaeger-collector와 jaeger-operator가 정상적으로 생성되었고, application pod(이 글에서는 cloud-l3-project-deploymet...)를 확인해보면 jaeger-agent 가 적용되어 갯수가 늘어난 것을 확인할 수 있다.
$ kubectl get pod --all-namespaces
NAMESPACE NAME READY STATUS RESTARTS AGE
default cloud-l3-project-deployment-664cf9944-w7x5t 2/2 Running 0 57s
kube-logging es-cluster-0 1/1 Running 0 4h48m
kube-logging es-cluster-1 1/1 Running 0 4h47m
kube-logging es-cluster-2 1/1 Running 0 4h47m
kube-logging fluentd-82td4 1/1 Running 0 4h23m
kube-logging fluentd-bwcg6 1/1 Running 0 4h23m
kube-logging kibana-866c457776-d5mdh 1/1 Running 0 121m
kube-system aws-node-9r5q5 1/1 Running 0 8h
kube-system aws-node-jk7jh 1/1 Running 0 8h
kube-system coredns-7dd7f84d9-8q86c 1/1 Running 0 8h
kube-system coredns-7dd7f84d9-ptd6t 1/1 Running 0 8h
kube-system kube-proxy-rcnrj 1/1 Running 0 8h
kube-system kube-proxy-vn6v4 1/1 Running 0 8h
observability jaeger-collector-554bdff77d-mgsph 1/1 Running 0 58s
observability jaeger-operator-c886b9cfb-wq8z8 1/1 Running 0 2m43s
observability jaeger-query-67d75b9f5-4zptb 2/2 Running 0 58s
kubectl logs를 통해 로그를 보려면, container 이름을 하나로 지정하라고 에러메시지가 나온다. sidecar 패턴이 적용되었기 때문에 두개의 container가 동작중이기 때문이다.
application pod에 적용된 jaeger-agent 로그를 확인하려면 기존 로그 검색 명령어에 "-c jaeger-agent" 를 붙이면 된다.
명령어 :
$ kubectl get pod --all-namespaces
$ kubectl logs [Pod Name] -c [Conatainer Name]
실행 결과 :
$ kubectl logs cloud-l3-project-deployment-84cf76b947-nhm6j
error: a container name must be specified for pod cloud-l3-project-deployment-84cf76b947-nhm6j, choose one of: [cloud-l3-project jaeger-agent]
$ kubectl logs cloud-l3-project-deployment-84cf76b947-nhm6j -c jaeger-agent
2020/08/04 15:30:35 maxprocs: Leaving GOMAXPROCS=2: CPU quota undefined
{"level":"info","ts":1596555035.9601805,"caller":"flags/service.go:116","msg":"Mounting metrics handler on admin server","route":"/metrics"}
{"level":"info","ts":1596555035.960354,"caller":"flags/admin.go:120","msg":"Mounting health check on admin server","route":"/"}
{"level":"info","ts":1596555035.9604359,"caller":"flags/admin.go:126","msg":"Starting admin HTTP server","http-addr":":14271"}
{"level":"info","ts":1596555035.9604492,"caller":"flags/admin.go:112","msg":"Admin server started","http.host-port":"[::]:14271","health-status":"unavailable"}
{"level":"warn","ts":1596555035.9604678,"caller":"reporter/flags.go:61","msg":"Using deprecated configuration","option":"jaeger.tags"}
{"level":"info","ts":1596555035.9635174,"caller":"grpc/builder.go:66","msg":"Agent requested insecure grpc connection to collector(s)"}
{"level":"info","ts":1596555035.9635756,"caller":"grpc@v1.27.1/clientconn.go:106","msg":"parsed scheme: \"dns\"","system":"grpc","grpc_log":true}
{"level":"info","ts":1596555035.9664893,"caller":"command-line-arguments/main.go:78","msg":"Starting agent"}
{"level":"info","ts":1596555035.9666686,"caller":"healthcheck/handler.go:128","msg":"Health Check state change","status":"ready"}
{"level":"info","ts":1596555035.967417,"caller":"app/agent.go:69","msg":"Starting jaeger-agent HTTP server","http-port":5778}
{"level":"info","ts":1596555035.9743881,"caller":"dns/dns_resolver.go:212","msg":"ccResolverWrapper: sending update to cc: {[] <nil> <nil>}","system":"grpc","grpc_log":true}
{"level":"info","ts":1596555035.974565,"caller":"grpc@v1.27.1/clientconn.go:948","msg":"ClientConn switching balancer to \"round_robin\"","system":"grpc","grpc_log":true}
{"level":"info","ts":1596555071.0139737,"caller":"dns/dns_resolver.go:212","msg":"ccResolverWrapper: sending update to cc: {[{192.168.177.117:14250 <nil> 0 <nil>}] <nil> <nil>}","system":"grpc","grpc_log":true}
{"level":"info","ts":1596555071.014129,"caller":"base/balancer.go:196","msg":"roundrobinPicker: newPicker called with info: {map[]}","system":"grpc","grpc_log":true}
{"level":"info","ts":1596555071.0172608,"caller":"base/balancer.go:196","msg":"roundrobinPicker: newPicker called with info: {map[0xc000117f80:{{192.168.177.117:14250 <nil> 0 <nil>}}]}","system":"grpc","grpc_log":true}
4. 테스트
이제 테스트를 진행해보도록 하자.
jaeger의 서비스를 확인하여 호스트 주소 및 포트번호를 확인하자.
$ kubectl get svc --all-namespaces
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
default cloud-l3-service1 LoadBalancer 10.100.239.63 ad6c0fd43d0074450a17f473020028ed-1642550222.ap-northeast-2.elb.amazonaws.com 8081:31782/TCP 5h51m
default kubernetes ClusterIP 10.100.0.1 <none> 443/TCP 8h
kube-logging elasticsearch ClusterIP 10.100.171.21 <none> 9200/TCP,9300/TCP 4h58m
kube-logging kibana LoadBalancer 10.100.146.209 ace88e4cd03b742faac85a4debc78bde-1447862165.ap-northeast-2.elb.amazonaws.com 5601:31534/TCP 131m
kube-system kube-dns ClusterIP 10.100.0.10 <none> 53/UDP,53/TCP 8h
observability jaeger-collector ClusterIP 10.100.51.61 <none> 9411/TCP,14250/TCP,14267/TCP,14268/TCP 11m
observability jaeger-collector-headless ClusterIP None <none> 9411/TCP,14250/TCP,14267/TCP,14268/TCP 11m
observability jaeger-operator-metrics ClusterIP 10.100.28.54 <none> 8383/TCP,8686/TCP 12m
observability jaeger-query ClusterIP 10.100.35.66 <none> 16686/TCP 11m
확인해보니 jaeger-query를 위한 로드밸런서는 미존재한다. 따라서 외부에서 접속을 할 수 없다
jaeger를 생성하면 ingress도 자동 생성되기 때문에, ingress 정보를 한번 확인해보자.
ingress도 호스트가 정상적으로 생성되지 않은 것을 확인할 수 있다. 아마 프라이빗 서브넷에 클러스터를 구축했기 때문에 ingress가 외부에서 접속 가능한 로드밸런서로 생성되지 않은 것처럼 보인다.
$ kubectl get ingress --all-namespaces
NAMESPACE NAME HOSTS ADDRESS PORTS AGE
observability jaeger-query * 80 16m
그러면 ingress를 신규로 만들고, 스프링부트와 jaeger를 모두 호출할 수 있게 만들어보자.
우선 스프링부트 서비스도 ingress를 통해서 서비스가 호출되므로, 기존에 작성되었던 스프링부트 서비스의 service.yaml파일의 type을 LoadBalancer에서 ClusterIP로 변경하고 쿠버네티스 클러스터에 적용하자.
apiVersion: v1
kind: Service
metadata:
name: cloud-l3-service1
spec:
ports:
- name: "8080"
port: 8081
targetPort: 8080
selector:
app: cloud-l3-project
type: ClusterIP
# type: LoadBalancer
$ kubectl apply -f service.yaml
다음으로 ingress controller를 쿠버네티스 클러스터에 올려보자.
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/kops/master/addons/ingress-nginx/v1.6.0.yaml
마지막으로 ingress 매니페스트 파일을 생성하자. 매니페스트 파일에는 스프링부트 서비스와 jaeger query 호출 정보를 입력한다.
입력이 완료되었으면 쿠버네티스 클러스터에 적용시키자.
ingress.yaml :
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: cloud-l3-ingress
spec:
rules:
- host:
http:
paths:
- path: /api/v1/cafe/
backend:
serviceName: cloud-l3-service1
servicePort: 8081
- path: /
backend:
serviceName: jaeger-query
servicePort: 16686
$ kubectl apply -f ingress.yaml
그리고 리소스 정보를 확인해보면 ingress 및 로드밸런서가 모두 정상 수행되는 것을 확인할 수 있다.
$ kubectl get ingress --all-namespaces
NAMESPACE NAME HOSTS ADDRESS PORTS AGE
default cloud-l3-ingress * a9088dc527422440994c9e65bf674e2f-1345041008.ap-northeast-2.elb.amazonaws.com 80 91s
observability jaeger-query * a9088dc527422440994c9e65bf674e2f-1345041008.ap-northeast-2.elb.amazonaws.com 80 61m
$ kubectl get svc --all-namespaces
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
default cloud-l3-service1 ClusterIP 10.100.21.44 <none> 8081/TCP 15m
default kubernetes ClusterIP 10.100.0.1 <none> 443/TCP 9h
kube-ingress ingress-nginx LoadBalancer 10.100.120.244 a9088dc527422440994c9e65bf674e2f-1345041008.ap-northeast-2.elb.amazonaws.com 80:31843/TCP,443:31998/TCP 12m
kube-ingress nginx-default-backend ClusterIP 10.100.5.41 <none> 80/TCP 12m
kube-logging elasticsearch ClusterIP 10.100.171.21 <none> 9200/TCP,9300/TCP 5h49m
kube-logging kibana LoadBalancer 10.100.146.209 ace88e4cd03b742faac85a4debc78bde-1447862165.ap-northeast-2.elb.amazonaws.com 5601:31534/TCP 3h2m
kube-system kube-dns ClusterIP 10.100.0.10 <none> 53/UDP,53/TCP 9h
observability jaeger-collector ClusterIP 10.100.51.61 <none> 9411/TCP,14250/TCP,14267/TCP,14268/TCP 62m
observability jaeger-collector-headless ClusterIP None <none> 9411/TCP,14250/TCP,14267/TCP,14268/TCP 62m
observability jaeger-operator-metrics ClusterIP 10.100.28.54 <none> 8383/TCP,8686/TCP 63m
observability jaeger-query ClusterIP 10.100.35.66 <none> 16686/TCP 62m
ingress에 설정된 URI를 통해서 스프링부트 서비스를 호출해보자.
다음으로 Jaeger UI를 호출해보자. 정상 호출되는 것을 확인할 수 있다.
참고
http://www.vinsguru.com/spring-boot-distributed-tracing-with-jaeger/
https://www.jaegertracing.io/docs/1.18/operator/