見出し画像

Elastic Beanstalk から EKS へ移行した話 (3/3) ~メトリクス・ログ編~

こんばんは。
株式会社 POL にてエンジニアをしている山田高寛です。
EKS 移行話の第 3 部を語りたいと思います。
今回はメトリクス・ログ収集基盤の構築についてです。

メトリクス・ログ収集基盤の構築

EKS 移行に伴い、メトリクス・ログ収集基盤も刷新しました。今までは CloudWatch に全てを任せていましたが、CloudWatch カスタムメトリクスの料金が意外と高くつくので CloudWatch から Grafana + Prometheus 環境に移行しました。

移行するからには、今まで CloudWatch が提供してきた下記の機能を提供できるようにしなければなりません。

・リソース使用率など各種メトリクスの収集
・コンテナログの収集
・各種メトリクス・ログの永続化

今回は上記機能をそれぞれ下記のツールを利用することで実現しました。

・Grafana: 収集したメトリクス・ログの可視化
・Prometheus: メトリクスの収集 & 短期永続化
・Thanos: メトリクスの長期永続化
・Loki: コンテナログの収集 & 永続化

Grafana: 収集したメトリクス・ログの可視化

画像3

Grafana [1] はメトリクス・ログの可視化ツールです。Grafana 自体はメトリクスデータの収集・保存はできないため、各種データソースからメトリクスデータを持ってきて、グラフとして可視化し、複数のグラフを集めたダッシュボードを表示・管理することができます。また、Grafana にはアラート機能もあり、グラフに対してアラートを設定でき、Slack などにグラフ付きで通知することもできます。

個人的には、有志がダッシュボードを公開しているため、公開 ID を指定するだけで同じダッシュボードが取り込むことができ、ダッシュボードの作成の手間が省けるのが嬉しいです [2]。

Prometheus: メトリクスの収集 & 短期永続化

画像4

Prometheus [3] は SoundCloud が開発した OSS の監視用ツールです。Prometheus は下記役割を担っております。

・メトリクスなどの時系列データの収集 (Pull 型)
・メトリクスなどの時系列データの保存 (短期用途)
・アラート処理

Prometheus の便利な点がメトリクスの収集の設定が楽に行えることです。Prometheus は Pull 型のメトリクス収集 (Prometheus が監視対象のコンポーネントにメトリクスデータを HTTP リクエストする) を採用しています。アプリケーション自体にメトリクスデータを HTTP エンドポイントにて提供している場合は良いですが、対応していないのがほとんどであるかと思います。そこで、exporter と呼ばれるコンポーネントが用意されております。exporter は監視対象のコンポーネントのメトリクスデータを収集して、HTTP エンドポイントにて収集したメトリクスデータを提供します。更に、この exporter は主要な監視対象のものが公開されているため、ほとんどの場合、自前で用意する必要はありません [4]。

Prometheus にもグラフ描画機能もありますが、Grafana のほうが高機能なため、デバック用途のみに限定したほうが良いです。

Prometheus に収集されたメトリクスは Prometheus が稼働しているサーバー・コンテナのローカルファイルシステムに格納されます。もちろん、ローカルファイルシステム上にあるデータはサーバーもしくはコンテナのライフサイクルに依存するため、永続化するためには別の手段を考える必要があります。

Thanos: メトリクスの長期永続化

画像5

Thanos [5] は Prometheus のメトリクスデータの長期保存を実現するツールです。Thanos は Prometheus からメトリクスデータをクエリして、S3 などのオブジェクトストレージに格納します。また、S3 などのオブジェクトストレージからメトリクスデータを読み取る際にも Prometheus が提供しているのと同じ形式でのリクエストを提供しているため、Grafana のデータソースを Prometheus から Thanos に変更するだけでメトリクスの保存期間を長期間にすることができます。また、S3 に格納したデータも保存しっぱなしにするのではなく、保存期間に応じたダウンサンプリングも行えます。

更に、Thanos を導入することで Prometheus の高可用性を実現できます。Prometheus は単体で高可用性をサポートしておらず、複数の Prometheus を立ち上げるとメトリクスデータが重複します。Thanos を通して Prometheus のデータを引っ張ってきた場合は Prometheus が収集したメトリクスデータのラベルをもとにデータの重複排除を実施してくれます。

Loki: コンテナログの収集 & 永続化

画像6

