こんにちは。 アドテクスタジオの Central Infrastructure Agency(CIA) の青山真也です。

本記事は CyberAgent Developers Advent Calendar 2017 の 1 日目の記事となります。

CIA は 15 名程のアドテクスタジオの横軸インフラ組織として、プライベートクラウドの提供や、プライベート/パブリック両方のインフラサポートを行っています。 ちなみにこの Advent Calendar に CIA は 6 名参加(実に 1/4!)してますので、インフラに興味がある方は是非他の記事も読んでみて下さい。

最近ではアドテクスタジオでも、コンテナによる開発が主流となってきており、GKE で開発することが多くなってきています。 一方で、アドテクスタジオは大規模な OpenStack ベースのプライベートクラウド基盤も保有しており、Container as a Service を提供しています。

 

その名も、

ake

 

今までも要素技術の紹介や外部勉強会で AKE のことを少し話したりしたことは有りましたが、本記事では AKE の話をまるっとさせて頂けたらなと思います。

 

基礎知識

まず前提として、コンテナとオーケストレーションエンジンの話を少しだけしておきます。

いわゆる Docker などのコンテナランタイムを利用して、1つ1つコンテナを起動する場合、VM 上に systemd でプロセスを起動させている状態に近いため、スケーリングや負荷分散、リソースの管理、障害時の復旧作業、スケジューリングやノード管理などを管理者が全て管理する必要が出てきてしまいます。

 

coe01

 

coe02

 

coe03

 

coe04

 

一方で、コンテナオーケストレーションエンジンを利用することで、管理者はこれらの操作を自動化することができるため、現在はコンテナを利用する際には必須の機能となってきています。

GCP GKE や AWS ECS に代表されるコンテナ基盤もオーケストレーションエンジンを元にしたサービスです。

 

コンテナ基盤の提供方法

オンプレ環境で Kubernetes のコンテナ基盤を提供する際に考えられる方法は2つあります。

  • 1つの大きな共有 Kubernetes クラスタを構築し、Namespace でテナント分離を行なう方法
  • テナントやシステムごとに Kubernetes クラスタを構築する方法

なお、AKE では後者の「テナントやユーザごとに Kubernetes クラスタ」を構築する方式を採用しました。

 

1つ目の「1つの大きな共有 Kubernetes クラスタ」方式の問題点

 

large_cluster

 

Quota の制御が困難

ResourceQuota を利用することにより、Kubernetes の Namespace に対してリソース制限をかけることが可能です。しかし、requests / limits が設定されていない場合にはベストエフォートでコンテナが起動してしまうため、Noisy Neighbor となってしまう場合があります。 LimitRange を利用することでデフォルト値を設定することは可能ですが、ユーザによってはベストエフォートでコンテナを動かしたい場合などもあるため、1つのクラスタで全部のユーザの要望に応えるのは困難です。

 

Network の分離が困難

AKE では flannel を利用しています。 flannel を利用した構成では、NetworkPolicy を利用することが出来ないため、Pod のネットワークを分離することが困難です。 Calico、Cilium、Romana などを利用している場合には、Network Policy 利用することができるため、分離することが可能です。

 

many core なベアメタルノードを利用しにくい

複数テナントが共有で1つのクラスタを利用する提供方式場合、Kubernetes Node となるマシンはベアメタルを利用することになるかと思います。 ベアメタルノードをわざわざVMに分割してもいいですが、オーバーヘッドが大きい点・ベアメタルノード障害時には全VMが落ちてしまうことから Kubernetes のスケジューリングが困難であるため、あまりおすすめできません。 また、many core のベアメタルノードに小負荷の大量のコンテナを起動させた場合、kubelet がコンテナのヘルスチェックをする負荷が増大し、ライフサイクルの間隔が長くなったりしがちなのも問題としてあげられます。

 

スケーラビリティ問題

上記で少し触れましたが、Kubernetes v1.8 クラスタのスケーラビリティは下記の通りです。

  • Node 数 5000 以下
  • Pod 数 150000 以下
  • Container 数 300000 以下
  • 1 Node 辺りのPod 数 100 以下

