Kubernetes The Hard Way を AWS で実施する

f:id:kyagi:20180216023000p:plain

Kubernetes The Hard Way とは

Kubernetes の公式ドキュメントで Setup の項目をたどると kubeadm を使う例が記載されている。いくつかの機能はまだ alpha/beta であるものの公式でも kubeadm をクラスタ構築に使う流れを後押ししたいように見える。確かに kubeadm (や各ベンダの構築ツール)を使ったクラスタ構築は楽なのだけれど、ツール内部でやっていることを理解しない限りただツールに使われるだけになってしまうのと、トラブル発生時に原因把握もできなくなってしまう。ここでは従来よりスクラッチからのクラスタ構築として定評のある Kubernetes The Hard Way を AWS で実施していく。なお本家は GCP での構築例であり、AWS のサポートには消極的な様子。ここでは少し古いものの AWS 版の Hard Way を公開している方がいるのでそちらをベースに進めながら、AWS 依存の構成やバージョンのアップデートなどでハマった点を備忘録がてらに残しておく。

実際に Kubernetes The Hardway on AWS を完走するまでにやったことと注意点

  • VPC, Subnet, RouteTable, Internet Gateway などは awscli ではなく CloudFormation で実施したほうが便利。何回かやり直すことになると思うので再実行しやすいのと振り返りにも役立つ。 f:id:kyagi:20190603024052p:plain
  • 各手順もそれぞれシェルスクリプトにまとめておくと再実行しやすいし、あとでプロビジョニングツール用に変換もできる。slawekzachcial さんの AWS 版では内部IP取得のために無駄にだらだら awscli を長く利用していたり、user-data に埋め込んでいるところがあるが、そこは固定で埋め込んでしまったほうが混乱がなくてよい。 f:id:kyagi:20190603024504p:plain
  • slawekzachcial さんの例では kubelet v1.9.0 だが kubelet v1.9.0 + ubuntu 18.04 だと couldn't propagate object cache: timed out waiting for the condition になるので controllers, workres ともに v1.12.7 にアップデートする。
  • pods "coredns-595db6f9cb-svfj5" is forbidden: User "system:node:ip-10-240-0-20" cannot patch pods/status in the namespace "kube-system" を解決するために clusterrole も更新する。
  • slawekzachcial さんの例では kubelet + cri-containerd で構築しているが kubelet + containerd に置き換える。cri-containerd はあくまで過渡期の一時用であり、containerd 1.1 から完全体となっているため。
  • 公式ドキュメントでも記載されているが、最低限のマシンスペックとして 2GB RAM, 2 CPUs が必要とされる。最初ケチって t3.nano で構築していたが、これだと workers の構築が完了し kubelet, kube-proxy を動作させると controllers 側が劇的に重くなるので t3.small にした。 https://kubernetes.io/docs/setup/independent/install-kubeadm/
  • EC2 の src/dst チェック外しておく。ENI(Elastic Network Interface) で作成した 10.200.0.0/16 のパケットが ENI に届けられず破棄されてしまう。
  • Route Table の「Main Table」を変更するのを忘れずに。
  • Securigy Group で DNS(udp:53) が塞がれていると、kube-dns service(10.32.0.10) から各 endpoint の coredns pod へ通信できない。いっそ Pod CIDR のネットワーク帯は All traffic を通すのも検証段階ではよいと思う(= All traffic from 10.200.0.0/24, 10.200.0.0/16)。これで node(ec2) からも dig が通るようになる。
  • coreDNS は以下が通れば OK。
ubuntu@ip-10-240-0-10:~/80-k8s-hardway$ dig +short @10.200.0.25 www.google.com kubernetes.default.svc.cluster.local kube-dns.kube-system.svc.cluster.local
172.217.164.164
10.32.0.1
10.32.0.10
  • 正しく完走できれば最終的には以下のようになるはず。 f:id:kyagi:20190603025052p:plain
  • Kubernetes のネットワークモデルについては以下のドキュメントがとてもわかりやすい。AWS を元にして書かれているのと、内部で amazon-vpc-cni-k8s という CNI プラグインを紹介しているがこれは Hard Way で ENI と Route Table を利用して POD CIDR への経路を用意したのと同じように思える。

sookocheff.com

GCP ではなく AWS で Kubernetes The Hard Way を実施するのは、AWS 依存の設定もあり、文字通り以上にハードだったけれど、そのぶん Controll-pane や worker 、そして CNI の概要が理解できたと思う。ただ完全には把握できず、やはり Kubernetes 難しいと改めて感じたのと、バージョンごとに機能や設定がどんどん変わっていくので追随していかないと構築例もあっというまに時代遅れになってしまうと感じた (_o_)

世界各都市の現地時刻を表示するコマンドを作った