最後に、Loki [6] ですが、Grafana を作成したのと同じ Grafana Lab 社が開発したこちらはログ収集から永続化までを担当します。もちろん、Grafana と連携が可能で、Loki で収集したログデータを Grafana 上で確認できます。Loki の特徴は Prometheus に似た形式でログ収集を行えることです。Promtail というログ収集コンポーネントがいて、監視対象からログの収集を行うだけでなく、ログデータに収集対象のメタデータが格納されたラベルを付与します。このラベルに収集元の情報が入っているため、生のログデータを解析せずにログのフィルタリング・検索を行えます。また、収集したログデータは DynamoDB と S3 を利用することで永続化することができます。

各種ツールのインストールについて

続いて、各種ツールのインストール方法について紹介します。

Grafana と Prometheus のインストールには Prometheus Operator [7] という第 1 部で紹介した Custom Resource Definition と Custom Controller を利用して Prometheus の管理を自動化したツールを使用しました。実際には Prometheus Operator をもっと使いやすくした kube-prometheus [8] というツールを利用しました。Thanos のインストール (Thanos のコンポーネントのうち、Prometheus からメトリクスデータを収集する Thanos sidecar は kube-prometheus でインストールされます) には kube-thanos [9] というツールを、Loki と Promtail は Helm でインストールしました。設定がえらくしんどかった & 情報が少なかったので詳細を共有します。

Grafana と Prometheus のインストール

まず、jsonnet という go 製のツールをインストール、初期化 & 設定ファイル (example.jsonnet) の作成、monitoring Namespace の作成を行っておきます。

$ GO111MODULE="on" go get github.com/jsonnet-bundler/jsonnet-bundler/cmd/jb
$ jb init
$ jb install github.com/coreos/kube-prometheus/jsonnet/kube-prometheus@release-0.5
$ kubectl create namespace monitoring
$ touch example.jsonnet 

example.jsonnet では、Thanos sidecar の有効化、デプロイされる Pod の Node Affiniry と Toleration の設定、grafana の設定を行います

local k = import 'ksonnet/ksonnet.beta.3/k.libsonnet';
local ingress = k.extensions.v1beta1.ingress;
local ingressRule = ingress.mixin.spec.rulesType;
local httpIngressPath = ingressRule.mixin.http.pathsType;
local statefulSet = k.apps.v1beta2.statefulSet;
local toleration = statefulSet.mixin.spec.template.spec.tolerationsType;
local nodeAffinity = statefulSet.mixin.spec.template.spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecutionType;
local matchExpression = nodeAffinity.mixin.preference.matchExpressionsType;
local kp =
 (import 'kube-prometheus/kube-prometheus.libsonnet') +
 (import 'kube-prometheus/kube-prometheus-node-ports.libsonnet') +
 (import 'kube-prometheus/kube-prometheus-thanos-sidecar.libsonnet') +
 {
   _config+:: {
     tolerations+:: [
       {
         key: 'spotInstance',
         operator: 'Equal',
         value: 'true',
         effect: 'NoSchedule',
       }
     ]
   },
 
   local withTolerations() = {
     tolerations+: [
       toleration.new() + (
       if std.objectHas(t, 'key') then toleration.withKey(t.key) else toleration) + (
       if std.objectHas(t, 'operator') then toleration.withOperator(t.operator) else toleration) + (
       if std.objectHas(t, 'value') then toleration.withValue(t.value) else toleration) + (
       if std.objectHas(t, 'effect') then toleration.withEffect(t.effect) else toleration),
       for t in $._config.tolerations
     ],
   },
   local affinity() = {
     affinity+: {
       nodeAffinity: {
         preferredDuringSchedulingIgnoredDuringExecution: [
           nodeAffinity.new() + 
           nodeAffinity.withWeight(100) +
           nodeAffinity.mixin.preference.withMatchExpressions([
             matchExpression.new() +
             matchExpression.withKey('lifecycle') +
             matchExpression.withOperator('In') +
             matchExpression.withValues(['Ec2Spot']),
           ]),
         ],
       },
     },
   },
   alertmanager+:: {
     alertmanager+: {
       spec+:
         withTolerations() + 
         affinity(),
     },
   },
 
   prometheus+: {
     local p = self,
     
     prometheus+: {
       spec+:
         withTolerations() + 
         affinity(),
     },
   },
   
   kubeStateMetrics+: {
     deployment+: {
       spec+: {
         template+: {
           spec+:
             withTolerations() + 
             affinity(),
         },
       },
     },
   },
   
   nodeExporter+: {
     daemonset+: {
       spec+: {
         template+: {
           spec+:
             withTolerations() + 
             affinity(),
         },
       },
     },
   },
   
   prometheusAdapter+: {
     deployment+: {
       spec+: {
         template+: {
           spec+:
             withTolerations() + 
             affinity(),
         },
       },
     },
   },
   
   grafana+:: {
     deployment+: {
       spec+: {
         template+: {
           spec+:
             withTolerations() + 
             affinity(),
         },
       },
     },
   }
 } +
 {
   _config+:: {
     namespace: 'monitoring',
     prometheus+:: {
       namespaces: ["default", "kube-system", "monitoring"],
     },
     grafana+:: {
       config: {
         sections: {
           "auth": {
             disable_login_form: true
           },
           "auth.basic": {
             enabled: false
           },
         },
       },
     },
   },
 };
{ ['setup/0namespace-' + name]: kp.kubePrometheus[name] for name in std.objectFields(kp.kubePrometheus) } +
{
 ['setup/prometheus-operator-' + name]: kp.prometheusOperator[name]
 for name in std.filter((function(name) name != 'serviceMonitor'), std.objectFields(kp.prometheusOperator))
} +
// serviceMonitor is separated so that it can be created after the CRDs are ready
{ 'prometheus-operator-serviceMonitor': kp.prometheusOperator.serviceMonitor } +
{ ['node-exporter-' + name]: kp.nodeExporter[name] for name in std.objectFields(kp.nodeExporter) } +
{ ['kube-state-metrics-' + name]: kp.kubeStateMetrics[name] for name in std.objectFields(kp.kubeStateMetrics) } +
{ ['alertmanager-' + name]: kp.alertmanager[name] for name in std.objectFields(kp.alertmanager) } +
{ ['prometheus-' + name]: kp.prometheus[name] for name in std.objectFields(kp.prometheus) } +
{ ['prometheus-adapter-' + name]: kp.prometheusAdapter[name] for name in std.objectFields(kp.prometheusAdapter) } +
{ ['grafana-' + name]: kp.grafana[name] for name in std.objectFields(kp.grafana) } +

