Consul

consul は、サービスとクロスプロビジョニングおよびマルチクラウド環境でチームがネットワーク接続を安全に管理できるようにするソリューションです。サービスディスカバリ、サービスメッシュ、トラフィックガバナンス、ネットワークインフラの自動更新などの一連の機能を提供します。
公式ドキュメント:Consul by HashiCorp
オープンソースアドレス:hashicorp/consul
Consul は HashiCorp 社がオープンソース化したサービスディスカバリと登録ツールで、Raft 選挙アルゴリズムを採用し、ツール自体は Go 言語で開発されているため、デプロイが非常に簡単です。Consul には以下の特徴があります。
- サービスディスカバリ
- サービス登録
- ヘルスチェック
- キーバリューストア
- マルチデータセンター
実際、consul ができることはサービスディスカバリだけでなく、分散設定センターとしても使用できます。同様のオープンソースツールも多数あります。例えば zookeeper、nacos などですが、ここでは詳しく紹介しません。
インストール
Ubuntu の場合、以下のコマンドを実行して apt でインストールします。
$ wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
$ echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
$ sudo apt update && sudo apt install consulまたは、公式サイトから対応するインストールパッケージをダウンロードすることもできます。Install Consul
consul は Go で開発されているため、インストールパッケージ自体は単一のバイナリ実行ファイルであり、インストールも非常に簡単です。インストール成功後、以下のコマンドを実行してバージョンを確認します。
$ consul version正常に出力されれば問題ありません。
Consul v1.16.1
Revision e0ab4d29
Build Date 2023-08-05T21:56:29Z
Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)クイックスタート
以下では、consul 単一ノードを迅速に構築する方法を紹介します。通常、単一ノードは開発中のテスト用です。単一ノードが正常に動作すれば、マルチノードクラスターも問題なく動作するはずです。単一ノードの構築は非常に簡単で、1 行のコマンドだけで完了します。
$ consul agent -dev -bind=192.168.48.141 -data-dir=/tmp/consul -ui -node=dev01通常、以下のような出力があります。
==> Starting Consul agent...
Version: '1.16.1'
Build Date: '2023-08-05 21:56:29 +0000 UTC'
Node ID: 'be6f6b8d-9668-f7ff-8709-ed57c72ffdec'
Node name: 'dev01'
Datacenter: 'dc1' (Segment: '<all>')
Server: true (Bootstrap: false)
Client Addr: [127.0.0.1] (HTTP: 8500, HTTPS: -1, gRPC: 8502, gRPC-TLS: 8503, DNS: 8600)
Cluster Addr: 192.168.48.141 (LAN: 8301, WAN: 8302)
Gossip Encryption: false
Auto-Encrypt-TLS: false
ACL Enabled: false
Reporting Enabled: false
ACL Default Policy: allow
HTTPS TLS: Verify Incoming: false, Verify Outgoing: false, Min Version: TLSv1_2
gRPC TLS: Verify Incoming: false, Min Version: TLSv1_2
Internal RPC TLS: Verify Incoming: false, Verify Outgoing: false (Verify Hostname: false), Min Version: TLSv1_2
==> Log data will now stream in as it occurs:
2023-08-25T17:23:33.763+0800 [DEBUG] agent.grpc.balancer: switching server: target=consul://dc1.be6f6b8d-9668-f7ff-8709-ed57c72ffdec/server.dc1 from=<none> to=<none>
2023-08-25T17:23:33.767+0800 [INFO] agent.server.raft: initial configuration: index=1 servers="[{Suffrage:Voter ID:be6f6b8d-9668-f7ff-8709-ed57c72ffdec Address:192.168.48.141:8300}]"簡単に説明します。
agentはサブコマンドで、consul のコアコマンドです。consul agentは新しい consul エージェントを実行します。各ノードはエージェントです。devはエージェントの実行モードで、合計 3 種類あります:dev、client、serverbindは LAN 通信アドレスで、ポートはデフォルトで 8301 です。通常、この値はサーバーの内部 IP アドレスです。advertiseは WAN 通信アドレスで、ポートはデフォルトで 8302 です。通常、この値はサーバーの外部 IP アドレスです。data-dirはデータ保存ディレクトリです。config-dirは設定保存ディレクトリで、consul はディレクトリ内のすべての json ファイルを読み込みます。bootstrapは、現在のサーバーがガイドモードに入ることを示します。Raft 選挙時に自分自身に投票します。クラスター内でこのモードのサーバーは 1 つを超えることはできません。bootstrap-expectは、クラスター内で期待されるサーバーの数です。指定された数に達するまで、クラスターは選挙投票を開始しません。bootstrapと同時に使用することはできません。retry-joinは、エージェント起動後に指定されたノードに参加し続けます。以下のサービスディスカバリ方法もサポートしています。aliyun aws azure digitalocean gce hcp k8s linode mdns os packet scaleway softlayer tencentcloud triton vsphereuiは Web バックグラウンドを実行します。nodeは実行ノード名で、クラスター内で一意である必要があります。
TIP
エージェントパラメータの詳細については、Agents - CLI Reference | Consul | HashiCorp Developer をご覧ください。一部のパラメータは企業版でのみ使用可能であることに注意してください。
正常に実行された後、127.0.0.1:8500 にアクセスすると、Web インターフェースを閲覧できます。