また、ここでいうスケーラビリティの条件は下記のとおりです。

  • API へのリクエストのうち 99% が 1 sec 以内
  • コンテナの起動が 5 sec 以内

マルチマスター構成時の挙動、kube-dns の反映時間、Service (iptables) の切り替え時間などはチェックされていないため、巨大なクラスタを構築する際は要件に合致する性能を出せるかを綿密に確認する必要がありそうです。

 

2つ目の「テナントやユーザごとに Kubernetes クラスタ」方式の問題点

 

each_cluster

 

リソース効率の低下

Kubernetes の魅力の一つでもあるリソース効率の向上という面では、Kubernetes クラスタが分割されることによって低下してしまいます。 一方、クラスタを分割した場合には YAML マニフェストファイルなどがほぼ同じ物が利用可能であるため、メリットとも捉えることが出来ます。

 

ノードサイズの縮小

Kubernetes の特性上、プロダクション環境で利用する場合には Kubernetes node の数は最低でも 3 以上が好ましいです。 一方で、many core の物理サーバをそのまま 3 台割り当ててしまうと、スモールスタートが非常に困難になってしまいます。 そのため、Kubernetes node は低コア数のベアメタルや VM に制限されてしまうことが多くなってしまいます。

 

OpenStack 上への Kubernetes の展開方法

アドテクスタジオでは、プライベートクラウド基盤としてOpenStack を利用しています。 OpenStack で Kubernetes を展開するにあたり、

  • OpenStack Magnum を利用
  • kubeadm や tectonic などの自動構築ツールを利用
  • 自分達で1から作り込む

といった選択肢があるかと思います。AKEでは、最後の「自分たちで 1 から作り込むという」一番ハードな選択肢を取りました。しかしながら、コンテナ基盤を提供するにあたり Kubernetes について深い知見がないままサービスを提供してしまうと、障害時の初動が遅れてしまいます。私達としては、初手に時間と人的コストは掛かってしまうものの、障害時に調べるのではなく事前に深い知見を習得できるという点からも、1から作り込むことを選択しました。

 

OpenStack Magnum を利用した Kubernetes の展開の問題点

OpenStack Magnum は数ある OpenStack Component の 1 つで、Kubernetes などのオーケストレーションエンジンを展開するためのものです。 条件として OpenStack の L3 Network 機能(LoadBalancer as a Service を含む)が必要となっていますが、弊社の OpenStack 環境では低レイテンシを重視する関係から L2 Network を利用しているため、利用することができません。 OpenStack を利用してプライベートクラウドを構築しているところでは、L2 を選択していることが多いのではないでしょうか。 また、2016 年後半から Magnum の目指すゴールが変わったりしたため、完成度や今後の動向を考えると選択が難しいといった背景もありました。

OpenStack_Project_Magnum_horizontalOpenStack_Project_Zun_horizontal

 

各種構築ツールを使用した場合の問題点

Kubernetes を自動構築するツールは多々あります。 弊社が AKE を作り始めたときは完成度がイマイチだったものも多かったですが、最近は本当に簡単に Kubernetes クラスタを構築することができるようになってきました。 しかしながら、クラスタ構築時にサーバを構築すると

  • 時間が掛かる
  • バージョンの差異が出やすい

と言った問題があるほか、構築ツール依存のナレッジになりやすいため、問題があった場合の対処に遅れが生じる可能背があります。 また、OpenStack Magnum にも言える問題ですが、チューニング・AdmissionControl などの拡張機能の有効化など、細かい設定が難しいといった問題もあります。

 

自分たちで 1 から作り込む場合の問題点

自分たちで1から作り込む場合の問題は、とにかく工数がかかってしまうことです。 一方で、2 名体制でも半年ほどで正式リリースすることができるほどに作り込むことは可能なため、不可能な選択肢では無いと思います。

 

OS のイメージ化

以前は OpenStack Heat を用いてクラスタ構築時にバイナリの配置やパッケージのインストールを行っていましたが、現在の AKE では幾つかの観点からクラスタに利用する OS のイメージ化を行っています。

 