(import 'kube-prometheus/kube-prometheus-thanos-sidecar.libsonnet') という箇所が Thanos sidecar の有効化を、withTolerations と affinity という箇所が Node Affinity と Tolerations の設定を、grafana+:: というブロックで grafana の設定ファイルを記載できます。

続いて、Thanos sidecar が S3 にアクセスする際に利用する認証情報を作成します。

$ touch thanos-config.yaml
$ kubectl -n monitoring create secret generic thanos-objectstorage --from-file=thanos.yaml=./thanos-config.yaml

最後に、Kubernetes マニフェストファイルを作成します。build.sh は公式の GitHub ページの README に記載のあるものをコピーします [10]。

$ touch build.sh
$ sudo chmod u+x build.sh 
$ go get github.com/brancz/gojsontoyaml
$ go get github.com/google/go-jsonnet/cmd/jsonnet
$ ./build.sh example.jsonnet
$ kubectl apply -f manifests/setup
$ kubectl apply -f manifests/

これで Grafana と Prometheus、Thanos sidecar のインストールが完了しました。

Thanos のインストール

次に、Thanos の残りのコンポーネントをインストールしていきます。
まず、同じ用に初期化 & 設定ファイルを作成します。

$ jb init
$ jb install github.com/thanos-io/kube-thanos/jsonnet/kube-thanos@master
$ touch example.jsonnet

example.jsonnet では Node Affinity と Toleration の設定、とインストールするコンポーネントの選択を行います。

local k = import 'ksonnet/ksonnet.beta.4/k.libsonnet';
local sts = k.apps.v1.statefulSet;
local deployment = k.apps.v1.deployment;
local t = (import 'kube-thanos/thanos.libsonnet');
local statefulSet = k.apps.v1beta2.statefulSet;
local toleration = statefulSet.mixin.spec.template.spec.tolerationsType;
local nodeAffinity = statefulSet.mixin.spec.template.spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecutionType;
local matchExpression = nodeAffinity.mixin.preference.matchExpressionsType;
local commonConfig = {
 config+:: {
   local cfg = self,
   namespace: 'monitoring',
   version: 'v0.13.0-rc.0',
   image: 'quay.io/thanos/thanos:' + cfg.version,
   objectStorageConfig: {
     name: 'thanos-objectstorage',
     key: 'thanos.yaml',
   },
   volumeClaimTemplate: {
     spec: {
       accessModes: ['ReadWriteOnce'],
       resources: {
         requests: {
           storage: '10Gi',
         },
       },
     },
   },
   tolerations+:: [{
     key: 'spotInstance',
     operator: 'Equal',
     value: 'true',
     effect: 'NoSchedule',
   }]
 },
};
 
