kube-prometheusをjsonnet-builderでカスタマイズした時の対応メモ

# はじめに

kube-prometheusは、k8sクラスターでprometheus, grafana, alertmanagerを動作させるための便利なOperatorとして配布されています。Prometheus Operatorを単独で利用するにはGrafanaなどの周辺のアプリケーションも動作させなければいけないため、それらを一括でデプロイしてくれるのは確かに便利です。

しかしNetworkPolicyがかなり厳格に設定されていて、基本的にはデプロイしたPod間の疎通のみが許可されています。

外部からのWebブラウザーでのアクセスは想定されておらず、ガイドされているWebブラウザーからの通信もport-fowardのみが記載されています。

Ingressを経由して、Grafanaにアクセスする方法を検討した際のメモを中心に、jsonnetで様々なカスタマイズをした顛末について記載しておきます。

この記事ではまずGrafanaにアクセスするためにdefault構成をdeploy後にランタイムを変更したad-hocな方法について記載しています。次にGrafanaを含む他の変更についてjsonnet-builderを使ってカスタマイズした構成情報をdeployした方法を載せています。

# 環境

## 本番系クラスター

* Kubernetes v1.25.6 (Kubespray v2.21.0)
* kube-prometheus v0.12.0

## 検証系クラスター

* Kubernetes v1.27.5 (Kubespray v2.23.0)
* kube-prometheus v0.13.0



## 事前準備

jsonnet, gojson2yaml, jbコマンドは、手順に従って最新版を ~/go/bin/ に配置しています。このディレクトリはPATH環境変数に登録しています。

