我らの時代のコンテナデプロイ – Nomad, Consul, Vault
2020年12月11日 金曜日
CONTENTS
【IIJ 2020 TECHアドベントカレンダー 12/12(土)の記事です】
システムクラウド本部の坂口です。
私たちは現在、新サービスを開発しています。
今やサービス開発ではコンテナ技術を無視できません。 我々のサービスでも開発したアプリケーション群は自動でコンテナイメージ化され、様々な開発者が簡単に手元で動作確認できるようになっています。
マイクロサービスアーキテクチャを採用し柔軟かつ高速な開発体制を敷けた一方で、それらをデプロイするコンテナ管理基盤をどうするかが問題になります。 今回はコンテナオーケストレーションシステムとしてnomad、並びにhashicorpのプロダクトを利用した感想をご紹介します。
デプロイを巡る冒険
Kubernetes
コンテナオーケストレーションシステムと聞いてまず思い浮かぶのはKubernetesだと思います。
Kubernetesは非常に強力ですが、1つのサービスのために構築して本当に旨味があるでしょうか。 学習コストも高くメンテナンスにも時間を取られるシステムを、果たして本当に運用していけるでしょうか。
マネージドなKubernetes環境があるなら話は別です。面倒なことは全部押し付けておいしいとこだけ味わい尽くすべきだと思います。 IIJにもIKEというKubernetesディストリビューションが存在しますが、様々なサービス基盤の制約上ここでコンテナを管理するには至りませんでした。
自分たちの手の届く範囲で構築から運用まで行うことを考えると、Kubernetesはあまりにも巨大すぎました。
ではその他の選択肢に何があるでしょうか。
Docker Swarm
Docker SwarmはDockerネイティブなクラスタリングツールです。
コンテナエンジンとしてDockerを利用しているのなら、簡単に導入できてすぐさまクラスタを組むことができる優れものです。
しかしDocker Swarmはあくまでクラスタリングツールで、その他の部分において非力さが目立ちます。リソース量を見て柔軟にコンテナを配置するノードを選択したりするのがいささか面倒です。
Apache Mesos with Marathon
コンテナを管理する方法として、Apache MesosとMarathonの組み合わせがあります。
Mesosは複数のサーバリソースを管理するリソースマネージャです。ただしMesosだけではスケジューラとして汎用的な機能しか持たないため、Mesos上でコンテナのスケジューリングを行うためのフレームワークが必要になります。これがMarathonです。
MesosとMarathonを利用する場合スケジューリングのロジックは自分で実装する必要がある上、クラスタの情報をZookeeperという外部システムで管理しなければなりません。
もっと簡単に導入できるコンテナオーケストレーションシステムは無いのでしょうか。
Nomad・Nomad・Nomad
ここで満を持して登場するNomadは、Hashicorpによって開発されているコンテナオーケストレーションシステムです。
シングルバイナリで動作するため導入も簡単で、コンテナを管理するために必要な機能が一通りそろっています。その特徴を見てみましょう。
構成要素
Nomadのプロセス自身のことをAgentと呼びます。AgentはServerモードとClientモードのどちらか、または両方(Developmentモード)で起動できます。
ServerはNomadのクラスタを管理する役割を持ちます。
1台のLeaderと、必要であれば任意の数のFollowerを用意して可用性を向上させることも可能です。後述するJobを受け取り、ClientにTaskの割り当て(Allocation)をするために用意されます。
Clientは接続しているServerからTaskを受け取り実行します。
また、ClientはTaskに対してHeartbeatを送信したりといった機能も備えます。
Task/Job/Allocation
NomadではTaskと呼ばれる最小実行単位と、1つ以上のTaskがどのように動作すべきかを宣言的に定義したJobをファイルに記載して実行します。
TaskはKubernetesにおけるContainerに似ています。何のDockerイメージを利用しどの程度のリソースを割り当てて実行するか、どのような環境変数を渡すかなどの情報を記載します。
JobはKubernetesにおけるDeploymentに似ています。どのTaskをどのClientで起動するか、何並列でローリングアップデートするかなどを定義します。
そしてTaskをClientに割り当てることをAllocationと呼びます。Clientに割り当てられたTask自体のこともAllocationと呼ぶので、注意が必要です。
動かしてみる
Nomadを試すのは非常に簡単です。
バイナリを持ってきて配置し、Developmentモードで起動すれば、Server/Client両方の機能を持つAgentが起動します。
==> No configuration files loaded ==> Starting Nomad agent...
node statusやserver membersコマンドで情報を確認できます。
$ nomad node status ID DC Name Class Drain Eligibility Status b93628b9 dc1 test <none> false eligible ready $ nomad server members Name Address Port Status Leader Protocol Build Datacenter Region test.global 127.0.0.1 4648 alive true 2 0.11.1 dc1 global
job initコマンドで、サンプルとなるJobファイルを生成します。
Jobファイルの拡張子は.nomadで、サンプルJobはRedisのコンテナを立ち上げる単純なものです。
$ nomad job init Example job file written to example.nomad $ cat example.nomad # There can only be a single job definition per file. This job is named # "example" so it will create a job with the ID and Name "example". ...
job runコマンドでJobファイルを指定すると、JobをNomadに登録でき、statusコマンドにJob名を与えるとJobのステータスを確認できます。
$ nomad job run example.nomad ==> Monitoring evaluation "b048c906" Evaluation triggered by job "example" Evaluation within deployment: "b6f14fd4" Allocation "6c2b2deb" created: node "b93628b9", group "cache" Evaluation status changed: "pending" -> "complete" ==> Evaluation "b048c906" finished with status "complete" $ nomad status example ID = example Name = example Submit Date = 2020-11-12T02:01:49+09:00 Type = service Priority = 50 $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES f575c3e93a46 redis:3.2 "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 127.0.0.1:30446->6379/tcp, 127.0.0.1:30446->6379/udp redis-6c2b2deb-bcb1-16ce-2762-cfc623699731
statusコマンドでJobを確認した際、末尾にはAllocationの情報が表示されます。
ここでいうAllocationはClientに割り当てられたTask自身のことで、このIDを指定してalloc logsコマンドを利用すればログの表示も可能です。
$ nomad job status example | tail -n3 Allocations ID Node ID Task Group Version Desired Status Created Modified a4c2f53d 0767a615 cache 0 run running 48s ago 47s ago $ nomad alloc logs a4c2f53d 1:C 11 Nov 17:59:19.463 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf _._ _.-``__ ''-._ _.-`` `. `_. ''-._ Redis 3.2.12 (00000000/0) 64 bit .-`` .-```. ```\/ _.,_ ''-._ ( ' , .-` | `, ) Running in standalone mode |`-._`-...-` __...-.``-._|'` _.-'| Port: 6379 | `-._ `._ / _.-' | PID: 1 `-._ `-._ `-./ _.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | http://redis.io `-._ `-._`-.__.-'_.-' _.-' |`-._`-._ `-.__.-' _.-'_.-'| | `-._`-._ _.-'_.-' | `-._ `-._`-.__.-'_.-' _.-' `-._ `-.__.-' _.-' `-._ _.-' `-.__.-'
あっという間にredisのコンテナを立ち上げることができました。
Evaluation
Nomadは宣言的に定義されたJobの状態に近づけるため、評価(Evaluation)をします。
例えば、サンプルJobではRedisコンテナを1つだけ立ち上げますが、並列で3つのコンテナを起動するよう変更したとします。
job planコマンドを利用すればスケジューラをdry-runできます。
$ nomad job plan example.nomad +/- Job: "example" +/- Task Group: "cache" (2 create, 1 in-place update) +/- Count: "1" => "3" (forces create) Task: "redis" Scheduler dry-run: - All tasks successfully allocated. Job Modify Index: 10 To submit the job with version verification run: nomad job run -check-index 10 example.nomad ...
dry-run実行時に生成されるcheck-indexを利用すれば、前回にplanコマンドを実行してからJobに変更が行われていないかをチェックできます。
$ nomad job run -check-index 10 example.nomad ==> Monitoring evaluation "3865c6d0" Evaluation triggered by job "example" Evaluation within deployment: "f2a023cc" Allocation "29a5452d" created: node "0767a615", group "cache" Allocation "8cf19af6" created: node "0767a615", group "cache" Allocation "a4c2f53d" modified: node "0767a615", group "cache" Evaluation status changed: "pending" -> "complete" ==> Evaluation "3865c6d0" finished with status "complete"
この時、行われているEvaluationこそがNomadのスケジューリングの要です。
現在のJobの状態と与えられたJobの差を評価し、定義されたJobの状態に遷移させるための仕組みがEvaluationということです。
今回はRedisコンテナが1つしか起動されていない現状と、3つ起動したいという与えられたJobの差を汲み、追加で2つのコンテナを起動するというEvaluationが行われました。
Nomadまとめ
ここまで、Nomadの一連の動作と特徴を紹介してきました。
Jobを作りこむことでリソースの空き具合を加味してTaskをAllocationしたり、ヘルスチェックをして動作していないようであれば再度立ち上げなおすこともできます。
しかし、こういった作りこみによってTaskがどのClientで動作するのかがわからなくなる可能性があります。柔軟なスケールアウトやスケールインを実現したり負荷分散を行おうと考えた場合、これはNomadのみならず他のコンテナオーケストレーションシステムでも課題になってくる点ですね。
ここで必要になるのが、サービスがネットワーク上でどこにあるかを特定するサービスディスカバリの概念です。
残念ながらNomad単体ではこの機能を実現できません。なにか良い方法はないでしょうか。
Consul日和
こんな時に利用されるのがConsulです。
ConsulはNomadと同様Hashicorpによって開発されているクラスタ管理ツールで、もちろんシングルバイナリで動作します。DockerSwarmでもサービスディスカバリにはEtcd、Zookeeperと並んでConsulの利用が推奨されています。
同じHashicorpのプロダクトということで親和性も高く導入が簡単なので、次はこちらのConsulを紹介します。
機能
上述したように、Consulはサービスディスカバリの機能を持ちます。登録されたサービスがどのClientで稼働しているかを登録しておき、HTTPの他Consulが標準で備えているDNSの機能でも情報取得が可能です。
また登録されたサービスに対するヘルスチェック機能も持ち、ステータスの確認もできます。Key-Valueストアの機能も備えており、サービス間でのデータ共有も簡単に行えます。
更にデフォルトでNomadとの連携機能を持っているため、Consulが動作しているサーバ上でNomadが起動されると自動的にConsulに登録が行われます。NomadもConsulと連携している状態でJobを投入すれば、自動でConsulにサービスを登録するため、とても簡単にサービスディスカバリを実現できます。
では、実際に動かしてみましょう。
動かしてみる
ConsulのAgentもServerモードとClientモードで動作します。
試すだけならNomadと同様、Developmentモードで起動すれば両方の機能を持つAgentが起動します。
$ consul agent -dev ==> Starting Consul agent... Version: 'v1.7.3' Node ID: '43b8a602-eff2-7af6-c636-1b21225cce48' Node name: 'office' Datacenter: 'dc1' (Segment: '<all>') Server: true (Bootstrap: false) Client Addr: [127.0.0.1] (HTTP: 8500, HTTPS: -1, gRPC: 8502, DNS: 8600) Cluster Addr: 127.0.0.1 (LAN: 8301, WAN: 8302) Encrypt: Gossip: false, TLS-Outgoing: false, TLS-Incoming: false, Auto-Encrypt-TLS: false ...
この状態でDevelopmentモードのNomadを起動し、サンプルJobを実行します。
サンプルJobではサービスの名前がredis-cacheと定義されているので、このサービスの情報をDNSで引いてみます。なおデフォルトでConsulのDNSポートは8600番ポートを使用するので、ここに向けてdigコマンドを実行してみます。
$ dig @127.0.0.1 -p 8600 redis-cache.service.consul A +short 127.0.0.1
今回はAレコードのみを引いていますが、Job側で公開するポート番号を指定している場合はSRVレコードを引くことでこの情報も確認できます。
試しに1234番ポートで待ち受けるようJobを書き換えて実行しました。
$ dig @127.0.0.1 -p 8600 redis-cache.service.consul SRV +short 1 1 1234 7f000001.addr.dc1.consul.
Consulまとめ
NomadとConsulを組み合わせることで、無事どこのClientにTaskがAllocationされているかがわかるようになりました。DNSでも情報を引くことができ、サービスが何のポートで待ち受けているかまで取得できることもわかりました。
実はConsulはNomad Server/Clientの情報を自分で管理してくれているので、Nomad自体はお互いの情報を知らずともConsulと連携さえできていれば互いにクラスタが組めます。このあたりはHashicorp製品群であるからこその手軽さですね。
ここまでで、NomadとConsulの組み合わせがコンテナオーケストレーションシステムとして十分の機能を備えていることがわかったと思います。
ですが、Hashicorpは更に便利なツールを用意しています。
Vaultの歌を聴け
皆さんは機密情報をどのように管理しているでしょうか。
DBやホストへのログインパスワード、APIトークンなどをコンフィグファイルに直接記載したりしていないでしょうか。これら機密情報の取り扱いは難しく、システムやプロジェクトが大きくなると管理しなければならない情報が多く、スコープも複雑になっていきます。
VaultはNomad, Consul同様Hashicorpの開発している機密情報管理ツールです。当然のことながらシングルバイナリで動作し、Nomadからでも利用しやすく設計されています。
最後に、簡単にVaultの紹介をしてこの記事を終わりましょう。
機能
Vaultは基本的にセキュアなKey-Valueストアです。
任意のKey-Valueが保存可能で、保存の際に暗号化されます。
動的な機密情報の作成機能を持ち、必要な時だけ要求に応じた情報を作成しておいて不要な時は消える/消せるようになっています。また、暗号化/復号機能が存在し、データの保存無しにこれらを実行できます。
更に機密情報の期限が設定可能で、期限が切れると自動的にアクセスできなくなるような使い方もできます。
API等で期限の延長や、期限が切れる前の削除も可能です。
Nomad連携
NomadとVaultの連携は非常に簡単で、VaultのTokenをNomadの設定ファイルに仕込むだけです。
Tokenは、どのような情報にアクセスできるかを制御できるPolicyと紐づけて発行する認証情報です。
例えばDBの機密情報のみに対して読み取りを許可するPolicyを作っておけば、これに紐づけたTokenが仮に流出したとしても他のシステムへのアクセスや、DBの機密情報の削除といった操作を防げます。
更にNomadのJobファイルのTemplateには、Vaultから取得した機密情報を埋め込むことも可能です。
これにより、Tokenが設定されたNomadは実行時に自動的にVaultへ機密情報を問い合わせ、実行するJobの環境変数にDBのパスワードやポート番号を仕込むこともできます。
このように、PolicyとTokenを使いこなせれば機密情報の管理は格段に楽になります。
Vaultまとめ
Vaultを動かすところまで紹介できればよかったのですが、余白が足りませんでした。
Vaultは、様々な便利機能を持つNomadとの親和性が高いKey-Valueストアです。
どのTokenで認証が行われたか、いつアクセスがあったかなど、すべての処理はログを残すこともできる優れものですので是非使ってみてください。
まとめ
はじめに、構築が簡単で運用の難しすぎないコンテナオーケストレーションシステムとしてNomadを紹介しました。
次に、コンテナ基盤として必須の機能であるサービスディスカバリを実現するツールとしてConsulを紹介しました。
最後に、Nomadから使いやすい機密情報管理ツールであるVaultを紹介しました。
これらはすべてシングルバイナリで動作するため、環境構築も簡単です。
Ansibleでバイナリと設定ファイルを配布すれば、簡単にノードを追加できます。
Kubernetesを構築・運用するリソースはないけれど、コンテナを利用したマイクロサービスアーキテクチャを取り入れたい方、試してみてはいかがでしょうか。