パッケージやバイナリのバージョンがクラスタ内で差異がでる

Kubernetes や Docker などのバージョンはハンドリングすることで固定することが可能です。 しかし、カーネルや依存パッケージのバージョンが予期せずズレる場合があるため、イメージ化をしています。 Docker が Moby に変わったタイミングで 1.12.0 などから 17.03.0.ce に変わったように、コンテナ周りは技術の流れが非常に早いため、構築ツールなどを使う場合には注意が必要です。 また、スケーリング操作は既存のクラスタと同一のノードがスケールすることを想定していることが多いですが、構築ツール等を使っている場合には完全同一は達成できない可能性が非常に高くなってしまいます。

 

クラスタ構築やスケーリング時の時間が大幅に掛かる

以前の AKE ではイメージ化を行っていなかったため、初回のクラスタ構築に6分程度かかっていましたが、今は 2 分程度で構築が完了します。 また、スケーリング時も3分程から1分ちょっとに短縮することが出来ました。

 

ネットワークの高速化

Kubernetes では、クラスタ内に仮想的なオーバーレイネットワークを構築します。 そのため、各 Kubernetes ノード内の Internal Cluster Network を L2 で相互に何かしらの方法で接続する必要があります。 公式ドキュメントなどの構成では、Flannel の VXLAN type を利用する例が多いかと思います。 一般的に VXLAN はオーバーヘッドが非常に大きいため、弊社の環境では host-gw type を利用しています。

 

akenetwork

 

VXLAN の場合にはそれぞれの Kubernetes ノード間の接続は VXLAN を用いて行われます。そのため、コンテナから別ノード上のコンテナに送られるイーサネットフレームは、Kubernetes ノード外に出るタイミングでカプセル化が行われ、対向の Kubernetes ノード上まで転送されたのち、デカプセル化が行われます。

host-gw の場合には、コンテナから別の Kubernetes ノード上のコンテナ上に送られる際に通常の IP Routing が行われます。Flannel がホスト上のルーティングテーブルにエントリを追加するような形で実現されているため、L2 で直接つながっている必要がありますが、特別な機能を利用するわけではないため機能依存が少ないといった特徴があります。

簡単に計測したところだと、host-gw ではワイヤーレート近いスループットが出ていましたが、VXLAN の場合はワイヤーレートの 20 % 〜程度しかスループットが出ませんでした。 どうしても VXLAN を利用したい場合には、NIC に offload するなど別途方法を考える必要がありそうです。

 

Cloud Provider (OpenStack) と連携した Kubernetes Cluster の構築

OpenStack 上へ Kubernetes クラスタを構築する際は、OpenStack Heat と呼ばれる CloudFormation 相当のオーケストレーションツールを使って展開しています。 Magnum も内部的には Heat を利用しているため、似たようなことをしている部分もあります

ちなみに Kubernetes の kube-apiserver や kube-controller-manager などのプログラムを展開する場合には、juju などのようにバイナリファイルを配置して systemd で起動させるパターンと、kubeadm のようにコンテナとして起動させるパターンの2種類を選択することが出来ます。
AKE ではイメージ化を行っておりバイナリも内包させていることからも、systemd で起動させる方法を選択しました。

 

arch01

 

Kubernetes の Master 及び Node に関しては Resource Group として作成してスケーリング可能な構成にしています。

 

arch02

 

Multi Master 構成をする際には LoadBalancer を使って VIP を割り当てますが、この部分に関しては Heat の Custom Resource を自作して対応しています。 OpenStack Heat では、リソースを簡単に拡張することができるため、こういったスタックリソースを簡単に拡張することが可能です。

 

arch03

 

アドテクスタジオの OpenStack 環境には Route53 相当の OpenStack Designate という DNS as a Service を展開しています。 AKE の CLI からクラスタを操作する場合に、OpenStack の API サーバに問い合わせを行い、クラスタ名から VIP を取得するよりも DNS RoundRobin を利用した方が早いため、簡易的な VIP として利用しています。

 