仕事上、東京とロサンゼルスの時間を気にする必要があるので、時差を知りたい時は環境変数 TZ にそれぞれの都市を入れて date していたのを peco を使って拡張してみた。timedatectl list-timezones で抜き出したタイムゾーンのリストを peco でフィルタして TZ 変数に渡して date を叩くだけだが、なかなか便利。(^_^;

GitHub - kyagi/localtime: Select and show localtime in cities you pick

f:id:kyagi:20190308031720p:plain f:id:kyagi:20190308031726p:plain f:id:kyagi:20190308031734p:plain f:id:kyagi:20190308031747p:plain

使いかた

  • localtime と入力すると都市名を選ぶプロンプトが表示され、インクリメンタルサーチが走る(内部で peco を使用)
  • 複数の候補を選びたいときは peco の Ctrl+S を利用すればそのまま表示。
  • タイムゾーンを書いた設定ファイルを -c オプションで指定するとその都市の時刻を表示(ここでは東京とロサンゼルスを指定)

IntelliJ から Docker イメージの開発作業を行う

f:id:kyagi:20190303201557p:plain

IntelliJ の Docker Integration プラグインを使う

Dockerfile の修正、ビルド、イメージの動作確認を行うのに、今まではターミナルから docker build -> run -> attach をしていたけれど、全て IntelliJ のインターフェイスでサイクルを回した効率がよかった。

f:id:kyagi:20190303201311p:plain

f:id:kyagi:20190303201423p:plain

IntelliJ だとイメージやコンテナの一覧が見やすいし、右クリックから docker コマンドの操作は大体可能。

f:id:kyagi:20190303203144p:plain

ローカルの k8s クラスタで動かしている各 pod の内部情報は kubectl や k8s dashboard からも参照できるものの、IntelliJ からのほうが見やすい情報もある。

最近はすっかり IntelliJ (JetBrains IDE) の環境に染まってしまった。そういえば git も IntelliJ 経由でしか使用していない。どのツールもそうだけど、最初は使い方や基本概念を学ぶためにコマンドラインから入り、慣れてきた後はより効率的なインターフェイスで利用するのがベストだと思う。かつてはそれが Emacs だったけれど、数年前から完全に IntelliJ に置き換わってしまった感がある。年々 JetBrains 製プロダクトへの依存度があがっていることは自覚しているが、これほど完成された作業環境をお手軽に、柔軟に、素早く、しかも管理コストを少なく構築できるソフトウェアは他にないのではないだろうか。(^_^;

mac mini 2018 向けのディスプレイケーブルとマイク

年末に mac mini 2018 を購入した際に発生した問題がふたつ。

  • それまでのディスプレイケーブルが使えない。USB Type C to HDMI のものを書い直さないといけない。
  • マイクがない(マイクがないと日課のオンライン英会話が mac mini できない)。

しょうがないので以下のディスプレイケーブルとマイクを購入した。mac mini には USB 3.0 ポートは 2 つしかなく、HHKB とマウスで埋めてしまっていたのでマイク用に USB Type C から USB 3.0 へ変化するアダプタも一緒に購入。きちんと動作するのか若干不安だったが、どちらも問題なく動いたのでよかった。他のデバイスもそうだが USB Type C が普及するまではもう少し時間がかかりそうだ...。

ちょっとひっかかったのが外部マイクを接続した際に「システム環境設定」>「セキュリティとプライバシー」>「プライバシー」からマイクに接続するアクセス権をアプリケーションごとに設定する必要があった。

f:id:kyagi:20190226000044p:plain

以上、mac mini 2018 で同様にケーブル難民、マイク難民となっている方への情報提供になれば幸いです。(^_^;

Docker Desktop for Mac の Kubernetes 機能を使用して Mac 上に Kubernetes クラスタを構築する

f:id:kyagi:20180216023000p:plain

Mac でお手軽 Kubernetes クラスタ構築

難しい、となかなか敷居が高くなりがちな Kubernetes だが Docker Desktop for Mac の Kubernetes 機能を利用すると、お手軽に Mac に Kubernetes クラスタを構築することができる。

Docker Desktop for Mac とは?

Mac の Docker 環境の最新版。少し前までは Docker Toolbox が使用されていたが、現在では Docker Desktop for Mac がスタンダードになっている。18.06 から Kubernetes 機能も搭載された。

f:id:kyagi:20190224163753p:plain

インストール

Docker.dmg をダウンロードしてインストールする。 https://hub.docker.com/editions/community/docker-ce-desktop-mac

Kubernetes 機能の有効化

Preferences から Kubernetes タブに進んで、「Enable Kubernetes」にチェックを入れるとバックグラウンドで Kubernetes が動き出す。

f:id:kyagi:20190224164507p:plain

Kubernetes Dashboard の有効化

以下のドキュメント通りに kubectl createkubectl proxy を打った後、指定の localhost:8001 の URL をブラウザで開く。

https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/

Kubernetes Dashboard のアクセス

Dashboard のアクセスには認証が必要になる。ネームスペース kube-system で起動している deployment-controller のトークンを利用してアクセスする。

$ kubectl -n kube-system get secret | grep deployment-controller | awk '{print $1}' | xargs -I{} kubectl describe secret {} -n kube-system | grep ^token | awk '{print $2}'

f:id:kyagi:20190224164556p:plain f:id:kyagi:20190224164612p:plain

go の docker イメージや拙作の rod(統合 REPL 環境)を動かす

$ kubectl run golang --image=golang:1.11.5-stretch --command -- tail -f /dev/null
deployment.apps "golang" created

$ kubectl run rod --image=kyagi/rod --command -- tail -f /dev/null
deployment.apps "rod" created

$ kubectl get deployment
NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
golang    1         1         1            1           19s
rod       1         1         1            1           2m
ubuntu    1         1         1            1           5d

f:id:kyagi:20190224170512p:plain

まとめ

Docker Desktop for Mac を開発で利用しているのであれば、せっかく追加された Kubernetes 機能を使わないのはもったいない。難しいと思われがちな(実際、自前で構築するのは難しい) Kubernetes クラスタをお手軽に構築できるので「Kubernetes ってこんな感じなんだ」とまずは感触をつかみたい方にもオススメの方法と言える。いったん構築してしまえば kubectl や Dashboard から「作って壊す」のはもちろん、いろいろな操作ができる。Docker 単体や Docker compose を使って開発環境を構築するよりも効率的なことも多い はず。(^_^)

※個人的には imagePullPolicyAlways から IfNotPresent にすることで docker build したイメージを dockerhub に push する手間がなくなり、修正&確認のサイクルが回しやすくなるのがありがたい。イメージを修正した場合は build した後に、単にその pod を削除して自動再生された pod で確認すればよい。( ・ㅂ・)و ̑̑

Go 1.11 からはじめるプロジェクトでは、パッケージマネージャは dep ではなく go mod(Go Modules) を使おう

go mod(Go Modules) がこれからのスタンダード

Go を使いはじめるにあたってパッケージマネージャを探した時の話。ghq や glide といった過去の遺産に加えて godep, dep (ややこしい) と百花繚乱な様子が窺え、初見では、将来的に何が天下統一してくれるのか、わからずじまいだった。公式で紹介されている dep を使えばいいのかな、と会社の同僚に相談したところ dep すらももう古くて、go mod(Go Modules) がこれからのスタンダードになるという話だった。

確かに dep のページでも "official experiment." と明記されている。

github.com

正式採用された go mod(Go Modules) と過去のツールたち

パッケージマネージャとして Go Module(GO111MODULE) のプロポーザルが正式に受け入れられたことと、過去の実験的な試行(GO15VENDOREXPERIMENT)として百花繚乱のツール群が紹介されている。

PackageManagementTools · golang/go Wiki · GitHub

Go 1.11 から go mod が試験的に組み込まれ、1.13 で完全に組み込まれるとのこと。vgo はあくまで go mod のプロトタイプなので 1.11 からは使う必要がない。

Modules · golang/go Wiki · GitHub

Go 1.11 からはじめるプロジェクトでは dep ではなく go mod(Go Modules) を使おう、という議論

github.com

まだ dep を使っていないプロジェクトで 1.11 から開発をはじめる場合dep を使う意味はほとんどありません。
dep は様々な問題を抱えており将来的には使われなくなるので、そこに学習の時間を割くのは避けた方がいいでしょう。
新しく 1.11 から開発をはじめるのであれば、dep ではなく Go modules を使いましょう。

Kubernetes プロジェクトも dep は完全に飛ばして godep から Go modules への移行を計画しています
(godep -> dep はダメだったので godep -> go modules へ移行予定)。
当初 Kubernetes プロジェクトは godep から dep への移行を検討したものの結局うまくいかなかったのです。

go mod の使い方

https://github.com/golang/go/wiki/Modules#quick-start

go mod init してから go build するとライブラリが自動的にダウンロードされて $GOPATH/pkg に格納される。この点は sbt build と似ている。ソースの中で使用されている外部ライブラリが検出され go.mod と go.sum という二つのファイルが生成されるが、go.mod がバージョン情報を記録するファイルで go.sum はその生情報となっている。外部ライブラリのバージョンを更新したい場合は go.mod を編集した後に go build し直す、という流れになる。私は Ruby ユーザなので go.mod = Gemfile, go.sum = Gemfile.lock というようなイメージで捉えている。 ₍₍(ง˘ω˘)ว⁾⁾

$ cd $GOPATH && mkdir hello && cd hello
$ pwd
/home/kyagi/lab/go/hello 
$ vi hello.go

$ go mod init github.com/kyagi/hello/
go: creating new go.mod: module github.com/kyagi/hello/

$ go build
go: finding rsc.io/quote v1.5.2
go: downloading rsc.io/quote v1.5.2
go: finding rsc.io/sampler v1.3.0
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: downloading rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c

$ find $GOPATH/pkg/mod | grep quote
/home/kyagi/lab/go/pkg/mod/cache/download/rsc.io/quote
/home/kyagi/lab/go/pkg/mod/cache/download/rsc.io/quote/@v
/home/kyagi/lab/go/pkg/mod/cache/download/rsc.io/quote/@v/v1.5.2.mod
(... snip ...)

$ ls
go.mod  go.sum  hello.go

$ cat go.mod
module github.com/kyagi/hello/

require (
    rsc.io/quote v1.5.2
)

$ cat go.sum
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

$ go run hello.go
Hello, world.

GOPATH のいままでとこれから

GO111MODULE になると、$GOPATH は go build 時に $GOPATH/pkg/mod に必要なライブラリがダウンロードされるだけの役割になる、と個人的に理解している。つまり $GOPATH はほとんどシステム的なライブラリ置き場になる(Ruby でいう .gem や Scala でいう .ivy2 に近い役割のディレクトリ)。これに伴ってプロジェクトのディレクトリも GO111MODULE 以前に推奨されていた $GOPATH/src/ ではなく、どこにおいてもいいという認識でいる。

いままで) $GOPATH/src/sandbox で開発をすることが推奨されていた
これから) どこで開発をしてもよい(/tmp/sandbox でも $HOME/lab/sandbox でも)。go build 時に go.mod で指定しているライブラリは $GOPATH/pkg/mod にダウンロード(キャッシュ)される

プロジェクトのディレクトリをどこにおいてもよく(どこで開発してもよく)、GOPATH は go get 時に $GOPATH/src に、 go build 時に $GOPATH/pkg/mod 配下にライブラリがダウンロードされるキャッシュ置き場としての役割になったので、個人環境の設定は以下のように変更している。

いままで) export GOPATH="$HOME/lab/go"
これから) export GOPATH="$HOME/.go"

