カカクコムでサービスインフラを担当

田村慧氏(以下、田村):それでは『コンテナセキュリティとAdmissionWebhookによるデプロイ制限』と題しまして、株式会社カカクコムの、前半は私田村から、後半は田中から発表します。

まずは自己紹介から。私は田村慧と申します。株式会社カカクコム システムプラットフォーム部 第2インフラサービスチームに所属しています。

業務としては食べログ、Icotto、キナリノといったWebサービスのインフラ、その中でも主にネットワークやOS周りなど低レイヤーを担当しています。

Kubernetes歴としては約1年ほどで「Kubernetesチョットワカル」状態です(笑)。今日はよろしくお願いします。

続きまして田中さんも自己紹介お願いできますか?

田中亮平氏(以下、田中):はい。田中亮平と申します。所属としては先ほどの田村と同じです。業務としてはサービスインフラを担当していて、これも同じなのですが、食べログ、キナリノ、Icottoというサイトを主に担当しています。

サーバー周りを担当していまして、LinuxやVMware、最近はPrometheusなども触っています。僕もKubernetesは勉強し始めてからまだ1年弱くらいの、まだまだ勉強中の身ですが、最近CKAを取得したりしています。

田村:それでははじめに弊社を紹介させてください。弊社カカクコムはインターネットメディア事業を行っています。有名どころのサービスで言うと、価格.comや食べログなどのサービスがあります。ちょうど10月1日からGoToイートキャンペーンも始まっているので、ぜひみなさん使ってもらえればと思います。

いろいろサービスを展開しているのですが、サービス全体の管理サーバー台数が物理サーバーは約600台、仮想サーバーは約3,400台という規模で運用しています。

続いて部署紹介ですが、私が所属しているシステムプラットフォーム部の業務は、カカクコムが提供する全サービスのインフラを管理しています。物理サーバー、仮想サーバー、OS、ミドルウェア設定などと、ASの運用、DCのネットワーク管理、あと内製のCDNがあるので、そちらの運用もやっています。

システムプラットフォーム部はインフラ全部を担当していまして、上にある各事業部に開発エンジニアが所属しています。インフラ担当、開発エンジニアが部署単位で分かれている部署構成になっています。

本発表の流れですが、私からははじめに弊社でKubernetesを導入するに至った背景について説明します。次にKubernetesの構成について説明します。また現在VM運用をしていますが、VM運用からKubernetes運用への移行における検討課題として、コンテナセキュリティが出てきましたので、そちらの点についても説明します。

後半の田中からは、コンテナセキュリティについてコンテナイメージの脆弱性スキャン、および脆弱性スキャンのCIへの組み込み、またAdmissionWebhookによるデプロイ制限について説明します。

Kubernetesを導入した理由

それでは私の発表を始めます。目次は、先ほど概要を説明したので割愛します。

まずKubernetes導入に至るまでですが、導入背景として、一昨年(2018年)ごろから開発エンジニアの開発環境でのコンテナ利用事例が非常に増加してきました。そこで、将来的にコンテナを本番稼働させることを見据えて、コンテナ稼働基盤として省コストで運用可能なKubernetesの導入を決定しました。

ここでいう省コストというのは、Kubernetesがクラスタ構成である、障害発生時に自動復旧してくれるセルフヒーリング機能をもつ、また急なアクセス増により負荷が上昇しても動的なスケールが可能かつ容易にワーカーをスケーリングできるオートスケーリング機能をもち、開発エンジニア視点で運用コストが低いという意味です。この3点を選定理由にKubernetesを導入しています。

環境はオンプレミスで提供しています。オンプレミスで提供している理由ですが、クラウドと比較してオンプレミスが低コストというのがあります。

というのも弊社、先ほど申し上げましたように仮想サーバーが3,400台とそこそこの規模感で運用しており、それらが全部Kubernetes上で稼働することになりますと、圧倒的にオンプレミスのほうが低コストだと試算しましたのでオンプレミスで提供しています。

ただオンプレミスのKubernetesのみしか使えないのかと言われるとそうではなく、各種クラウドサービスとはダイレクトコネクトしており、利用可能となっています。実際一部のサービスはGKE上で稼働しています。

構成の全体像

弊社のKubernetes環境の構成の全体像はこちらです。基本的にサービスごとにシングルテナント構成となっています。アプライアンスのLBがありまして、そちらにVIPを設けて、その配下に各ワーカーノードを紐づけてクラスタを形成しています。

LB配下のワーカーの中にはIngress-ControllerとしてNginx Ingressを用意しており、すべてのアクセスはこちらのNginx-Ingressを介します。。Ingress配下には開発エンジニア用に専用のNamespaceを設けていまして、開発エンジニアはこちらのNamespaceの範囲内でしか作業できないような構成となっています。

実際のトラフィックの流れを説明します。例えばfoo.example.comといったURLにクライアントがアクセスしようとすると、まずDNSから対応するレコードを参照して、LBにあるVIPにアクセスします。

