kakakakakku blog

Weekly Tech Blog: Keep on Learning!

kubectl drain コマンドのオプションを試す : --ignore-daemonsets と --force

Kubernetes で kubectl drain コマンドを実行すると,指定したノード上の Pod を安全に削除して,そのノードには新しく Pod をスケジューリングしないようにステータスを変更できる.例えば,Kubernetes クラスターの運用として,ノードをメンテナンスするときなどに kubectl drain コマンドを使う.詳しくは以下のドキュメントに載っている.

今回の記事では kubectl drain コマンドの挙動やオプションを試しながら理解を深めていく.以下はサンプルとして kubectl drain コマンドを実行する流れをアニメーション GIF にしてみた!🎨

f:id:kakku22:20210210163033g:plain

準備 : Kubernetes クラスターを構築する 🐳

今回は kind を使って,Mac 上に検証用の Kubernetes クラスター(複数ノード構成)を構築する.バージョンは v1.19.1 を使う.まず,以下のように kind-config.yaml を作る.

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
- role: worker

次に kind create cluster コマンドを実行する.ノードは kind-workerkind-worker2kind-worker3 となる.

$ kind create cluster --config kind-config.yaml

$ kubectl get nodes
NAME                 STATUS   ROLES    AGE     VERSION
kind-control-plane   Ready    master   5m40s   v1.19.1
kind-worker          Ready    <none>   5m      v1.19.1
kind-worker2         Ready    <none>   5m      v1.19.1
kind-worker3         Ready    <none>   5m      v1.19.1

kind に関しては以下の記事に詳しくまとめてあるので合わせて読んでもらえると!

kakakakakku.hatenablog.com

--ignore-daemonsets オプションを試す 🐳

まず,Deployment で nginx の Pod を3個起動する.kubectl get pods -o wide の結果から,3個の Pod が全て異なるノードで起動していることが確認できる.

$ kubectl apply -f deployment.yaml
deployment.apps/sandbox-drain-nginx created

$ kubectl get pods -o wide
NAME                                  READY   STATUS    RESTARTS   AGE   IP           NODE           NOMINATED NODE   READINESS GATES
sandbox-drain-nginx-f9f7485cd-6frr4   1/1     Running   0          30s   10.244.1.3   kind-worker2   <none>           <none>
sandbox-drain-nginx-f9f7485cd-cldp5   1/1     Running   0          30s   10.244.2.3   kind-worker3   <none>           <none>
sandbox-drain-nginx-f9f7485cd-h2b27   1/1     Running   0          30s   10.244.3.3   kind-worker    <none>           <none>

さっそく kubectl drain コマンドを実行して,ノード kind-worker をドレインする.すると error: cannot delete DaemonSet-managed Pods というエラーが出てしまう.

$ kubectl drain kind-worker
node/kind-worker cordoned
error: unable to drain node "kind-worker", aborting command...

There are pending nodes to be drained:
 kind-worker
error: cannot delete DaemonSet-managed Pods (use --ignore-daemonsets to ignore): kube-system/kindnet-7xzxc, kube-system/kube-proxy-c8m58

デフォルトの挙動では,ノードに DaemonSet 経由で作られた Pod が起動しているとエラーになる.今回の検証環境だと kindnetkube-proxDaemonSet として起動している.エラーメッセージにも出ている通り,--ignore-daemonsets オプションを使う必要がある.多くの場面で使うオプションだと思う.なお kubectl drain コマンドに失敗しても,ノード自体は SchedulingDisabled ステータスになってしまうため,kubectl uncordon コマンドで戻しておく.

$ kubectl get daemonsets -n kube-system
NAME         DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
kindnet      4         4         4       4            4           <none>                   20m
kube-proxy   4         4         4       4            4           kubernetes.io/os=linux   20m

$ kubectl uncordon kind-worker
node/kind-worker uncordoned

もう1度 kubectl drain コマンドを実行する.今度は --ignore-daemonsets オプションを使う.すると,正常にドレインできた.また kubectl get nodes コマンドの結果からわかる通り,ノード kind-worker のステータスは Ready,SchedulingDisabled となり,新しく Pod をスケジューリングしないようになっている.そして kubectl get pods -o wide コマンドの結果から,新しい Pod はノード kind-worker2 で起動していることもわかる.最後は kubectl uncordon コマンドでステータスを戻しておく.

$ kubectl drain kind-worker --ignore-daemonsets
node/kind-worker cordoned
WARNING: ignoring DaemonSet-managed Pods: kube-system/kindnet-7xzxc, kube-system/kube-proxy-c8m58
evicting pod local-path-storage/local-path-provisioner-78776bfc44-95lwf
evicting pod default/sandbox-drain-nginx-f9f7485cd-h2b27
pod/sandbox-drain-nginx-f9f7485cd-h2b27 evicted
pod/local-path-provisioner-78776bfc44-95lwf evicted
node/kind-worker evicted

$ kubectl get nodes
NAME                 STATUS                     ROLES    AGE   VERSION
kind-control-plane   Ready                      master   30m   v1.19.1
kind-worker          Ready,SchedulingDisabled   <none>   30m   v1.19.1
kind-worker2         Ready                      <none>   30m   v1.19.1
kind-worker3         Ready                      <none>   30m   v1.19.1

$ kubectl get pods -o wide
NAME                                  READY   STATUS    RESTARTS   AGE     IP           NODE           NOMINATED NODE   READINESS GATES
sandbox-drain-nginx-f9f7485cd-6frr4   1/1     Running   0          5m40s   10.244.1.3   kind-worker2   <none>           <none>
sandbox-drain-nginx-f9f7485cd-cldp5   1/1     Running   0          5m40s   10.244.2.3   kind-worker3   <none>           <none>
sandbox-drain-nginx-f9f7485cd-lc24j   1/1     Running   0          60s     10.244.1.4   kind-worker2   <none>           <none>

$ kubectl uncordon kind-worker
node/kind-worker uncordoned

