Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

CUEを小さく使って環境別のYAMLファイルをtemplate化する

こんにちはあるいはこんばんは

ふそやん@azihsoynです。 こちらの記事はGunosy Advent Calendar 2021の11日目の記事です。

昨日の記事はjohnmanjiroさんの DenoでTodoリストAPIを作ってみた でした。

今回はYAMLの生成にCUEを使ってみた話を紹介します。 本記事ではCUEがどういうものかは触れずに、どのように使ったかを説明します。

f:id:azihsoyn:20211210171225p:plain

背景

グノシーのサーバーサイドはk8s上で動いているものが多く、そのため色んな設定ファイルがYAMLで管理されています。

グノシーのサーバーサイドの環境は主に3種類あり、

  • 本番環境(prd)
  • ステージング環境(stg)
  • 個別の開発環境(dev)

それぞれの設定ファイルを個別に用意するのはメンテナンスコストが高いため、templateに環境毎に違う値を埋め込んで生成するという方法で管理していました。

envoyの設定ファイルもその1つで、CUEを使う前はenvsubstで環境変数を埋め込んで環境別の設定ファイルを生成していました。

こちらはYAMLの一部です

# envoy.template.yaml
static_resources:  
  clusters:  
    - name: microservice_api_cluster  
      drain_connections_on_host_removal: true  
      connect_timeout: 0.250s  
      type: STRICT_DNS  
      lb_policy: round_robin  
      http2_protocol_options: { }  
      health_checks:  
        - timeout: 1s  
          interval: 1s  
          unhealthy_threshold: 3  
          healthy_threshold: 5  
          unhealthy_interval: 0.1s  
          reuse_connection: false  
          grpc_health_check: { }  
      common_lb_config:  
        ignore_new_hosts_until_first_hc: true  
      load_assignment:  
        cluster_name: microservice_api_cluster  
        endpoints:  
          - lb_endpoints:  
              - endpoint:  
                  address:  
                    socket_address:  
                      address: ${MICROSERVICE_API_HOST}  
                      port_value: 80

MICROSERVICE_API_HOSTが環境毎に違う値です。

# envoy.env
MICROSERVICE_API_HOST=stg-microservice-api.default.svc.cluster.local

このようなenvファイルを用意して

env $(cat envoy.env) envsubst < envoy.template.yaml

このようにenvsubstコマンドを使って環境変数を展開したYAMLファイルを生成していました。

この方法でも問題ないサービスもあるのですが、

prd, stg環境ではenvoyのjwtを検証するhttp_filterを使いたいが、個別のdev環境では認証をなくしたい

という要件が出てきました。

具体的にはこのようなYAMLです。

# envoy.yaml
http_filters:
  # ここから
  - name: envoy.filters.http.jwt_authn
    config:
      providers:
        onelogin:
          issuer: https://gunosy.onelogin.com/oidc/2
          forward_payload_header: "X-Verified-Jwt-Payload"
          forward: true
          from_headers:
            - name: x-amzn-oidc-accesstoken
          remote_jwks:
            http_uri:
              uri: https://gunosy.onelogin.com/oidc/2/certs
              cluster: onelogin_jwks_cluster
              timeout: "3.0s"
            cache_duration:
              seconds: 300
      rules:
        - match:
            prefix: /api/ping
        - match:
            prefix: /
          requires:
            provider_name: onelogin
  # ここまでを環境によって出し分けたい
  - name: envoy.gzip
    config: {}
  - name: envoy.router
    config: {}

envsubstでは値の埋め込みはできますが条件によってブロックを消したりすることはできない(そもそもYAML専用のコマンドではない)ので、別のツールを探す必要がありました。

ツール選定

YAMLを生成するツールに関してはk8s全盛期の昨今色んな解決策があります(というかtemplateエンジンを使えば無限にありますね)。

  • dhall
  • jsonnet
  • ytt
  • ...etc

いくつか調べた結果CUEに落ち着きました。

CUEを選択した理由ですが、

  • 書き慣れているGoと似ている
  • templateっぽい書き方をしなくていい
  • YAMLと似ている
  • 標準のformatterがある
  • 既存のYAMLをimportできる