次に、LBからいずれかのワーカーノードへL4分散されます。今回はワーカー2に振り分けられたと想定し、Nginx Ingress側でHostヘッダーを識別し、Ingressに紐づいたServiceへ振り分けられます。

次にServiceに紐づくPodへルーティングされ、Podでリクエストの処理がなされ、クライアントへレスポンスが返る、といった流れになります。

次に監視構成です。弊社のKubernetesの各ノードには、メトリクス収集用にPrometheusがあります。各Podのサイドカーとしてfluentdも用意しています。

サイドカーであるfluentdからKubernetes外部のElasticsearch VMにログを収集しまして、Kibanaで可視化をしています。

またPrometheusはKubernetes外部にもVM上で構築しており、各ノードのPrometheusからフェデレーションして最終的にGrafanaで可視化するという構成を取っています。こちらでメトリクス収集と監視も行なっています。

各担当者の作業範囲ですが、インフラ担当、我々プラットフォーム部はKubernetes基盤の構築、運用、監視を行っています。

開発エンジニアの作業範囲は、専用のNamespaceを設けていますので、Namespac配下でのアプリケーションデプロイ、そのアプリケーションが正常に同化しているかを監視してもらっています。こちら、専用に設けたNamespac以外での操作は開発エンジニアはできないような仕組みとなっています。

VM運用からKubernetes運用に移行する際の検討課題

VM運用からKubernetes運用への移行における検討課題として、基本的なフローに変更はなかったものの、セキュリティ面ではこれまでVMをまるごと担保していたのに対して、今度はレイヤーがコンテナへと変化するため、複数の検討課題が浮上してきました。

まず1点目は、コンテナに対応した脆弱性診断ツールが必要になりました。こちらはなぜかと言いますと、今までVMに対して使用していた脆弱性診断ツールが、コンテナまで診断してくれなかったため、新たにツールを選定する必要が出てきました。

また2点目として、信用できるコンテナレジストリからのみのpull可能な制限が必要になりました。これはなぜかと言いますと、Kubernetes上にデプロイするコンテナイメージは信頼できるものなのか? という懸念点があるためです。

デプロイするイメージは開発エンジニアのほうで作りますが、そもそもイメージを作るときのベースイメージは、フルスクラッチで作っているのか? それともどこかから取得してきたものを使っていて、中にマルウェアなどが含まれていないか? という懸念がありました。

さらに、Kubernetesにデプロイするときにレジストリを指定しない場合、基本的にはデフォルトでDockerHubから取得されるようになっていますので、そういったDockerHubなど外部からコンテナイメージを取得していないかという懸念がありまして、本当に信用できるコンテナレジストリからのみpullを可能にする制限が必要となりました。

こちら2つについて、後半田中より、解決方法について説明します。

私の発表をまとめます。まず弊社では、コスト面からオンプレミスのKubernetes環境を提供しています。開発エンジニアはアプリケーションのデプロイのみ可能となっていまして、Kubernetes基盤の構築、運用、監視はインフラ担当であるプラットフォーム部が管轄しています。

3点目に、コンテナを本番稼働させるうえで、セキュリティ担保のためのさまざまな考慮が必要になります。以上で、私の発表を終了します。ご清聴ありがとうございました。引き続き、田中さんよろしくお願いします。

コンテナイメージのスキャン

田中:私からKubernetesクラスタで利用するコンテナイメージにセキュリティの確保が必要という課題に対して、弊社で取り組んだ内容について紹介したいと思います。

内容としましては、コンテナイメージのスキャン、CI/CDの構成、CIへの組み込み、AdmissionWebhookによるデプロイ制限、最後にAdmissionWebhookの注意点という順で話を進めていきます。

まずコンテナイメージのスキャンについてですが、KubernetesクラスタにデプロイするPodで利用するコンテナイメージ、あるいはベースイメージは、インターネット上から取得したものになるとは思うんですが、脆弱性やマルウェアが含まれている可能性があります。

これに対して、当たり前ではあるんですが、デプロイする前にイメージのスキャンツールを利用して、スキャンを実施しています。ツールについては、いくつか選択肢はあると思いますが、弊社ではAqua Security社の製品を利用しています。またPodをデプロイする前に、その都度スキャンをするのは大変なので、CIへスキャンを組み込んで、自動化するようにしています。

CI/CDの構成

どのようにCIに組み込んでいるかを説明する前に、弊社でのCI/CDの構成について説明します。ツールや構成としては、特にめずらしいものを利用しているわけではありませんが、レポジトリとCIにはGitLab、CDにはArgo CDを利用しています。

開発担当者がアプリケーションレポジトリにpushしますと、CIの中でビルドしたイメージをレジストリにpushして、マニフェストレポジトリにMerge Requestを送ります。マニフェストレポジトリに変更があれば、Argo CDからKubernetesにデプロイします。

弊社では、レジストリを開発環境用とステージング・本番環境用で2つ用意していて、環境によってCIの処理を変化させていますので、その点について説明します。

まず開発環境ですが、CIの流れとして、イメージをビルドしたあとスキャンを実施して、開発環境用のレジストリにpushします。開発環境ではスキャンの結果脆弱性の有無に関わらず、レジストリにpushします。開発環境ではこのレジストリのイメージを利用します。