[Customizing Kube-Prometheus](https://github.com/prometheus-operator/kube-prometheus/blob/main/docs/customizing.md)

## 導入方法

README.mdのQuick Startセクションにあるとおりに実施します。
特別な手順は含まれていませんが、確認のために残します。

```bash:repositoryのダウンロードとcheckout
$ git clone https://github.com/prometheus-operator/kube-prometheus.git
$ cd kube-prometheus

## Compatibility Matrixを参照し、適切なtagをcheckoutする
$ git checkout refs/tags/v0.12.0 -b my_v0.12.0
```

Releaseブランチを使うように指示されているので、こちらをマージします。

```bash:作成したlocalブランチへのリモートブランチのマージ
$ git pull origin release-0.12
```

```bash:
$ sudo kubectl apply --server-side -f manifests/setup
$ sudo kubectl wait \
        --for condition=Established \
        --all CustomResourceDefinition \
        --namespace=monitoring         
$ sudo kubectl apply -f manifests/
```

## フロントエンド Reverse Proxy Server

K8sクラスターはバックエンドのPrivate Network内部にあるため、組織内からアクセスできるようにネットワーク境界でnginxを稼動しています。

最新の設定例については公式ガイドを参照してください。

https://grafana.com/tutorials/run-grafana-behind-a-proxy/

Reverse ProxyとしてK8sのIngressに転送していますが、Grafana関係の設定は次のようになっています。

```nginx:/etc/nginx/cond.d/server.conf
    map $http_upgrade $connection_upgrade {  
        default upgrade;
        ''      close;
    }
    location /grafana/ {
        proxy_pass    https://192.168.200.99;
        allow 10.0.0.0/8;
        deny all;

        ## auth proxy用の設定
        proxy_set_header X-WEBAUTH-USER reader;
        proxy_set_header Authorization "";
        
        ## /grafana/api/live/ 用のWS設定
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
    }
```

:::note
以前は``Host``フィールドに``$http_host``を指定していましたが、これを``$host``に変更しています。また、``$connection_upgrade``が不定な値になっていたようだったので、明示的にmapを加えました。
:::

### Grafana AuthProxyの初期設定

情報公開用にGrafana画面を公開するためViewer権限を持つユーザーでアクセスするよう強制します。

https://grafana.com/docs/grafana/latest/setup-grafana/configure-security/configure-authentication/auth-proxy/

設定時にはX-WEBAUTH-USERとAuthorizationヘッダーは付けずにコメントアウトした状態で、allow行で接続できる端末を限定しておきます。

次に初期状態のGrafanaにadminユーザーでログインし、``reader``ユーザーをViewer権限で作成しておきます。

最後に掲載した設定に戻すことで、Webブラウザからは``reader``ユーザーでのアクセスが強制されます。

# 課題とGrafana側の対応を探る

デフォルト状態のGrafanaはトップレベル('/')にデプロイされてしまうため、/grafana/のようなsubpathでアクセスしたいcontext-rootの変更方法が分かりませんでした。

Grafanaのドキュメントを確認するとcontext-rootを変更することは可能ですが、あまり情報はなさそうです。Grafanaのガイドを元に、ConfigMap経由で変更可能か試してみます。

* https://grafana.com/tutorials/run-grafana-behind-a-proxy/

まずは手掛かりを探すために、Podの中から設定ファイルの配置状況などを確認していきます。

幸いGrafanaのPodは、bashやlsなどのユーティリティコマンドが普通に使えそうです。

いろいろ探すと /etc/grafana/grafana.ini があって、定義から secret/grafana-config がマップされていることが分かります。

```bash:作業の概要
## Podの中に移動 + 探索
$ sudo kubectl -n monitoring exec -it grafana-9f58f8675-4n957 -- bash
bash-5.1$ ls /etc/grafana/
grafana.ini   provisioning

## /etc/grafana がどうやって設定されているかDeploymentの定義を確認
$ sudo kubectl -n monitoring get deploy grafana -o yaml
...
        - mountPath: /etc/grafana                   
          name: grafana-config 
...
      - name: grafana-config
        secret:
          defaultMode: 420
          secretName: grafana-config
...

## Secretオブジェクトから grafana-config の内容を確認
$ sudo kubectl -n monitoring get secret grafana-config -o yaml
apiVersion: v1
data:
  grafana.ini: W2RhdGVfZm9ybWF0c10KZGVmYXVsdF90aW1lem9uZSA9IFVUQwo=
kind: Secret
...
```

Base64になっているようなので、デコードして内容を確認します。

```bash:base64コマンドによるデコード
$ echo "W2RhdGVfZm9ybWF0c10KZGVmYXVsdF90aW1lem9uZSA9IFVUQwo=" | base64 -d
[date_formats]
default_timezone = UTC
```

既存のgrafana.iniファイルの内容に[server]スタンザを付け加えた設定ファイルを加えます。

```bash:root_urlなどの設定を加えた設定ファイルをBase64でエンコードする
$ cat <<EOF | base64
[date_formats]
default_timezone = UTC
[server]
domain = grafana.example.com
root_url = %(protocol)s://%(domain)s:%(http_port)s/grafana/
serve_from_sub_path = true
EOF
W2RhdGVfZm9ybWF0c10KZGVmYXVsdF90aW1lem9uZSA9IFVUQwpbc2VydmVyXQpkb21haW4gPSBn
cmFmYW5hLmV4YW1wbGUuY29tCnJvb3RfdXJsID0gJShwcm90b2NvbClzOi8vJShkb21haW4pczol
KGh0dHBfcG9ydClzL2dyYWZhbmEvCnNlcnZlX2Zyb21fc3ViX3BhdGggPSB0cnVlCg==
```

このdomainにはTLS, HTTP/2を有効にしたNginxが稼動するProxyサーバー名を指定しています。
特にそのようなProxyサーバーを使わない場合には、Webブラウザに指定するホスト名(Ingressに割り当てるIPアドレスなど)を指定します。

```yaml:$ sudo kubectl -n monitoring edit secret grafana-config   
# Please edit the object below. Lines beginning with a '#' will be ignored,
# and an empty file will abort the edit. If an error occurs while saving this file will be
# reopened with the relevant failures.
#                                          
apiVersion: v1  
data:                                                                           
  grafana.ini: |
      W2RhdGVfZm9ybWF0c10KZGVmYXVsdF90aW1lem9uZSA9IFVUQwpbc2VydmVyXQpkb21haW4gPSBn
      cmFmYW5hLmV4YW1wbGUuY29tCnJvb3RfdXJsID0gJShwcm90b2NvbClzOi8vJShkb21haW4pczol
      KGh0dHBfcG9ydClzL2dyYWZhbmEvCnNlcnZlX2Zyb21fc3ViX3BhdGggPSB0cnVlCg==           
kind: Secret           
...          
```

平文で編集してもBase64にエンコードされるはずですが、ともかくcontext-rootを変更したため、grafanaのPodを再起動しただけでは、ReadinessProbeに失敗します。再起動はせずにDeploymentオブジェクトを変更して、/api/health の部分を編集します。

```bash:
$ sudo kubectl -n monitoring edit deploy grafana
```

APIにアクセスしているパスを変更します。

```yaml:編集箇所
...
        readinessProbe:
          failureThreshold: 3
          httpGet:
            path: /grafana/api/health
            port: http
            scheme: HTTP
...
```

保存して抜けると自動的にPodは再起動されます。Reconsileの機構は本当に便利ですよね。

# IngressからのGrafanaへの接続

実際のWebブラウザからのアクセスは、Global IPを持っているdomain=行に指定したサーバー名のnginxにHTTPSで接続し、そこから http://192.168.110.79/ にプロキシーしています。

ここではIngressについての初期設定を含めた変更点についてまとめておきます。

## Ingressの初期設定

既にIngressを利用していれば後述するIngressオブジェクト等を追加するだけで良いと思いますので、ここは飛ばしてください。参考までにkubesprayなどでIngress Controllerを有効にしただけの状態を想定して、Ingressの初期設定ファイルを掲載しておきます。

IngressClassが一つもないので、後でIngressオブジェクトの定義で使うためあらかじめ**ingressclass/nginx**を定義しておきます。

```yaml:IngressClass定義
---
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  name: nginx
spec:
  controller: k8s.io/ingress-nginx
```

Webブラウザからアクセスするために静的なアドレスをアサインします。
k8sが稼動している192.168.110.0/24 は内側のネットワークで、Webブラウザが配置されているネットワークです。

```yaml:Serviceオブジェクト
---
apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  type: LoadBalancer
  loadBalancerIP: "192.168.110.79"
  ports:
  - name: http
    port: 80
    targetPort: http
  selector:
    app.kubernetes.io/name: ingress-nginx
```

これらの設定によってWebブラウザから192.168.110.79を通して、Ingressに接続できるようになりました。

## Ingressオブジェクトの作成

```yaml:Ingressオブジェクト
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
  labels:
    group: ingress-nginx
  namespace: ingress-nginx
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - backend:
          service:
            name: monitoring-grafana-svc 
            port:
              number: 3000
        path: /grafana
        pathType: Prefix
```

name:に指定したmonitoring-grafana-svcの名前で、Serviceオブジェクトを定義します。

```yaml:service/monitoring-grafana-svcオブジェクト
---
apiVersion: v1
kind: Service
metadata:
  name: monitoring-grafana-svc
  labels:
    group: ingress-nginx
  namespace: ingress-nginx
spec:
  type: ExternalName
  externalName: grafana.monitoring.svc.cluster.local
```

## NetworkPolicyの変更

IngressからGrafanaに接続しようとしても、503エラーが出力されます。
冒頭に書いたとおり、NetworkPolicyが厳格なため、Ingressからのアクセス許可を追加で設定する必要があります。

現状のNetworkPolicyを確認します。

``$ sudo kubectl -n monitoring get networkpolicy grafana -o yaml`` の出力は以下のとおりです。

```yaml:Grafana用のNetworkPolicy
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
spec:
  egress:
  - {}
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app.kubernetes.io/name: prometheus
    ports:
    - port: 3000
      protocol: TCP
  podSelector:
    matchLabels:
      app.kubernetes.io/component: grafana
      app.kubernetes.io/name: grafana
      app.kubernetes.io/part-of: kube-prometheus
  policyTypes:
  - Egress
  - Ingress
```

Prometheus PodからGrafana Podに向いた接続は許可されていますが、その他のアクセスは許可されていません。

ここにIngressからの接続を、namespaceSelectorを使って許可します。


```yaml:編集後のGrafana用のNetworkPolicy
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
spec:
  egress:
  - {}
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app.kubernetes.io/name: prometheus
    - namespaceSelector:
        matchLabels:
          name: ingress-nginx
    ports:
    - port: 3000
      protocol: TCP
  podSelector:
    matchLabels:
      app.kubernetes.io/component: grafana
      app.kubernetes.io/name: grafana
      app.kubernetes.io/part-of: kube-prometheus
  policyTypes:
  - Egress
  - Ingress
```

このpodSelectorとnamespaceSelectorの両方に'-'を付けて並列に配置している場合には、**OR**条件となります。この例で2番目の要素に"-"を付けないと、[{ podSelector: { matchLabels: ... }, namespaceSelector: { matchLabels: ... } }]とまとめられ、結果、**AND**条件として処理されます。

ここまでで、https://grafana.example.com/grafana/ を通してコンソールにアクセスできるようになっています。

### NetworkPolicyのもう一つ別の解放

次のようにform:ブロックを増やすことでも同様の結果が得られます。

```yaml:formを並列にした別解
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
spec:
  egress:
  - {}
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app.kubernetes.io/name: prometheus
    ports:
    - port: 3000
      protocol: TCP
  - from:
    - namespaceSelector:
        matchLabels:
          name: ingress-nginx
    ports:
    - port: 3000
      protocol: TCP
  podSelector:
    matchLabels:
      app.kubernetes.io/component: grafana
      app.kubernetes.io/name: grafana
      app.kubernetes.io/part-of: kube-prometheus
  policyTypes:
  - Egress
  - Ingress
```

# Grafana UIのデフォルトパスワード

この環境で動作しているGrafanaのデフォルトの(ID, Password)は``(admin, admin)``です。

これはGrafanaの公式ドキュメントに記載されているとおりです。

ここまでで、GrafanaのUIに接続することはできました。


# 【準備作業】jsonnet-builderを使ったkube-prometheusのカスタマイズ

デプロイ後のSecretやDeploymentオブジェクトを編集する方法は手軽で、最初の一歩としては問題ありませんが、kube-prometheusはそう思っていないようです。

kube-prometheusで想定されているjsonnet-builderを利用してカスタマイズした定義ファイルを生成する場合には、あらかじめ次のような準備作業が必要です。

あらかじめjsonnetコマンドなどをgoコマンドを使って導入しておきます。

```bash:関連コマンドのインストール
$ go install -a github.com/jsonnet-bundler/jsonnet-bundler/cmd/jb@latest
$ go install github.com/brancz/gojsontoyaml@latest
$ go install github.com/google/go-jsonnet/cmd/jsonnet@latest
```

インストールされたコマンドは、~/go/bin/に配置されます。
ここをPATH環境変数に追加する方法はいくつもありますが、Ubuntuであれば ~/.bash_aliases というデフォルトで存在すれば読み込まれるalias設定用のファイルを利用するのがお勧めです。

~/.bashrcを直接編集してしまうと/etc/skel/.bashrc がアップグレードなどで変更されれば、変更点を統合(merge)する手間がかかります。~/.bash_aliasesを利用すると~/.bashrcは/etc/skel/からコピーするだけで済むためです。

公式ドキュメントのcustomizing.mdでは、別のディレクトリでファイルを生成する手順が紹介されていますが、v0.12.0ブランチで作業を進めます。

# 【カスタマイズ例1】IngressからGrafanaへの通信を許可する

先ほどまで直接ランタイムを変更してきた内容を宣言的に実施するため、**examples/kustomize.jsonnet** ファイルを変更し、変更済みのYAMLファイルを生成します。

## namespaceSelectorの追加

次のような変更を加えます。

``values+::{}``の中ではなく、並列に記載されている点に注意してください。

```jsonnet:examples.kustomize.jsonnetの一部抜粋
local kp =                
  (import 'kube-prometheus/main.libsonnet') +
    (import 'kube-prometheus/addons/managed-cluster.libsonnet') +
 {                    
    values+:: { 
      ...
    },
    // 以下のブロックを追加
    grafana+: {
      networkPolicy+: {
        spec+: {
          ingress+: [{
            from: [{
              namespaceSelector: {
                matchLabels: {
                  'name': 'ingress-nginx',
                },
              },
            }],
            ports: [{
              port: 3000,
              protocol: "TCP",
            }],
          }],
        },
      },
    },
  };
```

## grafana.iniの編集

context_rootを変更し、サブパスを利用するため grafana.ini への変更内容を次のように加えます。

```jsonnet:examples/kustomize.jsonnetの変更箇所
local kp =
  (import 'kube-prometheus/main.libsonnet') + 
    (import 'kube-prometheus/addons/managed-cluster.libsonnet') +
  {
    values+:: {
      common+: {
        namespace: 'monitoring',
      },
      grafana+:: {
        config+: {
          sections+: {
            server: { domain: 'grafana.examples.com', root_url: '%(protocol)s://%(domain)s:%(http_port)s/grafana/', serve_from_sub_pa
th: true },
          },
        },
      },
    },
  };
```

## ReadinessProbeのpathの変更

以下のように編集します。

```jsonnet:NetworkPolicyを変更するために追加したgrafana+:{}の内部に追記
local kp =                
  (import 'kube-prometheus/main.libsonnet') +
    (import 'kube-prometheus/addons/managed-cluster.libsonnet') +
 {                    
    values+:: { 
      ...
    },
    grafana+: {
      networkPolicy+: {
        ...
      },
      // 以下のブロックを追加
      deployment+: {
        spec+: {
          template+: {
            spec+: {
              containers: std.map(
                function(container)
                  if container.name == "grafana" then
                    container {
                      readinessProbe+: {
                        httpGet+: {
                          path: "/grafana/api/health"
                        },
                      },
                    }
                  else
                    container,
                super.containers
              ),
            },
          },
        },
      },
    },
  };
```

あらかじめ **vendor/grafana/grafana.libsonnet** で準備されている local defaults = の containers変数は新しいサイドカーなどの別イメージのコンテナ一式を追加するには利用できますが、既存のGrafanaのコンテナに関連する設定(env等)を変更することができない点に注意してください。

面倒だったのはcontainers:は配列になっているため、``containers+:`` のようにオブジェクトの要素を変更する表記は使えないため ``std.map()`` と ``super.containers`` を利用して各要素に関数を適用する必要がありました。

配列の要素が1つだという前提でよければ、if〜else文は不要になるため、もう少し短くなりますが将来の変更時に予想しない結果になる可能性があるためこのような表記が安全だろうと思います。

if〜else文を削除する場合、``super.containers``の直前にある``},``が必要です。ここに掲載している``else``文の直前の**{**にはカンマはありませんでした。単純にif文の行を削除したり追加するだけではエラーになります。jsonnetは使いこなすにはなかなか難しい言語です。

### ChatGPTの活用

jsonnetは始めてだったので、ここは方法を試行錯誤して悩んだ部分ではあって、最終的に次のようなやり取りを経て、前述の方法に辿り付きました。

* https://chat.openai.com/share/85e6eee2-1b26-47fe-b879-b7e01872476a

最初の試行錯誤がなければ到達できなかったかもしれないとは思いつつ、指示が適切であれば便利な場面があるのは確かです。

## NetworkPolicy/prometheus-k8sにadapterからの通信を許可する

v0.12.0ではNetworkPolicyの設定が不十分でadapterからprometheus本体への通信がブロックされています。

以下のように編集します。

```jsonnet:NetworkPolicyを変更するためにprometheus-k8s+:{}を追加
local kp =                                                       
  (import 'kube-prometheus/main.libsonnet') + {      
    values+:: {           
      common+: {        
        ...        
      },                                                                
    },
    prometheus+: {
      networkPolicy+: {
        spec+: {                                  
          ingress+: [{                   
            from: [{           
              podSelector: {
                matchLabels: {                                                                                                  
                  'app.kubernetes.io/name': 'prometheus-adapter',
                },
              },
            }],
            ports: [{                                    
              port: 9090,                
              protocol: "TCP", 
            }],         
          }],                                                                                                                   
        },        
      },
    },
  };

```

## Manifestsの生成とmanifestsの差分

あとは、Makefileを使ってYAMLファイルを生成します。

```bash:
$ rm -rf manifests
$ make generate
```

生成された manifests/grafana-networkPolicy.yaml ファイルは手動で変更したものと同様に次のようになります。

```diff:manifests/grafana-networkPolicy.yamlの差分
diff --git a/manifests/grafana-networkPolicy.yaml b/manifests/grafana-networkPolicy.yaml
index cab676c8..a259b618 100644
--- a/manifests/grafana-networkPolicy.yaml
+++ b/manifests/grafana-networkPolicy.yaml
@@ -16,6 +16,9 @@ spec:
     - podSelector:
         matchLabels:
           app.kubernetes.io/name: prometheus
+    - namespaceSelector:
+        matchLabels:
+          name: ingress-nginx
     ports:
     - port: 3000
       protocol: TCP
```

grafana.iniは次のようになります。

```diff:manifests/grafana-config.yamlの差分
diff --git a/manifests/grafana-config.yaml b/manifests/grafana-config.yaml
index 10d9c6a9..beab186a 100644
--- a/manifests/grafana-config.yaml
+++ b/manifests/grafana-config.yaml
@@ -12,4 +12,8 @@ stringData:
   grafana.ini: |
     [date_formats]
     default_timezone = UTC
+    [server]
+    domain = grafana.example.com
+    root_url = %(protocol)s://%(domain)s:%(http_port)s/grafana/
+    serve_from_sub_path = true
 type: Opaque
```

grafana-deployment.yamlは次のようになりました。

```diff:manifests/grafana-deployment.yamlの差分
diff --git a/manifests/grafana-deployment.yaml b/manifests/grafana-deployment.yaml
index eca41b6e..60fa54a6 100644
--- a/manifests/grafana-deployment.yaml
+++ b/manifests/grafana-deployment.yaml
@@ -18,7 +18,7 @@ spec:
   template:
     metadata:
       annotations:
-        checksum/grafana-config: adbde4cde1aa3ca57c408943af53e6f7
+        checksum/grafana-config: c9018225d844e770a4c521083fe592b9
         checksum/grafana-dashboardproviders: d8fb24844314114bed088b83042b1bdb
         checksum/grafana-datasources: 0800bab7ea1e2d8ad5c09586d089e033
       labels:
@@ -37,7 +37,7 @@ spec:
           name: http
         readinessProbe:
           httpGet:
-            path: /api/health
+            path: /grafana/api/health
             port: http
         resources:
           limits:
```

ここまでの変更を反映して、manifests/ を更新すると、``$ git status``の出力は次のようになります。

```bash:git statusの出力
On branch my_v0.12.0
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   examples/kustomize.jsonnet
        modified:   jsonnet/kube-prometheus/components/grafana.libsonnet
        modified:   manifests/grafana-config.yaml
        modified:   manifests/grafana-deployment.yaml
        modified:   manifests/grafana-networkPolicy.yaml

no changes added to commit (use "git add" and/or "git commit -a")
```

アップグレード方法は、通常のインストール方法と同じです。

# 【カスタマイズ例2】 NetworkPolicyを全て削除する

examples/networkpolicies-disabled.jsonnet が準備されているので、examples/kustomize.jsonnet の代わりに指定すればNetworkPolicy関連のファイルがmanifests/に出力されなくなります。

```diff:【方法1】Makefileの変更
diff --git a/Makefile b/Makefile
index 8a438b21..6cf226a0 100644
--- a/Makefile
+++ b/Makefile
@@ -39,7 +39,7 @@ check-docs: $(MDOX_BIN) $(shell find examples) build.sh example.jsonnet
 .PHONY: generate
 generate: manifests
 
-manifests: examples/kustomize.jsonnet $(GOJSONTOYAML_BIN) vendor
+manifests: examples/networkpolicies-disabled.jsonnet $(GOJSONTOYAML_BIN) vendor
        ./build.sh $<
 
 vendor: $(JB_BIN) jsonnetfile.json jsonnetfile.lock.json
```

この方法では先ほどのgrafana.iniの変更分も含めてファイルを編集する必要があるので少し面倒です。

やはり examples/kustomize.jsonnet を活かして、networkpolicies-disabled の機能を取り込むため次のように編集します。

```diff:【方法2】examples/kustomize.jsonnetの差分
diff --git a/examples/kustomize.jsonnet b/examples/kustomize.jsonnet
index c12f98b3..5186d9b6 100644
--- a/examples/kustomize.jsonnet
+++ b/examples/kustomize.jsonnet
@@ -1,9 +1,17 @@
 local kp =
-  (import 'kube-prometheus/main.libsonnet') + {
+  (import 'kube-prometheus/main.libsonnet') + 
+  (import 'kube-prometheus/addons/networkpolicies-disabled.libsonnet') + {
     values+:: {
       common+: {
         namespace: 'monitoring',
       },
+      grafana+:: {
+        config+: {
+          sections+: {
+            "server": {domain: "grafana.example.com", root_url: "%(protocol)s://%(domain)s:%(http_port)s/grafana/", serve_from_sub_path:
 true},
+          },
+        },
+      },
     },
   };


```

方法2でこれまでの変更を反映すると``$ git status``の出力は次のようになります。

```bash:
$ git status
On branch my_v0.12.0
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   examples/kustomize.jsonnet
        modified:   jsonnet/kube-prometheus/components/grafana.libsonnet
        modified:   kustomization.yaml
        deleted:    manifests/alertmanager-networkPolicy.yaml
        deleted:    manifests/blackboxExporter-networkPolicy.yaml
        deleted:    manifests/grafana-networkPolicy.yaml
        deleted:    manifests/kubeStateMetrics-networkPolicy.yaml
        deleted:    manifests/nodeExporter-networkPolicy.yaml
        deleted:    manifests/prometheus-networkPolicy.yaml
        deleted:    manifests/prometheusAdapter-networkPolicy.yaml
        deleted:    manifests/prometheusOperator-networkPolicy.yaml

no changes added to commit (use "git add" and/or "git commit -a")
```

この方法は必須ではないので、これは行わずに個別にポートを有効化しても問題ありません。

# 【カスタマイズ例3】アラートメールの送信先を指定する

テスト環境ではメールを送信させる必要はありませんが、本番環境であれば異常はできるだけ早く把握したいものです。

今回はSeverityに応じてメールを送信させます。

このニーズは当然想定されているので、変更は簡単です。

```jsonnet:examples/kustomize.jsonnetの先頭部分
local kp =
  (import 'kube-prometheus/main.libsonnet') + {
    values+:: {
      common+: {
        namespace: 'monitoring',
      },
      alertmanager+:: {
        config+: {
          global+: {
            resolve_timeout: '5m',
            smtp_smarthost: 'smtp.example.com:25',
            smtp_from: 'grafana@example.com',
          },
          receivers: [
            {
              name: 'Critical',
              email_configs: [{
                to: 'admin@example.com',
              }],
            },
            { name: 'Default', },
            { name: 'Watchdog', },
            { name: 'null', },
          ],
        },
      },
     },
    },
  };
```

これだけで Severity が **Critical** に分類されるアラートが発生した場合にメールが送信されます。

# 【カスタマイズ例4】KubeSchedulerDown, KubeControllerManagerDown のアラートを止める

環境によっては kube-scheduler や kube-controller-manager に対する監視が適当でない場合があります。

これらは ciritcal に分類されているため、メールを送信するようにした途端に定期的に警告メールを受信することになるかもしれません。

これを停止するために現在では便利な定義が **jsonnet/kube-prometheus/addons** にあるので、次のようなimport文を **examples/kustomize.jsonnet** に追加しましょう。

```jsonnet:examples/kustomize.jsonnetの先頭部分
local kp =
  (import 'kube-prometheus/main.libsonnet') + 
    (import 'kube-prometheus/addons/managed-cluster.libsonnet') +
  {
```

1行加えるだけですが、**{** と **+** に注意してください。

diffでみると次のようになっています。

```diff:同じ変更箇所のdiff出力
diff --git a/examples/kustomize.jsonnet b/examples/kustomize.jsonnet
index 04f151ad..ee33e27e 100644
--- a/examples/kustomize.jsonnet
+++ b/examples/kustomize.jsonnet
@@ -1,5 +1,7 @@
 local kp =
-  (import 'kube-prometheus/main.libsonnet') + {
+  (import 'kube-prometheus/main.libsonnet') + 
+    (import 'kube-prometheus/addons/managed-cluster.libsonnet') +
+  {
     values+:: {
       common+: {
         namespace: 'monitoring',
```

この結果出力されるYAMLファイルを ``$ kubectl apply -f manifests`` で適用すればアラートは消えますが、気になる場合には不要になった ServiceMonitor オブジェクトを削除しても良いかもしれません。

```bash:
$ kubectl -n monitoring delete servicemonitor kube-controller-manager
$ kubectl -n monitoring delete servicemonitor kube-scheduler
```

# 【カスタマイズ例5】node-exporterのCPUThrottlingHighアラートを止めたい

Severity が info なのでメールは出ませんが、気にある場合には次のようなカスタマイズが可能です。

どれくらいのリソースを指定するのが適当かは環境によって変化するため、元の値の2倍、3倍などと変更して様子をみてください。

```diff:
diff --git a/examples/kustomize.jsonnet b/examples/kustomize.jsonnet
index 0cfc8d3a..687c4d15 100644
--- a/examples/kustomize.jsonnet
+++ b/examples/kustomize.jsonnet
@@ -12,6 +14,32 @@ local kp =
           },
         },
       },
+      nodeExporter+:: {
+        resources+: {
+          requests+: { cpu: "306m", },
+          limits+: { cpu: "750m", },
+        },
+      },
+      blackboxExporter+:: {
+        resources+: {
+          requests+: { cpu: "80m", },
+          limits+: { cpu: "160m", },
+        },
+      },
+      kubeStateMetrics+:: {
+        kubeRbacProxyMain+:: {
+          resources+: {
+            requests+: { cpu: "80m", },
+            limits+: { cpu: "160m", },
+          },
+        },
+        kubeRbacProxySelf+:: {
+          resources+: {
+            requests+: { cpu: "40m", },
+            limits+: { cpu: "80m", },
+          },
+        },
+      },
     },
   };
```

# 【カスタマイズ例6】 Rook/CephのFileSystemのinodesアラートを消したい

Rook/Cephが提供するstorageclass/rook-cephfsはReadWriteManyでアクセスできるStorage機能ですが、inodesが動的に割り当てられるにも関わらず、ある程度のファイル数が書き込まれると上限に到達してしまいます。

これが原因で見掛け上は``df -i``コマンドの出力でinodeの使用率が100%になってしまいアラートが発砲されます。

このため **KubePersistentVolumeInodesFillingUp** ルールに、次のようなルールを加えれば解決できることはad-hocに定義を編集して突き止めています。

```yaml:
        unless on(namespace, persistentvolumeclaim)
        kube_persistentvolumeclaim_info{storageclass="rook-cephfs"} == 1
```

このルールは上限が1m毎と、1h毎の2つがあるので、これらを識別して個別にルールをアップデートしなければいけません。

このための変更を加えた examples/kustomize.jsonnet は次のようになりました。

```diff:examples/kustomize.jsonnetの差分
diff --git a/examples/kustomize.jsonnet b/examples/kustomize.jsonnet
index c12f98b3..09141738 100644
--- a/examples/kustomize.jsonnet
+++ b/examples/kustomize.jsonnet
@@ -5,6 +5,34 @@ local kp =
         namespace: 'monitoring',
       },
     },
+    kubernetesControlPlane+: {
+      prometheusRule+: {
+        spec+: {
+          groups: std.map(
+            function(group)
+              if group.name == 'kubernetes-storage' then
+                group {
+                  rules: std.map(
+                    function(rule)
+                      if rule.alert == 'KubePersistentVolumeInodesFillingUp' then
+                        rule {
+                          expr+: |||
+                            unless on(namespace, persistentvolumeclaim)
+                            kube_persistentvolumeclaim_info{storageclass="rook-cephfs"} == 1
+                          |||
+                       }
+                      else
+                        rule,
+                    super.rules
+                  ),
+                }
+              else
+                group,
+            super.groups
+          ),
+        },
+      },
+    },
   };
 
 local manifests =
```

``make generate``で生成されたYAMLファイルでも差分を確認しておきます。

```diff:manifests/kubernetesControlPlane-prometheusRule.yamlの差分
diff --git a/manifests/kubernetesControlPlane-prometheusRule.yaml b/manifests/kubernetesControlPlane-prometheusRule.yaml
index ce0fefd6..0dd03e65 100644
--- a/manifests/kubernetesControlPlane-prometheusRule.yaml
+++ b/manifests/kubernetesControlPlane-prometheusRule.yaml
@@ -440,6 +440,8 @@ spec:
         kube_persistentvolumeclaim_access_mode{ access_mode="ReadOnlyMany"} == 1
         unless on(namespace, persistentvolumeclaim)
         kube_persistentvolumeclaim_labels{label_excluded_from_alerts="true"} == 1
+        unless on(namespace, persistentvolumeclaim)
+        kube_persistentvolumeclaim_info{storageclass="rook-cephfs"} == 1
       for: 1m
       labels:
         severity: critical
@@ -465,6 +467,8 @@ spec:
         kube_persistentvolumeclaim_access_mode{ access_mode="ReadOnlyMany"} == 1
         unless on(namespace, persistentvolumeclaim)
         kube_persistentvolumeclaim_labels{label_excluded_from_alerts="true"} == 1
+        unless on(namespace, persistentvolumeclaim)
+        kube_persistentvolumeclaim_info{storageclass="rook-cephfs"} == 1
       for: 1h
       labels:
         severity: warning
```

## 参考資料

kube-prometheusに含まれている変更例は新しいルールを加えるもので、デフォルトルールの一部だけを変更するというトリッキーな作業については下記の資料が参考になりました。

* https://groups.google.com/g/prometheus-users/c/ha6wLqFlFTc


# 【カスタマイズ例7】Rook/CephのmetricsとDashBoardを追加したい

次のような環境で検証しています。

* Rook/Ceph v1.11.10 (Ceph v17.2.6、namespace: rook-ceph)

## Rook/Ceph側での設定

rookのGitリポジトリにはmonitroingのためのYAMLファイル等が含まれています。

* rook/deploy/examples/monitoring/service-monitor.yaml
* rook/deploy/examples/monitoring/localrules.yaml

この2つのYAMLファイルは、namespace: rook-ceph 側に設定します。
YAMLファイル中にnamespace: rook-cephは指定されているので、編集は不要です。

```bash:
$ sudo kubectl -n rook-ceph apply -f rook/deploy/examples/monitoring/service-monitor.yaml
$ sudo kubectl -n rook-ceph apply -f rook/deploy/examples/monitoring/localrules.yaml
```

これでPrometheus上でmetricsが収集されるようになります。

```bash:設定の確認
$ sudo kubectl -n rook-ceph get ServiceMonitor rook-ceph-mgr
$ sudo kubectl -n rook-ceph get PrometheusRule prometheus-ceph-rules
```

## Prometheus側での設定

Grafana Dashboardの定義は次の公式サイトにリンクが掲載されています。

* https://rook.io/docs/rook/latest/Storage-Configuration/Monitoring/ceph-monitoring/#grafana-dashboards

ここからダウンロードできる次のファイルを利用します。

* 2842_rev17.json (Ceph - Cluster)
* 5336_rev9.json (Ceph - OSD (Single))
* 5342_rev9.json (Ceph - Pools)

これらのファイルは namespace: monitoring 側でConfigMapオブジェクトとして登録します。

```bash:
$ sudo kubectl -n monitoring create configmap grafana-dashboard-ceph-cluster --from-file=2842_rev17.json
$ sudo kubectl -n monitoring create configmap grafana-dashboard-ceph-osd --from-file=5336_rev9.json
$ sudo kubectl -n monitoring create configmap grafana-dashboard-ceph-pools --from-file=5342_rev9.json
```

Kube-Prometheus側では examples/kustomize.jsonnet に次のような変更を加えます。

```jsonnet:examples/kustomize.jsonnetファイルの主要部分抜粋
local kp =
  (import 'kube-prometheus/main.libsonnet') + 
    (import 'kube-prometheus/addons/managed-cluster.libsonnet') +
  {
    values+:: {
      common+: {
        namespace: 'monitoring',
      },
      prometheus+: {
        namespaces+: ['rook-ceph'],
      },
    },
    grafana+: {
      networkPolicy+: {
         // 省略
      },
      deployment+: {
        spec+: {
          template+: {
            spec+: {
              containers: std.map(
                function(container)
                  if container.name == "grafana" then
                    container {
                      readinessProbe+: {
                        httpGet+: {
                          path: "/grafana/api/health"
                        },
                      },
                      volumeMounts+: [
                        {
                          mountPath: "/grafana-dashboard-definitions/0/ceph-cluster",
                          name: "grafana-dashboard-ceph-cluster",
                          readOnly: false,
                        },
                        {
                          mountPath: "/grafana-dashboard-definitions/0/ceph-osd",
                          name: "grafana-dashboard-ceph-osd",
                          readOnly: false,
                        },
                        {
                          mountPath: "/grafana-dashboard-definitions/0/ceph-pool",
                          name: "grafana-dashboard-ceph-pools",
                          readOnly: false,
                        },
                      ],
                    }
                  else
                    container,
                super.containers
              ),
              volumes+: [
                {
                  configMap: {
                    name: "grafana-dashboard-ceph-cluster",
                  },
                  name: "grafana-dashboard-ceph-cluster",
                },
                {
                  configMap: {
                    name: "grafana-dashboard-ceph-osd",
                  },
                  name: "grafana-dashboard-ceph-osd",
                },
                {
                  configMap: {
                    name: "grafana-dashboard-ceph-pools",
                  },
                  name: "grafana-dashboard-ceph-pools",
                },
              ],
            },
          },
        },
      },
    },
  };
```

examples/kustomize.jsonnet を編集したら、``make generate`` で manifests/ を更新し、kube apply コマンドで適用します。

Podの再起動などは不要で、しばらく待ってからGrafanaのDashboards画面にアクセスすると追加した3つのボードが表示されます。

## localrules.yamlファイルの修正

Rook/Cephに付属しているlocalrules.yamlに含まれる次のアラートはharmlessにしたいので、ファイルを修正してseverityをinfoに修正しています。

* CephNodeNetworkPacketDrops
* CephNodeNetworkPacketErrors
* CephNodeInconsistentMTU

rook/deploy/examples/monitoring/localrules.yaml を修正し、kube applyすると即座に反映されます。

# 現在利用している examples/kustomize.jsonnet ファイルの全体

参考までに現在利用している kustomize.jsonnet ファイルを掲載しておきます。

カスタマイズ例7のrook.ioに対する変更を含んでいるため、rook側のmonitoringディレクトリにあるYAMLファイルを"warning"→"info"に変更するなどの対応をしてから適応し、Grafana用の追加dashboard定義の追加も必要です。

```jsonnet:examples/kustomize.jsonnet
local kp =                         
  (import 'kube-prometheus/main.libsonnet') +            
  (import 'kube-prometheus/addons/managed-cluster.libsonnet') + {
    values+:: {                  
      common+: {                        
        namespace: 'monitoring',                       
      },                                             
      nodeExporter+:: {                              
        resources+: {     
          requests+: { cpu: "500m", },                                                                                                      
          limits+: { cpu: "1000m", },                   
        },                                                                                                                  
      },                                                         
      blackboxExporter+:: {                            
        resources+: {                                                            
          requests+: { cpu: "80m", },
          limits+: { cpu: "160m", },                                             
        },                                                             
      },                                                                                    
      kubeStateMetrics+:: {                             
        resources+: {                          
          requests+: { cpu: "50m", },                                             
          limits+: { cpu: "300m", },                           
        },                                
        kubeRbacProxyMain+:: {
          resources+: {        
            requests+: { cpu: "120m", },
            limits+: { cpu: "320m", },      
          },                                                     
        },                      
        kubeRbacProxySelf+:: {
          resources+: {  
            requests+: { cpu: "120m", },
            limits+: { cpu: "320m", },
          },                                               
        },                         
      },                                                 
      grafana+:: {                                   
        resources+: {                    
          requests+: { cpu: "300m", },  
          limits+: { cpu: "600m", },                   
        },                                                                                                                                  
        config+: {                                   
          sections+: {    
            server: { domain: 'admin.example.com', root_url: '%(protocol)s://%(domain)s:%(http_port)s/grafana/', serve_from_sub_path: true }
,                                                       
            "auth.proxy": { enabled: true, auto_sign_up: true, header_name: "X-WEBAUTH-USER", header_property: "username" },
          },                                                     
        },                                             
      },
      alertmanager+:: {                                                                                                           [121/1857]
        config+: {                                                               
          global+: {                                                   
            resolve_timeout: '5m',                                                          
            smtp_smarthost: 'smarthost1.example.com:25',
            smtp_from: 'admin-dev@example.com',                         
            smtp_require_tls: false,                                              
          },                                                   
          receivers: [                    
            {                                                                
              name: 'Critical',
              email_configs: [{
                to: 'admin-dev@example.com',
                require_tls: false,                              
              }],               
            },                                      
            {
              name: 'Warning',
              email_configs: [{
                to: 'admin-dev@example.com',
                require_tls: false,
              }],
            },
            { name: 'Default', },
            { name: 'Watchdog', },
            { name: 'null', },
          ],
        },
      },
      prometheus+: {
        namespaces+: ['rook-ceph'],
      },
    },
    grafana+: {
      networkPolicy+: {
        spec+: {
          ingress+: [{
            from: [{
              namespaceSelector: {
                matchLabels: {
                  'name': 'ingress-nginx',
                },
              },
            }],
            ports: [{
              port: 3000,
              protocol: "TCP",
            }],
          }],
        },
      },
      deployment+: {                                                                                                               [70/1857]
        spec+: {
          template+: {
            spec+: {
              containers: std.map(
                function(container)
                  if container.name == "grafana" then
                    container {
                      readinessProbe+: {
                        httpGet+: {
                          path: "/grafana/api/health"
                        },
                      },
                      volumeMounts+: [
                        {
                          mountPath: "/grafana-dashboard-definitions/0/ceph-cluster",
                          name: "grafana-dashboard-ceph-cluster",
                          readOnly: false,
                        },
                        {
                          mountPath: "/grafana-dashboard-definitions/0/ceph-osd",
                          name: "grafana-dashboard-ceph-osd",
                          readOnly: false,
                        },
                        {
                          mountPath: "/grafana-dashboard-definitions/0/ceph-pool",
                          name: "grafana-dashboard-ceph-pools",
                          readOnly: false,
                        },
                      ],
                    }
                  else
                    container,
                super.containers
              ),
              volumes+: [
                {
                  configMap: {
                    name: "grafana-dashboard-ceph-cluster",
                  },
                  name: "grafana-dashboard-ceph-cluster",
                },
                {
                  configMap: {
                    name: "grafana-dashboard-ceph-osd",
                  },
                  name: "grafana-dashboard-ceph-osd",
                },
                {
                  configMap: {
                    name: "grafana-dashboard-ceph-pools",
                  },
                  name: "grafana-dashboard-ceph-pools",
                },
              ],
            },
          },
        },
      },
    },
    prometheus+: {                                                                                                                 [10/1857]
      networkPolicy+: {
        spec+: {
          ingress+: [{
            from: [{
              podSelector: {
                matchLabels: {
                  'app.kubernetes.io/name': 'prometheus-adapter',
                },
              },
            }],
            ports: [{
              port: 9090,
              protocol: "TCP",
            }],
          }],
        },
      },
    },
    kubernetesControlPlane+: {
      prometheusRule+: {
        spec+: {
          groups: std.map(
            function(group)
              if group.name == 'kubernetes-storage' then
                group {
                  rules: std.map(
                    function(rule)
                      if rule.alert == 'KubePersistentVolumeInodesFillingUp' then
                        rule {
                          expr+: |||
                            unless on(namespace, persistentvolumeclaim)
                            kube_persistentvolumeclaim_info{storageclass="rook-cephfs"} == 1
                          |||
                       }
                      else
                        rule,
                    super.rules
                  ),
                }
              else
                group,
            super.groups
          ),
        },
      },
    },
  };

local manifests =
  {
    ['setup/' + resource]: kp[component][resource]
    for component in std.objectFields(kp)
    for resource in std.filter(
      function(resource)
        kp[component][resource].kind == 'CustomResourceDefinition' || kp[component][resource].kind == 'Namespace', std.objectFields(kp[compo
nent])
    )
  } +
  {
    [component + '-' + resource]: kp[component][resource]
    for component in std.objectFields(kp)
    for resource in std.filter(
      function(resource)
        kp[component][resource].kind != 'CustomResourceDefinition' && kp[component][resource].kind != 'Namespace', std.objectFields(kp[compo
nent])
    )
  };

local kustomizationResourceFile(name) = './manifests/' + name + '.yaml';
local kustomization = {
  apiVersion: 'kustomize.config.k8s.io/v1beta1',
  kind: 'Kustomization',
  resources: std.map(kustomizationResourceFile, std.objectFields(manifests)),
};

manifests {
  '../kustomization': kustomization,
}
```

テストのためまだ説明していないrequire_tls: falseの設定が含まれていますが、これはなくてもsmarthostの25番ポートに接続し、認証無しでメールを送信してくれますので不要です。

また、この構成を利用する際には grafana-dashboard-ceph-cluster などのConfigMapオブジェクトを追加している必要があります。

# アップグレード (v0.12.0 → v0.13.0 → v0.14.0)

Kubernetesをv1.25.6からv1.27.7に更新したタイミングで、Prometheusもアップグレードすることにしました。

v0.12.0が動いた状態でk8sをアップグレードしても問題なく動作しているようにみえるので、必ずしも更新する必要はないと思いますが、手順を掲載しておきます。

## kube-prometheusの更新

kube-prometheusでv0.13.0ブランチをcheckoutします。

```bash:
$ cd kube-prometheus
$ git status
## 変更箇所を全てcommitしたら次に進む
$ git checkout main
$ git pull
$ git checkout refs/tags/v0.13.0 -b my_v0.13.0
```

これでmy_0.13.0ブランチに入っていることを確認してから次に進みます。

```bash:
$ git branch
  main
  my_v0.12.0
* my_v0.13.0
```

## examples/kustomize.jsonnet ファイルの更新

v0.12.0で利用していたファイルで上書きします。

```bash:my_v0.12.0ブランチからkustomize.jsonnetファイルをコピーする
$ git checkout my_v0.12.0 examples/kustomize.jsonnet
```

## Rookの更新がないことを確認する

余計なアラートが上がるだけで実害はありませんが、rook.io側ではlocalrules.yamlでseverityを変更しているので、もし変更されてしまっていれば、これを確認して必要であれば適用します。

確認には、次の手順で追加したConfigMapオブジェクトを確認してください。

```bash:
$ sudo kubectl -n monitoring get cm grafana-dashboard-ceph-pools grafana-dashboard-ceph-osd grafana-dashboard-ceph-cluster
NAME                             DATA   AGE
grafana-dashboard-ceph-pools     1      118d
grafana-dashboard-ceph-osd       1      118d
grafana-dashboard-ceph-cluster   1      118d
```

このように表示されれば追加の手順は不要です。

## Manifetsの生成

examples/kustomize.jsonnetに修正するべき点がなければ、そのままYAMLファイルを生成します。

```bash:YAMLファイルの生成
$ make generate
```

``git diff``などで差分から変更内容を確認します。

```bash:YAMLファイルの適用
$ sudo kubectl apply --server-side -f manifests/setup
$ sudo kubectl wait \
	--for condition=Established \
	--all CustomResourceDefinition \
	--namespace=monitoring
$ sudo kubectl apply -f manifests/
```

# さいごに

jsonnet-builderを使ってkube-prometheusをカスタマイズするのは始めてでしたが、後から変更するのに比べるとかなり大変だったのは間違いありません。いまでも選択した手段が適切だったのか困惑しています。

ただその後のアラートメールの送信や、KubeSchedulerDownアラートの削除をしたことを考えるとjsonnetによるカスタマイズは適切な方法だと思います。

jsonnetは便利なツールだとは思いますが、全ての変更をjsonnetで行うのだとこの技術にこだわるよりも柔軟に変更手段を選択する方が良さそうです。

昔、LinuxでRHEL系のdistributionを選択する時に自作アプリのRPMを作成して登録しようとして大変な思いをしたことを思い出します。技術自体の目的や内容が素晴しくても、それに費す時間とのバランスも考える必要があります。

またjsonnetは学ぶ価値のある技術だと思いますが、m4マクロ並みに大変そうな雰囲気を感じています。

使いこなすには [https://jsonnet.org/](https://jsonnet.org/) のドキュメントを読んで、ある程度は裏側の処理方法を把握する必要がありそうです。


以上