Kubernetes に関する投稿を表示しています

Google Cloud Build + kaniko を試す

Cloud Build のドキュメントを見ていたら、ビルドを高速化するのに kaniko を使うソリューションがあるよ、と書かれていたので、試してみる。

Kaniko キャッシュの使用 | Cloud Build | Google Cloud
GitHub - GoogleContainerTools/kaniko: Build Container Images In Kubernetes

kaniko っていうのはすごいざっくりと理解したのは Docker なしに k8s 上で Docker イメージをビルドできるよ、というもの。
で、そのとき対象の Dockerfile に関して、レイヤーごとに全部キャッシュを取ってくれるので、差分があった部分以降をビルドするだけになり docker build よりも高速化できる、という話っぽい。通常 docker build するときも、レイヤーごとの情報を持っていて、変わった部分以降をビルドすることができる。できるが、そのキャッシュのバックエンドとか、マルチステージにしたときのビルドステップの進め方とかをいい感じにしたっていう話なんじゃあないかなあ。(それ以上の理解はできていない)

さて、ビルドする対象となる Dockerfile はこういう具合。
マルチステージで go で書かれたアプリケーションサーバのビルド、からのそのアプリケーションコンテナのイメージを作る。

FROM golang:1.12 as builder
ARG GITHUB_TOKEN
ARG VERSION
ENV GO111MODULE=on

WORKDIR /go/src/github.com/foo/bar

COPY go.mod go.sum ./
RUN echo "machine github.com login ${GITHUB_TOKEN}" > ~/.netrc
RUN go mod download

COPY . ./
RUN CGO_ENABLED=0 GOOS=linux go install -v -ldflags "-X main.version=${VERSION}" github.com/foo/bar/cmd/server

FROM alpine:latest

RUN apk add --update --no-cache ca-certificates tzdata
COPY --from=builder /go/bin/server /bin/server
CMD ["/bin/server"]

それでもとの cloudbuild.yaml はこうなっている。
変数をバケツリレーしていく。

steps:
- name: gcr.io/cloud-builders/docker
  args:
  - build
  - -t
  - $_IMAGE
  - --build-arg
  - GITHUB_TOKEN=$_GITHUB_TOKEN
  - --build-arg
  - VERSION=$_VERSION
  - -f
  - $_DOCKERFILE_PATH
  - .
images:
  - $_IMAGE

で、これを kaniko に対応させるとこうなる。

steps:
- name: 'gcr.io/kaniko-project/executor:latest'
  args:
  - --destination=$_IMAGE
  - --dockerfile=$_DOCKERFILE_PATH
  - --cache=true
  - --cache-ttl=6h
  - --build-arg=GITHUB_TOKEN=$_GITHUB_TOKEN
  - --build-arg=VERSION=$_VERSION

その他 GCP 設定や Dockerfile などなどはそのままで実行することはできた。

できたが結果はちょっと微妙で、めっちゃ早くなったかというとそうでもない。。。

ビルド時、主に時間がかかるのが go mod download と go install (実際は go build が時間かかる)のふたつで、アプリケーションのコードを変えただけの場合は go mod のキャッシュが効いてくれるが、レイヤーを持ってくる(?)のにやっぱり時間がかかるので、プラスマイナスみて、ややマイナス、みたいな状態。。

具体的な時間としては kaniko を使う前の元々ので 5 分くらい。 kaniko してフルキャッシュ状態(再実行しただけ)で 1-2 分。 go mod のキャッシュが効いてて 2-4 分くらい。キャッシュがまったくなし = go.mod が変更された場合で 5-7 分くらい。ちゃんと測ってなくて数回ためした程度で、実はもっと早いかもしれない。そこは試す余地がある。

あとは単純にお財布で戦って、マシンサイズを上げて強い子にすると早くなるんじゃないかなあ~。


そもそも、コンテナイメージの作成を早くするのが目的なら CI 上でクロスコンパイルしたバイナリを Docker にいれちゃうのがよさそう。
というか CI 上でも golang:1.12 なイメージでビルドするならば別にクロスコンパイルでもないのか。

マルチステージな Dockerfile でアプリケーションのビルドもまるっと!と Cloud Build でビルドすると、余計なゴミが入りにくい、という良い点はありそう。