ステージング環境では少し異なっていて、イメージをビルドしたあとスキャンを実施して、先ほどのレジストリとは異なるステージング・本番用の別レジストリにpushします。

またこのスキャンの結果、脆弱性が検出された場合は、CIを失敗させてpushしないようなCIを組んでいます。こうすることで、ステージングと本番ではスキャンが成功したイメージのみを利用する仕組みを取っています。

脆弱性が発見されたらpushしないと書いてありますが、脆弱性にはそれぞれ重大度があるので、すべての脆弱性について対応が必要とはしていません。

このあたりの基準については、試行錯誤の段階ではあるんですが、現状は外部からのリクエストが届くか否かを基準にして、届くのであればMedium以上、届かないのであれば重大度はCriticalの脆弱性のみ引っかかるように、スキャンポリシーを設定しています。

これ以外にも、別途セキュリティを担当している部門から、脆弱性の情報が提供されたりすることはあるので、細かく言うとほかにもいろいろ見ていますが、機械的にはこのようなフィルターをかけています。

その中で、上記の基準、MediumやCriticalっていう基準に引っかかっても、例えば利用していないライブラリの脆弱性だったり、アプリケーションには関係ない脆弱性が検出されることもあります。その場合は、Issueにまとめて対応不要の理由を記載して、対象イメージのスキャン時には引っかからないように、スキャンツールを設定しています。

AdmissionWebhookによるデプロイ制限

次に、AdmissionWebhookによるデプロイ制限です。開発環境用とステージング・本番環境用でレジストリを分けたんですが、Kubernetesクラスタでは当然どのレジストリからもってきたイメージでも利用可能になっています。そのため、誤りなどによって本番環境に脆弱性を含む可能性があるイメージのデプロイを防ぐ必要があると考えました。

弊社ではこの課題に対して、Kubernetesの機能として用意されているAdmissionWebhookを利用して、本番環境へのデプロイ時にpullできるレジストリを制限する仕組みを導入しています。この図で言うと、ステージング・本番環境のレジストリからのみpullできるような仕組みです。

AdmissionWebhookは、特に新しい機能というわけではないので、多くの方がご存知かと思いますが、一応どのような機能なのか説明します。Kubernetesに対して、例えばPodを作成したり更新したり、リソースへの操作を行なった際に、その操作に対してWebhookをトリガーする機能です。

AdmissionWebhookの中には、細かく分けると2つの機能がありまして、操作内容を編集変更できるMutating Admissionという機能と、内容次第で操作を拒否できるValidating Admissionという機能です。

この図はKubernetesの公式ブログからもってきたものですが、左からAPIリクエストと書いているのが操作になりまして、認証などを通過して、最終的にetcdに書き込まれるまでに、Mutating AdmissionとValidating Admissionが動作する流れです。

今回はValidating Admissionを利用して、Podの作成のリクエストに対してWebhookをトリガーするように設定しています。Webhook先はPodになるんですが、そこでイメージのレジストリを確認していて、指定のレジストリでなければPodの作成を拒否するように設定しています。

ちなみにこのPodでは、Goで実装したWebサーバーが稼働しています。このような制限をかけることで、弊社では本番環境のKubernetes環境のセキュリティを担保するようにしています。

AdmissionWebhookの注意点

AdmissionWebhookの注意点なんですが、なんらかの原因でWebhook先のPodが停止したときに問題が発生します。図のようにWebhook先のPodがすべて停止すると、Kubernetesのセルフヒーリング機能で、Podを再作成しようとします。

このとき、要はPodを作成しに行きますので、Validating Admissionが走るんですが、Webhook先が停止しているので、Podが作成されなくなってデッドロックのような状態に陥ってしまいます。Webhook先のPodが作成されないので、さらにあらゆるPodが作成できなくなるという状況に陥ります。

対策ですが、最低限、Webhook先のPodはValidating Admissionの対象外になるように設定する必要があります。例えば弊社の場合、Webhook先のPodはkube-systemのNamespaceにデプロイしていて、kube-systemのPodに対してはValidating Admissionの対象外に設定しています。

Validating Admissionの設定で、このようなkube-systemを外す設定が可能になっています。kube-systemだけでなく、指定のNamespaceを対象外とするという設定が可能です。ただ実は、この注意点は公式ドキュメントにも書いてあるんですが、弊社ではこれに嵌って、Podが作成されなくなるという事態に陥ったことがあります。

最後にまとめです。弊社では、コンテナイメージのスキャンはCIに組み込んで自動化しています。脆弱性について対応済みのイメージ専用のレジストリを用意していて、ステージング・本番には、このイメージのみを利用するようにしています。

またAdmissionWebhookを利用して、誤りなどで脆弱性未対応のイメージを本番環境へデプロイしないように設定しています。注意点としては、そのWebhook先のPodが停止した場合に、Podが作成されないデッドロックのような状態になるのを防ぐ必要があります。

以上になります。ご清聴ありがとうございました。