Unyablog.

のにれんのブログ

Docker で jq が使いたくなったら gojq の Image を使えば良さそう

タイトル通りのメモ。

jq を Docker で使いたかったけど、公式で提供されている image が明示されておらず、それっぽいものも Updated 6 years ago となっている。

github.com

野良 Image はあまり使いたくないので、どうしたものか。

gojq を使おう

ここで、jq の Go にポートしたものとして gojq がある。いくつか差異はある ものの、基本的に同じ機能が使える。

gojq は Docker image が公式に公開されているので、これを使うことにした。

$ docker run --rm -i ghcr.io/itchyny/gojq
gojq - Go implementation of jq

Version: 0.12.5 (rev: 727b4b5/go1.17)
...

便利!

GitHub Actions で Docker push をするときに、特定のブランチについて latest タグをつける

DockerHub の autobuild が無料では出来なくなったので GitHub Actions で Docker build / push を行う作業をしていた。

基本的にはスムーズに移行できたが、 latest tag を自動で生成する部分だけちょっと詰まったのでメモ。

問題

Docker Hub の autobuild では Default branch に push すると latest tag が自動で生成されるようになっていたが、 GitHub Actions を docker-publish workflow ベースで構築すると生成されない。

どうするか

↓の issue にある通り。

github.com

詳細

この Workflow では、latest タグがつくかどうかは docker/metadata-action でどのような metadata が吐かれるかによって決まっていて、ここで image の push 先として latest が含まれると後続の Action で latest に push される。

そして、 metadata-action では、 https://github.com/docker/metadata-action#latest-tag にある通り flavorlatest で latest タグを出力するかどうかが決まる。デフォルトでは auto となっており、その場合タグが push された場合にのみつくようになっている。ここに true を明示的に設定すると、常に latest タグがつくようになる。

そのため、特定のブランチに push した場合につくように変更するには、flavorlatest を特定のブランチの場合にのみ true にすれば良い。具体的には、Issue にある通り下のようにする。

flavor: |
  latest=${{ github.ref == 'refs/heads/main' }}

docker-compose を Docker で動かす

docker-compose を Docker 内で実行するメモ。

背景

docker-compose は便利なツールだが、Linux サーバー上で動かすときは入れ方に悩む。

Docker engine は Docker 公式 apt リポジトリから入れることができるけど、docker-compose はレポジトリには存在しない。

公式ドキュメント では curl でインストールする方法が書いてある。一度インストールする分にはこれで良いものの、更新を考えるとイマイチ。

また、DebianUbuntu の公式リポジトリには docker-compose パッケージが存在しているけど、新しくなかったりする。

そこで、docker-compose の Docker image を使って動かしてみる。

やり方

公式ドキュメント の "Alternative install options" にはラッパー(run.sh)を使ったやり方が書いてあり、 run.sh/usr/local/bin/docker-compose に置くよう案内している。

github.com

ただ、できれば docker コマンドのみでシンプルにやりたい。

run.sh を使わずにやる

run.sh はそこまで特殊なことはしておらず、

  1. Docker UNIX socket の mount
  2. ホスト側の Working directory の mount
  3. コンテナの Working directory をホスト側と同じに
  4. ホスト側の Home directory の mount
  5. 環境変数の維持

などを行いつつ docker/compose image を起動している(詳細は run.sh のコメント参照)*1

多くの場合*2は、1-3 があれば十分なはず。これをふまえて、docker コマンドで docker-compose を最小限使うには↓のコマンドになる:

$ docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock -v `pwd`:`pwd` -w `pwd` docker/compose:{version} --help
Define and run multi-container applications with Docker.

Usage:
  docker-compose [-f <arg>...] [--profile <name>...] [options] [--] [COMMAND] [ARGS...]
  docker-compose -h|--help

Options:
...

これで、docker-compose をダウンロードしなくても Docker があれば compose を立ち上げることができる。

*1:Volume などで Working directory 外からの参照がいるのであれば自分でフラグを追加してねというスタンス

*2:混み入ったことをせず、環境変数や Home directory 配下の config なども使わない場合

replicaCount: 1 と Drain とダウンタイム

Kubernetes で Node を Drain するとき、replicaCount: 1 な ReplicaSet (Service) はある程度のダウンダイムを許容せざるを得ない。

関連 issue は以下。

github.com

問題

  • ReplicaSet X があり、その replicaCount は 1 にしている
    • また、同じ Selector で Service X を作成している
  • Node A に ReplicaSet X の Pod X1 が 1つある

状況で Node A を Drain すると、

  • Pod X1 が Terminate される
  • ReplicaSet X に設定した replicaCount に従って、新しい Pod X2 が別の Node に立つ

といった挙動となり、Pod X1 が Terminate されてから Pod X2 が Ready になるまでは Service X に属する Ready な Pod がない状態になる。その結果、ダウンタイムが生じる。

Terminate されてから Pod が Scheduling されるまではほとんど同時なので、ダウンタイムは Container のセットアップが終わるまでの時間と大体同じになる。

PDB は無力

このような問題の対策として Pod Disruption Budget (PDB) で minAvailable を設定することが考えられるが、残念ながら replicaCount: 1 の場合は仕様上うまく動かない。

PDB は Drain の抑制を行うものの、レプリカ数を良い感じに調整してくれるものではない。なので、上の条件からさらに PDB で minAvailable を 1 に設定した状態で Node A を Drain すると、

  • Pod X1 は PDB があるので Terminate されない
  • ReplicaSet X から見ると Pod X1 が Healthy な状態で存在しているので、新たな Pod を別 Node にスケジューリングを行うこともしない

ということで単に Pod X1 が消えずに Node A が一生 Drain されないことになる。

どうするか?

