freeeの開発情報ポータルサイト

KubernetesでのService公開方法に関する検証 - Ingress Controllerの活用

freeeでSREをしている河村(at-k)です。

freeeでは、既存・新規サービスのマイクロサービス化を推進しており、効率的なマイクロサービスの運用を実現するためにKubernetesを積極活用しています。Kubernetesはコンテナのオーケストレーションツールであり、コンテナ化されたマイクロサービスを管理・運用していく上で大きな効果が期待されます。

Kubernetesでは、複数のノード(例えばAWS EC2 Instance)を組み合わせてクラスタを構成し、そのクラスタ上にコンテナが指定された構成(manifest)で配置されます。Kubernetesはコンテナ構成を自律的に維持する機能を持ち、運用コストや耐障害率を改善します。また、クラスタに配置されたサービスに対しては、具体的などのノードにコンテナをスケールするか、といった詳細にとらわれることなく、宣言的にサービスを定義し、細かな運用はKubernetesにまかせるといったことができます。本稿では後者の、Kubernetes内サービスを外部に公開する手法について行った検証を解説します。

Kubernetesクラスタ内のサービスを公開する方法はいくつかありますが、最近社内で検証を行ったIngress Controller、特にNGINX Ingress Controllerについて紹介し、導入方法及び、カスタマイズ方法としてgRPC通信を有効にする手順を解説します。他の方法としては、例えばIstioや、envoyを利用する、ということも考えられましたが、freeeではNGINXの本番運用経験が豊富なことと、Ingress以外でもKubernetes上でNGINXを利用するあてがあったこと、envoyでないとできないというクリティカルな要件が「まだ」なかったこと、Ingressの実装としてはNGINX Ingress Controllerが一番こなれている(機能の豊富さとメンテナのアクティブさ、本番事例)といった点を考慮しました。

Kubernetesについてある程度の基本的知識を想定していますが、公式が提供しているTutorials - Kubernetesがよくまとまっており、1時間程度でキャッチアップ出来ますので、興味があればこの機会にいかがでしょうか。

Minikubeを使ったサービスの公開

まず例として、Minikubeを使った公式チュートリアル(Hello Minikube - Kubernetes)をベースに、サービスの外部公開まで実施します。MinikubeのインストールとImageの作成までは省略しますが、以下のコマンドで指定しているhello-nodeは、上記の公式チュートリアルで作成した、”Hello World!” を返すWeb ServerのImageです。

> kubectl run hello-node --image=hello-node:v1 --port=8080

> kubectl expose deployment hello-node --type=LoadBalancer

> kubectl get svc
NAME           TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
hello-node     LoadBalancer       10.96.19.86     <none>        8080:32623/TCP   1m

> minikube service hello-node --url
http://192.168.99.100:32623

> curl http://192.168.99.100:32623
Hello World!

LoadBalancer Type(NodePort Typeでも可)のServiceを作成し、Service毎に割り当てられたポート(例では32623)を通してアクセスします。この様な方法は、サービスがどこで動いているのか一見してわかりやすく感じます。しかし、サービスを追加するたびに、未使用portを払い出したり、portへのネットワーク疎通を確保するなどのKubernetesの外側のインフラを別途整備する必要があります。公開するサービスが少なければ良いですが、数が増えてくると煩雑になります。

参考

Kubernetes NodePort vs LoadBalancer vs Ingress? When should I use what?

Services - Kubernetes Ingress - Kubernetes

Nginx Ingress Controllerを用いたサービス公開

そこで、次に紹介するIngressとIngress Controllerを用いて、それらの課題の解決を図ります。Serviceの前段にIngressを配置する構成で、それらのIngressに対し、ルーティングを行うIngress Controllerがクラスタ内に最低1つ置かれます。Ingress ControllerはInboundトラフィックに対し、要求ホスト名とIngressで公開されるホスト名を突き合わせてルーティングを行います。先程の例と比較すると以下の図の様になります。