--force オプションを試す 🐳

今度は Deployment は使わずに nginx の Pod を単体で1個起動しておく.ここで kubectl drain コマンドを実行して,ノード kind-worker をドレインすると,今度は error: cannot delete Pods not managed by ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet というエラーが出てしまう.

$ kubectl apply -f pod.yaml
pod/sandbox-drain-nginx created

$ kubectl get pods -o wide
NAME                  READY   STATUS    RESTARTS   AGE   IP           NODE          NOMINATED NODE   READINESS GATES
sandbox-drain-nginx   1/1     Running   0          30s   10.244.3.4   kind-worker   <none>           <none>

$ kubectl drain kind-worker --ignore-daemonsets
node/kind-worker cordoned
error: unable to drain node "kind-worker", aborting command...

There are pending nodes to be drained:
 kind-worker
error: cannot delete Pods not managed by ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet (use --force to override): default/sandbox-drain-nginx

$ kubectl uncordon kind-worker
node/kind-worker uncordoned

エラーメッセージにも出ている通り,ReplicaSet などを使わずに単体で Pod を起動している場合にドレインできないようになっている.そこで --force オプションを使う必要がある.ポイントとしては,あくまで Pod を単体で起動しているため,削除された Pod を復旧する必要がある.もう1度起動したり,そもそも Deployment を使うべきだと思う.

$ kubectl drain kind-worker --ignore-daemonsets --force
node/kind-worker cordoned
WARNING: deleting Pods not managed by ReplicationController, ReplicaSet, Job, DaemonSet or StatefulSet: default/sandbox-drain-nginx; ignoring DaemonSet-managed Pods: kube-system/kindnet-7xzxc, kube-system/kube-proxy-c8m58
evicting pod default/sandbox-drain-nginx
pod/sandbox-drain-nginx evicted
node/kind-worker evicted

$ kubectl get nodes
NAME                 STATUS                     ROLES    AGE   VERSION
kind-control-plane   Ready                      master   40m   v1.19.1
kind-worker          Ready,SchedulingDisabled   <none>   40m   v1.19.1
kind-worker2         Ready                      <none>   40m   v1.19.1
kind-worker3         Ready                      <none>   40m   v1.19.1

$ kubectl get pods -o wide
No resources found in default namespace.

$ kubectl uncordon kind-worker
node/kind-worker uncordoned

まとめ

今回は kubectl drain コマンドで使えるオプションとして --ignore-daemonsets--force を試した.他にも Pod を終了するまでの「猶予時間(秒)」を設定する --grace-period や,ドレイン中の「タイムアウト時間(秒)」を設定する --timeout などもある.実際に現場で kubectl drain コマンドを使う場合は PodDisruptionBudget (PDB) と組み合わせることも多いと思う.PodDisruptionBudget (PDB) に関してはまた今度記事を書く予定!

kubernetes.io

マイクロサービス化を目指すなら移行パターンを学ぼう /「モノリスからマイクロサービスへ」を読んだ

2020年12月に出版された「モノリスからマイクロサービスへ」を読んだ.本書はタイトルの通り「マイクロサービス移行」に関連するトピックにフォーカスしている.マイクロサービスを学ぶならこの本!とよく紹介している「マイクロサービスアーキテクチャ」の著者 Sam Newman の続編となる.原著「Monolith To Microservices」は,2019年12月に出版されている.

僕自身は技術講師として「マイクロサービス」に関連した研修を担当していることもあり,本書は絶対に読もう!と楽しみにしていた(原著は読もう読もうと積読していた😇).今回は本書の翻訳を担当された島田さん (@snoozer05) とレビューを担当されたこまさん(@koma_koma_d) からご連絡をいただき,本書を献本していただいた.本当にありがとうございます!

モノリスからマイクロサービスへ ―モノリスを進化させる実践移行ガイド

モノリスからマイクロサービスへ ―モノリスを進化させる実践移行ガイド

  • 作者:Sam Newman
  • 発売日: 2020/12/26
  • メディア: 単行本(ソフトカバー)

目次

マイクロサービス化を目指すときに頭を悩ますトピックの1つはやはり「移行」だと思う.マイクロサービスを新規構築することも難しいけど,歴史のあるモノリシックアプリケーションを分割して本番障害にも気を付けながら移行することにはまた別の難しさがある.本書は以下に載せた目次からもわかる通り,本当に「移行」に関連するトピックにフォーカスしている.マイクロサービス化を目指すなら必読と言えそう!

  • 1章「必要十分なマイクロサービス」
    • 1.1 マイクロサービスとは
    • 1.2 モノリス
    • 1.3 結合と凝集
    • 1.4 必要十分なドメイン駆動設計
    • 1.5 この章のまとめ
  • 2章「移行を計画する」
    • 2.1 目的を理解する
    • 2.2 マイクロサービスを選択する理由
    • 2.3 マイクロサービスが悪いアイデアのとき
    • 2.4 トレードオフ
    • 2.5 みんなを連れていく
    • 2.6 組織を変革する
    • 2.7 段階的に移行していくことの重要性
    • 2.8 変更のコスト
    • 2.9 どこから始めればよいか?
    • 2.10 ドメイン駆動設計
    • 2.11 組み合わせモデル
    • 2.12 チームを再編成する
    • 2.13 うまくいっているかどうかを分かるには?
    • 2.14 この章のまとめ
  • 3章「モノリスを分割する」
    • 3.1 モノリスを変更すべきかどうか
    • 3.2 移行パターン
    • 3.3 パターン:ストラングラーアプリケーション
    • 3.4 機能を移行しながら振る舞いを変える
    • 3.5 パターン:UI 合成
    • 3.6 パターン:抽象化によるブランチ
    • 3.7 パターン:同時実行
    • 3.8 パターン:デコレーティングコラボレーター
    • 3.9 パターン:変更データキャプチャ
    • 3.10 この章のまとめ
  • 4章「データベースを分割する」
    • 4.1 パターン:共有データベース
    • 4.2 しかし、できない!
    • 4.3 パターン:データベースビュー
    • 4.4 パターン:データベースをラップするサービス
    • 4.5 パターン:サービスのインターフェイスとしてのデータベース
    • 4.6 所有権を移す
    • 4.7 データ同期
    • 4.8 パターン:アプリケーションでのデータ同期
    • 4.9 パターン:トレーサー書き込み
    • 4.10 データベースを分割する
    • 4.11 データベースとコードのどちらを最初に分割するか
    • 4.12 スキーマ分割の例
    • 4.13 パターン:テーブルの分割
    • 4.14 パターン:外部キーのコードへの移動
    • 4.15 トランザクション
    • 4.16 サーガ
    • 4.17 この章のまとめ
  • 5章「成長の痛み」
    • 5.1 サービスが増えれば痛みも増える
    • 5.2 所有権のスケール
    • 5.3 破壊的変更
    • 5.4 レポーティング
    • 5.5 監視とトラブルシューティング
    • 5.6 開発者体験
    • 5.7 あまりにも多くのものを実行している
    • 5.8 エンドツーエンドテスト
    • 5.9 全体最適と局所最適
    • 5.10 堅牢性と回復性
    • 5.11 孤児サービス
    • 5.12 この章のまとめ
  • 6章「終わりに」
  • 付録 A「パターン一覧」