dev01 のアイコンが星になっているのは、それがリーダーノードであることを示しています。
終了時、他のノードが現在のノードの終了を認識できるように、プロセスを強制終了することは推奨されません。コマンドを使用できます。
consul leaveまたは
consul force-leavectrl+c で consul agent を優雅に終了することもできます。
概念
これは consul クラスターの模式図で、図は 2 つの部分に分かれています。コントロールプレーンとデータプレーンです。consul はコントロールプレーンのみを担当し、サービスクラスターとクライアントに分かれています。サービスクラスターはフォロワーとリーダーに分かれています。全体として、図の consul クラスターは 1 つのデータセンターを構成しています。以下でいくつかの用語を説明します。
- Agent(エージェント):またはノードと呼ぶ方が適切かもしれません。各エージェントは長時間実行されるデーモンプロセスで、HTTP および DNS インターフェースを公開し、ヘルスチェックとサービス同期を担当します。
- Server(サーバーエージェント):consul server として、その主な責任は Raft 選挙への参加、クラスター状態の維持、クエリへの応答、他のデータセンターとのデータ交換、およびリーダーと他のデータセンターへのクエリ転送です。
- Client(クライアントエージェント):client は server に比べてステートレスで、Raft 選挙に参加せず、すべてのリクエストを server に転送するだけです。バックグラウンドに関連する唯一のことは、LAN 流言転送(LAN gossip pool)です。
- Leader(リーダー):leader はすべての server のリーダーで、リーダーは 1 人だけです。leader は Raft 選挙アルゴリズムを通じて選出され、各リーダーには自分の任期があります。任期内に、他の server がどのようなリクエストを受け取っても leader に伝える必要があるため、leader のデータは最もタイムリーで最新です。
- Gossip(流言):Consul は Serf(同社の別の製品)に基づいて構築されており、gossip プロトコルを使用します。このプロトコルはノード間のランダム通信専用で、UDP に似ています。consul はこのプロトコルを使用してサービスクラスター間で相互通知を行います。
- Data Center(データセンター):LAN 内の consul クラスターはデータセンターと呼ばれます。consul はマルチデータセンターをサポートし、マルチデータセンターの通信方法は WAN gossip です。
TIP
詳細な用語と用語については、Glossary | Consul | HashiCorp Developer をご覧ください。
consul クラスターでは、server の数を厳密に制御する必要があります。因为它们直接参与到 LAN gossip 和 WAN gossip、Raft 選挙に参加し、データを保存するため、server が増えるほど通信コストが高くなります。一方、client の数は少し多くても問題ありません。転送のみを担当し、選挙に参加せず、リソース消費が低いです。図のクラスターでは、各サービスは client を通じて自身を server に登録し、server がダウンした場合、client は他の利用可能な server を自行で探します。
クラスター構築例
以下では、シンプルな consul マルチノードクラスターの例を構築します。まず 4 台の仮想マシンを準備します。