arch04

 

最後に Heat で Kubernetes のバイナリや systemd ファイルを展開する際に、OpenStack との機能連携を行っています。 Kubernetes には GCP、AWS、OpenStack といったクラウドプロバイダーと連携するための連携機能が用意されており、連携しない場合にはいくつかの機能が利用できません。

 

arch05

 

例えば、Keystone 認証を利用して OpenStack のユーザで Kubernetes クラスタへの認証を行っていたり、OpenStack Cinder の Volume Service と連携して、Dynamic Persistent Volume Provisioning を実現しています。

 

dynamic_persistent_volume

 

通常であれば OpenStack Neutron や Octavia の LBaaS を利用できる環境であれば、”type LoadBalnacer” の連携も可能ですが、アドテクノロジーの特性的に一切のレイテンシが許されないため弊社では利用していません。

 

Hardware LoadBalancer を利用した “type LoadBalancer” の実装(Kubernetes)

上記で話した通り、Kubernetes の CloudProvider 連携で LBaaS を利用可能なのですが、アドテクでは出来る限り低レイテンシなネットワークを提供する必要があったため、 Hardware LoadBalancer と連携した “type LoadBalancer” を実装しました。

以前は NodePort を作成した後、別途 Hardware LoadBalancer を操作することで似たようなことを行っていましたが、Kuberentes の世界から出て操作を行なう必要があり、利便性が非常に悪い状態でした。 また、構成的に SNAT + NAPT が必要になってしまい DSR が利用できないため、レイテンシの増加や LoadBalancer の負荷増大といった問題も生じてしまいました。

 

nodeport_hwlb

 

“type LoadBalancer” の構成では、Hardware LoadBalancer からのトラフィックを Kubernetes Node が受け取り、kube-proxy が生成する iptables のルールでコンテナに DNAT されます。 そのため、戻りのトラフィックは LoadBalancer を通る必要はなく、DSR 構成を実現することが可能です。

“type LoadBalancer” を実装するには、Kubernetes の CloudProvider に関するソースコードに対して実装を変更し、ビルドし直してあげる必要がありますが、「作成」「更新」「削除」時の Interface を実装するだけで良いため、簡単に実装することが可能です。

 

typelb

 

より詳細な実装に関しては https://adtech.cyberagent.io/techblog/archives/3127 にまとめてありますので、よければ参考にしてみて下さい。

OpenStack 環境ではない環境でも実装可能であり、”type LoadBalancer” はどの環境でも用意することが可能です。

 

GKE like な Ingress Controller の実装(Kubernetes)

Ingress は L7 LoadBalancer を使ったパスベースルーティングと SSL 終端を行なうリソースです。 GKE では Google Cloud LoadBalancer を使った Ingress Controller がマスター側に内包されており、Ingress リソースを作成するだけで L7 LoadBalancer を用意することが可能です。 一方で、オンプレミス環境や Cloud Provider が Ingress Controller を提供していない場合などでは Nginx Ingress Controller や Nghttpx Ingress Controller を利用する必要がありました。

Nginx Ingress Controller では GKE の Ingress Controller に比べて大きく 3 つの使い勝手が悪い部分があると考えられます。

 

1.Ingress リソースの状態を確認しても Status は何も分からない

例えば、GKE 上ではこのような表示になるのですが、

# kubectl get ing -o wide
NAME              HOSTS                 ADDRESS           PORTS     AGE
sample-ingress    sample1.example.com   xxx.xxx.xxx.xxx   80, 443   3h
sample-ingress2   sample2.example.com   xxx.xxx.xxx.xxx   80, 443   3h

Nginx Ingress の場合には下記のような表示となってしまい、Endpoint となるAddress を確認することができません。 これは Nginx Ingress を利用している場合には、Ingress リソースの Status が更新される処理が存在しないためです。

# kubectl get ing -o wide
NAME              HOSTS                 ADDRESS   PORTS     AGE
sample-ingress    sample1.example.com             80, 443   3h
sample-ingress2   sample2.example.com             80, 443   3h