全体的に読みやすく,気になる誤植もなかった.1点あるとすれば「分割」「分解」という言葉が混在して使われていて,意図的に使い分けているのか,原著の英単語に依存しているのか,判断できなかった.今回の書評記事においては,引用箇所を除いて「分割」に統一する.

www.oreilly.co.jp

マインドセット

本書を読む前に「2章 : 移行を計画する (P36)」に載っている以下の文章を読んでおくと良いと思う.何となく本書を読んでしまうと「なぜ移行パターンがこんなにあるのだろう?」「なぜここまで検討する必要があるのだろう?」「モノリシックアプリケーションに戻るべきでは?」と感じる場面があるかもしれない.いや誰しも感じるだろう.それほどに「マイクロサービス移行」に関連するトピックを深く学べるため,モチベーションを高く本書を読み進めていくことが重要だと思う.

マイクロサービスを使う理由を明確に持っていないことの問題点はさまざまだ。マイクロサービス化は、そのために投じる人員や費用という直接的な意味でも、フィーチャーの追加よりも移行作業を優先するという意味でも、多額な投資が必要になる。これは、移行のメリットを実感するまでに時間がかかるという事実によって、さらに状況を複雑にする。特として、それは移行に1年以上かけているにもかかわらず、なぜそれを始めたのかを思い出せなくなってしまうという状況へとつながってしまう。これはサンクコストの錯誤ではない。

達成したいことは何か?

「2章 : 移行を計画する」には他にも重要な内容が多く載っている.まず冒頭に「3つの重要な質問」が載っている.これは著者がマイクロサービス化を支援するときに実際に使っている質問とのことで,すぐに試せる.アジャイル開発でよく出てくる「エレベーターピッチ」「完了の定義」にも似ていて,マイクロサービス化を目指すメンバー間で認識を合わせておくことが重要だと思う.

  • ✅ 達成したいことは何か?
  • ✅ マイクロサービスの他に代替案はなかったか?
  • ✅ どうすれば移行がうまくいってるか分かるだろうか?

次に「マイクロサービスを選択する理由」「マイクロサービスが悪いアイデアのとき」という解説があり,特に「悪いアイデアのとき」は知っておくと良さそう.特に「サービス境界」を明確に見極められなかったり,プロダクトの需要を探るために多くのアイデアを試行錯誤しているフェーズのスタートアップではマイクロサービス自体が不適切になる可能性もある.

  • 😐 不明瞭なドメイン
  • 😐 スタートアップ
  • 😐 顧客の環境にインストールして管理するソフトウェア
  • 😐 もっともな理由を持たないとき

個人的には以下の文章には ( ゚д゚)ハッ! と気付かされた.過去の経験もあり「移行」こそ難しいと感じていたけど,あくまでこれは「技術的(設計思想的)な観点」「サービス継続的な観点」で考えていた.もし「適切にサービス境界を見極めて分割できるか」という観点で考えるとモノリシックアプリケーションでドメインに精通している方が良さそう.気付けて良かった!

システムを早々にマイクロサービスに分解すると、特にそのドメインに慣れていない場合には、コストがかさんでしまうことがある。多くの点で、既存のコードベースをマイクロサービスに分解することは、最初からマイクロサービスに向かおうとするよりもはるかに簡単だ。

組み合わせモデル

さらに「2章 : 移行を計画する」を読み進めていくと「どこから始めればよいか?」という内容も出てくる.「ドメイン駆動設計」を使うことに前提に「イベントストーミング」でドメインモデルを洗い出したり,ドメイン関係図を作って依存の少ないサービスから分割したり,Music Corp という題材で学べる.誰しも「影響が少ないサービスから分割する」と考えると思うけど,本書に以下のように書いてあって,本質を見失わずにマイクロサービス化を目指す難しさを再認識できた.そこで最終的には「バランス」となり「組み合わせモデル」も紹介されている.「分解しやすさ」「分解による利益」の2軸でマッピングし,どちらも高いサービスを「分解の良い候補」に決める.

しかし、市場投入時間を短縮することが目標で、請求書機能がほとんど変更されない機能である場合には、請求書機能の抽出は時間の使い方として適していないかもしれない。したがって、何が簡単で何が難しいかという視点と、マイクロサービス分割がもたらすメリットという視点を組み合わせる必要がある。

関連する内容は前に紹介したブログ「How to break a Monolith into Microservices」にも載っている.合わせて読んでもらえると良さそう!

kakakakakku.hatenablog.com

また本書で紹介されていた「イベントストーミング」も気になる.Leanpub で購入できる「Introducing EventStorming」を読んでみよう.