4 台の仮想マシンのうち、3 台が server で、1 台が client です。公式は server の数は奇数が良く、3 台以上を推奨しています。ここでは vm00-vm02 を server として、vm03 を client として使用します。
server の場合、以下のコマンドを実行して server agent を作成します。
consul agent -server -bind=vm_address -client=0.0.0.0 -data-dir=/tmp/consul/ -node=agent_name -uiclient の場合、以下のコマンドを実行して client agent を作成します。
consul agent -client=0.0.0.0 -bind=vm_address -data-dir=/tmp/consul/ -node=agent_name -ui実行するコマンドは以下の通りです。
# vm00
consul agent -server -bind=192.168.48.138 -client=0.0.0.0 -data-dir=/tmp/consul/ -node=agent01 -ui -bootstrap
# vm01
consul agent -server -bind=192.168.48.139 -client=0.0.0.0 -data-dir=/tmp/consul/ -node=agent02 -ui -retry-join=192.168.48.138
# vm02
consul agent -server -bind=192.168.48.140 -client=0.0.0.0 -data-dir=/tmp/consul/ -node=agent03 -ui -retry-join=192.168.48.138
# vm03
consul agent -bind=192.168.48.140 -client=0.0.0.0 -data-dir=/tmp/consul/ -node=agent03 -ui -retry-join=192.168.48.138一部のパラメータの説明
client、0.0.0.0はすべてのソースからのリクエストを許可することを意味します。server パラメータがなく client パラメータのみがある場合、エージェントは client モードで実行されることを示します。
すべてのエージェントが正常に実行されると、retry-join の役割は自動的に join コマンドを実行することと同等で、失敗すると継続的に試行します。デフォルトの再試行時間は 30 秒です。
$ consul join 192.168.48.138join 完了後、各ノードは互いの存在を認識します。vm00 は bootstrap モードを指定しているため、デフォルトのリーダーになります。bootstrap モードを指定しない場合、すべてのノードが join 時に指定したノードがデフォルトのリーダーになります。リーダーが選出される前に、クラスターは正常に動作せず、Web インターフェースにアクセスすると 500 が返され、一部のコマンドも正常に動作ません。クラスター内に bootstrap モードを指定したノードがある場合、クラスター内の他のノードは bootstrap モードを指定してはなりません。同時に、他のノードは bootstrap-expect パラメータを使用しないでください。使用すると自動的に無効になります。
この時、リーダーノードで(実際にはどのノードでも確認できます)データセンターのメンバー情報を実行して確認します。
$ consul members
Node Address Status Type Build Protocol DC Partition Segment
agent01 192.168.48.138:8301 alive server 1.16.1 2 dc1 default <all>
agent02 192.168.48.139:8301 alive server 1.16.1 2 dc1 default <all>
agent03 192.168.48.140:8301 alive server 1.16.1 2 dc1 default <all>
client01 192.168.48.141:8301 alive client 1.16.1 2 dc1 default <default>- Node:ノード名
- Address:通信アドレス
- Status:
aliveは存活、leftはオフライン - Type:エージェントの種類。server と client の 2 つのモード
- Build:ノードが使用する consul バージョン。consul は一定範囲内で異なるバージョンのノードと連携できます
- Protocol:使用する Raft プロトコルバージョン。これはすべてのノードで一致する必要があります
- DC:Data Center、データセンター。出力内のすべてのノードは dc1 データセンターに属します
- Partition:ノードが属するパーティション。企業版機能で、各ノードは同一パーティションのノードとのみ通信できます
- Segment:ノードが属するセグメント。企業版機能