local withTolerations() = {
 tolerations+: [
   toleration.new() + (
   if std.objectHas(t, 'key') then toleration.withKey(t.key) else toleration) + (
   if std.objectHas(t, 'operator') then toleration.withOperator(t.operator) else toleration) + (
   if std.objectHas(t, 'value') then toleration.withValue(t.value) else toleration) + (
   if std.objectHas(t, 'effect') then toleration.withEffect(t.effect) else toleration),
   for t in commonConfig.config.tolerations
 ],
};
local affinity() = {
 affinity+: {
   nodeAffinity: {
     preferredDuringSchedulingIgnoredDuringExecution: [
       nodeAffinity.new() + 
       nodeAffinity.withWeight(100) +
       nodeAffinity.mixin.preference.withMatchExpressions([
         matchExpression.new() +
         matchExpression.withKey('lifecycle') +
         matchExpression.withOperator('In') +
         matchExpression.withValues(['Ec2Spot']),
       ]),
     ],
   },
 },
};

local b = t.bucket + commonConfig + {
 config+:: {
   name: 'thanos-bucket',
   replicas: 1,
 },
 deployment+: {
   spec+: {
     template+: {
       spec+:
         withTolerations() + 
         affinity(),
     },
   },
 },
};
local c = t.compact + t.compact.withVolumeClaimTemplate + t.compact.withServiceMonitor + commonConfig + {
 config+:: {
   name: 'thanos-compact',
   replicas: 1,
 },
 statefulSet+: {
   spec+: {
     template+: {
       spec+:
         withTolerations() + 
         affinity(),
     },
   },
 },
};

local s = t.store + t.store.withVolumeClaimTemplate + t.store.withServiceMonitor + commonConfig + {
 config+:: {
   name: 'thanos-store',
   replicas: 1,
 },
 statefulSet+: {
   spec+: {
     template+: {
       spec+:
         withTolerations() + 
         affinity(),
     },
   },
 },
};
local q = t.query + t.query.withServiceMonitor + commonConfig + {
 config+:: {
   name: 'thanos-query',
   replicas: 1,
   stores: [
     'dnssrv+_grpc._tcp.prometheus-operated.monitoring.svc.cluster.local',
     'dnssrv+_grpc._tcp.thanos-store.monitoring.svc.cluster.local',
   ],
   replicaLabels: ['prometheus_replica'],
 },
 deployment+: {
   spec+: {
     template+: {
       spec+:
         withTolerations() + 
         affinity(),
     },
   },
 },
};

{ ['thanos-bucket-' + name]: b[name] for name in std.objectFields(b) } +
{ ['thanos-compact-' + name]: c[name] for name in std.objectFields(c) } +
{ ['thanos-store-' + name]: s[name] for name in std.objectFields(s) } +
{ ['thanos-query-' + name]: q[name] for name in std.objectFields(q) }

最後に、Kubernetes マニフェストファイルを作成します。build.sh は同様に公式の GitHub ページの README に記載のあるものをコピーします [11]。

$ touch build.sh
$ sudo chmod u+x build.sh 
$ ./build.sh example.jsonnet
$ kubectl apply -f manifests/

Loki のインストール

最後に、Loki + Promtail をインストールします。それぞれ、Helm を利用してインストールします。

まず、Loki のインストールです。

$ touch values.yaml
$ helm repo add loki https://grafana.github.io/loki/charts
$ helm upgrade --install loki --namespace=monitoring loki/loki -f values.yaml

values.yaml には Node Affinity と Toleration の設定、S3 と DynamoDB の設定を記載します。

tolerations: 
- key: "spotInstance"
 operator: "Equal"
 value: "true"
 effect: "NoSchedule"
affinity:
 nodeAffinity:
   requiredDuringSchedulingIgnoredDuringExecution:
     nodeSelectorTerms:
     - matchExpressions:
       - key: lifecycle
         operator: In
         values:
         - Ec2Spot
config:
 schema_config:
   configs:
   - from: 2020-05-15
     store: aws
     object_store: s3
     schema: v11
     index:
       prefix: loki_
 storage_config:
   aws:
     s3: s3://ap-northeast-1/<S3 バケット名>
     dynamodb:
       dynamodb_url: dynamodb://ap-northeast-1
 
 table_manager:
   creation_grace_period: 3h
   retention_deletes_enabled: false
   retention_period: 0s
   chunk_tables_provisioning:
     enable_ondemand_throughput_mode: true
     enable_inactive_throughput_on_demand_mode: true
   index_tables_provisioning:
     enable_ondemand_throughput_mode: true
     enable_inactive_throughput_on_demand_mode: true

続いて、Promtail です。