leanpub.com

パターン一覧

「3章 : モノリスを分割する」からはマイクロサービス化に使える「移行パターン」の話が続く.その前に最後に載っている「付録 A : パターン一覧」を見ておくと良いと思う.本書で紹介されているパターン一覧をザッと確認できる.マイクロサービス移行に使えるテクニックに「パターン」として名前が付いていることに価値がある.特に以下は印象に残っている.

  • ストラングラーアプリケーション (Strangler fig application)
  • UI 合成 (UI composition)
  • 抽象化によるブランチ (Branch by abstraction, BBA)
  • 同時実行 (Parallel run)
  • デコレーティングコラボレーター (Decorating collaborator)
  • データベースをラップするサービス (Database wrapping service)
  • トレーサー書き込み (Tracer write)
  • 複数スキーマストレージ (Multischema storage)
  • 外部キーのコードへの移動 (Move foreign-key relationship to code)

なお,移行以外に「マイクロサービス」に関連するパターンは「A pattern language for microservices」に多く載っている.特に関係性まで整理されていて素晴らしい.何度も何度も見直している.

microservices.io

ストラングラーアプリケーション (Strangler fig application)

「ストラングラーアプリケーション」はよく知られていると思う.2019年に名称が「Strangler」から「Strangler Fig Application」に変更された経緯もあり,本書でも「ストラングラーアプリケーション」と表記されている.イチジクの木の特性を比喩にして「古いサービスと新しいサービスを共存させつつ置き換えていく」パターンと言える.本書では「ストラングラーアプリケーション」だけで約20ページもあり,詳細に解説されている.図解もあってわかりやすい.

martinfowler.com

抽象化によるブランチ (Branch by abstraction, BBA)

「ストラングラーアプリケーション」に関連して,もし既存システムの中で参照依存の多いサービスを置き換える場合は「抽象化によるブランチ (Branch by abstraction, BBA)」が使える.このパターンは Martin Fowler のサイトにも載っていたのに今まで知らなかった.「抽象化」は英語だと「Abstraction」になり,以下のステップで構成されている.古い実装と新しい実装を混在できるため,新しい実装を進める Git ブランチが長い期間残り続けることもなく,どんどんと master (main) に merge できる.よって「トランクベース開発」とも相性が良いと言える.本書には「抽象化によるブランチ」のわかりやすい図解もある.

  1. 置き換える機能の抽象を作る
  2. 作成した抽象を使用するように既存機能のクライアントを変更する
  3. 機能を改良した抽象の実装を新たに作る(この新しい実装が新しいマイクロサービスを呼び出す)
  4. 新しい実装を使用するように抽象を切り替える
  5. 抽象を後始末し古い実装を削除する

martinfowler.com

同時実行 (Parallel run)

「ストラングラーアプリケーション」「抽象化によるブランチ」では,新しい実装を試して問題があれば切り戻すこともできるけど,よりハイリスクなサービスでは「同時実行 (Parallel run)」が使える.このパターンは古い実装と新しい実装をどちらも並行で実行して,結果を比較する(信頼する結果は古い実装).実装コストやパフォーマンス劣化などは気になるけど,本番環境で長い期間検証し続けられるという意味では使えそうな場面もありそう.他には「5% のみ」など,部分的にトラフィックを切り替えるなら「カナリアリリース」も使える.このように「同時実行」「カナリアリリース」「ダークローンチ」などをまとめた「プログレッシブデリバリー」という最近よく聞く言葉まで紹介されていて良かった.

なお「ストラングラーアプリケーション」「カナリアリリース」を組み合わせた具体的な移行例としては,AWS Summit New York の資料「Migrating monolithic applications with the strangler pattern」が参考になるので載せておく.

www.slideshare.net

データベースを分割するパターン

本書で存在感のある章は「4章 : データベースを分割する」かもしれない.移行の中でも難しく,モノリシックアプリケーションの歴史的な背景(技術的負債も含む)により分割しにくいことも多いと思う.本書では「データベース」に関連するトピックだけで約80ページも解説されている.研修でも「どうやってデータベースを分割すれば良いか?」「どうやって RDB から NoSQL に置き換えれば良いか?」という質問がある.今までに読んだ資料の中では「Managing Data in Microservices」は特に参考になるので載せておく.

www.slideshare.net

本書では「データベースを分割せずに工夫するパターン」「データベースを分割するパターン」が紹介されている.現実的にデータベースを分割できない場面もあるため,例えば「データベースをラップするサービス (Database wrapping service)」を使って,アプリケーションからデータベースを直接参照させずにサービス経由にするパターンがある.本書には「権限アプリケーションデータ」の例が載っていて参考になった.

他には「外部キーのコードへの移動 (Move foreign-key relationship to code)」もよく話題になるトピックだと思う.簡単に言えば,外部参照でテーブル同士が関連しているときに,データベースを直接参照するのではなく,アプリケーション側で API を使うように変更する.レイテンシーは気になるけど,要件次第ではあるし,そもそも「API 経由でデータを取得する」という実装に変化できることには大きい価値があると思う.特に JOIN に依存している場合など.

サーガ (Saga)

「4章 : データベースを分割する」の最後には「分散トランザクション」としてよく知られている「サーガ (Saga)」の解説が載っている.非同期的に整合性を取るようにトランザクションを実装しつつ,障害発生時には,再試行をしながら処理を継続する「前方回復」と,障害を元に戻す「後方回復」から選択できる.戻す場合は「補償トランザクション」と呼ばれる処理を実行する.

また「サーガ」には,コーディネーターを使う「オーケストレーションベースのサーガ」と運用責任を分散した「コレオグラフィベースのサーガ」の紹介があり,どちらも詳細に解説されている.また microservices.io も合わせて読んでおくと良いと思う.

microservices.io

本書に載っていた以下の表現は秀逸だった.「指揮と統制」「信頼と検証」!研修でも使えそう!