とはいえ、ワークスペースをアタッチしない、かつ go mod だけ共有してビルドしちゃえば Cloud Build でやるのとほぼ同じ環境になりそうな気がする。そうしてできたバイナリをコンテナ内に COPY して、コンテナレジストリに登録しちゃえばいいんじゃないかな。

kubectl で Service から Pod の紐づけを一発で確認した

まとめとしては -o json | jq な感じで頑張るのがとりあえずは正解っぽい。

$ kubectl get -o json endpoints | jq '.items[] | [.metadata.name, ([.subsets[].addresses[].targetRef.name] | join(", "))] | @tsv' --raw-output
http-server    http-86d67d8d47-46fm8, http-86d67d8d47-x487n, http-86d67d8d47-nxgdx
http-server-dev    http-dev-58fc975f8f-z2lvw, http-dev-58fc975f8f-tjczr

シェルむけの関数を組んで kubectl にオプション渡せるようにしたら便利、かも、と思ってざっくり書いた。
あたらしく Service を作ったりときにセレクタミスってないかな?とかを見るのに便利、かも。

#!/bin/sh
function kube_lookup_pod() {
    kubectl get -o json endpoints $@ \
    | jq -r \
      '["Service", "Pods"], (.items[] | [.metadata.name, ([.subsets[].addresses[].targetRef.name] | join(", "))]) | @tsv' \
    | column -t
}
$ kube_lookup_pod --namespace foo-namespace
Service               Pods
http-server           http-86d67d8d47-46fm8,      http-86d67d8d47-x487n,      http-86d67d8d47-nxgdx
http-server-dev       http-dev-58fc975f8f-z2lvw,  http-dev-58fc975f8f-tjczr

そこまでの経緯もメモっておく。

Kubernetes の Service の一覧はこうやってみえるが、これはどれに紐付いているかが分からない。

$ kubectl get svc
NAME                   TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)             AGE
http-server            ClusterIP   None         <none>        9100/TCP            10h
http-server-dev        ClusterIP   None         <none>        9100/TCP            10h

ワンチャン describe なら Endpoint がでてくるので、ギリギリわかる。

$ kubectl describe svc http-server
Name:              http-server
Namespace:         foo-namespace
Labels:            <none>
Annotations:       <none>
Selector:          app=http-server
Type:              ClusterIP
IP:                None
Port:              http-server  9100/TCP
TargetPort:        9100/TCP
Endpoints:         10.108.30.146:9100,10.108.35.228:9100,10.108.54.229:9100
Session Affinity:  None
Events:            <none>

Endpoint も get できるがこのままではわからない。

$ kubectl get endpoints
NAME                   ENDPOINTS                                                              AGE
http-server            10.108.30.146:9100,10.108.35.228:9100,10.108.54.229:9100               10h
http-server-dev        10.108.30.149:9100,10.108.32.247:9100                                  10h

describe してもだめ。

$ kubectl describe endpoints http-server
Name:         http-server
Namespace:    foo-namespace
Labels:       <none>
Annotations:  <none>
Subsets:
  Addresses:          10.108.30.146,10.108.35.228,10.108.54.229
  NotReadyAddresses:  <none>
  Ports:
    Name         Port  Protocol
    ----         ----  --------
    http-server  9100  TCP

Events:  <none>

おもむろに -o json やったら実はデータとしては取れることがわかった。