※そもそも SRE 的な立場でしか Go を触っていないので間違っていたらご指摘いただけるとありがたいです。 (´・ω・`)

parallel で k8s の複数 pod にコマンドを「並列」で流して結果を保存する

f:id:kyagi:20180216023000p:plain

k8s の複数 pod を横断してかつ「並列」でコマンドを実行する必要がある場合、どのような手段があるだろうか。例えばパフォーマンス測定のために jstat を複数 pod で実行したい場合、以下のように kubectl に pod を次々と「直列」で渡していっては、各 pod でコマンドの実行時間のズレが生じてしまう。何よりも jstat のように標準出力(stdout)を取られてしまうコマンドを kubectl に渡した場合、ひとつひとつ Ctrl+C していかなければならず、目的を果たすことができない。

複数 pod に「直列」でコマンドを実行する例

  1. pod リストを取得する
    $ kubectl get pods -lapp=myapp -o jsonpath="{.items[*].metadata.name}" | xargs -n 1 echo
    myapp-78ccc84d99-2cr24
    myapp-78ccc84d99-2pmp7
    myapp-78ccc84d99-59f4g
    myapp-78ccc84d99-5bwkc
    myapp-78ccc84d99-5d8wc
    myapp-78ccc84d99-5fzsd
    myapp-78ccc84d99-67qkr
  2. 取得した pod リストを kubectl に渡して date コマンドを実行するものの、最初の pod と最後の pod で実行時間のズレが生じてしまう。ターミナルを pod 数分開いて「せーの」で実施すれば実行時間のズレはある程度是正できるものの、数十から数百 pod ある環境では現実的ではない。
    $ while read p; do kubectl exec -it $p -c myapp -- date; done < <(kubectl get pods -lapp=myapp -o jsonpath="{.items[*].metadata.name}" | xargs -n 1 echo)

複数 pod に「並列」でコマンドを実行する例

  1. pod リストを取得する。parallel に渡すのに適したフォーマットにする。
    $ kubectl get pods -lapp=myapp -o jsonpath="{.items[*].metadata.name}"
    myapp-78ccc84d99-2cr24 myapp-78ccc84d99-2pmp7 myapp-78ccc84d99-59f4g myapp-78ccc84d99-5bwkc myapp-78ccc84d99-5d8wc myapp-78ccc84d99-5fzsd myapp-78ccc84d99-67qkr myapp-78ccc84d99-6glcz myapp-78ccc84d99-6htmf myapp-78ccc84d99-7dchn
  2. parallel を使って並列でコマンドを流す。jstat は標準出力(stdout)に連続して出力しようとするので、ここでは標準出力を out ディレクトリ配下に吐き出すように指定する。
    $ parallel --results out kubectl exec -it {} -c myapp -- /usr/bin/jstat -gcutil -t 1 1000 ::: $(kubectl get pods -lapp=myapp -o jsonpath="{.items[*].metadata.name}")
  3. 適当に時間が経過したところで Ctrl+C で jstat を中止する。out ディレクトリ配下を確認すると pod 名のディレクトリが自動作成され、jstat の標準出力が吐き出されていることが確認できる。
    $ tree out/
    out/
    └── 1
        ├── myapp-78ccc84d99-2cr24
        │   ├── stderr
        │   └── stdout
        ├── myapp-78ccc84d99-2pmp7
        │   ├── stderr
        │   └── stdout
        ├── myapp-78ccc84d99-59f4g
        │   ├── stderr
        │   └── stdout
        ├── myapp-78ccc84d99-5bwkc
        │   ├── stderr
        │   └── stdout
        ├── myapp-78ccc84d99-5d8wc
        │   ├── stderr
        │   └── stdout
        ├── myapp-78ccc84d99-5fzsd
        │   ├── stderr
        │   └── stdout
        ├── myapp-78ccc84d99-67qkr
        │   ├── stderr
        │   └── stdout
        ├── myapp-78ccc84d99-6glcz
        │   ├── stderr
        │   └── stdout
        └── myapp-78ccc84d99-6htmf
            ├── stderr
            └── stdout
    $ cat out/1/myapp-78ccc84d99-*/stdout
    Timestamp         S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
           339412.0  36.71   0.00  33.87  85.15  97.85  96.46 112186 6009.403   232   25.555 6034.958
           339413.1  36.71   0.00  62.38  85.15  97.85  96.46 112186 6009.403   232   25.555 6034.958
           339414.1  36.71   0.00  89.70  85.15  97.85  96.46 112186 6009.403   232   25.555 6034.958
           339415.1   0.00  37.42  24.00  85.20  97.85  96.46 112187 6009.455   232   25.555 6035.011
           339416.1   0.00  37.42  61.76  85.20  97.85  96.46 112187 6009.455   232   25.555 6035.011
           339417.1   0.00  37.42  89.42  85.20  97.85  96.46 112187 6009.455   232   25.555 6035.011
    Timestamp         S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
           339551.1   0.00  39.96  68.38  46.40  97.99  95.55 107987 6136.845   238   49.173 6186.018
           339552.1   0.00  39.96  93.15  46.40  97.99  95.55 107987 6136.845   238   49.173 6186.018
           339553.1  44.37   0.00  32.62  46.43  97.99  95.55 107988 6136.903   238   49.173 6186.075
           339554.1  44.37   0.00  88.96  46.43  97.99  95.55 107988 6136.903   238   49.173 6186.075
           339555.1   0.00  41.82  17.52  46.49  97.99  95.55 107989 6136.963   238   49.173 6186.135
           339556.1   0.00  41.82  64.14  46.49  97.99  95.55 107989 6136.963   238   49.173 6186.135
           339557.1  39.57   0.00   4.16  46.59  97.99  95.55 107990 6137.027   238   49.173 6186.200
    Timestamp         S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
           339522.0   0.00  35.20  92.33  45.48  97.82  96.47 112235 6360.675   236   25.843 6386.519
           339523.0  37.73   0.00  34.98  45.53  97.82  96.47 112236 6360.728   236   25.843 6386.572
           339524.0  37.73   0.00  79.99  45.53  97.82  96.47 112236 6360.728   236   25.843 6386.572
           339525.0   0.00  36.40  20.69  45.58  97.82  96.47 112237 6360.781   236   25.843 6386.624
           339526.0   0.00  36.40  64.99  45.58  97.82  96.47 112237 6360.781   236   25.843 6386.624
           339527.0  35.51   0.00  10.36  45.64  97.82  96.47 112238 6360.835   236   25.843 6386.678
           339528.0  35.51   0.00  55.57  45.64  97.82  96.47 112238 6360.835   236   25.843 6386.678
    Timestamp         S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
           339454.0  38.18   0.00  38.61  44.10  97.78  95.53 114078 6133.794   236   23.938 6157.731
           339455.1  38.18   0.00  82.87  44.10  97.78  95.53 114078 6133.794   236   23.938 6157.731
           339456.1   0.00  41.97  17.91  44.16  97.78  95.53 114079 6133.854   236   23.938 6157.792
           339457.1   0.00  41.97  51.55  44.16  97.78  95.53 114079 6133.854   236   23.938 6157.792
           339458.1   0.00  41.97  82.24  44.16  97.78  95.53 114079 6133.854   236   23.938 6157.792
    (... snip ...)

kubectl と parallel の組み合わせは相性がよいので組み合わせでいろいろなことができそう。(๑•̀ㅂ•́)و✧

※なお、複数 pod を横断してログを tail するには stern という素晴らしいツールがある。

tail -f /dev/null で調査用コンテナを起動してそのままずっと保持しておく

f:id:kyagi:20180216163134p:plain

Docker コンテナを ENTRYPOINT や CMD の指定なしにちょっとだけ立ち上げたいことがある。例えば Docker イメージで提供されている OS の環境で少しだけ作業をしたい場合など。そんな時は大抵 -d でバックグラウンド指定して sleep コマンドで適当に長い秒数を指定してコンテナを起動した後に docker exec -it /bin/bash してその OS に入っているパッケージやそのバージョンを調査していた。

OS の調査用コンテナを起動して 1 時間後にコンテナを終了させる

$ docker run -d ubuntu sh -c 'sleep 3600' # 1 時間後にコンテナは終了する
5ec839f1cdc7ec657583a4fa992d556946d3b7775e9991175da68653020511ed

$ docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS               NAMES
5ec839f1cdc7        ubuntu              "sh -c 'sleep 3600'"   7 seconds ago       Up 7 seconds                            eloquent_liskov

$ docker exec -it 5ec839f1cdc7 /bin/bash

root@5ec839f1cdc7:/# ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

root@5ec839f1cdc7:/# exit
exit

これだと毎回コンテナを起動し直すはめになるので、調査に複数日かかったりコンテナ状態を保持しておきたい場合は無限ループを与えていた。

OS の調査用コンテナを起動してそのままずっと保持しておく(無限ループ編)

$ docker run -d ubuntu sh -c 'while :; do sleep 3600; done' # 数字はなんでもよい
69f6b10563b922e53139ed5acb5ceb9526daebc857f4a6cc031833f50e1ca04d

ただ、無限ループを与えるよりも tail -f /dev/null を使った方が簡単だいうことを知った。

OS の調査用コンテナを起動してそのままずっと保持しておく(tail -f /dev/null 編)

$ docker run -d ubuntu sh -c 'tail -f /dev/null'
1a258e96395f01d47ec976082ec2d99c5ee5b70949cef02a3108df2255240ab0


$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
1a258e96395f        ubuntu              "sh -c 'tail -f /d..."   3 minutes ago       Up 3 minutes                            unruffled_wilson
69f6b10563b9        ubuntu              "sh -c 'while :; d..."   4 minutes ago       Up 4 minutes                            gracious_wescoff
5ec839f1cdc7        ubuntu              "sh -c 'sleep 3600'"     5 minutes ago       Up 5 minutes                            eloquent_liskov

$ docker exec -it 1a258e96395f /bin/bash
root@1a258e96395f:/#


root@1a258e96395f:/# apt-get update
(... snip ...)

root@1a258e96395f:/# apt-get install dateutils
(... snip ...)

root@1a258e96395f:/# dateutils.ddiff 2018-04-01 2018-12-31 # 今年が終わるまで後何日?
274

確かにこれは楽だ。(´・ω・`)