Ingress Controllerを使った構成とそうでない構成との違いの図。Ingressを使わない場合はサービスごとにポートを別々にしているが、Ingressを使用した場合は同じポートで2つのサービスを公開している

詳しくは実際の動作を見ていただくほうがわかりやすいかと思うので、ここからIngress Controllerの導入と動作例を見ていきます。Ingress Controllerとして利用できるものはいくつかあるようですが、ここではNGINX Ingress Controllerを試しました。Kubernetesが公式に開発していること、NGINXベースで高いスケーラビリティが期待できること、また、gRPCに対応していること、が採用理由です。

標準設定でのインストール

helm chartが用意されているので、標準的設定であればクラスタへのインストールは比較的容易です。helmはKubernetesにおけるパッケージ管理ツールで、パッケージの検索、インストール、標準設定からのカスタマイズ、設定のrevision管理、などの便利な機能が提供されています。ここでは詳細は省略します。

まずはinstall。

> helm search nginx-ingress
NAME                    CHART VERSION   APP VERSION     DESCRIPTION
stable/nginx-ingress    0.20.1          0.14.0          An nginx Ingress controller that uses ConfigMap...

> helm upgrade --install nginx-ingress stable/nginx-ingress

> helm list
NAME            REVISION        UPDATED                         STATUS          CHART                   NAMESPACE
nginx-ingress   1               Sun Jun 17 22:44:48 2018        DEPLOYED        nginx-ingress-0.20.1    default

helm installで何が行われるかは、以下のコマンドを実行することで、具体的にどういったmanifestが生成・適用されるかを確認することが出来ます。

> helm upgrade --install nginx-ingress stable/nginx-ingress --dry-run --debug | grep Source
...
# Source: nginx-ingress/templates/controller-service.yaml
# Source: nginx-ingress/templates/default-backend-service.yaml
# Source: nginx-ingress/templates/controller-deployment.yaml
# Source: nginx-ingress/templates/default-backend-deployment.yaml
...

Service/Deploymentがそれぞれ2つ宣言されていることがわかります。1つはIngress Controller本体、もう一つのdefault backendは、定義されていないhost nameでアクセスされた場合に用いられるServiceです。kubectlで配置されたServiceを見てみます。

> kubectl get svc
NAME                            TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)                      AGE
nginx-ingress-controller        LoadBalancer   10.103.136.223   <pending>     80:30774/TCP,443:31153/TCP   35m
nginx-ingress-default-backend   ClusterIP      10.106.62.223    <none>        80/TCP                       35m

動作検証

続いて、ingressの設定を行います。

> kubectl expose deployment hello-node --type=ClusterIP --name hello-node-svc --port 8080
service "hello-node-svc" exposed

> cat << EOF | kubectl create -f -
pipe heredoc> apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: hello-world
spec:
  rules:
  - host: hello-world
    http:
      paths:
      - path: /
        backend:
          serviceName: hello-node-svc
          servicePort: 8080
EOF

> kubectl get ingress
NAME            HOSTS              ADDRESS   PORTS     AGE
hello-world   hello-world                80        1m

このとき、ingress controllerのlogを見ていると以下のようにingressの追加が自動で検出され、設定が反映されたことがわかります。

> kubectl logs nginx-ingress-controller-786dc4f648-zlcqz -f

I0617 14:14:29.430140       6 event.go:218] Event(v1.ObjectReference{Kind:"Ingress", Namespace:"default", Name:"hello-world", UID:"bfa92545-7238-11e8-b043-080027992c84", APIVersion:"extensions", ResourceVersion:"995305", FieldPath:""}): type: 'Normal' reason: 'CREATE' Ingress default/hello-world
I0617 14:14:29.546514       6 controller.go:177] ingress backend successfully reloaded...

それではpodにアクセスします。

> minikube service nginx-ingress-controller --url
http://192.168.99.100:30774
http://192.168.99.100:31153

> sudo sh -c 'echo 192.168.99.100 hello-world >> /etc/hosts'

> curl http://hello-world:30774
Hello World!

Ingressに設定したhost nameを使うことで、hello world podにアクセス出来ました。

