Thought Machineでは、クラウドネイティブなコアバンキングプラットフォームである Vault Coreを開発しています。私たちが掲げる主な要件は以下の2点です。
- 数百万人のユーザーと毎秒数千件のトランザクションを処理できること
- さまざまなクラウドプロバイダーや独自のデータセンターへデプロイできること
さらに、コードを可能な限り迅速にデプロイしつつ、万が一の際には即座にロールバックできる体制を目指しています。
本記事では、私たちのインフラストラクチャを構成する3つの主要コンポーネントについて解説します。高速な開発を実現するCI/CDパイプライン、Thought Machine Vault Coreを支える基盤OSとネットワークスタック、そして開発者がサービスを監視するために利用するツール群です。
はじめに
2016年11月まで、私たちはCloudFormationとAuto Scalingグループを使用して、AWS EC2インスタンスにマイクロサービスをデプロイしていました。私たちは Troposphere の周辺に軽量なラッパーを開発し、開発者が独自のロードバランサー、セキュリティグループ、起動設定などをセットアップできるようにしていました。インフラはコードとして定義され、デプロイ前に自動テストを実行し、他のコードと同様のレビュープロセスを経て、モジュール化によって重複を避けることができました。実行時には、小さなPuppetモジュールが必要なアーティファクトをマシンにダウンロードし、実行環境を構成していました。
この手法はしばらくの間うまく機能していましたが、いくつかの欠点もありました。
- CloudFormationを使用していたため、AWSエコシステムに完全に依存していました。そのため、大幅な書き直しなしでは、他のクラウドプロバイダーへマイクロサービスをデプロイすることができませんでした。
- マイクロサービスのアップデートをロールアウトする際、VMの起動に長い時間(約5分)を要していました。
- マイクロサービスが失敗した際、その正確な原因を特定することが困難でした。セキュリティグループの変更が原因なのか、単にマイクロサービス内のバグなのかを判別できなかったのです。
- Puppetモジュールを別のリポジトリで管理していたため、マイクロサービスの実行時設定を更新する際に開発者の大きな障壁となっていました。
新しい手法を検討する時期が来ていました。私たちが求めていた主な要件は以下の通りです。
- エンジニアが自身のデプロイとロールアウトを真にコントロールできるようにすること。インフラチームが日常的な作業(新しいアプリケーションバージョンのロールアウトや、全く新しいマイクロサービスの追加など)のゲートキーパーになるべきではありません。プロダクトチームがインフラチームの関与なしに、プロトタイプから本番環境まで進められるようにする必要があります。
- マイクロサービスのデプロイとインフラの変更を明確に分離すること。これにより、障害発生時に誰が責任を持つべきかを容易に特定できるようになります。
- きめ細かな認証と認可、アプリケーションレベルのアクセス制御を実現し、開発者がクラウドプロバイダーのコンソール、API、UIに直接アクセスする必要性を排除すること。
- デプロイ設定をアプリケーションコードと同じ場所に保存し、変更のたびに複数のリポジトリを行き来する手間を省くこと。
- マイクロサービスのアップデートをロールアウトする際の起動時間を短縮すること。
- 障害調査のためにVMへSSH接続する必要をなくすこと。何らかの問題が発生した際、すべてのエンジニアがログや有用な情報を即座に確認できるようにすること。
そこで登場したのがKubernetesです。
Kubernetesの導入により、プロダクトチームと運用チームの責任分担が明確になりました。Service、Deployment、ConfigMapといった必要なプリミティブが提供されたことで、ソフトウェアを迅速かつ予測可能な方法でロールアウトできるようになりました。プロダクトチームは自らデプロイを管理し、運用チームはデプロイが円滑に進むよう監視サービスを提供するという体制が整いました。
Thought Machineでは、すべてのコードを単一のモノレポで管理しています。そこには複数の言語(主にPython、Go、Java)のコードに加え、Kubernetesのリソースファイルやインフラコードも含まれています。これにより、インフラチームは全体にわたる変更をアトミックに適用できます。以前はPuppetモジュールを別のリポジトリで管理していましたが、複数のリポジトリ間での変更やデプロイの調整は困難で、ミスも起こりやすいものでした。
私たちは厳格なコードレビューポリシーを設けており、すべての変更は少なくとも1名(コードの重要度に応じてシニア開発者を含む)によるレビューを必須としています。これは創業当初から徹底しているポリシーです。このプロセスは、メンバーの教育や設計の議論、あるいは単に他のエンジニアに変更内容を周知する手段として非常に有効であることが証明されています。プルリクエストが作成されると、すぐにビルドエージェントが変更を検知し、影響を受けるすべてのテストがバックグラウンドで実行されます(これには専用のオンプレミスサーバーを使用しています)。
ブランチがマスターにマージされると、影響を受けるDockerイメージを特定して再ビルドし、イメージの内容に基づいた一意のハッシュでタグ付けを行います。その後、Kubernetesオブジェクト定義のテンプレート化(後述)へと進みます。
私たちはDockerラベルを不変(イミュータブル)として扱っているため、イメージが変更されるたびにDeploymentのYAMLファイルを更新するのは非常に手間がかかります。そのため、ビルドの最後にこれらの値をテンプレートに埋め込むようにしています。コミットされるデプロイ設定の例は以下の通りです。
apiVersion: apps/v1
kind: Deployment
metadata:
name: service-deployment
labels:
app: my-service
spec:
[...]
containers:
- name: service-container
image: //backend/docker:service
- name: sidecar
image: //common/util:sidecar
注://で始まるイメージタグは、特定のビルドターゲット( plzを使用してビルド)を指しています。各ターゲットには、イメージ名やソースなどのメタデータが関連付けられており、各ディレクトリのBUILDファイルで指定されています。
ビルドが完了し、すべてのテストに合格すると、これらのイメージタグが確定するため、テンプレートに組み込むことができます。その結果、specは以下のようになります。
containers:
- name: service-container
image: 331510202792.dkr.ecr.us-west-1.amazonaws.com/backend/service:b71f1
- name: sidecar
image: 331510202792.dkr.ecr.us-west-1.amazonaws.com/common/sidecar:43a95
これらのKubernetesオブジェクトは、開発環境へ自動的にデプロイされます。
Kubernetesデプロイメント
OSおよびネットワークスタック
現在、KubernetesクラスターはAWS上にデプロイしていますが、GCPなど他のクラウドプロバイダーの活用も積極的に検討しています。 CoreOS Container Linux (すべてのEC2インスタンスで稼働するOS)を採用することで、最新のセキュリティパッチがリリースと同時に確実に適用されるという安心感を得ています(パッチは自動的に適用され、VMは人の手を介さずに再起動されます)。VMの起動時には基本的な設定が適用され、Kubeletはsystemdユニットとして実行されます。それ以外のすべてはKubernetes上で動作します。
私たちは Flannel を使用してレイヤー3のポッドネットワークを構築し、 Calico を使用してネットワークポリシーの適用を行っています。どちらも同じDaemonSetでデプロイされており、各VMに1つのレプリカが確実に配置されるようになっています。
Flannelは、ポッドが起動するたびに一意のIPアドレスを自動的に割り当てる役割を担っています。新しいワーカーノードがクラスターに追加されると、Flannelはクラスター全体のポッドネットワークから一定の範囲を確保し、他のノードとの競合を避けるために定期的にリースを更新します。これらのポッドIPは、kubectlを使用してポッドを照会する際に確認できます。
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
nginx-5vrpq 2/2 Running 0 11d 10.XXX.14.3 ip-10-XXX-72-162.eu-west-1.compute.internal
nginx-7j72k 2/2 Running 0 12d 10.XXX.12.2 ip-10-XXX-150-131.eu-west-1.compute.internal
nginx-q2f5j 2/2 Running 0 12d 10.XXX.11.19 ip-10-XXX-1-39.eu-west-1.compute.internal
デフォルトでは、クラスター内のすべてのポッドは相互に通信可能です。これにはセキュリティ上の懸念が伴います。例えば、エッジポッドやWebサーバー以外のすべてのポッドからのパブリックな外部向けネットワークトラフィックを制限したり、機密性の高いポッドをクラスターの他の部分から隔離したりしたい場合があります。この問題は ネットワークポリシーを使用することで解決できます。CalicoはFlannelと並行して動作し、ポッド間のネットワークトラフィックを制限するネットワークポリシーを指定できるようにします。これらのネットワークポリシーはポッドラベルのような高レベルの構成要素に基づいているため、プロダクトチームはデプロイメントファイルを作成するのと同じ言語を使用して、独自のポリシーを記述できます。Calicoは特権コンテナとして実行され、ネットワークポリシーの変更を監視し、それを適用するためにiptablesルールを書き換えます(実際には、1〜2秒という非常に短い時間で更新が適用されることを確認しています)。
クラスター全体のサービス
プロダクトチームがデプロイメントやロールアウトの全権を保持している一方で、インフラストラクチャチームは、クラスター内で実行されるすべてのデプロイメントが標準で利用できる一連のクラスター全体向けサービスを提供しています。これには、DNS解決、監視とアラート、ログ収集、シークレット管理、および社内製のリリース管理ツールが含まれます。
監視とアラート
メトリクスの収集には、 Prometheusは、SoundCloudで開発されたオープンソースツールです。プルモデルを採用しており、多数のエンドポイント(通常はクラスター内の特定のポッド)からメトリクスをスクレイピングし、時系列データを保存・クエリ(一般的にGrafanaなどのフロントエンドを使用)できるようにします。当社のクラスターレベルの監視システムは、すべてのポッドの基本的なメトリクスを提供しますが、開発者が独自のビジネスロジックに基づいたメトリクスを公開することも推奨しており、それらも自動的にスクレイピングされます。そのために、私たちは node-exporter DaemonSetをデプロイしています。これにより、各ノード上のポッドごとのCPUやメモリ使用量、ディスクおよびネットワークI/Oなどのメトリクスが公開されます。アラート( Prometheus Alert Managerを使用)は、特定の条件(例:サービスを支えるレプリカ数が一定のしきい値を下回った場合や、特定のSQLクエリのレイテンシが高すぎる場合など)が満たされるとトリガーされます。
私たちはすぐに単一インスタンスのPrometheus構成の限界に達したため、それ以降は複数のインスタンス間でメトリクスを統合(フェデレーション)しています。これにより、CPU使用率から個々のSQLクエリのレイテンシ、サービス間のRPCに至るまで、数千ものメトリクスを網羅し、スケールさせることが可能になりました。
ログ管理
監視設定と同様に、私たちは Fluentd DaemonSet をデプロイしており、各ノードで実行されているすべてのポッドのログ収集を担っています。ログメッセージはバッチ処理され、ElasticSearchクラスターに転送されます。その後、Kibanaを使用してログメッセージの検索や閲覧を行います。
シンプルなElasticSearch構成でテストした結果、少量のログメッセージであっても容易に過負荷になることが判明しました。現在のクラスターには3種類のポッドが含まれています。ESインデックスの保存を担うデータポッド(StatefulSetとしてデプロイ)、ESクラスターとのクエリのやり取りを処理するAPIポッド、そしてプロセス全体を調整するマスターポッド(いずれも標準のDeploymentとしてデプロイ)です。
シークレット管理
クラウドアプリケーション全般に言えることですが、マイクロサービスはデータベースのパスワードやAPIキーといった機密情報にアクセスする必要があります。これらの値はコードベースにコミットされることはなく、ローカル開発環境ではダミー値が使用されます。
これらのシークレットを暗号学的に安全な方法で保存するため、私たちは HashiCorp Vault とその Kubernetes認証バックエンドを活用しています。開発者は、デプロイメント内でマウントされたボリュームとして、アクセスが必要なシークレットを指定できます。これらは、デプロイメントのサービスアカウントを使用してHashiCorp Vaultで認証を行うinitコンテナによって、実行時に自動的に取得されます。これにより、各マイクロサービスがアクセスできるシークレットに対してきめ細かなポリシーを設定し、トークンの有効期間を短く保ち、必要に応じてシークレットをローテーションすることが可能になります。また、詳細な監査証跡に基づいて、異常なアクティビティを容易に検知できます。
私たちはHashiCorp Vaultを社内サービスや業務サービスにも利用しています。その主な用途は、短期間(約16時間)有効な 証明書 Kubernetesクラスターへのアクセスに使用します。エンジニアは通常1日1回( LDAP認証情報を使用して)ログインし、クラスターへのアクセス権を取得します。これらの証明書は RBACロールにマッピングされており、チームや組織全体で権限を簡単に更新できるようになっています。エンジニアは必要に応じて、より高い権限を持つ証明書をリクエストすることもできますが、その有効期限は30分から60分に制限されています。
結論
何事にも言えることですが、デプロイメント管理に万能薬は存在せず、Kubernetesも例外ではありません。最終的な結果としてKubernetesの導入は成功しており、今後も使い続ける予定ですが、そこに至るまでの道のりは決して平坦ではありませんでした。私たちは、これまで慣れ親しんできたものとは全く異なる用語体系を持つ多くの新しい技術を導入しました。これにはインフラチームだけでなく、特定のコンポーネントの所有権が移行することになるプロダクトチームに対しても、再教育が必要でした。抽象化のレイヤーが追加されたことで(特にネットワークレベルにおいて)、一部の状況ではデバッグが通常よりも困難になっています。
現在のKubernetesのセットアップには満足していますが、やるべきことはまだ山積みです。GCPやAzure(およびビルドエージェントや社内インフラ向けのオンプレミス環境)へのデプロイメントの拡大、マイクロサービス間の接続を改善するためのサービスメッシュ( Istio や Conduitなど)の調査、そしてデプロイメントプロセスのさらなる簡素化などです。
Thought Machineでは、バックエンド、フロントエンド、インフラ、セキュリティ、PM、セールスなど、あらゆる職種で採用を行っています。興味をお持ちの方は、ぜひ Workableからご応募ください。