同様に、ノードを終了したい場合は、consul leave を使用してノードを優雅に終了し、他のノードに終了することを通知する必要があります。マルチノードの場合、ノードの優雅な終了は特に重要です。これはデータの一貫性に関係するためです。
TIP
仮想マシンはデモ時にすべてのファイアウォールをオフにしましたが、実際の本番環境ではセキュリティのために有効にする必要があります。そのために、consul が使用するすべてのポートに注意を払う必要があります:Required Ports | Consul | HashiCorp Developer。
次に、データの一貫性を簡単にテストします。vm00 仮想マシンで以下のデータを追加します。
$ consul kv put sys_confg {"name":"consul"}
Success! Data written to: sys_confg保存後、HTTP API を通じて他のノードにアクセスすると、データが同様に存在することがわかります(その中の value は base64 エンコードです)。
$ curl http://192.168.48.138:8500/v1/kv/sys_confg
[{"LockIndex":0,"Key":"sys_confg","Flags":0,"Value":"ewogICJuYW1lIjoiY29uc3VsIgp9","CreateIndex":2518,"ModifyIndex":2518}]
$ curl http://192.168.48.139:8500/v1/kv/sys_confg
[{"LockIndex":0,"Key":"sys_confg","Flags":0,"Value":"ewogICJuYW1lIjoiY29uc3VsIgp9","CreateIndex":2518,"ModifyIndex":2518}]
$ curl http://192.168.48.140:8500/v1/kv/sys_confg
[{"LockIndex":0,"Key":"sys_confg","Flags":0,"Value":"ewogICJuYW1lIjoiY29uc3VsIgp9","CreateIndex":2518,"ModifyIndex":2518}]実際、consul が提供するサービスディスカバリと登録機能は、gossip プロトコルを通じて他のノードにブロードキャストされ、任意のノードが現在のデータセンターに参加すると、すべてのノードがこの変化を感知します。
マルチデータセンター構築例
5 台の仮想マシンを準備します。vm00-vm02 は前の例のクラスターで、dc1 データセンターに属し、そのままにします。vm03-vm04 は dc2 データセンターに属します。データセンターはエージェント起動時、デフォルトで dc1 です。

TIP
ここではデモのため、server のみを構築し、client は省略します。
まず vm03 を起動し、デフォルトのリーダーとして設定します。
$ consul agent -server -datacenter=dc2 -bind=192.168.48.141 -client=0.0.0.0 -data-dir=/tmp/consul/ -node=agent04 -ui -bootstrapvm04 を起動し、vm03 ノードに自動的に join させます。
$ consul agent -server -datacenter=dc2 -bind=192.168.48.142 -client=0.0.0.0 -data-dir=/tmp/consul/ -node=agent05 -ui -retry-join=192.168.48.141この時、それぞれ vm00 と vm03 で members を確認します。
# vm00-vm02
$ consul members
Node Address Status Type Build Protocol DC Partition Segment
agent01 192.168.48.138:8301 alive server 1.16.1 2 dc1 default <all>
agent02 192.168.48.139:8301 alive server 1.16.1 2 dc1 default <all>
agent03 192.168.48.140:8301 alive server 1.16.1 2 dc1 default <all>
# vm03-vm04
$ consul members
Node Address Status Type Build Protocol DC Partition Segment
agent04 192.168.48.141:8301 alive server 1.16.1 2 dc2 default <all>
agent05 192.168.48.142:8301 alive server 1.16.1 2 dc2 default <all>DC フィールドが異なることがわかります。ここでは仮想マシンでデモしているため、すべて同じサブネット内にありますが、現実には 2 つのデータセンターは遠隔地のサーバークラスターである可能性があります。次に、dc1 の任意の 1 つのノードを dc2 の任意の 1 つのノードに join させます。ここでは vm01 を vm03 に join させます。
$ consul join -wan 192.168.48.141
Successfully joined cluster by contacting 1 nodes.join 成功後、コマンドを実行して WAN members を確認します。
$ consul members -wan
Node Address Status Type Build Protocol DC Partition Segment
agent01.dc1 192.168.48.138:8302 alive server 1.16.1 2 dc1 default <all>
agent02.dc1 192.168.48.139:8302 alive server 1.16.1 2 dc1 default <all>
agent03.dc1 192.168.48.140:8302 alive server 1.16.1 2 dc1 default <all>
agent04.dc2 192.168.48.141:8302 alive server 1.16.1 2 dc2 default <all>
agent05.dc2 192.168.48.142:8302 alive server 1.16.1 2 dc2 default <all>
$ consul catalog datacenters
dc2
dc1dc1 の任意の 1 つのノードが dc2 の任意の 1 つのノードに join するだけで、2 つのデータセンターのすべてのノードがこの変化を感知します。members を確認する際も、2 つのデータセンターのノードを見ることができます。
次に、vm00 ノードで KV データを 1 つ追加してみます。
$ consul kv put name consul
Success! Data written to: namevm01 ノードでデータを読み取ってみると、同一データセンターのデータは同期されていることがわかります。
$ consul kv get name
consulその後、異なるデータセンターの vm03 でデータを読み取ってみると、異なるデータセンターのデータは同期されていないことがわかります。
$ consul kv get name
Error! No key exists at: nameマルチデータセンターのデータ同期を行いたい場合は、hashicorp/consul-replicate: Consul cross-DC KV replication daemon を参照してください。
サービス登録とディスカバリ

