Skip to main content

Ingress Nginxは何をしてるのか

NGINX Ingress Controllerは何を起動して実装はどうなっているかを読んだので書く
これを読むと実装を読んだ気分になれる

このエントリーをはてなブックマークに追加

前置き

ここで書いている内容は全ての事は書いてないし気になったところだけを順番に見ている。
公開後は随時追記や修正をしていくつもりである。

間違っている箇所があったりしたらTwitter(@let_constant)へDMとかで教えてください。

日付 内容
2020-07-19 ingress-nginx-2.7.0 の内容で公開

NGINX Ingress Controllerとは

NginxをベースイメージとしてリバースプロキシやLBとして動作するIngress実装である。
L7で動作するのでホスト名やパスでのルーティング、SSLのオフロード等が実現できる。

他のIngress実装はTraefikContourがあり、ここにまとめられている。

お気持ち

ServiceはClusterIPで公開する限りServiceにIPが振られるのでIngressはそれを見ていると思っていた。
ではそうなるとSticky Sessionsはどうやって実現しているのか不思議になった。
How it worksを読むと普通に書いてあるが、Endpointsを使用している。

これを知った時感動した。

ところで Nginx Ingress Controller のことを nginx-ingress なのか ingress-nginx って言えばいいのかいつもわからなくなる。正解を知りたい。

全体像

まずは大まかな全体像の図を貼る。
NodePort で公開されていたり、Job により Secret が生成されていたりする。

all.png

実装の話

バージョンは2020-06-21(日)時点で最新のingress-nginx-2.7.0
マニフェストはbaremetal/deploy.yaml

マニフェストを見るとcontrollerのDeploymentやJobが動いている事がわかる。
Podとして動いているものを順番に見ていく事にする。

ingress-nginx-controller

イメージとしては下記のような順番で生成されている。

  1. alpine:3.11
  2. quay.io/kubernetes-ingress-controller/nginx:e3c49c52f4b74fe47ad65d6f3266a02e8b6b622f
  3. quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.33.0

ビルド

ビルドは build/build.sh で行なっていて3つのバイナリを作っている。

go build \
  -ldflags "-s -w \
    -X ${PKG}/version.RELEASE=${TAG} \
    -X ${PKG}/version.COMMIT=${GIT_COMMIT} \
    -X ${PKG}/version.REPO=${REPO_INFO}" \
  -o "rootfs/bin/${ARCH}/nginx-ingress-controller" "${PKG}/cmd/nginx"

go build \
  -ldflags "-s -w \
    -X ${PKG}/version.RELEASE=${TAG} \
    -X ${PKG}/version.COMMIT=${GIT_COMMIT} \
    -X ${PKG}/version.REPO=${REPO_INFO}" \
  -o "rootfs/bin/${ARCH}/dbg" "${PKG}/cmd/dbg"

go build \
  -ldflags "-s -w \
    -X ${PKG}/version.RELEASE=${TAG} \
    -X ${PKG}/version.COMMIT=${GIT_COMMIT} \
    -X ${PKG}/version.REPO=${REPO_INFO}" \
  -o "rootfs/bin/${ARCH}/wait-shutdown" "${PKG}/cmd/waitshutdown"

controller本体

マニフェスト

提供されているマニフェストでは下記のような引数で起動している。
全ての引数の詳細はCommand line argumentsに書いてある。

  • --election-id はHPAやreplica数を増やした場合にリーダー選出するためのidである、リーダー選出する実装についてはここ
  • --ingress-class はクラスタ内で複数のIngress Controllerを動かしている際に指定が必要になる、GKEを使ってる場合とかはちゃんと指定する必要がある
  • --validating-webhookAdmission Controller起動してシンタックスエラーがないか等を調べる、指定しない場合は起動されない
args:
  - /nginx-ingress-controller
  - --election-id=ingress-controller-leader
  - --ingress-class=nginx
  - --configmap=ingress-nginx/ingress-nginx-controller
  - --validating-webhook=:8443
  - --validating-webhook-certificate=/usr/local/certificates/cert
  - --validating-webhook-key=/usr/local/certificates/key

main.go

フラグのパースやサーバーの起動を実装している。

  • Kubernetesのバージョンが v1.14.0 v1.18.0 と比べてログを出したり IngressClass リソースから取得したりする
    v1.14.0 未満では k8s.io/api/extensions/v1beta1 を使うようにログに出る
    v1.18.0 からは IngressClass リソースが実装されていて、kubernetes.io/ingress.class アノテーションは非推奨になった

  • ヘルスチェック用のサーバー起動

  • GoやNginxのメトリクスを収集するためのCollectorの起動