などがポイント高かったです。

CUEを使うと先述のブロックの出し分けをどのように解決できるかというと、

# envoy.cue
_env: "prd" | "stg" | "dev" @tag(env)

http_filters: [
    if _env != "dev" {
        name: "envoy.filters.http.jwt_authn"
        config: {
            providers: onelogin: {
                issuer:                 "https://gunosy.onelogin.com/oidc/2"
                forward_payload_header: "X-Verified-Jwt-Payload"
                forward:                true
                from_headers: [{
                    name: "x-amzn-oidc-accesstoken"
                }]
                remote_jwks: {
                    http_uri: {
                        uri:     "https://gunosy.onelogin.com/oidc/2/certs"
                        cluster: "onelogin_jwks_cluster"
                        timeout: "3.0s"
                    }
                    cache_duration: seconds: 300
                }
            }
            rules: [{
                match: prefix: "/api/ping"
            }, {
                match: prefix:           "/"
                requires: provider_name: "onelogin"
            }]
        }
    },
    {
        name: "envoy.gzip"
        config: {}
    },
    {
        name: "envoy.router"
        config: {}
    },
]

このようなCUEファイルを用意して

cue eval -t env=stg envoy.cue  --out yaml

or

cue eval -t env=dev envoy.cue  --out yaml

と実行することでブロックを出し分けることができます。

envsubstで行っていた値の埋め込みもCUEで実現できます。

# envoy.cue
package envoy

static_resources: clusters: [{
    name:                              "microservice_api_cluster"
    drain_connections_on_host_removal: true
    connect_timeout:                   "0.250s"
    type:                              "STRICT_DNS"
    lb_policy:                         "round_robin"
    http2_protocol_options: {}
    health_checks: [{
        timeout:             "1s"
        interval:            "1s"
        unhealthy_threshold: 3
        healthy_threshold:   5
        unhealthy_interval:  "0.1s"
        reuse_connection:    false
        grpc_health_check: {}
    }]
    common_lb_config: ignore_new_hosts_until_first_hc: true
    load_assignment: {
        cluster_name: "microservice_api_cluster"
        endpoints: [{
            lb_endpoints: [{
                endpoint: address: socket_address: {
                    address:    _MICROSERVICE_API_HOST
                    port_value: 80
                }
            }]
        }]
    }
}]
# env.cue
package envoy

_MICROSERVICE_API_HOST: "stg-microservice-api.default.svc.cluster.local"
cue eval env.cue envoy.cue --out yaml

ここで、YAMLを生成するだけなのにCUEの構文覚えるのめんどくさいよと思った方。安心してください。元のYAMLをimportしてCUEファイルにすることができます。

cue import <yaml file>

あとは必要な箇所を少し直せばtemplateとして使えると思います。

さいごに

CUE自体は2年前ぐらいに フューチャーさんの技術ブログ で読んでから気になってはいたのですが、ついに導入という運びになりました。

今回はCUEのほんの一部の機能を使ってYAMLをtemplate化しました。

実はCUEはこれ以外にも多種多様な機能があり、全貌を知ってから利用しようとするとオーバースペックに思えてしまうかもしれません。小さく使うところから始めることをおすすめします。

CUEのその他の機能が気になったらまずはチュートリアルからやってみるのがよいと思います。


余談ですが、このブログを書くに当たって公式のチュートリアルを一通りさらっておこうと思ったのですが、どうやら一部ドキュメントが古いらしくチュートリアルの通りのoutputになりません(ignoreオプションが動かないとのこと)。

github.com

github.com

現在CUEの最新バージョンは0.4.0ですが、0.2.2で動かすと公式のチュートリアルと同じ出力になりました。 試してみる場合は気にとめておくとよいかもです(実際にツールとして利用する場合は0.4.0の方が機能追加やbug fixなども含まれているので最新版を使う方がよいと思います)。

余談終わり


YAMLファイルの生成は他にも色々な方法があると思うので便利なツールがあったら教えていただけると嬉しいです。

明日は大曽根さんの『インスティテューショナルメモリとメタアナリシス』という記事です。お楽しみに!