はじめまして、サイトオペレーション本部の岡崎(@yutaokaz)です。
今回は、ヤフーに Gimbal という OSS を導入して OpenStack と Kubernetes の Service Discovery と L7 Loadbalancing を実現しましたので、Gimbal の紹介と実現方法をこの Tech Blog に書きたいと思います。
はじめに
サーバーは物理サーバーをそのまま利用するのではなく、仮想化を行い、その上でアプリケーションを動作させることが既に主流になっているかと思います。
ヤフーでも社内に構築した OpenStack 仮想サーバー環境や、Kubernetes コンテナ環境に移行が進んでいます。
仮想化により柔軟にサーバーの構成を変更することが可能になりましたが、冗長化の設定は手動で変更が必要であったり、複数クラスタをまたいだ冗長化が難しい、冗長化セット間に性能差があるためリソースを有効活用することが難しいなどの課題がありました。
これらの課題を解決するために、Yahoo! JAPAN, Actapio, Heptio の 3 社で共同開発を行い、Gimbal として今年の 4 月に OSS の公開を行いました。
リリース時の記事: https://blog.heptio.com/introducing-heptio-gimbal-bridging-cloud-native-and-traditional-infrastructure-9d6224bece5a
Gimbal について
Gimbal は Kubernetes 上で動作し、Service Discoverer と L7 Loadbalancer の機能を提供します。
Service Discoverer は OpenStack 用のもの、Kubernetes 用のもので別れており、指定されたクラスタの継続的な Service Discovery を行います。
L7 Loadbalancer には Envoy という OSS を利用しています。Envoy に設定を行うために Contour という OSS も同時に利用しています。
Gimbal システム全体の概要は以下の画像のようになっています。
画像引用元: https://github.com/heptio/gimbal/blob/master/docs/images/overview.png
L7 Loadbalancer の設定は IngressRoute という Kubernetes リソースを YAML ファイルに定義して、VirtualHost と Service Discoverer によって登録された Service との対応づけを行います。設定の適用には kubectl
コマンドを利用し、オンラインでの設定変更が可能になっています。
次項では実際に Gimbal を利用したサービス提供を行いながら、もう少し詳細を見ていきたいと思います。
Gimbal を利用したサービス提供
Gimbal による Service Discovery と L7 Loadbalancing 機能を利用したサービス提供を試してみたいと思います。
サービスの準備
OpenStack と Kubernetes 上にそれぞれサービスを作成します。
OpenStack サービス作成
下記の通り、80, 8000 ポートでサービスを提供するようなウェブサーバー 2 台を冗長化した loadbalancer を OpenStack project: yutaokaz-advent-calendar
に作成しました。
$ neutron lbaas-loadbalancer-show yutaokaz-service
+---------------------+------------------------------------------------+
| Field | Value |
+---------------------+------------------------------------------------+
| admin_state_up | True |
| description | |
| id | 8baa7a9b-d723-4558-be23-871b778a9f7d |
| listeners | {"id": "953df759-1f21-4bf0-9c34-c150f6b9a9b0"} |
| | {"id": "5d872299-158f-41fd-b108-0c980d59041d"} |
| name | yutaokaz-service |
| operating_status | ONLINE |
| pools | {"id": "3dddbff9-5be4-4df8-8e74-3cfe5d110998"} |
| | {"id": "5cb11f6d-7744-4336-8369-16a6a24032a9"} |
| provider | haproxy |
| provisioning_status | ACTIVE |
| tenant_id | 2facfc7214a64c2898f2584fe8b5fff3 |
| vip_address | < VIP > |
| vip_port_id | 851c5844-7e2e-4f2b-8123-9a1e0d85904a |
| vip_subnet_id | 81fa2f8e-f514-4bbd-8190-ccbecb87a558 |
+---------------------+------------------------------------------------+
HTTP レスポンスはどのサーバーからのレスポンスなのかを識別できるようにしてみました。
$ curl $VIP
OpenStack) server01:80
$ curl $VIP
OpenStack) server02:80
$ curl $VIP:8000
OpenStack) server01:8000
$ curl $VIP:8000
OpenStack) server02:8000
Kubernetes サービス作成
Kubernetes 上にも同様にサービスを作成していきます。
Namespace は先程作成した OpenStack project 名と同じ yutaokaz-advent-calendar
にしておきます。下記に今回作成したサービスの YAML ファイルを記載します。 (長くなりますので 02-service8000.yaml は省略します。)
01-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: yutaokaz-advent-calendar
02-service80.yaml
apiVersion: v1
kind: Service
metadata:
namespace: yutaokaz-advent-calendar
name: yutaokaz-service80
spec:
type: ClusterIP
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: yutaokaz-service80
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
namespace: yutaokaz-advent-calendar
name: yutaokaz-service80
labels:
app: yutaokaz-service80
spec:
replicas: 1
selector:
matchLabels:
app: yutaokaz-service80
template:
metadata:
labels:
app: yutaokaz-service80
spec:
containers:
- image: httpd:2.4
name: yutaokaz-service80
command:
- "/bin/bash"
- "-c"
- |
echo 'Kubernetes) service80' > htdocs/index.html && httpd-foreground
hostNetwork: true
ロードバランシングのために Gimbal をデプロイする Kubernetes クラスタと Kubernetes サービス Pod は IP 疎通が必要です。hostNetwork: true
を指定することで Pod は ClusterIP でなく、NodeIP を利用しますので、Gimbal の稼働するクラスタから疎通が可能になります。
早速これらのファイルを利用して、Kubernetes へサービスをデプロイしてみます。
$ kubectl apply -f advent-calendar/
namespace/yutaokaz-advent-calendar created
service/yutaokaz-service80 created
deployment.extensions/yutaokaz-service80 created
service/yutaokaz-service8000 created
deployment.extensions/yutaokaz-service8000 created
$ kubectl get namespaces yutaokaz-advent-calendar
NAME STATUS AGE
yutaokaz-advent-calendar Active 35s
$ kubectl get deployments -n yutaokaz-advent-calendar -o wide
NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
yutaokaz-service80 1 1 1 1 102s yutaokaz-service80 httpd:2.4 app=yutaokaz-service80
yutaokaz-service8000 1 1 1 1 101s yutaokaz-service8000 httpd:2.4 app=yutaokaz-service8000
$ kubectl get services -n yutaokaz-advent-calendar -o wide
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
yutaokaz-service80 ClusterIP 10.99.224.250 <none> 80/TCP 2m8s app=yutaokaz-service80
yutaokaz-service8000 ClusterIP 10.108.51.131 <none> 8000/TCP 2m7s app=yutaokaz-service8000
$ kubectl get pods -n yutaokaz-advent-calendar -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE
yutaokaz-service80-bdd85b9df-zthlv 1/1 Running 0 2m12s < Node1IP > yutaokaz-node1 <none>
yutaokaz-service8000-7bcdf747d6-4n7gs 1/1 Running 0 2m12s < Node2IP > yutaokaz-node2 <none>
問題なくデプロイできているようです。念のため HTTP レスポンスも確認しておきます。
$ curl $Node1IP
Kubernetes) service80
$ curl $Node2IP:8000
Kubernetes) service8000
レスポンスも期待した通りに得られているようです。
これでサービスの準備が完了しました。次は先程作成した OpenStack サービスと一緒に Service Discovery を行っていきます。
Service Discoverer の起動
先程作成した Kubernetes サービスクラスタとは別の Kubernetes クラスタ上に Service Discoverer をデプロイして、OpenStack と Kubernetes それぞれの Service Discovery を行ってみます。
ここからの手順は Gimbal README にも記載がありますので参考にしてみてください。
Service Discoverer 準備
今回は Gimbal Repository のファイルを利用して進めていきたいと思いますので、Git からソースコードを取得しておきます。
$ git clone https://github.com/heptio/gimbal.git
$ cd gimbal/deployment/
Service Discoverer を起動する前に、デプロイ先となる Kubernetes Namespace を作成しておきます。
$ kubectl apply -f gimbal-discoverer/01-common.yaml
namespace/gimbal-discovery created
serviceaccount/gimbal-discoverer created
clusterrolebinding.rbac.authorization.k8s.io/gimbal-discoverer created
clusterrole.rbac.authorization.k8s.io/gimbal-discoverer created
namespace/gimbal-discovery
が作成されました。
Service Discoverer はサービスを作成した OpenStack project 名または Kubernetes Namespace 名でサービスの登録を行いますので、この Kubernetes クラスタにも yutaokaz-advent-calendar
Namespace を作成しておきます。
$ kubectl create namespace yutaokaz-advent-calendar
namespace/yutaokaz-advent-calendar created
ここまでで準備は完了です。次は Service Discoverer を起動して、サービスを登録していきます。
OpenStack Service Discoverer
OpenStack Service Discoverer は OpenStack Identity v3 API と LBaaS v2 API を利用して、Service Discovery を行います。
OpenStack admin ユーザーとパスワード、OpenStack Keystone URL, https 通信時の CA 証明書を Kubernetes Secret として登録しておきます。backend-name
は複数の仮想基盤クラスタの Service Discovery を行ってもサービスの区別がつくように、Kubernetes Service 名の接頭語として利用されます。
$ kubectl -n gimbal-discovery create secret generic remote-discover-openstack \
--from-literal=username=admin \
--from-literal=password=abc123 \
--from-literal=auth-url=https://api.openstack:5000/ \
--from-file=certificate-authority-data=./ca.pem \
--from-literal=backend-name=os \
--from-literal=tenant-name=yutaokaz-advent-calendar
secret/remote-discover-openstack created
OpenStack Service Discoverer をデプロイします。
$ kubectl apply -f gimbal-discoverer/02-openstack-discoverer.yaml
deployment.extensions/openstack-discoverer created
# デプロイの確認
$ kubectl get pods -n gimbal-discovery
NAME READY STATUS RESTARTS AGE
openstack-discoverer-f97f6c495-gkr77 1/1 Running 0 15s
# ログの確認
$ kubectl logs -n gimbal-discovery openstack-discoverer-f97f6c495-gkr77
特にエラー等出力されていなければデプロイは成功です。
OpenStack の Service Discovery が始まっていますので、確認してみます。
$ kubectl get services -n yutaokaz-advent-calendar
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
os-8baa7a9b-d723-4558-be23-871b778a9f7d ClusterIP None <none> 8000/TCP,80/TCP 50s
$ kubectl get endpoints -n yutaokaz-advent-calendar
NAME ENDPOINTS AGE
os-8baa7a9b-d723-4558-be23-871b778a9f7d < Server1IP >:8000,< Server2IP >:8000,< Server1IP >:80 + 1 more... 70s
OpenStack loadbalancer が Kubernetes Service として、OpenStack pool member が Kubernetes Endpoint として、それぞれ登録されました。登録名は指定した接頭語と loadbalancer UUID を繋げたものになっています。
OpenStack Discoverer はデフォルトで 30 秒ごとに OpenStack から情報を取得し、変更があれば Kubernetes の情報を変更しますので、OpenStack loadbalancer の追加・削除や構成変更を行っても、Gimbal クラスタ側での作業は発生しません。
Kubernetes Service Discoverer
続けて、Kubernetes の Service Discovery を行ってみます。
OpenStack Discoverer の時と同様に Kubernetes Discoverer にも Kubernetes Secret を用意します。
$ kubectl -n gimbal-discovery create secret generic remote-discover-kubecfg \
--from-file=config=./config \
--from-literal=backend-name=k8s
secret/remote-discover-kubecfg created
config
には Service Discovery 対象の Kubernetes Config ファイルパスを指定します。backend-name
は OpenStack Discoverer 同様、サービス名の接頭語として利用されます。
準備ができましたので、Kubernetes Service Discoverer をデプロイします。
$ kubectl apply -f gimbal-discoverer/02-kubernetes-discoverer.yaml
deployment.extensions/k8s-kubernetes-discoverer created
# デプロイの確認
$ kubectl get pods -n gimbal-discovery
NAME READY STATUS RESTARTS AGE
k8s-kubernetes-discoverer-78975d5cf-gbw6f 1/1 Running 0 3s
openstack-discoverer-54588f4bbf-vxxxn 1/1 Running 0 17m
# ログの確認
$ kubectl logs -n gimbal-discovery k8s-kubernetes-discoverer-78975d5cf-gbw6f
正常に起動しているようであれば、Service Discovery の結果を確認してみます。
$ kubectl get services -n yutaokaz-advent-calendar
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
k8s-yutaokaz-service80 ClusterIP None <none> 80/TCP 30s
k8s-yutaokaz-service8000 ClusterIP None <none> 8000/TCP 30s
os-8baa7a9b-d723-4558-be23-871b778a9f7d ClusterIP None <none> 8000/TCP,80/TCP 17m
$ kubectl get endpoints -n yutaokaz-advent-calendar
NAME ENDPOINTS AGE
k8s-yutaokaz-service80 < Node1IP >:80 36s
k8s-yutaokaz-service8000 < Node2IP >:8000 36s
os-8baa7a9b-d723-4558-be23-871b778a9f7d < Server1IP >:8000,< Server2IP >:8000,< Server1IP >:80 + 1 more... 17m
k8s-
から始まる Kubernetes Service と Endpoint が追加されました。
今回は OpenStack クラスタ、Kubernetes クラスタともに 1 クラスタずつの Service Discovery を行ってみましたが、Kubernetes Secret と Service Discoverer のリソース名を変えて複数起動することで、複数クラスタの Service Discovery を行うことが可能になっています。
L7 Loadbalancer の作成
Service Discovery によって対象クラスタでの変更情報が継続的に反映されるようになりました。ここからは登録されたサービス情報を利用した L7 Loadbalancing を行い、外部にサービス公開をしていきたいと思います。
Contour, Envoy のデプロイ
L7 Loadbalancing を行う Envoy と、設定情報を提供するための Contour をデプロイします。
$ kubectl apply -f contour/
namespace/gimbal-contour created
serviceaccount/contour created
customresourcedefinition.apiextensions.k8s.io/ingressroutes.contour.heptio.com created
clusterrolebinding.rbac.authorization.k8s.io/contour created
clusterrole.rbac.authorization.k8s.io/contour created
service/contour created
service/envoy created
configmap/envoy-statsd created
deployment.apps/contour created
daemonset.extensions/envoy created
起動を確認してみると、各 Kubernetes Node 上に Contour: 1, Envoy: 2 pod が起動していることが分かります。
$ kubectl get pods -n gimbal-contour
NAME READY STATUS RESTARTS AGE
contour-76b7cd5d88-8f2xw 1/1 Running 0 17s
contour-76b7cd5d88-f6lxh 1/1 Running 0 17s
envoy-jjcm2 2/2 Running 0 17s
envoy-x74tk 2/2 Running 0 17s
これでロードバランスの準備は整いました。
L7 Loadbalancer 設定
起動したロードバランサの設定を行ってみます。下記のような YAML ファイルを作成します。
---
apiVersion: contour.heptio.com/v1beta1
kind: IngressRoute
metadata:
name: yutaokaz-advent-calendar
namespace: yutaokaz-advent-calendar
spec:
virtualhost:
fqdn: yutaokaz-advent-calendar.local
routes:
- match: /
services:
- name: os-8baa7a9b-d723-4558-be23-871b778a9f7d
port: 80
- name: k8s-yutaokaz-service80
port: 80
fqdn: yutaokaz-advent-calendar.local
のエンドポイント /
に OpenStack, Kubernetes のサービスを割り当てています。早速デプロイしてみます。
$ kubectl apply -f ingressroute.yaml
ingressroute.contour.heptio.com/yutaokaz-advent-calendar created
$ kubectl get ingressroutes -n yutaokaz-advent-calendar
NAME FQDN TLS SECRET FIRST ROUTE STATUS STATUS DESCRIPTION
yutaokaz-advent-calendar yutaokaz-advent-calendar.local / valid valid IngressRoute
無事にデプロイできました。YAML 定義にエラーがあった場合にはエラー終了しますので、メッセージに従ってファイルを修正してください。kubectl get
では設定した FQDN や ROUTE, STATUS の情報が出力されていることが確認できます。Gimbal Kubernetes クラスタ上で稼働している Envoy にリクエストして、レスポンスを確認してみます。
$ curl -H 'Host: yutaokaz-advent-calendar.local' $GimbalNodeIP
OpenStack) server01:80
$ curl -H 'Host: yutaokaz-advent-calendar.local' $GimbalNodeIP
Kubernetes) service80
$ curl -H 'Host: yutaokaz-advent-calendar.local' $GimbalNodeIP
OpenStack) server02:80
$ curl -H 'Host: yutaokaz-advent-calendar.local' $GimbalNodeIP
Kubernetes) service80
設定した通り、OpenStack と Kubernetes にロードバランスされています。
(デフォルトでは RoundRobin にてロードバランスされますが、正確にラウンドロビンで割り振られる訳ではないようです。レスポンスは分かりやすく表示しています。)
L7 Loadbalancing の機能も試してみたいと思います。/openstack
にアクセスしたときは、OpenStack の 8000 ポートにアクセスするようにしてみます。
サービスは複数ポート混ぜることも可能ですので、/
へのアクセスは複数ポートを混ぜた定義にしてみます。さらに OpenStack サービスは 1 Service に 2 台が設定されているので、weight を設定して各 Endpoint に均等にリクエストが到着するようにしてみます。
変更後の YAML ファイルを下記に記載します。
---
apiVersion: contour.heptio.com/v1beta1
kind: IngressRoute
metadata:
name: yutaokaz-advent-calendar
namespace: yutaokaz-advent-calendar
spec:
virtualhost:
fqdn: yutaokaz-advent-calendar.local
routes:
- match: /
services:
- name: k8s-yutaokaz-service80
port: 80
weight: 25
- name: k8s-yutaokaz-service8000
port: 8000
weight: 25
- name: os-8baa7a9b-d723-4558-be23-871b778a9f7d
port: 80
weight: 50
- match: /openstack
services:
- name: os-8baa7a9b-d723-4558-be23-871b778a9f7d
port: 8000
設定の適用とレスポンスの確認をしてみます。設定変更はサービス断なしに行うことができます。
# apply
$ kubectl apply -f ingressroute.yaml
ingressroute.contour.heptio.com/yutaokaz-advent-calendar configured
# weight, mixed port
$ curl -H 'Host: yutaokaz-advent-calendar.local' $GimbalNodeIP
OpenStack) server01:80
$ curl -H 'Host: yutaokaz-advent-calendar.local' $GimbalNodeIP
OpenStack) server02:80
$ curl -H 'Host: yutaokaz-advent-calendar.local' $GimbalNodeIP
Kubernetes) service80
$ curl -H 'Host: yutaokaz-advent-calendar.local' $GimbalNodeIP
Kubernetes) service8000
# /openstack
$ curl -H 'Host: yutaokaz-advent-calendar.local' $GimbalNodeIP/openstack/
OpenStack) /openstack server01:8000
$ curl -H 'Host: yutaokaz-advent-calendar.local' $GimbalNodeIP/openstack/
OpenStack) /openstack server02:8000
オンラインでの設定変更と L7 Loadbalancing, weight 設定機能を試してみました。
実サービスで利用する際には各 Endpoint へヘルスチェックを行い、障害時やメンテナンス時はサービスアウトさせる、ヘルスチェック機能があると便利です。Envoy にはヘルスチェックの機能がありますので利用してみます。
OpenStack VM 2 台の内の 1 台に /healthz
へのレスポンスを設定してみました。
$ curl -si $Server1IP/healthz | head -1
HTTP/1.1 200 OK
$ curl -si $Server2IP/healthz | head -1
HTTP/1.1 404 Not Found
サービスにヘルスチェックを設定するには以下のように記述します。
---
apiVersion: contour.heptio.com/v1beta1
kind: IngressRoute
metadata:
name: yutaokaz-advent-calendar
namespace: yutaokaz-advent-calendar
spec:
virtualhost:
fqdn: yutaokaz-advent-calendar.local
routes:
- match: /
services:
- name: os-8baa7a9b-d723-4558-be23-871b778a9f7d
port: 80
healthCheck:
path: /healthz
intervalSeconds: 10
設定を反映して、レスポンスを確認してみます。
# apply
$ kubectl apply -f ingressroute.yaml
ingressroute.contour.heptio.com/yutaokaz-advent-calendar configured
# request
$ curl -H 'Host: yutaokaz-advent-calendar.local' $GimbalNodeIP
OpenStack) server01:80
$ curl -H 'Host: yutaokaz-advent-calendar.local' $GimbalNodeIP
OpenStack) server01:80
server02 はサービスアウトと判断され、リクエストが割り振られなくなりました。
さらに細かいヘルスチェックの設定や、バランシング方法など、今回紹介できなかった機能もありますので、ぜひドキュメントも確認してみてください。
Gimbal とサービスの監視
Gimbal にはサービス監視のために Grafana と Prometheus も用意されています。
Gimbal の稼働している Kubernetes クラスタにデプロイすることで、各 Service Discoverer と Envoy のステータスを Prometheus がメトリクスを取得して、Grafana で確認できるようになっています。
デフォルトのダッシュボードでは Service Discoverer の状況、各サービスへの RPS や Latency などの情報を表示しています。
Gimbal Discovery
Contour
Envoy
サービスがサーバーエラーのレスポンスステータスコードを送信していたり、Latency の閾値を超えたなどの異常を検知した際には Alert Manager を利用したアラート通知を行うことも可能です。
Gimbal の制限
Gimbal を利用するには下記のような制限がありますので、利用する際には注意が必要です。
ヤフーでの対応はインデントを下げて記載しています。
- プロトコルは http/https のみ対応しています。
- ロードバランスを行うために、Gimbal クラスタからサービス Pod への IP 疎通が必要です。
- ユーザーからのリクエストは Gimbal の稼働している Kubernetes Node へ到着するので、Node の冗長化は別途検討が必要です。
- Kubernetes Node の冗長化にはハードウエアロードバランサを利用しています。
- IngressRoute VirtualHost Name 設定が重複するとエラーになります。
- IngressRoute VirtualHost を作成できる Kubernetes Namespace を admin に制限し、ユーザーには VirtualHost の作成を許可しない運用を行っています。
- Delegation の機能を利用することで、ユーザーは VirtualHost 内で自由に path やヘルスチェックの設定を行うことが可能です。
最後に
ここまで Gimbal を利用した OpenStack と Kubernetes の Service Discovery と L7 Loadbalancing の実現方法を紹介してきましたが、いかがでしたでしょうか。
Service Discovery 対象数にもよりますが、Discovery も十分に高速ですのでサービス提供までのスピードをひとつ上げることができるのではないかと思っています。
複数の仮想基盤クラスタをまとめたロードバランスが可能で、設定変更がサービス断なしに行えますので、新機能のカナリアデプロイを行う、サービスの高負荷を見越してクラスタリソースを追加しておく、高負荷が過ぎたらクラスタごと捨てる、仮想化基盤クラスタの移行を段階的に行う、仮想化基盤クラスタ自体のメンテナンスを行う等々、今まで難しかったさまざまなことが実現可能になるのではないかと期待しています。
先に述べたような複雑なことをしないまでも、Gimbal には Service Discovery の機能があり、L7 Loadbalancer である Envoy の性能が良く、ロードバランス先へのヘルスチェック機能もありますので、現在利用しているハードウエア/ソフトウエアロードバランサの置き換えも検討できるかもしれません。
ヤフーではまだ開発環境への導入に留まっていますが、今後本番環境にも展開していきたいと思っています。Gimbal の運用や開発、活用方法などで有益な情報が得られましたら、また何かしらの形で共有したいと思います。
もし検証や導入をされた方がいましたら、ブログなどで共有していただけるととてもうれしいです。
それでは明日のアドベントカレンダーもお楽しみに。
こちらの記事のご感想を聞かせください。
- 学びがある
- わかりやすい
- 新しい視点
ご感想ありがとうございました