$ kubectl -n ingress-nginx exec -it ingress-nginx-controller-7fd7d8df56-dv7s6 -- bash -c "curl -s localhost:10254/metrics | grep go_gc"
# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 3.58e-05
go_gc_duration_seconds{quantile="0.25"} 9.96e-05
go_gc_duration_seconds{quantile="0.5"} 0.0002579
go_gc_duration_seconds{quantile="0.75"} 0.0006037
go_gc_duration_seconds{quantile="1"} 0.009535
go_gc_duration_seconds_sum 0.0567369
go_gc_duration_seconds_count 75
  • pprof用のエンドポイントを生やす、デフォルトでは 10245 番のポートで開放されていてここで実装されている
  • controllerの起動

controller.NewNGINXController にはcontrollerの実装が書かれているので次はここの実装を見る
SIGTERM を投げることで停止されるようになっている。

ngx := controller.NewNGINXController(conf, mc)

mux := http.NewServeMux()
registerHealthz(nginx.HealthPath, ngx, mux)
registerMetrics(reg, mux)

go startHTTPServer(conf.ListenPorts.Health, mux)
go ngx.Start()

handleSigterm(ngx, func(code int) {
	os.Exit(code)
})

internal/ingress/controller/nginx.go

func NewNGINXController(*Configuration, metric.Collector) *NGINXController

  • EventBroadcaster の作成、ここにサンプルがあるがこれを使う事で Events に書き込む事ができる
eventBroadcaster := record.NewBroadcaster()
eventBroadcaster.StartLogging(klog.Infof)
eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{
	Interface: config.Client.CoreV1().Events(config.Namespace),
})
  • /etc/resolv.conf から nameserver の取得
  • Admission Controllerの設定
  • store.New でIngressやSecretといった各リソースのイベントが発火された際の処理を実装している
    イベントの発火後は引数の updateCh に送信し、ここで受信する。
n.store = store.New(
	config.Namespace,
	config.ConfigMapName,
	config.TCPConfigMapName,
	config.UDPConfigMapName,
	config.DefaultSSLCertificate,
	config.ResyncPeriod,
	config.Client,
	n.updateCh,
	pod,
	config.DisableCatchAll)
  • (*NGINXController).syncIngresssyncQueue のcallback関数として指定している
    キューにタスクが追加されるたびに syncIngress が実行される
    syncIngress では実際にEndpointsを取ったりしている、ここは詳しく見ていく: nginx.go#L139
n.syncQueue = task.NewTaskQueue(n.syncIngress)

func (*NGINXController) Start()

  • electionID を使ったリーダー選出し、Ingressリソースのステータスを更新するPodを決める、リーダー選出は client-go実装されている。

  • --enable-ssl-passthrough を指定してcontrollerを起動した場合に、 nginx.ingress.kubernetes.io/ssl-passthrough アノテーションのついたリソースでSSLパススルーを行うプロキシサーバーの起動

  • Admission Controller の起動

  • Nginx の起動

  • syncQueue の起動、Exponential Backoff によるリトライの実装

  • Nginx の起動

  • updateCh から受信したイベントを syncQueue に流す

func (*NGINXController) Stop() error

  • Admission Controller の停止
  • Nginx の停止、Nginx が停止しているかどうかはプロセスの一覧を取ってnginxという名前がないか見ているだけのシンプルな実装である

func (*NGINXController) syncIngress(interface{}) error

キューにタスクが追加されるたびにこの関数が実行される。
実行はここでしている。

endp, err := n.serviceEndpoints(svcKey, path.Backend.ServicePort.String())
  • service-upstream が指定されている場合は Endpoints ではなく Service に振られているIPを使う
  • テンプレートを評価し、nginx -s reload を実行する

Admission Controller

Admission Controller のHadnlerの実装はここでしている。

if n.cfg.ValidationWebhook != "" {
		n.validationWebhookServer = &http.Server{
			Addr:      config.ValidationWebhook,
			Handler:   adm_controler.NewAdmissionControllerServer(&adm_controler.IngressAdmission{Checker: n}),
			TLSConfig: ssl.NewTLSListener(n.cfg.ValidationWebhookCertPath, n.cfg.ValidationWebhookKeyPath).TLSConfig(),
		}
	}

NGINXControllerCheckIngress実装していて、
Admission Controller がリクエストを受け取ると (*IngressAdmission) HandleAdmission(*v1beta1.AdmissionReview) が叩かれてその中で CheckIngress が叩かれる。

(*NGINXController) CheckIngress(*networking.Ingress) error

下記のような事を行なっている。

  • kubernetes.io/ingress.class アノテーションが起動時に指定した --ingress-class と一致するかどうか
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: application-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  backend:
    serviceName: application-service
    servicePort: 8080
  • 起動時に --watch-namespace を指定していた場合、指定された Namescape かどうか
  • nginx -t による生成する予定の nginx.conf のテスト