追記:

docker run -itd ubuntu /bin/bash でバックグラウンドで起動してから、アタッチ(docker attach)とデタッチ(Ctrl+P, Ctrl+Q) するほうがもっと楽だった。(´・ω・`)

fluentd プラグイン sampling-filter と numeric-monitor を組み合わせて nginx のアクセスログから標本を抽出してパフォーマンス測定を実施する

ログの一部を抽出して計算するには sampling-filter + numeric-monitor の組み合わせがぴったり!

大規模データ収集のログコレクタとして fluentd を使用している場合、全部のログの中から一部を抽出してデータ分析を行いたい場合がないだろうか。例えば、サービスのレスポンスタイムが遅くなったり(早くなったり)していないかを測定することができれば、異常検知に役立つ。つまり、ログ(母集団)をデータベースまで運ぶ間にちょっとつまみ喰い(標本)するといった具合だ。平均や特定のパーセンタイル値を出しておけばパフォーマンス監視としては十分だと思う。こういった要求を満たすのに便利な fluentd プラグインの組み合わせが fluent-plugin-sampling-filterfluent-plugin-numeric-monitor だ。

nginx のアクセスログの一部を抽出してパフォーマンス測定を行う場合

ここでは具体例として nginx のアクセスログを考えてみる。例えば以下のアクセスログから vhost ごとの reqtime をサンプリングしてパフォーマンスの指標として vhost ごとの平均と 99 パーセンタイルを出してみる。

access.log

time:15/Mar/2018:01:00:00 +0900  host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.001   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:00 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.001   vhost:www.ayakumo.net
time:15/Mar/2018:01:00:00 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.002   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:00 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.002   vhost:www.ayakumo.net
time:15/Mar/2018:01:00:00 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.003   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:02 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.004   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:03 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.005   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:05 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.006   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:06 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.007   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:08 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.008   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:08 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.009   vhost:blog.ayakumo.net
time:15/Mar/2018:01:00:08 +0900 host:10.0.0.123 forwardedfor:12.123.123.123 req:GET /index.html HTTP/1.1    status:200  size:123    referer:-   ua:-    reqtime:0.010   vhost:blog.ayakumo.net

fluent プラグインの組み合わせによる実装

以下 4 つのプラグインを組み合わせることによりパフォーマンス測定を実装する。rewrite-tag-filter は昔の rewriteruleN 記法ではなく新しい <rule> ... </rule> 記法を使用することでマッチした部分をタグとして利用できるのが素晴らしい。

sample_td_agent.conf

<source>
  @type forward
  port 88888
</source>

<source>
  @type prometheus
</source>

<match nginx.access>
  @type rewrite_tag_filter
  <rule>
    key vhost
    pattern ^(.*)\.ayakumo\.net$
    tag nginx.access.$1
  </rule>
</match>

<match nginx.access.*>
  @type sampling_filter
  interval 100 # 1/100 sampling
  sample_unit all
  add_prefix sampled
</match>

<match sampled.nginx.access.*>
  @type forest
  subtype numeric_monitor
  <template>
    tag reqtime.${tag_parts[3]}
    unit minute
    aggregate all
    monitor_key reqtime
    percentiles 50,99
  </template>
</match>

<match reqtime.*>
  @type forest
  subtype copy
  <template>
    <store>
      @type prometheus
      <metric>
        name reqtime_percentile_50_${tag_parts[1]}
        type gauge
        desc The 50 percentile of reqtime per minute.
        key  percentile_50
      </metric>
      <metric>
        name reqtime_percentile_99_${tag_parts[1]}
        type gauge
        desc The 99 percentile of reqtime per minute.
        key  percentile_99
      </metric>
    </store>
    <store>
      @type stdout
    </store>
  </template>
</match>

<match **>
  @type null
</match>

nginx のアクセスログを擬似的に発行するために fluent-post を以下のように使用する。reqtime のところは $RANDOM を使う代わりに shuf -i 1-100 -n 1 でもよいかもしれない。

fluent-post.sh

for sld in www blog; do
  for i in `seq 1 100`; do
    r=$(($RANDOM % 100))
    /opt/td-agent/embedded/bin/fluent-post -p 88888 -t nginx.access -v time=time:15/Mar/2018:01:00:01 -v host=10.0.0.123 -v forwardedfor=12.123.123.123 -v req="GET /    index.html HTTP/1.1" -v status=200 -v referer="-" -v ua="-" -v reqtime=0.${r} -v vhost=${sld}.ayakumo.net
  done
done

こうすると @type stdout で td-agent.log に出力している numeric-monitor の基本機能により avg や num は自動的に計算されていることがわかる。www と blog の 2 つの sld(Second Level Domain) に対して 100 回(seq 1 100) 投げているが、サンプリング粒度を 1/100 にしているので(interval 100) 標本のサイズはそれぞれ 10 個づつ("num": 10) となる。

td-agent.log

2018-03-15 01:02:32 +0900 [info]: out_forest plants new output: numeric_monitor for tag 'sampled.nginx.access.www'
2018-03-15 01:02:32 +0900 reqtime.blog: {"num":10,"min":0.1,"max":0.98,"avg":0.587,"sum":5.87,"percentile_50":0.6,"percentile_99":0.94}
2018-03-15 01:02:32 +0900 [info]: out_forest plants new output: copy for tag 'reqtime.www'
2018-03-15 01:02:32 +0900 reqtime.www: {"num":10,"min":0.14,"max":0.79,"avg":0.433,"sum":4.33,"percentile_50":0.36,"percentile_99":0.7}

prometheus のメトリクス名としているパーセンタイル 50, 99 も取得できる。

$ curl -s http://localhost:24231/metrics/ | grep -v '^#'
reqtime_percentile_50_blog 0.6
reqtime_percentile_99_blog 0.94
reqtime_percentile_50_www 0.36
reqtime_percentile_99_www 0.7

fluentd のプラグインは便利なものがたくさんそろっているので、組み合わせ次第でいろいろなことができて本当に助かっている。(^_^)

新世代 grep の使い方 ripgrep(rg) と silver-searcher(ag)

はやいは正義

ripgrep が現在進行形で大正義らしい。名前からなんとなく「闇を切り裂く grep」みたいなイメージが浮かぶ。こう、アスファルトタイヤを切り裂きながら暗闇走り抜ける素敵なサムシング、を感じる... のはおぢさんだけ、か。 github.com

なぜ、rip という言葉が頭に浮かんだのかは覚えていない。何か "速い" とか "テキストを切り裂く" みたいなイメージが浮かんだんだ。rip が "Rest in Peace"、(魂よ)安らかに眠れ (つまり ripgrep によって grep の役目を終わらせるってこと) の略語たりうるってことに、その時はまだ気づいていなかった。

I don't remember how it popped into my head, but "rip" came up as something that meant "fast," as in, "to rip through your text." The fact that RIP is also an initialism for "Rest in Peace" (as in, "ripgrep kills grep") never really dawned on me. (FAQ.md)

インストールもはやくするためにワンライナを書いておく。バージョン 0.8.0 で鋭意開発中なので今年中にあと何回か叩くことになる気がする。

$ curl -sfSL https://github.com/BurntSushi/ripgrep/releases/download/0.8.0/ripgrep-0.8.0-x86_64-unknown-linux-musl.tar.gz | sudo tar xzf - -C /usr/local/bin/ --wildcards */rg --strip=1

所感

ag を踏襲しつつ速度をさらにはやめた感じ。新世代 grep 共通で ag も rg も結果表示が以下の形式になるのは同じ。ただこれはターミナルの表示上だけで行指向な本質はそのまま持っている。パイプで繋げるとマッチ行だけになる。デフォルトだとドット(.) ではじまるファイルは検索対象外となるが --hidden を使えば含めてくれる。

ファイル名
マッチ行

ag/rg 使い方逆引き一覧

やりたいこと ag (v2.1.0) rg (v0.8.0)
インストール(mac) $ brew install the_silver_searcher $ brew install ripgrep
インストール(ubuntu) $ sudo apt-get install silversearcher-ag 上記参照
サポートしているファイルタイプ一覧を出す $ ag --list-file-types $ rg --type-list
yml のファイルのリストを出す $ ag --yaml -l . $ rg -t yaml --files
yml のファイルで Deployment が含まれるファイルのリストを出す $ ag --yaml Deployment -l $ rg -t yaml Deployment -l
scala のファイルで case class を検索する $ ag --scala '^case class' $ rg -t scala '^case class'
scala のファイルで getOrElse を使っている(良い)箇所を検索する $ ag --scala -w getOrElse もしくは $ ag --scala '\bgetOrElse\b' $ rg -t scala -w getOrElse もしくは $ rg -t scala '\bgetOrElse\b'
scala のファイルで get を使っている(悪い)箇所を検索する $ ag --scala -w get $ rg -t scala -w get
scala のファイルで get を使っている箇所が全ファイル中どれくらいなのかの統計情報を出す $ ag --scala -w get --stats --stats-only なし
scala のファイルで get.wild を使っているワイルドな箇所を検索する(パイプつなぎのファイル名つき/行番号つき) $ ag --scala --filename --numbers -w get | ag wild $ rg -t scala --with-filename --line-number -w get | rg wild
色付き表示のまま less につなげる $ ag --scala get --pager 'less -R' $ rg -p -t scala get | less -R

ripgrep には統計の機能がないようだが、そこまで使わないし、作者がやる気になればすぐ作れるだろう。速度は rg のほうが早く感じたが両者とも (grep に感じる) ストレスを感じさせない速度だったので(てきとう)使いたいほうを使えばいいと思う。

複数行マッチをサポートする ag とサポートしない rg

例えば Kubernetes の yaml ファイルで以下のようなファイルがあるとする。ag は複数行マッチに対応しており --multiline オプションがデフォルトで有効になっているため ag --yaml 'name.*\n.*spec.*\n.*replicas' . でマッチできる。一方 rg は実装の複雑さとパフォーマンスへの影響から複数行マッチには対応していないし、今後もおそらく対応する予定がない。rg の作者の Andrew Gallant はこの機能差分は ag と rg の違いを示すいい例だとコメントしている。(^_^;

(... snip ...)
  name: user-ranking-app
spec:
  replicas: 10

github.com

検索結果からさらに検索したい時はパイプ/プロセス置換

例えば linux カーネルソースから cgroup_selvictim の両方を含んでいる行を検索したい時(例はてきとう)。パイプのほうがプロセス置換よりも若干速い。フィルタする時は最初の検索にファイル名表示 -H と行番号表示 -n をつけておくほうが便利。

$ time rg -nH -t c cgroup_sel | rg victim
mm/memcontrol.c:1285:int mem_cgroup_select_victim_node(struct mem_cgroup *memcg)
mm/memcontrol.c:1305:int mem_cgroup_select_victim_node(struct mem_cgroup *memcg)
mm/vmscan.c:3124:   nid = mem_cgroup_select_victim_node(memcg);
include/linux/memcontrol.h:428:int mem_cgroup_select_victim_node(struct mem_cgroup *memcg);

real  0m42.889s
user  0m1.455s
sys   0m4.252s
$ time rg victim <(rg -nH -t c cgroup_sel)
1:mm/memcontrol.c:1285:int mem_cgroup_select_victim_node(struct mem_cgroup *memcg)
2:mm/memcontrol.c:1305:int mem_cgroup_select_victim_node(struct mem_cgroup *memcg)
3:mm/vmscan.c:3124: nid = mem_cgroup_select_victim_node(memcg);
4:include/linux/memcontrol.h:428:int mem_cgroup_select_victim_node(struct mem_cgroup *memcg);

real  0m48.226s
user  0m1.958s
sys   0m6.691s

percol 連携に都合のいいオプション

alias s='ag --all-types --hidden --noheading --nonumbers --nofilename . ~/memo/*| percol'

grep は滅びぬ、なんでも蘇るさ

新世代 grep は速度や表示の点で使いやすいけれども、それでも grep が滅びることはない。速度だけを見れば完敗だけれど。grep の問題は歴史的なしがらみだと思う。長年使われてきたせいで、さまざまなスクリプトが依存しており、仕様を変えると世界中に様々な影響が出てきてしまう。高速化もできないことはないが、後方互換性を維持する慎重な開発方針にならざるを得ない、といったところだろうか。あれ、人間も同じようなことが起きるような... |ω・)

d.hatena.ne.jp