オーケストレーションベースのサーガが指揮と統制のアーキテクチャなら、コレオグラフィベースのサーガは信頼と検証のアーキテクチャだ。

まとめ

「モノリスからマイクロサービスへ」を読んだ.素晴らしかった!繰り返し読み直すことになりそう.本書はタイトルの通り「マイクロサービス移行」に関連するトピックにフォーカスしている.トピックが狭すぎると感じるかもしれないけど,それほどにマイクロサービス化を目指すチームにとって難しいトピックだし,指針となるパターンが1冊の本にまとまっていて,さらに日本語で読めることに価値がある.

本書で「パターン」を学んで,マイクロサービス化に成功した企業の事例などと組み合わせれば,今からマイクロサービス化に挑戦するチームに役立つと思う.個人的にもマイクロサービスに関連する研修で紹介できる内容が多くあった.

マイクロサービス化を目指すなら必読の1冊!読むべし!

モノリスからマイクロサービスへ ―モノリスを進化させる実践移行ガイド

モノリスからマイクロサービスへ ―モノリスを進化させる実践移行ガイド

  • 作者:Sam Newman
  • 発売日: 2020/12/26
  • メディア: 単行本(ソフトカバー)

Deployment に revisionHistoryLimit を設定して ReplicaSet のリビジョン数を制御する

Kubernetes で Deployment を使うときにマニフェストを修正して kubectl apply コマンドを繰り返し実行すると,ReplicaSet のリビジョンが増えていく.サンプルとして Deploymentimagenginx:1.8-alpinenginx:1.9-alpinenginx:1.10-alpine と修正しながらマニフェストを適用していくと以下のようになる.kubectl rollout history コマンドを使うとリビジョンも確認できる.また kubectl apply--record オプションを付けると操作履歴を記録できる.

# nginx:1.8-alpine
$ kubectl apply -f deployment.yaml --record
deployment.apps/sandbox-deployment-revision-history-limit created

# nginx:1.9-alpine
$ kubectl apply -f deployment.yaml --record
deployment.apps/sandbox-deployment-revision-history-limit configured

# nginx:1.10-alpine
$ kubectl apply -f deployment.yaml --record
deployment.apps/sandbox-deployment-revision-history-limit configured

$ kubectl rollout history deployment sandbox-deployment-revision-history-limit
deployment.apps/sandbox-deployment-revision-history-limit
REVISION  CHANGE-CAUSE
1         kubectl apply --filename=deployment.yaml --record=true
2         kubectl apply --filename=deployment.yaml --record=true
3         kubectl apply --filename=deployment.yaml --record=true

$ kubectl get deployments,replicasets
NAME                                                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/sandbox-deployment-revision-history-limit   3/3     3            3           91s

NAME                                                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/sandbox-deployment-revision-history-limit-769f75d55b   0         0         0       90s
replicaset.apps/sandbox-deployment-revision-history-limit-7b94db8c49   0         0         0       51s
replicaset.apps/sandbox-deployment-revision-history-limit-7f87f5f547   3         3         3       28s

また Deploymentkubectl rollout undo コマンドを使うと過去のリビジョンにロールバックできる.さらに --to-revision オプションと組み合わせれば,指定したリビジョンにロールバックすることもできる.ロールバックをしてもリビジョンは増えていく.

$ kubectl rollout undo deployment sandbox-deployment-revision-history-limit
deployment.apps/sandbox-deployment-revision-history-limit rolled back

$ kubectl rollout undo deployment sandbox-deployment-revision-history-limit --to-revision 1
deployment.apps/sandbox-deployment-revision-history-limit rolled back

$ kubectl rollout history deployment sandbox-deployment-revision-history-limit
deployment.apps/sandbox-deployment-revision-history-limit
REVISION  CHANGE-CAUSE
3         kubectl apply --filename=deployment.yaml --record=true
4         kubectl apply --filename=deployment.yaml --record=true
5         kubectl apply --filename=deployment.yaml --record=true

リビジョン数のデフォルト値は 10

残る ReplicaSet のリビジョン数は Deployment.spec.revisionHistoryLimit フィールドで設定できる.デフォルトは 10 となる.ドキュメントの表現だと「古いリビジョン (old revision)」となる.よって DESIRED = 0 になって残っている ReplicaSet のことを指す.

kubernetes.io

まず「デフォルト値」を確認するために,引き続き nginx:1.11-alpine から nginx:1.18-alpine まで修正しながらマニフェストを繰り返し適用していく.すると,ReplicaSet の古いリビジョン数は「10個」になった.

# nginx:1.11-alpine
$ kubectl apply -f deployment.yaml --record
deployment.apps/sandbox-deployment-revision-history-limit configured

# nginx:1.12-alpine
$ kubectl apply -f deployment.yaml --record
deployment.apps/sandbox-deployment-revision-history-limit configured

# nginx:1.13-alpine
$ kubectl apply -f deployment.yaml --record
deployment.apps/sandbox-deployment-revision-history-limit configured

# nginx:1.14-alpine
$ kubectl apply -f deployment.yaml --record
deployment.apps/sandbox-deployment-revision-history-limit configured

# nginx:1.15-alpine
$ kubectl apply -f deployment.yaml --record
deployment.apps/sandbox-deployment-revision-history-limit configured

# nginx:1.16-alpine
$ kubectl apply -f deployment.yaml --record
deployment.apps/sandbox-deployment-revision-history-limit configured

# nginx:1.17-alpine
$ kubectl apply -f deployment.yaml --record
deployment.apps/sandbox-deployment-revision-history-limit configured

# nginx:1.18-alpine
$ kubectl apply -f deployment.yaml --record
deployment.apps/sandbox-deployment-revision-history-limit configured

$ kubectl get deployments,replicasets
NAME                                                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/sandbox-deployment-revision-history-limit   3/3     3            3           5m