同じ要領で、"Hello World!"の代わりに"Hello Ingress!"と返すServiceを作ります。ImageとDeploymentの作成は省略、hello-ingress-nodeというDeploymentを作成したとします。

> kubectl expose deployment hello-ingress-node --type=ClusterIP --name hello-ing-node-svc --port 8080
service "hello-ing-node-svc" exposed

> cat << EOF | kubectl create -f -
pipe heredoc> apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: hello-ingress
spec:
  rules:
  - host: hello-ingress
    http:
      paths:
      - path: /
        backend:
          serviceName: hello-ing-node-svc
          servicePort: 8080
EOF

> sudo sh -c 'echo 192.168.99.100 hello-ingress >> /etc/hosts'

> curl http://hello-ingress:30774
Hello Ingress!

hello-worldとhello-ingressは、host nameは異なりますが、どちらも同じIPを指しており、portも同じなので、ネットワーク的には同一のものを参照しています。host nameを元にIngress Controllerが振り分けを行っています。

この様に、Ingress Controllerを配置しておけば、新しいサービスが追加される際に、公開ホスト名を定義したIngressを設定することで自動でサービスまでの動線を整備してくれます。これらの作業はKubernetes内で完結しているため、クラスタ内・外での責任範囲が明確化され、運用の簡易化・権限委譲を効率的に進めることが出来ます。 なお、ホスト名をDNSに登録する必要がありますが(例では/etc/hostsに登録)、こちらについてもexternal-dnsを使うことで、例えばAWSではIngressの追加時にRoute 53に自動設定することが出来ます(今回はMinikubeでの検証なので省略します)。

参考

Kubernetesのexternal-dnsでRoute53 RecordSetを自動作成する - Qiita

external-dns/aws.md at master · kubernetes-incubator/external-dns

カスタマイズ - gRPC用追加設定

デフォルトの設定でもおおよその目的は達せられましたが、実際の運用では設定をカスタマイズしたくなることもあると思います。ここでは、NGINX Ingress ControllerのBackendにgRPCアプリケーションを配置するための手順を説明します。 先にも書いたとおり、NGINX Ingress ControllerはgRPCに対応していますが、いくつか追加設定が必要になります。対象は、Ingress、Ingress Controller、場合によってはapplicationにも手を入れる必要があります。

検証環境として、grpc/grpc-goで配布されているgreeter_serverを配置した以下のDockerfileのコンテナを用いました。同様にgreeter_clientを使って疎通検証を行います。

FROM grpc/go:1.0
EXPOSE 50051
RUN go get -u google.golang.org/grpc/examples/helloworld/greeter_server
RUN go get -u google.golang.org/grpc/examples/helloworld/greeter_client
CMD greeter_server

Ingressの設定

gRPCサービスであることをAnnotationに記載する必要があります。metadataにannotationsとして、grpc-backendであることを明示します。

cat << EOF | kubectl create -f -
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    nginx.ingress.kubernetes.io/grpc-backend: "true"
  name: hello-grpc
spec:
  rules:
  - host: hello-grpc
    http:
      paths:
      - path: /
        backend:
          serviceName: hello-grpc-svc
          servicePort: 8080
EOF

Ingress Controllerの設定

NGINX Ingress ControllerのHTTPポートのデフォルトの設定では、HTTP2が有効になっていません。gRPCをHTTPで公開する場合は、追加の設定が必要です(HTTPSの場合は不要)。デフォルト設定で、HTTP2でIngress ControllerのHTTPポートにアクセスすると、次のようなログが出力されます。

172.17.0.1 - [172.17.0.1] - - [17/Jun/2018:16:26:34 +0000] "PRI * HTTP/2.0" 400 174 "-" "-" 0 0.001 [] - - - -

修正が必要な設定箇所は以下のあたりです。listen 443はhttp2が設定されていますが、listen 80にはされていません。