2.Ingress リソースを作成した後に、Nginx Ingress Controller Pod を立てて Service も作成する必要がある

Nginx Ingress では、Ingress リソースに割り当てる LoadBalancer 毎(分離された Endpoint 毎)に Ingress Controller となる Pod を用意し、Service を Ingress Controller に接続することで初めて L7 LoadBalancer を作成することができます。

GKE の場合には GCLB が NodePort に転送する形で Ingress リソースが実現されます。

 

bce7d3eb6148931466fad8b149c35379

 

一方で、Nginx Ingress の場合には Nginx Pod が L7 LoadBalancer 相当の処理を行います。外部疎通性のある Endpoint を払い出す場合には、type LoadBalancer などの Service を Nginx Pod に転送することで実現可能です。

 

4a530b97f4c683882c744917347fdec8

 

3.Ingress リソースの分離性を保つために Ingress リソースと Nginx Ingress Controller に識別子を設定する必要がある

GKE の場合には Ingress リソースを複数作ると GCLB が複数作成され、分離された状態なります。

 

c8c4a6cab4f3b392a5c12a1bac7fc1bf

 

一方で Nginx Ingress の場合には、未指定で Ingress リソースを複数作ると全く分離されない状態で構成されてしまいます。 分離を行うには、Ingress リソースに対して annotation に “kubernetes.io/ingress.class” を設定し、Nginx Ingress の起動オプションに –ingress-class を渡すことで、明示的に紐付けを行う必要があります。

 

9c8f310398d7ff74f8ad70540d916218

 

これらの問題を解決し、GKE と同等の使用感で利用できる Ingress Controller を自作してみました。 基本コンセプトは、上記の手動部分や Ingress リソースの Status を更新する機能を保持した Ingress Controller です。 そのため、ベースは Nginx Ingress Controller か Nghttpx Ingress Controller を利用しており、更新も各 Ingress Controller にまかせているため、仕組み自体はシンプルなものになっています。

初歩的な説明と細かい実装については Kubernetes Advent Calendar 1日目の記事として 「オンプレでも GKE Like な Ingress を使うために 自作 Ingress Controller を実装してみた」で詳しく公開していますので、ご興味のある方は是非参考にしてみて下さい。

 

Sonobuoy による e2e テスト

現在 Container Orchestration Engine のデファクトスタンダードとなった Kubernetes ですが、様々なベンダーが拡張した Kubernetes Distribution や、OSS ツールにより構築される Kubernetes Distribution や、クラウドプロバイダーが提供する Kubernetes Platform など、様々な環境を利用することが可能です。

Kubernetes は Cloud Native Computing Foundation (CNCF) により運営されている OSS のため、これらの Kubernetes Distribution & Platform が正しく Kubernetes として動作することを保証するために Conformance Program を提供しています。

Conformance Program では、Kubernetes クラスタに対して E2E のシナリオテスト(Sonobuoy)を実行することにより Kubernetes としての機能が備わっているかやクラスタの設定が標準化されたものになっているかを確認します。 その後、CNCF に結果を確認して貰うことで初めて Conformance Program をパスして Certified Kubernetes 認定されます。

ちなみに、この Certified Kubernetes の認定を受けることで初めて、プロダクト名に “Kubernetes” を含むことが許されるようになります。 Google の GKE も Certified Kubernetes 認定されたタイミングで Google Container Engine から Google Kubernetes Engine に改名したのもそのためです。

AKE では、”type LoadBalancer” のために Kubernetes に対してパッチをあてていたり、1 から Kubernetes Cluster を構築しているため E2E テストが欠かせません。そのため、AKE でもこの Conformance Program の E2E テストを実施しています。 現状は商用利用かつ CNCF のメンバーでもないため Certified Kubernetes に認定はされていないのですが、E2E テスト自体はパスしているため事実上互換性のある Kubernetes Platform として提供出来ています。

より詳細な Sonobuoy によりテスト実行に関しては https://adtech.cyberagent.io/techblog/archives/3739 に詳しくまとめておりますので、ご興味のある方は是非見てみて下さい。

 