NAME                                                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/sandbox-deployment-revision-history-limit-54f45b4c75   0         0         0       98s
replicaset.apps/sandbox-deployment-revision-history-limit-59b7cf78b9   0         0         0       2m16s
replicaset.apps/sandbox-deployment-revision-history-limit-6cb4d85b97   0         0         0       72s
replicaset.apps/sandbox-deployment-revision-history-limit-6d9d558bb6   0         0         0       84s
replicaset.apps/sandbox-deployment-revision-history-limit-6dfdf56756   0         0         0       60s
replicaset.apps/sandbox-deployment-revision-history-limit-769f75d55b   0         0         0       4m59s
replicaset.apps/sandbox-deployment-revision-history-limit-7b94db8c49   0         0         0       4m20s
replicaset.apps/sandbox-deployment-revision-history-limit-7dccdcc475   3         3         3       37s
replicaset.apps/sandbox-deployment-revision-history-limit-7f87f5f547   0         0         0       3m57s
replicaset.apps/sandbox-deployment-revision-history-limit-8565fb55f8   0         0         0       2m35s
replicaset.apps/sandbox-deployment-revision-history-limit-c544754bd    0         0         0       117s

次に nginx:1.19-alpine に修正してマニフェストを適用すると,以下のように最初に作られた replicaset.apps/sandbox-deployment-revision-history-limit-769f75d55b が削除された.なお,ドキュメントには The rest will be garbage-collected in the background. と書かれているため,kubectl apply のタイミングではなく,裏側で削除されていると言える.

# nginx:1.19-alpine
$ kubectl apply -f deployment.yaml --record
deployment.apps/sandbox-deployment-revision-history-limit configured

$ kubectl get deployments,replicasets
NAME                                                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/sandbox-deployment-revision-history-limit   3/3     3            3           6m

NAME                                                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/sandbox-deployment-revision-history-limit-54f45b4c75   0         0         0       2m38s
replicaset.apps/sandbox-deployment-revision-history-limit-59b7cf78b9   0         0         0       3m16s
replicaset.apps/sandbox-deployment-revision-history-limit-6cb4d85b97   0         0         0       2m12s
replicaset.apps/sandbox-deployment-revision-history-limit-6d9d558bb6   0         0         0       2m24s
replicaset.apps/sandbox-deployment-revision-history-limit-6dfdf56756   0         0         0       2m
replicaset.apps/sandbox-deployment-revision-history-limit-7b94db8c49   0         0         0       5m20s
replicaset.apps/sandbox-deployment-revision-history-limit-7dccdcc475   0         0         0       97s
replicaset.apps/sandbox-deployment-revision-history-limit-7f87f5f547   0         0         0       4m57s
replicaset.apps/sandbox-deployment-revision-history-limit-8565fb55f8   0         0         0       3m35s
replicaset.apps/sandbox-deployment-revision-history-limit-c544754bd    0         0         0       2m57s
replicaset.apps/sandbox-deployment-revision-history-limit-f9f7485cd    3         3         3       25s

.spec.revisionHistoryLimit を設定する

最後に Deployment のマニフェストに .spec.revisionHistoryLimit: 2 を追加する.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sandbox-deployment-revision-history-limit
  labels:
    app: nginx
spec:
  replicas: 3
  revisionHistoryLimit: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.19-alpine
        ports:
        - containerPort: 80

マニフェストを適用して,リビジョン数を確認すると,自動的に削除されていた.

$ kubectl apply -f deployment.yaml --record
deployment.apps/sandbox-deployment-revision-history-limit configured

$ kubectl get deployments,replicasets
NAME                                                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/sandbox-deployment-revision-history-limit   3/3     3            3           8m

NAME                                                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/sandbox-deployment-revision-history-limit-6dfdf56756   0         0         0       4m
replicaset.apps/sandbox-deployment-revision-history-limit-7dccdcc475   0         0         0       3m37s
replicaset.apps/sandbox-deployment-revision-history-limit-f9f7485cd    3         3         3       2m25s

まとめ

Kubernetes で Deployment.spec.revisionHistoryLimit フィールドを設定するとリビジョン数を制御できる.ドキュメントにはリビジョンを多く残すと,etcd のリソースを消費したり,kubectl get replicasets コマンドの結果が見にくくなると書いてある.最適値は状況によって異なるとは思うけど,デフォルト値 10 は多すぎるようにも感じる..spec.revisionHistoryLimit フィールドを覚えておこう!

ghq で「複数の root ディレクトリ」からリポジトリを探す

ghq「複数の root 設定」に対応している.例えば GitHub と CodeCommit と ${GOROOT}/src など,それぞれのディレクトリを対象に ghq list コマンドでリポジトリを探せる(個人的には ghq list + peco).最高すぎる!設定自体は gitconfig (~/.gitconfig) に以下のように設定する.独自に設定ファイルを持つのではなく gitconfig を活用するところにツールとしてのセンスの良さを感じる.

[ghq]
  root = ~/go/src
  root = ~/ghq/codecommit
  root = ~/ghq/github.com

プライマリ root(順番に気を付ける)

gitconfig として「複数の root 設定」は上書きされるため「1番最後の root がプライマリ root」になる.ghq root コマンドを実行すれば確認できる.ghq get コマンドを実行すれば,基本的には1番最後の root に取得される.動作確認をしておくと良いと思う.

$ ghq --version
ghq version 1.1.5 (rev:f5ac3e5)

$ ghq root
/Users/kakakakakku/ghq/github.com

$ ghq root --all
/Users/kakakakakku/ghq/github.com
/Users/kakakakakku/ghq/codecommit
/Users/kakakakakku/go/src

gitconfig の includeIf と組み合わせる

個人用と会社用など,gitconfig を変える場面は多いと思う.例えば user.nameuser. email など.その場合は gitconfig の includeIf を使うと便利で,もし ghq「複数の root 設定」を使うのであれば includeIf も一緒に設定しておくと良いと思う.詳しくは以下の記事を読んでもらえると!

kakakakakku.hatenablog.com

「ghq handbook」も合わせて読む