$ kubectl get endpoints http-server -o json
{
    "apiVersion": "v1",
    "kind": "Endpoints",
    "metadata": {
        "name": "http-server",
        ...省略...
    },
    "subsets": [
        {
            "addresses": [
                {
                    ...省略...
                    "ip": "10.108.30.146",
                    "nodeName": ".......",
                    "targetRef": {
                        "kind": "Pod",
                        "name": "http-86d67d8d47-46fm8",
                        "namespace": "foo-namespace",
...省略...

あとは jq でがんばったら取れる。

$ kubectl get -o json endpoints | jq '.items[] | [.metadata.name, ([.subsets[].addresses[].targetRef.name] | join(", "))] | @tsv' --raw-output
http-server    http-86d67d8d47-46fm8, http-86d67d8d47-x487n, http-86d67d8d47-nxgdx
http-server-dev    http-dev-58fc975f8f-z2lvw, http-dev-58fc975f8f-tjczr

書いてるときに試行錯誤してたけど jq の使い方について学びを得た

  • Object ならキーを選んで、配列ならインデックスを選ぶか [] で全選択して、というのをパイプしたら概ね抽出できる
  • join は配列に対してしか使えないので、一覧が出てきそうなやつでも配列にしてからパイプする
  • フラットな配列なら末尾で @csv や @tsv すると便利、そのあと column にパイプするとさらに便利
  • そのときは --raw-output オプションもセットでやると OK
  • あとはマニュアルとにらめっこ jq Manual (development version)

Kubernetes の Liveness と Readiness

簡単にまとめると Liveness に失敗したらザキ Readiness に失敗したらやけつく息。…いや逆にわからんような気もする。

Liveness は Pod が稼働できるかどうかの判定をする。判定に失敗した場合は Kubernetes がその Pod を作り直す。
Pod が提供するべき最低限の機能すら提供できない場合にこのチェックを失敗させるべきだろう。
たとえば自分のアプリケーションが待ち受けるサーバーを提供するんだけど、そこへの疎通が一切できないとか。それが最初から起きるならアプリケーションは起動しないだろうが、起動してから何かしらの理由でサーバが落ちるといったことが無いわけではないはず。

Readiness は Pod と疎通できるかの判定をする。判定に失敗すると Kubernetes が Service から切り離す。このとき Pod は生きたままになる。
デッカい初期データを読み込むなどと起動に時間のかかるようなアプリケーションだったとき、アプリケーションは無事に起動する(=Liveness をパスできる)が、リクエストを受け付けることができないので、そういった場合は Readiness を設定して、Serviceから切り離されるべきだろう。

 

あと、他のアプリケーションに依存している部分を Liveness や Readiness には入れないほうがよいはず。
その確認をいれていると、回り回って全ての依存関係が全滅するとか Liveness / Readiness のデッドロックが起こってしまいそうな予感がする。

A は B への疎通確認を Liveness にいれて、 B は C への疎通確認を Liveness にいれて、としたときに、仮に C が何かの影響で疎通できないような状態になると B が作り直され A が作り直され、結局 A → B → C とつながった機能が提供できなくなる。

接続できるけど Unavailable な状態、 HTTP でいうと 5xx なステータスを、アプリケーションあるいはプロキシするサイドカーな何かで返すほうがよいのでは?
ほら nginx も upstream につながらなくなっても nginx は終了せずに「upstream に繋がらないけど」というエラーが出るじゃん?
これを各 Pod でもできるようにしたほうがいいと思うのよ。

 

Liveness も Readiness もどちらも、監視までの時間、監視する間隔、判定失敗とするまでの許容する失敗回数といったオプション値を設定できる。
このあたりは公式ドキュメントを見るとまとまってるのでこまかい話はそっちを見るようにしたらよい。
Configure Liveness and Readiness Probes - Kubernetes

それと Liveness も Readiness も、どちらも時間や間隔の調整は必要なものの、基本的にはヘビーな処理が発生しないようにする必要がある。
Liveness や Readiness のチェックでかかる負荷によって、自分自身をを失敗させる原因にならならないようにするために。

kubectl port-forward to Service (追記Deployment も可)

kubectl port-forward って pod に対してだけしかできないと思っていたが Service に対してもできたみたい。

$ kubectl port-forward svc/my-service 5000

このとき、ポートフォワードを開始するタイミングでだけ k8s の名前解決が行われて、フォワードする先を決定する。
サービスに対してフォワードしているわけではないので、フォワードしたポートに何度リクエストを送ってもロードバランスされない。

Issue にもコメントがあるが port-forward はもともとがそういう用途ではなく 1 つの pod に対してフォワードするので、サービスに対して完全にフォワードするには pod がいなくなったときに再接続したりバランスするようにしたりなんだり、多くの機能追加になるので、別にしようやという話がある。

kubectl port-forward should allow forwarding to a Service · Issue #15180 · kubernetes/kubernetes · GitHub


追記: Deployment も対象にできたのしらなかった、ドキュメントをみてくれ!!

Use Port Forwarding to Access Applications in a Cluster - Kubernetes