CUE を利用した Envoy の設定ファイルの管理

Yuki Ito
KAUCHE Tech Blog
Published in
16 min readDec 18, 2022

こんにちは、株式会社カウシェの Architect の伊藤です。

本稿では、カウシェで実践している「CUE を利用した Envoy の設定ファイルの管理」についてご紹介します。

カウシェにおける Envoy の利用

Envoy は、Istio service mesh における Sidecar や、Google Cloud の Cloud Load Balancing における HTTP Load Balancer の実装などで利用されている、汎用的なネットワークプロキシです。カウシェでは、Microservices Architecture における API Gateway として Envoy を利用しています。

カウシェのアーキテクチャ

Envoy の設定は、YAML ファイルを用いて静的に記述する方法と、Controle Plane を用いて動的に配布する方法があります。カウシェでは、動的に配布する方法を視野に入れつつも、現状ではプロキシ先のサービスの数が比較的少ないため、静的に記述する方法を採用しています。

Envoy の設定ファイルの管理における課題

カウシェでは、下記のような複数の環境で Envoy を利用しています:

  • 本番環境
  • QA 環境
  • CI 環境
  • ローカル開発環境

プロキシ先のサービスのアドレスがそれぞれの環境ごとに異なるため、Envoy の設定ファイルは各環境ごとに専用のものを作成する必要があります。しかし、プロキシ先のアドレス以外の設定は共通するものが多く、それらを環境ごとの設定ファイルに直接記述してしまうと、下記のように重複した記述が多い状態になってしまいます:

  • 本番環境の Envoy の設定ファイル
static_resources:
listeners:
- name: example
address:
socket_address:
address: 0.0.0.0
port_value: 8080
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
access_log:
name: log
typed_config:
'@type': type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
log_format:
json_format:
protocol: '%PROTOCOL%'
method: '%REQ(:METHOD)%'
path: '%REQ(:PATH)%'
host: '%REQ(HOST)%'
http_filters:
- name: envoy.filters.http.router
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
route_config:
virtual_hosts:
- name: production.example.com
domains:
- production.example.com
routes:
- match:
prefix: /
route:
cluster: service-1
clusters:
- name: service-1
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: service-1
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: production-service-1.example.com
port_value: 443
  • QA 環境の Envoy の設定ファイル
static_resources:
listeners:
- name: example
address:
socket_address:
address: 0.0.0.0
port_value: 8080
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
access_log:
name: log
typed_config:
'@type': type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
log_format:
json_format:
protocol: '%PROTOCOL%'
method: '%REQ(:METHOD)%'
path: '%REQ(:PATH)%'
host: '%REQ(HOST)%'
http_filters:
- name: envoy.filters.http.router
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
route_config:
virtual_hosts:
- name: qa.example.com
domains:
- qa.example.com
routes:
- match:
prefix: /
route:
cluster: service-1
clusters:
- name: service-1
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: service-1
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: qa-service-1.example.com
port_value: 443

例えば、上記 2 つの環境の設定ファイルは、Cluster のアドレスや Route のドメインの設定は異なりますが、大部分の設定は共通しています。

共通する設定を一箇所に切り出して管理するために、当初は YAML のための Templating ツールであるytt の利用を検討していました。 ytt を利用することで、YAML 上に独自のコメントによる記法を用いて条件分岐やループといった処理を記述することができます。ytt は YAML の延長線上で様々な処理を記述できる優れたツールではありますが、処理が複雑化した場合に yttの記法によって記述された YAML を読み解くのが難しくなってしまうことを懸念して採用を見送りました。

上記の ytt をはじめとしたいくつかの手法を検討し、最終的には CUE を利用して設定ファイルを管理する方法にたどり着きました。

CUE による Envoy の設定ファイルの管理

CUE は、様々な構造のデータの定義や生成、Validation を行うことを目的とした言語です。CUE は、条件分岐やループ、関数呼び出しやモジュールなどの機能を備えており、データ構造を柔軟に表現することができます。YAML や JSON などの一般的に利用されるデータ構造についても標準機能として提供しており、CUE で記述したデータをそれらの構造に変換することができます。

カウシェでは、この CUE を利用して Envoy の設定ファイルを生成しています。具体的には、共通する設定を CUE の package として切り出し、各環境ごとに作成する CUE の package から利用することで重複した処理を一箇所にまとめて管理しています:

  • Envoy の共通する設定を切り出した CUE の package
package config

import (
"list"
)

#Input: {
hosts: [...#Host]
}

#Host: {
domain: string
routes: [...#Route]
}

#Route: {
path: string
upstream: #Upstream
}

#Upstream: {
name: string
address: string
port: number
}

#Bootstrap: {
input: #Input

