K8s上でElastic APMを動かして外部のGo製APIサーバーのリクエストをトレースする

kuberneteselasticsearchgolang

Elastic Cloud on Kubernetes (ECK)で Kubernetesクラスタ上にElasticsearch, KibanaとAPM Serverを立ち上げ、外部のGo製APIサーバーのリクエストをトレースする。 クラスタはGKEで作成し、ノードプールはn2-highmem-4(2vCPU, 13GB)の3台にした。

インストール

ElasticSearchやKibana, APM ServerのCRDやelastic-operatorなどをインストールする。

KubernetesのCustom Resource Definition(CRD)とCustom Controller - sambaiz-net

$ kubectl apply -f https://download.elastic.co/downloads/eck/0.9.0/all-in-one.yaml
customresourcedefinition.apiextensions.k8s.io/apmservers.apm.k8s.elastic.co created
customresourcedefinition.apiextensions.k8s.io/elasticsearches.elasticsearch.k8s.elastic.co created
customresourcedefinition.apiextensions.k8s.io/trustrelationships.elasticsearch.k8s.elastic.co created
customresourcedefinition.apiextensions.k8s.io/kibanas.kibana.k8s.elastic.co created
clusterrole.rbac.authorization.k8s.io/elastic-operator created
clusterrolebinding.rbac.authorization.k8s.io/elastic-operator created
namespace/elastic-system created
statefulset.apps/elastic-operator created
secret/webhook-server-secret created
serviceaccount/elastic-operator created
$ kubectl get pod -n elastic-system
NAME                 READY   STATUS    RESTARTS   AGE
elastic-operator-0   1/1     Running   1          91s

$ kubectl -n elastic-system logs -f statefulset.apps/elastic-operator
...
{"level":"info","ts":1569230702.0881083,"logger":"kubebuilder.controller","msg":"Starting workers","controller":"elasticsearch-controller","worker count":1}
{"level":"info","ts":1569230702.1144261,"logger":"kubebuilder.webhook","msg":"starting the webhook server."}

Resources

Elasticsearch

$ cat <<EOF | kubectl apply -f -
apiVersion: elasticsearch.k8s.elastic.co/v1alpha1
kind: Elasticsearch
metadata:
  name: myes
spec:
  version: 7.2.0
  nodes:
  - nodeCount: 1
    config:
      node.master: true
      node.data: true
      node.ingest: true
    volumeClaimTemplates:
      - metadata:
          name: elasticsearch-data
        spec:
          accessModes:
          - ReadWriteOnce
          resources:
            requests:
              storage: 10Gi
          storageClassName: standard
EOF

$ kubectl get elasticsearch
NAME   HEALTH   NODES   VERSION   PHASE         AGE
myes   green    1       7.2.0     Operational   4m

Podが立ち上がるまで少し時間がかかる。ユーザーelasticのパスワードはsecretにある。

$ kubectl get secret | grep 'myes'
myes-es-7jzg7sqxqt-certs         Opaque                                3      4m42s
myes-es-7jzg7sqxqt-config        Opaque                                1      4m42s
myes-es-elastic-user             Opaque                                1      4m42s
myes-es-http-ca-internal         Opaque                                2      4m43s
myes-es-http-certs-internal      Opaque                                2      4m43s
myes-es-http-certs-public        Opaque                                1      4m43s
myes-es-internal-users           Opaque                                3      4m42s
myes-es-transport-ca-internal    Opaque                                2      4m42s
myes-es-transport-certs-public   Opaque                                1      4m42s
myes-es-xpack-file-realm         Opaque                                3      4m42s

$ PASSWORD=$(kubectl get secret myes-es-elastic-user -o=jsonpath='{.data.elastic}' | base64 -D)
$ kubectl port-forward service/myes-es-http 9200 &
$ curl -u "elastic:$PASSWORD" -k "https://localhost:9200" | jq ".cluster_name"
"myes"

Kibana

port-forwardなしで外からアクセスできるようにserviceをLoadBalancerにした。

$ cat <<EOF | kubectl apply -f -
apiVersion: kibana.k8s.elastic.co/v1alpha1
kind: Kibana
metadata:
  name: mykibana
spec:
  version: 7.2.0
  nodeCount: 1
  http:
    service:
      spec:
        type: LoadBalancer
  elasticsearchRef:
    name: myes
EOF

$ kubectl get kibana
NAME       HEALTH   NODES   VERSION   AGE
mykibana   green    1       7.2.0     1m

$ open https://$(kubectl get service mykibana-kb-http -o=jsonpath='{.status.loadBalancer.ingress[0].ip}:{.spec.ports[0].port}')

ユーザーelasticでログインできる。

ApmServer

今回はクラスタ外から送るので同じくLoadBalancerにする。

$ cat <<EOF | kubectl apply -f -
apiVersion: apm.k8s.elastic.co/v1alpha1
kind: ApmServer
metadata:
  name: myapm-server
spec:
  version: 7.2.0
  nodeCount: 1
  http:
    service:
      spec:
        type: LoadBalancer
  elasticsearchRef:
    name: myes
EOF

$ kubectl get apmserver
NAME           HEALTH   NODES   VERSION   AGE
myapm-server   green    1       7.2.0     9m

KibanaからAPM Serverが動いていることを確認する。

APM Server status

APM Agentの設定

APM Agentをモニタリングするアプリケーションに入れる。

$ go get go.elastic.co/apm

$ export ELASTIC_APM_SERVER_URL=https://$(kubectl get service myapm-server-apm-http -o=jsonpath='{.status.loadBalancer.ingress[0].ip}:{.spec.ports[0].port}')

$ export ELASTIC_APM_SECRET_TOKEN=$(kubectl get secret/myapm-server-apm-token -o=jsonpath='{.data.secret-token}' | base64 -D)

module/apmhttpmodule/apmsqlを使うと、 リクエスト全体のTransactionに対してそれぞれの処理部分がSpanとしてトレースされる。 contextを渡してやらないとTransactionを追跡できずSpanが作成されない。 自前でTransactionやSpanを作成することもできる。

import (
	"net/http"

	"go.elastic.co/apm/module/apmhttp"
	"go.elastic.co/apm/module/apmsql"
	_ "go.elastic.co/apm/module/apmsql/mysql"
)

var tracingClient = apmhttp.WrapClient(http.DefaultClient)

func main() {
	mux := http.NewServeMux()
  mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
    	db, err := apmsql.Open("mysql", dsn)
      tracingClient.Do(req.WithContext(ctx))
      fmt.Fprintf(w, "ok")
  })
	http.ListenAndServe(":8080", apmhttp.Wrap(mux))
}

実行しCheck agent statusしてもNo data has been received from agents yetから変わらない。 デフォルトでログが出ないのでELASTIC_APM_LOG_FILEに値を入れてログを見てみたところ証明書で引っかかっていることが分かった。 ELASTIC_APM_VERIFY_SERVER_CERTをfalseにしたところ送られるようになったのでLaunch APMする。

$ export ELASTIC_APM_LOG_FILE=stderr
{"level":"error","time":"2019-10-01T19:18:15+09:00","message":"config request failed: sending config request failed: Get https://104.155.169.150:8200/config/v1/agents?service.name=isucari: x509: cannot validate certificate for 104.155.169.150 because it doesn't contain any IP SANs"}

$ export ELASTIC_APM_VERIFY_SERVER_CERT=false

ISUCON9の予選問題に入れてみた。 リクエスト中の外部サーバーへのリクエストやDBのクエリのタイミングやかかっている時間がTimeline上に表示される。

Timeline

参考

How to instrument your Go application with the Elastic APM Go agent | Elastic Blog