$ touch values.yaml
$ helm upgrade --install promtail --namespace=monitoring loki/promtail -f values.yamlこちらも values.yaml には Node Affinity と Toleration の設定を記載します。
tolerations: 
- key: "spotInstance"
 operator: "Equal"
 value: "true"
 effect: "NoSchedule"
affinity:
 nodeAffinity:
   requiredDuringSchedulingIgnoredDuringExecution:
     nodeSelectorTerms:
     - matchExpressions:
       - key: lifecycle
         operator: In
         values:
         - Ec2Spot
loki:
 serviceName: "loki"  # Defaults to "${RELEASE}-loki" if not set
 servicePort: 3100
 serviceScheme: http

これで、全てのコンポーネントのインストールは完了です。

実際の Grafana のダッシュボードは以下のようになります。

画像2

画像1

メトリクスもログも 1 箇所から確認できるので楽ちんですね。1 月のコストも正確なリクエスト量を計算していませんが、S3 + オンデマンドの DynamoDB で $50 にも達していません。

まとめと今後の展望

今確認したら、ちょうど Elastic Beanstalk から EKS への移行から 1 ヶ月が経過していました。現在のところ、Production 環境では 1 度も障害は発生しておらず、毎日ぐっすり寝られています。Develop 環境も複数立ち上げられるようになり、エンジニアがバンバン開発を行えるようになりました。メトリクス・ログ収集基盤を独自のものを採用することで、CloudWatch をはじめとしたマネージドサービスのありがたみを実感しました。

今後は以下のようなことを進めていきたいと思っています。

・EBS CSI ドライバを利用したスナップショットからのボリューム作成
・サービスメッシュの導入
・カナリアリリースの対応
・kubeflow

1 つ目は Develop 環境の紹介をした際に少し話題にあげた、EBS CSI ドライバを利用して MySQL のデータを EBS スナップショットからリストアできるようにしたいです。2 つ目はサービスメッシュを導入して、柔軟なトラフィックコントロールとトラフィックに対する Observality を向上させたいです。Istio [12] もしくは AWS App Mesh [13] を試してみようと思っています。3 つ目は、Spinnaker と Prometheus を用いているのでカナリアリリースに対応したいです。おそらくサービスメッシュが必要になりそうなので、サービスメッシュ導入後になりそうです。最後に個人的に興味があるのが kubeflow [14] です。kubeflow は機械学習ワークフロー管理ツールで、kubeflow を利用して機械学習基盤を作成してみたいです。

株式会社 POL プロダクト部について

株式会社 POL ではエンジニアを募集しております!
今回、福利厚生に新しく 1 人 1 リモート検証環境の提供が追加されました!
Kubernets 環境で開発したい貴方、または Kubernetes 環境を触ってみたい貴方、どしどし応募してください!

エンジニア採用 | 株式会社POL | 研究者が、社会をもっと良くする未来を創るpol.co.jp

参考資料

[1] Grafana Features | Grafana Labs
https://grafana.com/grafana/
[2] Grafana Dashboards - discover and share dashboards for Grafana. | Grafana Labs
https://grafana.com/grafana/dashboards|
[3] Prometheus - Monitoring system & time series database
https://prometheus.io/
[4] Exporters and integrations | Prometheus
https://prometheus.io/docs/instrumenting/exporters/
[5] Thanos - Highly available Prometheus setup with long term storage capabilities
https://thanos.io/
[6] Grafana Loki | Grafana Labs
https://grafana.com/oss/loki/
[7] prometheus-operator/prometheus-operator: Prometheus Operator creates/configures/manages Prometheus clusters atop Kubernetes
https://github.com/prometheus-operator/prometheus-operator
[8] prometheus-operator/kube-prometheus: Use Prometheus to monitor Kubernetes and applications running on Kubernetes
https://github.com/prometheus-operator/kube-prometheus
[9] thanos-io/kube-thanos: Kubernetes specific configuration for deploying Thanos.
https://github.com/thanos-io/kube-thanos
[10] prometheus-operator/kube-prometheus: Use Prometheus to monitor Kubernetes and applications running on Kubernetes
https://github.com/prometheus-operator/kube-prometheus#compiling
[11] thanos-io/kube-thanos: Kubernetes specific configuration for deploying Thanos.
https://github.com/thanos-io/kube-thanos#compiling
[12] Istio
https://istio.io/
[13] AWS App Mesh(マイクロサービスをモニタリングおよびコントロールする)| AWS
https://aws.amazon.com/jp/app-mesh/
[14] Kubeflow
https://www.kubeflow.org/


この記事が気に入ったらサポートをしてみませんか?