minikube > k exec nginx-ingress-controller-786dc4f648-2k4hh cat /etc/nginx/nginx.conf | grep listen
                listen 80 default_server  backlog=511;
                listen [::]:80 default_server  backlog=511;
                listen 443  default_server  backlog=511 ssl http2;
                listen [::]:443  default_server  backlog=511 ssl http2;

そこで、修正を入れた設定ファイルをロードするように、helm chartsを補正します。NGINX Ingress Controllerは、カスタムテンプレートからnginx.confを生成する仕組みになっているため、このテンプレートを自前のものに差し替えます。

まず、オリジナルのテンプレートを取ってきます。

> kubectl exec nginx-ingress-controller-786dc4f648-2k4hh cat /etc/nginx/template/nginx.tmpl > nginx.tmpl.org

これに対し、以下のように変更を加えます。わかりにくいですが、listen 80 にhttp2を追加しています。

> diff nginx.tmpl.org nginx.tmpl
702c696
<         listen {{ $address }}:{{ $all.ListenPorts.HTTP }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }}{{end}};
---
>         listen {{ $address }}:{{ $all.ListenPorts.HTTP }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }} http2{{end}};
704c698
<         listen {{ $all.ListenPorts.HTTP }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }}{{end}};
---
>         listen {{ $all.ListenPorts.HTTP }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }} http2{{end}};
708c702
<         listen {{ $address }}:{{ $all.ListenPorts.HTTP }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }}{{ end }};
---
>         listen {{ $address }}:{{ $all.ListenPorts.HTTP }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }} http2{{ end }};
710c704
<         listen [::]:{{ $all.ListenPorts.HTTP }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }}{{ end }};
---
>         listen [::]:{{ $all.ListenPorts.HTTP }}{{ if $all.Cfg.UseProxyProtocol }} proxy_protocol{{ end }}{{ if eq $server.Hostname "_"}} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }} http2{{ end }};

これを元にconfigmapを作成します。

> kubectl create configmap nginx-custom-tmpl --from-file=nginx.tmpl
configmap "nginx-custom-tmpl" created

configmapをマウントするようにvalues.yamlを書き換え、helm upgradeします。

> cat <<EOF > values.yaml
controller:
  name: controller
  ## Override NGINX template
  customTemplate:
    configMapName: nginx-custom-tmpl
    configMapKey: nginx.tmpl
EOF

> helm upgrade --install nginx-ingress stable/nginx-ingress -f values.yaml
参考

ingress-nginx version 0.13 for grpc not work · Issue #2444 · Kubernetes/ingress-nginx

動作確認

アクセス先を、Nginx Ingress Controllerに変更したgreeter_clientを実行します。

> go run greeter_client/main.go
2018/06/18 02:01:40 Greeting: Hello world

うまく疎通したようです。

アプリケーション側対応(必要であれば)

ここまででIngress ControllerのgRPC対応は完了ですが、アプリケーション側に修正が必要なケースもあります。gRPC通信が時々うまくいかず(全部失敗するわけではない)、NGINX Ingress Controllerのログに

upstream sent invalid http2 table index: 64 while reading response header from upstream

といったエラーが出ている場合は、go-grpcの既知のバグが原因である可能性があり、その場合は、修正PRが取り込まれたバージョンが必要です。詳しくは下記のIssueを参考にしてください。

gRPC servers cannot use dynamic HPACK · Issue #2405 · kubernetes/ingress-nginx

終わりに

Kubernetesのサービス公開方法として、Nginx Ingress Controllerを使った方法を紹介しました。個別にNodePort設定をするよりも、シンプルかつ容易にサービスの外部公開が可能になります。

Kubernetesはコンテナ管理のための新しい基盤であり、日々多くの機能が提案・追加されています。そのため、情報が不足していたり、まだデファクトが定まっていなかったりする部分が多く、学習・導入コストが高くつく可能性があります。一方で、Kubernetesの提供するフレームワークは非常に強力であり、複雑化しがちなマイクロサービス構成を、集約・統合的にまとめることで、サービスの運用を大幅に効率化できる可能性を持っています。本稿が、読者の皆様の何かしらのご助力になれば幸いです。