技術開発部の相原です。
以前にブログで書きましたが、LIFULLでは主要サービスのほぼ全てがKubernetesで稼働しています。
Kubernetesをアプリケーション実行基盤として本番運用するためにはデプロイやモニタリング・ログ、セキュリティなど考えることが多くどこから手を付ければよいか困ることがあるでしょう。
そこで今回は既に数年の運用実績のあるLIFULLのアプリケーション実行基盤で利用しているKubernetesエコシステムについて紹介します。 全て書くと数が膨大になるので今回はクラスタ周りを中心に、必要とするソフトウェアの数が多いモニタリング・ログまでとします。(それでも大作になりそうですが...)
- kubernetes/kops
- projectcalico/calico
- coredns/coredns
- node-local-dns
- kubernetes-sigs/aws-iam-authenticator
- kubernetes/autoscaler
- kubernetes-sigs/descheduler
- istio/istio
- prometheus/prometheus
- prometheus/alertmanager
- knative
- thanos-io/thanos
- grafana/grafana
- tricksterproxy/trickster
- DirectXMan12/k8s-prometheus-adapter
- kubernetes-sigs/metrics-server
- kubernetes/kube-state-metrics
- kubernetes/node-problem-detector
- prometheus/node_exporter
- kubecost/cost-model
- kaidotdev/kube-trivy-exporter
- fluent/fluentd
- kaidotdev/events-logger
- 最後に
kubernetes/kops
kopsはAWSやOpenStackなどへのKubernetesクラスタ構築を行うクラスタ管理ツールです。 KubernetesのIn-place Upgradeにも対応していたり、最近はKubernetesのバージョンアップへの追従の速度が上がっていて、このエントリの執筆時点でKubernetes 1.17に対応していて開発も活発です。
LIFULLでは主にAWSが利用されているため、このkopsを利用してAWS上にKubernetesクラスタを構築しました。 当初はAWSにEKSが無かったためkopsで構築をしましたが、EKSのリリース後も既に後述の認証部分やクラスタのオートスケールを実現をしていてEKSへ移行する旨味が少ないと判断して現在も使い続けています。
kopsからkube-apiserverのAuditログの設定ができるので併せて有効にしてあります。
また、ノードに多くのワークロードをデプロイするとkubeletが稼働するためのリソースが不足してしまうことがあるため、 spec.kubelet.kubeReserved
から必要なリソースをkubeletに確保しておくことを忘れないようにしましょう。
LIFULLではスポットインスタンスの活用も進んでいて、preemptiveなワークロードの多くはスポットインスタンスで稼働しています。 単にスポットインスタンスにデプロイしてしまうとAWS側によるスポットインスタンスの停止でPodが安全に終了できないため、スポットインスタンス停止のシグナルを受けてノードのDrainを実行する以下のスクリプトをDaemonSetで動かすことで対処しています。
#!/usr/bin/env bash INTERVAL=1 # Spot instance terminates 2 min after notification # https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/spot-interruptions.html#spot-instance-termination-notices SPOT_LIFESPAN=120 while true; do test "$(curl -sSL http://169.254.169.254/latest/meta-data/spot/termination-time -o /dev/null -w '%{http_code}')" -eq 200 && break sleep $INTERVAL done (sleep $(($SPOT_LIFESPAN - 30)) && kubectl delete pod --field-selector="spec.nodeName=$NODE_NAME,metadata.namespace!=kube-system" --all-namespaces --wait=false) & kubectl drain $NODE_NAME --force --ignore-daemonsets --delete-local-data sleep $SPOT_LIFESPAN
NODE_NAMEはKubernetesのDownward APIによってデプロイされているノードの名前です。
projectcalico/calico
kopsは構築時にCNIを指定できるためそこでcalicoを採用しています。
calicoはBGPを用いたハイパフォーマンスなL3ネットワークを構築するためのCNIプラグインで、NetworkPolicyの利用も可能です。
kopsからAWS VPCというCNIプラグインを採用した場合、NetworkPolicyが利用できなかったり、PodがそれぞれVPC内のIPを使うためIPが枯渇する可能性があることにご注意ください。
coredns/coredns
同様にDNSのコンポーネントについても選択できるためCoreDNSを採用しました。 CoreDNSはCNCFのGraduated Projectで、Kubernetes 1.13からはデフォルトでCoreDNSが利用されるようになりました。
kopsを用いてCoreDNSをインストールした場合、そのままではSIGTERMを適切に解釈せずRolling Update時にリクエストを取りこぼしてしまうため、 インストール時に以下のようにlameduckプラグインを有効にする必要があることにご注意ください。
$ kubectl get configmap -n kube-system coredns -o yaml | sed -r 's/^(\s+)health$/\1health {\n\1 lameduck 5s\n\1}/' | kubectl apply -f - $ kubectl rollout restart -n kube-system deployment/coredns
またkopsを利用してCoreDNSを有効にすると共にインストールされるcoredns-autoscalerについてもデフォルトだと適切にスケールしない可能性があるため調整を忘れないようにしてください
node-local-dns
node-local-dnsはその名の通り、Nodeごとに名前解決の結果をキャッシュするためのコンポーネントです。
これが必要となる詳しい理由は以下にありますのでご参照ください。
またクリティカルなところで言うと、kube-proxyのiptables実装を利用している際にUDPのEndpointの一覧の更新がうまくいかないケースがあるというものがあります。
kubernetes/proxier.go at 4505d5b182c85388627f1b5a648a2da377026871 · kubernetes/kubernetes · GitHub
kube-dnsはそのまま使うとUDPで使うことになるためこの問題をはらんでおり、一度現象が発生してしまうとEndpointの更新が走るまで一部の名前解決のリクエストがblackholeされることになります。
node-local-dnsはこの問題に対しても有効に機能します。
Motivationの項にある通りiptablesによるDNATとConnection Trackingをskipすることができることに加え、node-local-dnsからkube-dnsへのリクエストはTCPにアップグレードされるため、少なくともkube-dnsに関してはこの問題が発生しなくなります。
ipvs実装はGAしたものの未だIssueが目立つ状況でお困りの方も多いと思いますので、node-local-dnsがお勧めです。
当然、名前解決のキャッシュによるパフォーマンス向上も期待することができます。
kubernetes-sigs/aws-iam-authenticator
こちらは旧heptio/authenticatorで、kube-apiserverに対するIAM Roleでの認証を実現するためのものです。
我々のKubernetesクラスタはMulti Tenantで運用しているため、各チームごとにIAM Roleを作成し権限の管理をしています。
kubernetes/autoscaler
kopsにはノードをオートスケールさせる機能がないため、リソースの確保量に応じてノードをスケールアウトさせるためにkubernetes/autoscalerを利用しています。
同様にリソース確保量の少ないノードをスケールインする機能も備えていて、スケールイン時にはDrainをして安全にワークロードを退避させることができます。
AWSでkubernetes/autoscalerを利用する際に注意しておかなければならないのは、AWSのAutoScalingGroupにはAvailability Zone間で台数を均等に保つための挙動があるということです。 この処理はAutoScalingGroup側で行われるため、ノードのDrainが行われずPodを安全に終了することができません。 これを防ぐためにはAvailabilityZoneごとにNodeGroupを分ける必要があります。
また前述のスポットインスタンスの活用に際してもスポットインスタンス用のNodeGroupを作成していて、kubernetes/autoscalerの持つexpanderという機能を利用してスポットインスタンスが優先的に利用されるように設定しています。
kubernetes-sigs/descheduler
前述のkubernetes/autoscalerを利用してノードのスケールアウトをしていると、スケールアウト直後にノード間でリソース確保量にばらつきが出るという問題があります。 極端にリソース確保量の多いノードが存在している場合、そのノードでQoSの低いPodのEvictが起きる可能性が高くなってしまいます。 その問題を解決するのがdeschedulerです。
ポリシーを設定することで閾値を超えたノードからPodを対象のノードに移動させることができます。
RemoveDuplicates
を有効にすることで同一ワークロードのPodが複数同じノードにある場合に退避させることもできますが、そういった制約を持たせたい場合は素直に antiAffinity
を設定した方がいいため利用していません。
istio/istio
IstioはEnvoyをデータプレーンとして利用するサービスメッシュで、Kubernetesクラスタに対して優れたTraffic Managementや高いObservabilityを提供します。
Istioに関しては過去のエントリで触れているのでこちらも併せてご覧ください。
我々のチームでは開発者がKubernetes Manifestを書く労力を削減するため、Kubernetesを利用するにあたってのプラクティスを盛り込んだManifestを生成するツールを提供しています。
このツールから生成されるManifestではIstioの利用が前提となっており、サービス間通信のCircuit BreakerやRetrying, Timeoutなどがデフォルトで有効化されるようになっていて、これにより多くのmicroservicesが安全に稼働するようになりました。
同様にIstioレイヤでのアクセスロギングも有効化されるようになっており、このツールによって開発者が意識することなくLIFULL共通のアクセスログフォーマットでログが吐けるようになっています。 アクセスログフォーマットが共通されたことでアプリケーション実行基盤として共通のログ分析を提供するにあたっても役立っています。
安くない運用コストを割いてIstioを導入しても実際に使われなければ絵に描いた餅となってしまいます。 このようにManifest生成ツールと組み合わせることで社内でIstioが広く利用される状況が作られました。
prometheus/prometheus
PrometheusはCNCFのGraduated Projectのモニタリングシステムです。
KubernetesのService Discoveryを使ってメトリクスを収集することができるためKubernetesとの親和性が高く、余計なエージェントを入れることなくクラスタの監視を開始することができます。
我々のチームでは新規開発プロジェクト向けのGitHubのTemplate Repositoryを提供していて、このTemplate Repositoryを利用してリポジトリを作成することでスムーズにKubernetesクラスタにアプリケーションをデプロイできるようになっています。
このTemplate RepositoryにはPrometheusのアラートのルールの自動生成の機能が備わっており、開発者が簡単にPrometheusでアラートの設定ができるようになっています。
また、Prometheusは単体で長期のメトリクスを保存することは想定されておらず、長期のメトリクスを保存するためには後述のThanosのようなソフトウェアを別途用意する必要がある点にご注意ください。
prometheus/alertmanager
AlertmanagerはPrometheusから送信されてきたアラートをSlackやWebhookなどを通して通知するためのコンポーネントです。
前述のアラートのルール自動生成時に適切なSeverityを付与するようにしていて、AlertmanagerではSeverityに応じて適切な頻度で適切なSlackチャンネルにアラートを通知するようになっています。
またAmazon SNSをイベントソースとしてLambda FunctionによるAmazon Connectを利用したオンコールシステムも開発していて、Alertmanagerと組み合わせることで必要に応じてオンコール担当者へ電話での通知も可能となっています。
knative
KnativeはIstioなどのサービスメッシュ上に構築する、いわゆるサーバレスワークロードを実行するためのソフトウェアです。
イベント駆動のワークロードや、前述のAlertmanagerのWebhookと連携させてアラートに伴うオペレーションの自動化に利用しています。
Kafkaを用いたChannelの永続化やメッセージングの提供も準備しているため、今後多くのワークロードで利用していく予定です。
thanos-io/thanos
ThanosはCNCFのSandbox ProjectのPrometheus向けLong Term Storageです。
Object Storageにメトリクスを格納することで長期のメトリクスを安価に保存できるという特徴を持ちます。 多くのクラウドプロバイダはS3やGCSのようなObject Storageを提供しているため単にそれらを利用するだけになります。
同種のソフトウェアの場合、自前で永続化されたストレージを用意する必要があったりCassandraやDynamoDBのようなデータストアを要求されますが、Thanosの場合は比較的セットアップが容易という点もメリットです。
ただし、Indexのキャッシュにmemcachedを利用できるものの同種のソフトウェアと比較して相対的にメトリクス取得の速度が遅くなるため、Thanos側でサポートしているShardingを利用しながら高速化に努める必要があります。 また、長期のメトリクスを用いたPrometheusのRuleの評価に関してもいい方法がなくこれも課題の一つです。
Long Term Storageであると共にPrometheusの冗長性の問題も解決していて、重複メトリクス除去の機能を備えているため複数台のPrometheusを立てて冗長化しつつ、Thanosを介してクエリすることで重複を除去して結果を返すことができます。
grafana/grafana
GrafanaはPrometheusなどをバックエンドとするVisualizationツールです。
先日リリースされたGrafana v7ではJaegerやZipkinといった分散トレーシングもバックエンドとして利用できるようになり、ログストレージにも同様に対応していることからモニタリングに関する一切の情報をGrafanaで表示することができるようになりました。
我々のチームでは開発者が簡単にダッシュボードを作成できるようgrafonnet-libというGrafanaコミュニティが提供しているライブラリをベースにJsonnetの整備も進めており、現在全てのダッシュボードがJsonnetで管理されています。 モニタリングのプラクティスに則ったダッシュボードのパネルを多く提供していて、開発者は必要なパネルをJonnnet形式で配置するだけで洗練されたダッシュボードを手に入れることができます。
tricksterproxy/trickster
TricksterはComcastが開発するPrometheusのような時系列データベースの前段に置くキャッシュサーバです。
前述のようにThanosは運用が簡単な一方でメトリクスの取得が遅いため、このTricksterを挟むことで高速化を図っています。
あらかじめよく使われるGrafanaのダッシュボードから叩かれるクエリのキャッシュを裏で生成しておくことで、ダッシュボード閲覧時の体験をよくするような工夫もしてきました。
DirectXMan12/k8s-prometheus-adapter
DirectXMan12/k8s-prometheus-adapterはKubernetesのHorizontal Pod AutoscalerでPrometheusのメトリクスを利用するための custom.metrics.k8s.io
実装です。
CPUの使用率でのスケールアウトが適さないケースは多くあり、そういったニーズに応えるべくPrometheusのメトリクスを利用できるようにしています。
例えば以前のエントリで紹介したGitHub ActionsのSelf-hosted Runnerもin-queueなJobの数をPrometheusのメトリクスとして公開しておいて、その値を元にスケールアウトさせています。
実験的な試みとしてあるメトリクスの未来の予測値をもとにしたスケールアウトも始めていて、これも同様に時系列分析の結果をPrometheusに格納することで実現しているなど、こういった柔軟なスケールには必要不可欠なコンポーネントです。
kubernetes-sigs/metrics-server
metrics-serverは kubectl top
やHPAを用いたCPUによるスケールをする際に必要なコンポーネントで、元々使われていたHeapsterというソフトウェアの後継です。
kubelet内のcAdvisorからメトリクスを収集してAPIを通してそのメトリクスをクラスタに対して公開する役目を持ちます。
Prometheusは直接cAdvisorからメトリクスを取得することができるのでHPAでCPUによるスケールをさせたい場合は前述のprometheus-adapterを用いることで不要にも思えますが、 kubectl top
などでも利用されるため基本的には導入することになると思います。
Kubernetesのメトリクスパイプラインは少し複雑なのでこちらの資料がおすすめです。
kubernetes/kube-state-metrics
kube-state-metricsはKubernetesのObject単位でのメトリクスを生成するコンポーネントです。
似たような名前のmetrics-serverとは似て非なるもので、metrics-serverはkubeletをデータソースとするのに対し、kube-state-metricsではkube-apiserverをデータソースとし、例えばPodのCPUのRequestsやDeploymentのReplicasなど、Kubernetes Resourceに関するメトリクスを公開します。
その点についてはドキュメントでも注記されています。
HPAのMaxに達してしまっている、DeploymentのRolloutが止まっているなどといったアラートは運用する上で必須のため、こちらもmetrics-serverと同様に基本的には導入することになるでしょう。
kubernetes/node-problem-detector
node-problem-detectorはその名の通りNodeの異常を検知するためのPrometheus Exporterです。
基本的には /dev/kmsg
をデータソースとしたKernelレベルでの異常の検知に利用され、例えば task xxx blocked for more than 120 seconds
のようなハングやOOM Killingなどを検知することができます。
Prometheus向けにメトリクスも提供するため、既存のPrometheusによる監視にこういったKernelレベルのモニタリングを組み込むことができます。
prometheus/node_exporter
node_exporterはNode単位でのメトリクスを公開するためのPrometheus Exporterです。
Node全体でのCPUやネットワークの利用量に加えて、PSIに対応しているKernelであればCPUやメモリのPressureを公開することができます。
PSIとはLinux Kernelの機能であるPressure Stalled Informationの略で、詳しくは以下をご覧ください。
PSI - Pressure Stall Information — The Linux Kernel documentation
これは有名なモニタリングの方法論USEメソッドで監視を推奨されているSaturation、つまりリソースの飽和状態を表すメトリクスで、PSIから取得したメトリクスを監視することでリソースのOvercommitによるパフォーマンスの低下に気づくことが可能となります。
参考
kubecost/cost-model
kubecost/cost-modelはクラウドプロバイダのインスタンスタイプに応じたインスタンスの金額を公開するPrometheus Exporterです。
クラスタ上に存在するNodeの一覧を取得し、Nodeそれぞれのインスタンスタイプからインスタンスの料金、CPU1コアあたりメモリ1GBあたりの推定料金をメトリクスとして生成します。
これを用いて各アプリケーションのダッシュボードに推定の月次コストを表示するようにして、開発者に日々コストを意識してもらうようにしています。
金額はフィクションです。
kaidotdev/kube-trivy-exporter
kaidotdev/kube-trivy-exporterは拙作のクラスタ内に存在する全てのDocker Imageに対してaquasecurity/trivyを実行し、脆弱性情報を公開するPrometheus Exporterです。
社内のセキュリティエンジニアから要望を受けて開発し、申し訳程度にOSSとして公開しているものです。
これも同様に各アプリケーションのダッシュボードに脆弱性情報を表示するために利用していて、開発者が日頃から脆弱性を意識するような文化づくりに貢献しています。
fluent/fluentd
fluentdは言わずと知れたログコレクタで、具体的にはfluentd-kubernetes-daemonsetをデプロイしてNodeのログを集約しています。
LIFULLではログから得られたレスポンスタイムの統計情報をダッシュボードで閲覧するためにPrometheusのメトリクスとして公開したり、ログフォーマットの統一や各種変換処理をfluentdのレイヤで行なっているため、スケールさせたい単位でfluentdクラスタを分けてfluentd-kubernetes-daemonsetを入り口として多段のログパイプラインを構築しています。
ここでログが欠損するとクラスタ全体のログが失われてしまうため、bufferの設定には気を使う必要があります。
kaidotdev/events-logger
こちらも拙作のコンポーネントで、kaidotdev/events-loggerはKubernetesのEventsを単に標準出力するだけのものです。
もともと監視にDatadogを使っていたのでKubernetesのEventsをDatadogで確認することができていたのですが、Prometheusに移行してKubernetes Eventsを集約して監視することができなくなったため開発しました。
fluentd側でその情報をPrometheusにも公開していて、ログの監視と同じようにKubernetes Eventsを監視できるようにしています。
例えばContainerGCFailedといったNodeの異常を示すものや、大量のNodeNotReadyの発生に気付けるようアラートを仕込んでいます。
最後に
このようにKubernetesをProduction Gradeで運用しようと思うと多くのソフトウェアの導入が必要となります。
一部はDatadogのようなサービスを利用することで置き換えることはできますが、これ以外にもデプロイやセキュリティ、内部統制など考えること多くあります。
導入をお考えの方には慎重な検討の手助けに、既に導入済みの方には少しでも参考になれば嬉しいです。