CKA 取得者 2 名の Kubernetes Expert によるサポート

AKE は私(青山)と makocchi の 2 名体制で構築・運用しています。 2 名とも 9 月に GA となった Certified Kubernetes Administrator (CKA) を全世界で138 番目と 150 番目に取得しており、Kubernetes の内部アーキテクチャは勿論のこと、前述の type LoadBalancer や GKE Like な Ingress Controller を実現するためにコードの実装レベルでの理解もしています。 そのため、AKE だけではなく、パブリッククラウド等の Kubernetes 周りで何か問題が起こった際でも、迅速に対応を行えるようになっています。

どんな環境を使う場合でも、万が一の障害時に何も出来ないのは困るため、サービス提供者としては万全の状態で提供したいと考えています。

logo_cka_whitetext-2

 

マルチコンテナオーケストレーションエンジンの対応

散々 Kubernetes の話をしてきましたが、実は AKE は複数のコンテナオーケストレーションエンジンに対応しています。 現状ですと、Kubernetes と Docker Swarm の 2 つのみに対応していますが、社内からの要望があれば別のものも対応する予定です。

docker-swarm-kubernetes

 

そのため、もちろん Swarm に関しても深い知見を持っており、チューニングを施したクラスタを展開しています。 例えば、Swarm の Ingress と呼ばれるオーバーレイネットワークでは IPVS を利用してコンテナへのロードバランシングを行っています。 通常のカーネルを利用して Swarm を展開してしまうと IPVS のハッシュテーブルサイズが小さいことでトラフィックが中々捌けない問題があるため、独自でビルドしたカーネルを利用しています。 こう書くと非常に簡単な話に見えるのですが、この回答に行くつくためには Swarm の複雑な Network Namespace を理解し、通常の方法では見れない Namespace 内の状態を確認することで初めて行くつくことが可能でした。

Kubernetes に関しても iptables によるバランシングに限界が見えてきており、IPVS を利用した方式を検討中との issue もあがっているため、いずれは同様に独自でビルドしたカーネルを使う必要がありそうです。

余談ですが、GKE が Google Container Engine から Google Kubernetes Engine に変わったタイミングで AKE も Adtech Kubernetes Engine に変更することを検討したのですが、Swarm もいるので見送りました。

 

付加価値の提供

複数のコンテナオーケストレーションエンジンに対応することもそうですが、オンプレでコンテナ基盤を提供するにあたって、付加価値の提供も行っています。 例えば、AKE では プラグイン機構を有しており、datadog や prometheus と連携したクラスタを構築したり、ElasticSearch + Fluentd + Kibana のログ基盤にコンテナのログ集約を行なうようにしたクラスタを構築することができるようにしています。

 

Future Works

今後の展望はいくつかあります。

 

L7 Baremetal LoadBalancer for Ingress

1つ目は GKE のように L7 の Baremetal LoadBalancer 用の Ingress Controller を実装することで、Ingress 利用時のレイテンシを抑えたいと考えています。 現状のアドテクスタジオの LoadBalancer では、全てのリクエストに関して SSL 終端やパスベースのルーティングをできるほど LoadBalancer の性能に余裕があるわけではないため、現状は L4 LoadBalancing + GKE Like Nginx Ingress Controller 構成となっています。

 

Container Runtime の対応

2つ目は CRI-O や rkt などのコンテナランタイムに対応することです。 AKE でクラスタを構築する際には、

  • コンテナオーケストレーションエンジン
  • コンテナランタイム

それぞれの種別とバージョンを指定して構築ができるように実装しています。 要件や特徴によりコンテナランタイムを変えることで、より使いやすく高機能なコンテナ基盤を構築していきたいと思います。

 

まとめ

AKE いかがでしたでしょうか。使いたくなってきたでしょうか。

残念ながら CyberAgent グループ内でのみ利用可能な基盤のため、利用していただくことが出来ないのが残念です。

しかし、オンプレ環境で Kubernetes as a Service を構築したいと考えている方がいらっしゃいましたら、是非参考にして頂けると良いかなと思います。