Issue にある通り replicaCount が1つである限りどうしようもないので、replicaCount を増やすことになる。

replicaCount を増やす

replicaCount を増やすと 1 つの Node が死んでも別の Pod 生きており、ダウンタイムは生じない。PDB があると複数ノードが同時に Drain されることも抑制されるのでなお良い。

Drain 前に rollup して Drain 後に rolldown する

要するに replicaCount を一時的に増やす。

…このように replicaCount を増やすのが正攻法だと思うが、今回は個人のどうでもいいクラスタなので、コンテナが立つまでの十数秒のダウンタイムは許容することにした。

k8s: 手元で削除したリソースをクラスタからも消したい

k8syaml を kustomize で管理している。ここからリソースを消しても kustomize はそれを感知しないためクラスタからは削除されない。

どうするか

kpt を使う。これを見つけたのは deprecated になった inventory からリンクが貼られていたため。

kpt はいくつかの機能で構成されているが、そのうち live を使う(それ以外の機能はあまり知らない *1)。

kpt live では apply 時に適用対象のリソースを列挙して ConfigMap に保存する。これによって、次回以降の apply 時に適用するリソースと以前に適用したリソースを比較することができ、不要になったリソースが何であるかを判別することができる。

googlecontainertools.github.io

kpt を使う

kpt をインストールしたあとに、まず ConfigMap を作るためのリソースを作成する。

$ kpt live init .

これで inventory-template.yaml ができる。これを kustomize.yaml に含めておく。

resources:
  - ...
  - inventory-template.yaml

そして build したものを kpt live apply で適用する。

$ kustomize build | kpt live apply

その結果、2回目からは不要なリソースを消してくれるようになる。

以下おまけ

kpt を使わない方法

Kustomize で CommonLabels を使うことで kubectl apply --prune と組み合わせることもできる。

commonLabels:
  app.kubernetes.io/managed-by: kustomize

として

$ kustomize build | kubectl apply -f - --prune -l app.kubernetes.io/managed-by=kustomize

すれば二回目以降は prune される。ただ、

のでやめた。

その他の方法

GitOps 的な方面で色々ある。

  • terraform

    Kustomize integraion がある。ドキュメントを読むとできそう。

    ただそのために terraform を導入するのも面倒なのでやめた。

  • flux2

    Git レポジトリ経由ではなく手元から実行できる方法が分からなかったのでやめた。削除してくれるかまでは見ていない。

  • Argo CD

    Argo CD はパージしてくれる機能がありそう。ローカル実行もできそうだったけど、kpt のほうが(ほぼ)手元で完結してシンプルで良さそうに見えたのでやめた*2

*1:別のコマンドとしてくれたほうが分かりやすいと思ったり

*2:CD がほしいわけではないのです

Vertical Pod Autoscaler の limits 周りの挙動について

Kubernetes で memory の requests を管理するのに Vertical Pod Autoscaler (VPA) を使っている。

github.com

VPA はリソースの使用量の実績に基づいて良い感じに limits と requests を調整してくれるものだが、 limits の設定に関してちょっと困ったのでメモ。

参考: https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler#limits-control

VPA がどう limits を設定するか

VPA が limits を設定するときは、 LimitResources などの制約がない場合、以下のようなアルゴリズムで設定される。

  1. VPA が requests を算出する
  2. 算出した requests に、元々設定されていた requests と limits の比をかけて新しい limits とする

すなわち、メモリの requests を 500Mi, limits を 1000Mi とした Pod に対して、VPA が新しい requests を 700Mi とすれば、新たな limits は 1400Mi となる。

requests を設定していないとき問題

ここで、Pod に limits のみを設定し、requests を設定していない場合はどうなるか。このとき、k8s は requests を limits と同じ量に設定するので、VPA は requests と同量の limits を推奨するようになる *1

requests は普段の使用量ベースで自動設定されるので、limits に同じ値が設定されるとほとんど burst できない Pod となってしまう。 起動時ちょっとだけメモリを多めに使う、みたいな Pod がある場合に OOM killer で落ち続けるようになってしまった。

解決策

解決策としては以下が考えられる。

  • limits だけではなく request を設定する

    request を設定する際には VPA の挙動を考慮する必要がある

  • limits を設定せず request のみを設定する

    こうすると VPA によって limits が減ることはない

今回はめんどくさかったので後者にした。

limits がない結果ノードのメモリが不足する可能性があるが、 requests は普段調整されるためそれほど起こらないはず。もし頻繁に起きるなら、普段は問題ないが Burst したときにメモリが足りないということであり、それは各ノードの余剰メモリが足りないということであるので、そういった問題として対処することにした *2

*1:requests が何らかの理由で設定されない場合も VPA によって同量になる https://github.com/kubernetes/autoscaler/blob/2542e8c884a8e25634d3b8f43243fa8706007f30/vertical-pod-autoscaler/pkg/utils/vpa/limit_and_request_scaling.go#L69-L72

*2:LimitRanges ぐらいは設定してもいいかもしれない

Kubernetes ヒヤリハット ― デフォルトに戻すときはちゃんと書く

Kubernetes で Service Account を特定のものから default に戻そうとして、Pod の Template から serviceAccountName を消して apply した。

         spec:
-          serviceAccountName: old-service-account
           restartPolicy: Never
           containers:
           - ...

しかし、適用しても作られる Pod の serviceAccountNameold-service-account のままだった。

serviceAccountName を消すだけでは apply しても unchanged となり、 default に戻るわけではないらしい。

ちゃんと default って書き直しましょう。

         spec:
-          serviceAccountName: old-service-account
+          serviceAccountName: default
           restartPolicy: Never
           containers:
           - ...

教訓: 冪等性があると思ってはいけない!