去年 ghq を導入するときに合わせて読んだ「ghq handbook」にも「複数の root 設定」の話題が載っている.もしこれから ghq の導入を検討しているなら読んでみると良いと思う.僕にとって ghq は本当に必須すぎて使えないとリポジトリを探せないかも!笑

kakakakakku.hatenablog.com

Vertical Pod Autoscaler の Recommender で「推奨値」のみを算出する

Kubernetes で VPA (Vertical Pod Autoscaler) を使うと,Pod に設定する CPU / Memory リクエスト値をオートスケールできる.CPU / Memory リクエスト値 (spec.containers[].resources.requests) に対する「スケールアップ」「スケールダウン」とも言える.今回は VPA を試していく.なお,VPA コンポーネントは Kubernetes Autoscaler の GitHub リポジトリに含まれている.

github.com

準備 1 : Kubernetes クラスターを構築する 🐳

今回は kind を使って,Mac 上に検証用の Kubernetes クラスターを構築する.以下のように v1.19.1 を使う.

$ kind create cluster
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.19.1) 🖼
 ✓ Preparing nodes 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
Set kubectl context to "kind-kind"
You can now use your cluster with:

kubectl cluster-info --context kind-kind

Thanks for using kind! 😊

$ kubectl get nodes
NAME                 STATUS   ROLES    AGE    VERSION
kind-control-plane   Ready    master   100s   v1.19.1

準備 2 : Metrics Server をインストールする 🐳

構築した Kubernetes クラスターに Metrics Server をインストールする.基本的には GitHub リポジトリに載っている kubectl apply コマンドを使って components.yaml を適用する.

$ kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

しかし,Mac 上で Metrics Server 動かすために metrics-server の Deployment に kubelet 関連のオプションを追加しておく必要がある.少なくとも今回の環境では修正する必要があった.components.yaml を以下のように修正する.

(中略)

spec:
  serviceAccountName: metrics-server
  volumes:
  # mount in tmp so we can safely use from-scratch images and/or read-only containers
  - name: tmp-dir
    emptyDir: {}
  containers:
  - name: metrics-server
    image: k8s.gcr.io/metrics-server-amd64:v0.3.6
    imagePullPolicy: IfNotPresent
    args:
      - --cert-dir=/tmp
      - --secure-port=4443
+     - --kubelet-insecure-tls
+     - --kubelet-preferred-address-types=InternalIP

(中略)

修正した components.yamlkubectl apply コマンドで適用して,最終的に kubectl top node コマンドが実行できるようになっていれば,metrics-server はうまくインストールできている.

$ kubectl apply -f components.yaml
serviceaccount/metrics-server created
clusterrole.rbac.authorization.k8s.io/system:aggregated-metrics-reader created
clusterrole.rbac.authorization.k8s.io/system:metrics-server created
rolebinding.rbac.authorization.k8s.io/metrics-server-auth-reader created
clusterrolebinding.rbac.authorization.k8s.io/metrics-server:system:auth-delegator created
clusterrolebinding.rbac.authorization.k8s.io/system:metrics-server created
service/metrics-server created
deployment.apps/metrics-server created
apiservice.apiregistration.k8s.io/v1beta1.metrics.k8s.io created

$ kubectl top node

github.com

準備 3 : VPA (Vertical Pod Autoscaler) をインストールする 🐳

Kubernetes Autoscaler の GitHub リポジトリに VPA (Vertical Pod Autoscaler) をインストールするスクリプトが含まれているため,リポジトリを git clone しておく.そして vertical-pod-autoscaler/hack/vpa-up.sh を実行する.

$ cd vertical-pod-autoscaler
$ ./hack/vpa-up.sh

すると,以下のように CRD (Custom Resources Definition)Service AccountDeployment を確認できるようになる.

$ kubectl get crds
NAME                                                  CREATED AT
verticalpodautoscalercheckpoints.autoscaling.k8s.io   2021-01-18T06:00:00Z
verticalpodautoscalers.autoscaling.k8s.io             2021-01-18T06:00:00Z

$ kubectl get serviceaccounts -n kube-system | egrep 'NAME|vpa'
NAME                                 SECRETS   AGE
vpa-admission-controller             1         10m
vpa-recommender                      1         10m
vpa-updater                          1         10m

$ kubectl get deployments -n kube-system | egrep 'NAME|vpa'
NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
vpa-admission-controller   1/1     1            1           11m
vpa-recommender            1/1     1            1           11m
vpa-updater                1/1     1            1           11m

$ kubectl get pods -n kube-system | egrep 'NAME|vpa'
NAME                                         READY   STATUS    RESTARTS   AGE
vpa-admission-controller-664b5997b7-4x7gx    1/1     Running   0          12m
vpa-recommender-5b768c88-nwhg6               1/1     Running   0          12m
vpa-updater-c9ffff655-vs8wt                  1/1     Running   0          12m

VPA (Vertical Pod Autoscaler) コンポーネント 🐳

VPA (Vertical Pod Autoscaler) を試す前にコンポーネントを整理しておく.大きく3種類ある.コンポーネント名から推測すると Updater によって Pod が再作成されそうだけど,実際には Admission Controller と組み合わせて再作成される.

  • Recommender : リソース消費を監視して CPU / Memory リクエスト値の推奨値を算出する
  • Updater : Pod を強制終了して再作成できるようにする
  • Admission Controller : 推奨値を適用した Pod を再作成する

kubernetes.io

VPA (Vertical Pod Autoscaler) を試す 🐳

同じく Kubernetes Autoscaler の GitHub リポジトリに公開されている Vertical Pod Autoscaler のサンプル hamster.yaml を使う.以下のようにマニフェストを適用すると,VerticalPodAutoscalerDeployment(アプリケーション用)が作成される.

$ cd vertical-pod-autoscaler
$ kubectl apply -f examples/hamster.yaml
verticalpodautoscaler.autoscaling.k8s.io/hamster-vpa created
deployment.apps/hamster created