consul サービス登録の方法は 2 種類あります。設定ファイル登録と API 登録です。テストを簡単にするために、事前に Hello World サービス(gRPC 記事の例)を準備し、2 つの異なる場所にデプロイします。設定ファイル登録の方法については、Register external services with Consul service discovery | Consul | HashiCorp Developer を参照してください。ここでは HTTP API を通じた登録のみを紹介します。
TIP
ローカルサービス(consul client と同じ場所)の場合、agent service 登録を直接使用できます。それ以外の場合は、catalog register を使用して登録する必要があります。
consul は HTTP API の SDK を提供しています。他の言語の SDK については、Libraries and SDKs - HTTP API | Consul | HashiCorp Developer を参照してください。ここでは Go の依存関係をダウンロードします。
go get github.com/hashicorp/consul/apiサービス起動時に consul に積極的にサービスを登録し、サービス終了時に consul にサービスの登録を解除します。以下はその例です。
package main
import (
consulapi "github.com/hashicorp/consul/api"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "grpc_learn/helloworld/hello"
"log"
"net"
)
var (
server01 = &consulapi.AgentService{
// 一意である必要があります
ID: "hello-service1",
Service: "hello-service",
// 2 つデプロイ。1 つのポートは 8080、もう 1 つは 8081
Port: 8080,
}
)
// サービス登録
func Register() {
client, _ := consulapi.NewClient(&consulapi.Config{Address: "192.168.48.138:8500"})
_, _ = client.Catalog().Register(&consulapi.CatalogRegistration{
Node: "hello-server",
Address: "192.168.2.10",
Service: server01,
}, nil)
}
// サービス登録解除
func DeRegister() {
client, _ := consulapi.NewClient(&consulapi.Config{Address: "192.168.48.138:8500"})
_, _ = client.Catalog().Deregister(&consulapi.CatalogDeregistration{
Node: "hello-server",
Address: "192.168.2.10",
ServiceID: server01.ID,
}, nil)
}
func main() {
Register()
defer DeRegister()
// ポートをリッスン
listen, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
// gprc サーバーを作成
server := grpc.NewServer(
grpc.Creds(insecure.NewCredentials()),
)
// サービス登録
pb.RegisterSayHelloServer(server, &HelloRpc{})
log.Println("server running...")
// 実行
err = server.Serve(listen)
if err != nil {
panic(err)
}
}クライアントコードは consul カスタムリゾルバーを使用して、登録センターに対応するサービスを照会し、実際のアドレスに解決します。
package myresolver
import (
"fmt"
consulapi "github.com/hashicorp/consul/api"
"google.golang.org/grpc/resolver"
)
func NewConsulResolverBuilder(address string) ConsulResolverBuilder {
return ConsulResolverBuilder{consulAddress: address}
}
type ConsulResolverBuilder struct {
consulAddress string
}
func (c ConsulResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
consulResolver, err := newConsulResolver(c.consulAddress, target, cc)
if err != nil {
return nil, err
}
consulResolver.resolve()
return consulResolver, nil
}
func (c ConsulResolverBuilder) Scheme() string {
return "consul"
}
func newConsulResolver(address string, target resolver.Target, cc resolver.ClientConn) (ConsulResolver, error) {
var reso ConsulResolver
client, err := consulapi.NewClient(&consulapi.Config{Address: address})
if err != nil {
return reso, err
}
return ConsulResolver{
target: target,
cc: cc,
client: client,
}, nil
}
type ConsulResolver struct {
target resolver.Target
cc resolver.ClientConn
client *consulapi.Client
}
func (c ConsulResolver) resolve() {
service := c.target.URL.Opaque
services, _, err := c.client.Catalog().Service(service, "", nil)
if err != nil {
c.cc.ReportError(err)
return
}
var adds []resolver.Address
for _, catalogService := range services {
adds = append(adds, resolver.Address{Addr: fmt.Sprintf(fmt.Sprintf("%s:%d", catalogService.Address, catalogService.ServicePort))})
}
c.cc.UpdateState(resolver.State{
Addresses: adds,
// ラウンドロビンポリシー
ServiceConfig: c.cc.ParseServiceConfig(
`{"loadBalancingPolicy":"round_robin"}`),
})
}
func (c ConsulResolver) ResolveNow(options resolver.ResolveNowOptions) {
c.resolve()
}
func (c ConsulResolver) Close() {
}クライアントは起動時にリゾルバーを登録します。
package main
import (
"context"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/resolver"
"grpc_learn/helloworld/client/myresolver"
hello2 "grpc_learn/helloworld/hello"
"log"
"time"
)
func init() {
// builder を登録
resolver.Register(
// カスタムの consul リゾルバーを登録
myresolver.NewConsulResolverBuilder("192.168.48.138:8500"),
)
}
func main() {
// 接続を確立。暗号化検証なし
conn, err := grpc.Dial("consul:hello-service",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
panic(err)
}
defer conn.Close()
// クライアントを作成
client := hello2.NewSayHelloClient(conn)
for range time.Tick(time.Second) {
// リモート呼び出し
helloRep, err := client.Hello(context.Background(), &hello2.HelloReq{Name: "client"})
if err != nil {
panic(err)
}
log.Printf("received grpc resp: %+v", helloRep.String())
}
}まずサーバー側を起動し、次にクライアントを起動します。サーバー側は 2 つあり、同じサービスを提供しますが、アドレスが異なります。クライアントのロードバランシングポリシーはラウンドロビンで、サーバー側のログ間隔からポリシーが有効になっていることがわかります。
2023/08/29 17:39:54 server running...
2023/08/29 21:03:46 received grpc req: name:"client"
2023/08/29 21:03:48 received grpc req: name:"client"
2023/08/29 21:03:50 received grpc req: name:"client"
2023/08/29 21:03:52 received grpc req: name:"client"
2023/08/29 21:03:54 received grpc req: name:"client"
2023/08/29 21:03:56 received grpc req: name:"client"
2023/08/29 21:03:58 received grpc req: name:"client"
2023/08/29 21:04:00 received grpc req: name:"client"以上が、consul と gRPC を組み合わせてサービス登録とディスカバリを実現するシンプルなケースです。