config: {
static_resources: {
listeners: [{
name: "example"
address: socket_address: {
address: "0.0.0.0"
port_value: 8080
}
filter_chains: [{
filters: [{
name: "envoy.filters.network.http_connection_manager"
typed_config: {
"@type": "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager"
access_log: {
name: "log"
typed_config: {
"@type": "type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog"
log_format:
json_format: {
protocol: "%PROTOCOL%"
method: "%REQ(:METHOD)%"
path: "%REQ(:PATH)%"
host: "%REQ(HOST)%"
}
}
}
http_filters: [
{
name: "envoy.filters.http.router"
typed_config: "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router"
},
]
route_config: virtual_hosts: [
for host in input.hosts {
name: host.domain
domains: [
host.domain,
]
routes: [
for r in host.routes {
match: {
prefix: r.path
}
route: {
cluster: r.upstream.name
}
},
]
},
]
}
}]
}]
}]

clusters: list.FlattenN([ for host in input.hosts {
[
for route in list.Concat([host.routes]) {
name: route.upstream.name
type: "STRICT_DNS"
lb_policy: "ROUND_ROBIN"
load_assignment: {
cluster_name: route.upstream.name
endpoints: [{
lb_endpoints: [{
endpoint: address: socket_address: {
address: route.upstream.address
port_value: route.upstream.port
}
}]
}]
}
},
]
},
], 1)
}
}
}
  • 本番環境の Envoy の設定を表現する CUE の package
package production

import "path/to/config"

bootstrap: config.#Bootstrap & {
input: config.#Input & {
hosts: [
config.#Host & {
domain: "production.example.com"
routes: [
config.#Route & {
path: "/"
upstream: config.#Upstream & {
name: "service-1"
address: "production-service-1.example.com"
port: 443
}
},
]
},
]
}
}
  • QA 環境の Envoy の設定を表現する CUE の package
package qa

import "path/to/config"

bootstrap: config.#Bootstrap & {
input: config.#Input & {
hosts: [
config.#Host & {
domain: "qa.example.com"
routes: [
config.#Route & {
path: "/"
upstream: config.#Upstream & {
name: "service-1"
address: "qa-service-1.example.com"
port: 443
}
},
]
},
]
}
}

上記のように、共通する設定を 1 つの package として切り出し、各環境ごとの package から利用することで、アドレスやドメインなどの環境ごとに異なる値以外を一箇所にまとめて管理しています。これらの CUE で記述した Envoy の設定を YAML ファイルとして出力するために、下記のようなコマンドを CUE で実装しています:

  • 各環境の CUE ファイルを元に YAML ファイルを生成する CUE のコマンド
package dump

import (
"encoding/yaml"
"tool/file"

"path/to/production"
"path/to/qa"
"path/to/ci"
"path/to/local"
)

command: gen: {
env: =~"^(production|qa|ci|local)$" | *_|_ @tag(env)

bootstrap_production: production.bootstrap
bootstrap_qa: qa.bootstrap
bootstrap_ci: ci.bootstrap
bootstrap_local: local.bootstrap

write: file.Create & {
filename: "./env/\(env)/envoy.yaml"
if env == "production" {
contents: yaml.Marshal(bootstrap_production.config)
}
if env == "qa" {
contents: yaml.Marshal(bootstrap_qa.config)
}
if env == "ci" {
contents: yaml.Marshal(bootstrap_ci.config)
}
if env == "local" {
contents: yaml.Marshal(bootstrap_local.config)
}
}
}

この CUE のコマンドを下記のように実行することで、CUE で記述した設定を、Envoy に渡すための YAML ファイルとして出力しています:

> cue -t env=production gen

簡潔性のために、上記で紹介した CUE の実装からはカウシェで実際に利用している「環境による Wasm Filter の出し分け」や「特定の条件による HTTP リクエストやレスポンスへのヘッダーの付与」といったような複雑な処理は省きましたが、CUE を用いることでそれらの処理も package として切り出して再利用することが可能になっています。

おわりに

本稿では、カウシェで実践している CUE を利用した Envoy の設定ファイルの管理について解説しました。CUE は表現力が高く様々な構造のデータに対応しているので、Envoy の設定にとどまらず、例えば GitHub Actions などの設定に応用していくことも検討しています。みまさまもぜひ、CUE を利用した 設定ファイルの管理をお試しいただければと思います。

宣伝

カウシェでは、「世界一楽しいショッピング体験をつくる」ための Site Releiability Engineering を先導してくださるエンジニアを探しています。本稿で紹介したような CUE や Envoy の活用を含めて、常に「より良いアーキテクチャ」を模索しているエキサイティングな環境なので、興味を持っていただいた場合はぜひ採用情報をご覧いただければと思います。

また、「カウシェについて聞いてみたい」「本稿で紹介した CUE と Envoy の活用について聞いてみたい」といったようなことがございましたら、筆者の Twitter の DM にて連絡いただければお答えさせていただきます。

--

--