Leader Election

Leaderとなった PodIngress のステータスを毎秒同期している。

client-go実装されていて、 ConfigMap を使いロックをしている。
Leaderが落ちたりして renewTime の更新ができなかった場合は他の PodrenewTimeholderIdentity を更新しLeaderに昇格する。

$ kubectl -n ingress-nginx get cm/ingress-controller-leader-nginx -o yaml
apiVersion: v1
kind: ConfigMap
metadata:
  annotations:
    control-plane.alpha.kubernetes.io/leader: '{"holderIdentity":"ingress-nginx-controller-75f84dfcd7-f22xq","leaseDurationSeconds":30,"acquireTime":"2020-06-22T14:38:14Z","renewTime":"2020-07-15T18:22:19Z","leaderTransitions":0}'
  creationTimestamp: "2020-06-22T14:38:14Z"
  managedFields:
  - apiVersion: v1
    fieldsType: FieldsV1
    fieldsV1:
      f:metadata:
        f:annotations:
          .: {}
          f:control-plane.alpha.kubernetes.io/leader: {}
    manager: nginx-ingress-controller
    operation: Update
    time: "2020-07-15T18:22:19Z"
  name: ingress-controller-leader-nginx
  namespace: ingress-nginx
  resourceVersion: "295822"
  selfLink: /api/v1/namespaces/ingress-nginx/configmaps/ingress-controller-leader-nginx
  uid: 00f21dfe-d5d9-4dc0-becf-e7bbd45be4ff

wait-shutdown

シンプルな実装だった。

err := exec.Command("bash", "-c", "pkill -SIGTERM -f nginx-ingress-controller").Run()
if err != nil {
	klog.Errorf("unexpected error terminating ingress controller: %v", err)
	os.Exit(1)
}

// wait for the NGINX process to terminate
timer := time.NewTicker(time.Second * 1)
for range timer.C {
	if !nginx.IsRunning() {
		timer.Stop()
		break
    }
}

dbg

デバッグ用のCLIである。

$ /dbg --help
dbg is a tool for quickly inspecting the state of the nginx instance

Usage:
  dbg [command]

Available Commands:
  backends    Inspect the dynamically-loaded backends information
  certs       Inspect dynamic SSL certificates
  conf        Dump the contents of /etc/nginx/nginx.conf
  general     Output the general dynamic lua state
  help        Help about any command

Flags:
  -h, --help   help for dbg

Use "dbg [command] --help" for more information about a command.

ingress-nginx-admission-create

kube-webhook-certgenに実装がある。
これは何をしているかというと期限の長い自己証明書を生成してシークレットに設定してくれるやつだ。

指定されているnamespaceのSecretが存在しなかった場合のみ生成されるようになっている。
ここではAdmission Controllerの証明書を生成しようとしているのがわかる。

- name: create
  image: jettech/kube-webhook-certgen:v1.2.0
  imagePullPolicy: IfNotPresent
  args:
    - create
    - --host=ingress-nginx-controller-admission,ingress-nginx-controller-admission.ingress-nginx.svc
    - --namespace=ingress-nginx
    - --secret-name=ingress-nginx-admission

100年後で生成されているのが確認できる。

$ kubectl -n ingress-nginx describe secret/ingress-nginx-admission
Name:         ingress-nginx-admission
Namespace:    ingress-nginx
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
cert:  623 bytes
key:   227 bytes
ca:    485 bytes

$ kubectl -n ingress-nginx get secret/ingress-nginx-admission -o jsonpath="{.data.ca}" | base64 --decode | openssl x509 -noout -dates
notBefore=Jun 22 14:32:42 2020 GMT
notAfter=May 29 14:32:42 2120 GMT

ingress-nginx-admission-patch

ではpatchは何をしているのか

webhook-name に指定されている名前の ValidatingWebhookConfiguration を取得し、
実装としては各webhookに対して先程生成した自己証明書と failure-policy を設定している。

- name: create
  image: jettech/kube-webhook-certgen:v1.2.0
  imagePullPolicy: IfNotPresent
  args:
  - create
  - --host=ingress-nginx-controller-admission,ingress-nginx-controller-admission.ingress-nginx.svc
  - --namespace=ingress-nginx
  - --secret-name=ingress-nginx-admission

何をしているかはわかったがこれはcreateと一緒に済ませる事ができそうな感じがしてるので
これが導入された経緯も探してみたが特に書いてないので jettech/kube-webhook-certgen についても深追いしてみる。

jettech/kube-webhook-certgen

結論から書くと設計思想のようなものは見つける事ができなかった。

helmチャートのPRを見るとわかるが、 Implementation is inspired by the prometheus operator webhooks と書いていて 同じ仕組みで動いている。