ポイントは VerticalPodAutoscaler で CPU / Memory リクエスト値の minAllowedmaxAllowed を設定している点で,具体的なマニフェストは以下に載せておく.そして Deployment にも CPU / Memory リクエスト値を設定している.

  • VerticalPodAutoscaler(オートスケールする範囲値)
    • CPU : 100m ~ 1 (1000m)
    • Memory : 50Mi ~ 500Mi
  • Deployment(基本値)
    • CPU : 100m
    • Memory : 50Mi
apiVersion: "autoscaling.k8s.io/v1"
kind: VerticalPodAutoscaler
metadata:
  name: hamster-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: hamster
  resourcePolicy:
    containerPolicies:
      - containerName: '*'
        minAllowed:
          cpu: 100m
          memory: 50Mi
        maxAllowed:
          cpu: 1
          memory: 500Mi
        controlledResources: ["cpu", "memory"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hamster
spec:
  selector:
    matchLabels:
      app: hamster
  replicas: 2
  template:
    metadata:
      labels:
        app: hamster
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534 # nobody
      containers:
        - name: hamster
          image: k8s.gcr.io/ubuntu-slim:0.1
          resources:
            requests:
              cpu: 100m
              memory: 50Mi
          command: ["/bin/sh"]
          args:
            - "-c"
            - "while true; do timeout 0.5s yes >/dev/null; sleep 0.5s; done"

実際に Deployment によって作成された Pod のリクエスト値を確認すると,以下のようになっていた.Deployment に設定したリクエスト値よりも大きくなっている.VPA (Vertical Pod Autoscaler) によって「スケールアップ」されたと言える.

$ kubectl describe pods hamster-96d4585b7-qcb7b
Name:         hamster-96d4585b7-qcb7b
Namespace:    default
(中略)
Containers:
  hamster:
    Requests:
      cpu:        511m
      memory:     262144k
(中略)

VerticalPodAutoscaler に updatePolicy を設定する 🐳

VPA (Vertical Pod Autoscaler) には大きく3種類の updatePolicy がある.

  • Auto (Recreate) : Pod の作成時にリソースリクエストを割り当てて後でまた更新する(デフォルト)
  • Initial : Pod の作成時のみにリソースリクエストを割り当てる
  • Off : 推奨値の算出だけを行う

既に動作確認をしたデフォルト設定は Auto だけど,例えば updatePolicyOff を設定すれば,Recommender を使って推奨値を計算するけど,実際に Pod には反映しないという活用もできる.さっそく試していく.まず,以下のように VerticalPodAutoscaler のマニフェストを修正して updatePolicy を設定する.

apiVersion: "autoscaling.k8s.io/v1"
kind: VerticalPodAutoscaler
metadata:
  name: hamster-vpa
spec:
  targetRef:
    apiVersion: "apps/v1"
    kind: Deployment
    name: hamster
+ updatePolicy:
+   updateMode: "Off"
  resourcePolicy:
    containerPolicies:
      - containerName: '*'
        minAllowed:
          cpu: 100m
          memory: 50Mi
        maxAllowed:
          cpu: 1
          memory: 500Mi
        controlledResources: ["cpu", "memory"]

マニフェストを適用してから Deployment を再起動する.

$ cd vertical-pod-autoscaler
$ kubectl apply -f examples/hamster.yaml
verticalpodautoscaler.autoscaling.k8s.io/hamster-vpa configured
deployment.apps/hamster unchanged

$ kubectl rollout restart deployment hamster
deployment.apps/hamster restarted

すると,今度は Deployment に設定したリクエスト値のままになった.

$ kubectl describe pods hamster-66959c47bf-q54g4
Name:         hamster-96d4585b7-qcb7b
Namespace:    default
(中略)
Containers:
  hamster:
    Requests:
      cpu:        100m
      memory:     50Mi
(中略)

そして Recommender による推奨値を確認するために kubectl describe vpa コマンドを実行すると,期待通りに Recommendation として推奨値が算出されている.CPU / Memory リクエスト値のチューニングに使えて便利!

$ kubectl describe vpa hamster-vpa
Name:         hamster-vpa
Namespace:    default
(中略)
Status:
  Conditions:
    Last Transition Time:  2021-01-18T07:00:00Z
    Status:                True
    Type:                  RecommendationProvided
  Recommendation:
    Container Recommendations:
      Container Name:  hamster
      Lower Bound:
        Cpu:     415m
        Memory:  262144k
      Target:
        Cpu:     511m
        Memory:  262144k
      Uncapped Target:
        Cpu:     511m
        Memory:  262144k
      Upper Bound:
        Cpu:     1
        Memory:  371500k

Updater と Admission Controller を停止する 🐳

VPA (Vertical Pod Autoscaler) コンポーネント は疎結合に実装されているため,推奨値を算出するだけなら Recommender 以外は起動しなくても使える.実際に以下のように UpdaterAdmission Controller を削除して,同様に動作確認をしたら,問題なく kubectl describe vpa コマンドで推奨値を確認できた.

$ kubectl delete deployments vpa-updater -n kube-system
deployment.apps "vpa-updater" deleted

$ kubectl delete deployments vpa-admission-controller -n kube-system
deployment.apps "vpa-admission-controller" deleted

$ kubectl get deployments -n kube-system | egrep 'NAME|vpa'
NAME              READY   UP-TO-DATE   AVAILABLE   AGE
vpa-recommender   1/1     1            1           60m

まとめ 🐳

Kubernetes で VPA (Vertical Pod Autoscaler) を使うと,Pod に設定する CPU / Memory リクエスト値をオートスケールできる.VPA (Vertical Pod Autoscaler) コンポーネントは大きく3種類あり,役割を理解しておくと良いと思う.また updatePolicyOff を設定すると,Recommender を使って推奨値を計算するけど,実際に Pod には反映しないという活用もできる.

github.com

関連記事 🐳

Kubernetes クラスターを構築した kind に関しては以下の記事にまとめてある.

kakakakakku.hatenablog.com