在反向代理后面运行Grafana

我在 使用Helm 3在Kubernetes集群部署Prometheus和Grafana 为了快速解决服务输出,采用了 NodePort 模式,此时在节点输出的端口是随机(可以指定,但是范围限定为 30000–32767 )。为了方便用户访问,在主机上配置 Nginx反向代理 以便用户能够以众所周知的固定服务端口访问。

  • /etc/nginx/sites-enabled 目录下软连接 /etc/nginx/sites-availabled 目录下的3个自定义配置文件:

    alertmanager
    grafana
    prometheus
    

alertmanager 配置文件内容:

NGINX 反向代理 alertmanager 配置
upstream alertmanager {
    server 192.168.6.115:30903;
}

server {
    listen 9093;
    #listen [::]:80;

    server_name alertmanager alertmanager.cloud-atlas.io;

    location / {
        proxy_set_header Host $http_host;
	proxy_pass http://alertmanager;
    }
}
NGINX 反向代理 grafana 配置
upstream grafana {
    server 192.168.6.115:30080;
}

server {
    listen 80;
    #listen [::]:80;

    server_name grafana grafana.cloud-atlas.io;

    location / {
        proxy_set_header Host $http_host;
	proxy_pass http://grafana;
    }
}
NGINX 反向代理 prometheus 配置
upstream prometheus {
    server 192.168.6.115:30090;
}

server {
    listen 9092;
    #listen [::]:80;

    server_name prometheus prometheus.cloud-atlas.io;

    location / {
        proxy_set_header Host $http_host;
	proxy_pass http://prometheus;
    }
}

备注

虽然 kube-prometheus-stackvalues.yaml 对于配置域名方式访问 Grafana 需要配置 domain (见下文)。但是我实践发现,对于 Nginx反向代理 ,仅参考 Run Grafana behind a reverse proxy 完成 Nginx反向代理 配置就可以以域名访问Grafana面板。

成功

9090 端口绑定错误

最初我按照 Prometheus监控 默认对外服务端口 9090 来配置 Nginx ,但是启动失败 journalctl -xeu nginx.service 检查服务器启动日志:

nginx反向代理prometheus,绑定9090端口失败: 和系统 Cockpit服务器统一管理平台 默认端口冲突
Apr 21 22:47:04 zcloud.staging.huatai.me nginx[26271]: nginx: [emerg] bind() to 0.0.0.0:9090 failed (98: Unknown error)
...
Apr 21 22:47:07 zcloud.staging.huatai.me nginx[26271]: nginx: [emerg] still could not bind()
Apr 21 22:47:07 zcloud.staging.huatai.me systemd[1]: nginx.service: Control process exited, code=exited, status=1/FAILURE
Subject: Unit process exited

通过 lsof | grep 9090 可以看到是 Systemd进程管理器 绑定了 9090 端口:

systemd       1                            root   56u     IPv6              41495      0t0        TCP *:9090 (LISTEN)

然后通过 grep -R 9090 /etc/systemd/* 可以看到该端口默认被 Cockpit服务器统一管理平台 占用:

system/sockets.target.wants/cockpit.socket:ListenStream=9090

所以将 Prometheus监控 端口修订为 9092

“401 Unauthorized”报错

生产环境,同样使用 kube-prometheus-stack 部署Grafana ( 方法同 在Kubernetes集群(z-k8s)部署集成GPU监控的Prometheus和Grafana ),使用IP访问运行正常。但是我切换到 Nginx反向代理 之后,虽然按照 Run Grafana behind a reverse proxy 配置了NGINX,但是发现域名访问会出现报错 401 Unauthorized

我采用了 更新Kubernetes集群的Prometheus配置 : 先修订 kube-prometheus-stack.values ,修订了 domain 配置,举例:

## Hostnames.
## Must be provided if Ingress is enable.
##
# hosts:
#   - grafana.domain.com
hosts:
  - grafana.example.com

但是我发现 Grafana 容器启动后,我检查 configMap 配置:

kubectl -n prometheus edit cm kube-prometheus-stack-1681228346-grafana

显示内容:

检查 Grafana 的 configMap 发现 domain 是空的
apiVersion: v1
data:
  grafana.ini: |
    [analytics]
    check_for_updates = true
    [grafana_net]
    url = https://grafana.net
    [log]
    mode = console
    [paths]
    data = /var/lib/grafana/
    logs = /var/log/grafana
    plugins = /var/lib/grafana/plugins
    provisioning = /etc/grafana/provisioning
    [server]
    domain = ''
kind: ConfigMap
...

并且登陆到容器内部:

kubectl -n prometheus exec -it  kube-prometheus-stack-1681228346-grafana-fb4695b7-2qhpp -c grafana -- /bin/sh

也可以看到 /etc/grafana/grafana.ini 完全对应这个配置,也就是 domain 是空的

我手工修订了 configMap ,然后删除重建pod

现在可以看到容器内部 /etc/grafana/grafana.ini 页更新了:

[server]
domain = 'grafana.example.com'

但是还是没有解决 “401 Unauthorized”报错,而且不仅域名不能访问,IP也不行了(之前也不是可以,只是cookie没有失效无需认证而已;实际重建容器,即使IP也不能登陆了)。

反复尝试,发现:

  • 配置了 kube-prometheus-stack.values 同时手工修改 cm kube-prometheus-stack-1681228346-grafana 会导致IP和域名访问都失效

  • 去除 kube-prometheus-stack.values 配置(即不使用 kube-prometheus-stack 域名配置),但是手工修改 cm kube-prometheus-stack-1681228346-grafana ,此时容器内部 /etc/grafana/grafana.ini 会注入 domain = 'grafana.example.com' ,但是域名访问不行,不过IP访问却是正常。目前先用IP访问,待查

差异对比

但是我线下测试环境验证是通过的,所以考虑生产和线下环境的差异:

  • 线上生产环境采用了 2次 反向代理,也就是在我的 Nginx反向代理 前面还有一层 NGINX 做反向代理(不是我的管理范围,所以我无法检查具体配置)

  • 生产环境的第一层NGINX反向代理启用了 SSL ,也就是第一层反向代理到我的第二层NGINX反向代理,我的第二层NGINX反向代理是没有SSL加密的

  • 怀疑第一层反向代理的HTTP头部改写可能触发了Grafana的安全限制

验证思路:

  • 将二层反向代理改为 iptables端口转发(port forwarding) ,然后直接本地绑定 /etc/hosts 以域名方式访问,验证是否实现可以域名形式访问Grafana NodePort 模式(注意,没有配置Grafana的 grafana.inidomain )

  • 将二层反向代理改为 iptables端口转发(port forwarding) ,这样看第一层反向代理是否可以实现域名方式访问 Grafana(其实现在就是 iptables端口转发(port forwarding) 访问 NodePort 模式,只是没有经过第一层反向代理的域名访问方式而是使用IP访问)

  • 本地通过 /etc/hosts 绑定域名解析,绕过第一层NGINX反向代理,直接访问第二层反向代理(第二层由 iptables端口转发(port forwarding) 改为 Nginx反向代理 ),看能否实现域名方式访问 Grafana (类似我在线下测试环境部署)

  • 在二层反向代理配置 proxy_hide_header X-Frame-Options;

  • 通过 curl检查HTTP请求头部 对比检查上述访问的差异

验证一

在物理主机主机上采用 iptables端口转发(port forwarding) ,本地绑定 /etc/hosts 以域名方式访问:

  • 验证成功,可以直接使用域名访问 Grafana NodePort 模式(没有配置Grafana的 grafana.inidomain )

验证二

在物理主机上保持 iptables端口转发(port forwarding) ,然后去除本地绑定 /etc/hosts ,这样客户端访问就会通过域名直接访问第一层反向代理,通过反向代理访问 iptables端口转发(port forwarding) 来访问 Grafana NodePort :

第一层反向代理采用了SSL卸载,也就是强制转跳https,然后反向代理到后端物理主机 iptables端口转发(port forwarding) 80 端口,来访问 Grafana NodePort

果然,出问题在这里,此时再重现了 401 Unauthroized 报错

  • 检查 grafana 日志:

    kubectl -n prometheus logs kube-prometheus-stack-1681228346-grafana-849b55868d-j62r9 -c grafana --follow
    

可以看到:

logger=http.server t=2023-04-22T13:15:36.598575746Z level=info msg="Successful Login" User=admin@localhost
logger=context userId=1 orgId=1 uname=admin t=2023-04-22T13:15:37.039013543Z level=info msg="Request Completed" method=GET path=/api/live/ws status=-1 remote_addr=192.168.147.143 time_ms=0 duration=739.195µs size=0 referer= handler=/api/live/ws
logger=context userId=0 orgId=0 uname= t=2023-04-22T13:15:37.084924294Z level=info msg="Request Completed" method=GET path=/api/login/ping status=401 remote_addr=192.168.147.143 time_ms=0 duration=374.008µs size=26 referer=https://grafana.cloud-atlas.io/ handler=/api/login/ping
logger=context userId=0 orgId=0 uname= t=2023-04-22T13:15:37.182058314Z level=info msg="Request Completed" method=GET path=/ status=302 remote_addr=192.168.147.143 time_ms=0 duration=348.336µs size=29 referer=https://grafana.cloud-atlas.io/ handler=/

注意到 grafana 记录了 admin@localhost 登陆成功,但是 https 域名是否会和 Grafana 自身的 80 端口不一致而导致异常呢?

果然,在 Grafana / HTTPS / Nginx Proxy 帖子中提到了修订 grafana.ini 配置,在 grafana 端依然是 protocol = http ,但是要修改 root_url = https://my.domain.name/ 告知域名通过反向代理访问

  • 对比 ConfigMap 配置:

    kubectl -n prometheus edit cm kube-prometheus-stack-1681228346-grafana
    

可以看到当前默认的 grafanaConfigMap 是:

默认grafana ConfigMap
apiVersion: v1
data:
  grafana.ini: |
    [analytics]
    check_for_updates = true
    [grafana_net]
    url = https://grafana.net
    [log]
    mode = console
    [paths]
    data = /var/lib/grafana/
    logs = /var/log/grafana
    plugins = /var/lib/grafana/plugins
    provisioning = /etc/grafana/provisioning
    [server]
    domain = ''
kind: ConfigMap

登陆到容器内部检查:

kubectl -n prometheus exec -it kube-prometheus-stack-1681228346-grafana-849b55868d-j62r9 -c grafana -- /bin/bash

可以看到 /etc/grafana/grafana.ini 内容和 ConfigMap 对应:

默认grafana.ini配置,和ConfigMap对应
[analytics]
check_for_updates = true
[grafana_net]
url = https://grafana.net
[log]
mode = console
[paths]
data = /var/lib/grafana/
logs = /var/log/grafana
plugins = /var/lib/grafana/plugins
provisioning = /etc/grafana/provisioning
[server]
domain = ''
  • 修订 ConfigMap 配置:

修订grafana ConfigMap
apiVersion: v1
data:
  grafana.ini: |
    [analytics]
    check_for_updates = true
    [grafana_net]
    url = https://grafana.net
    [log]
    mode = console
    [paths]
    data = /var/lib/grafana/
    logs = /var/log/grafana
    plugins = /var/lib/grafana/plugins
    provisioning = /etc/grafana/provisioning
    [server]
    domain = grafana.cloud-atlas.io
    protocol = http
    enforce_domain = true
    root_url = https://grafana.cloud-atlas.io/
kind: ConfigMap

备注

如果配置了 enforce_domain = true ,就会强制使用域名访问,即使你使用了IP地址访问,也会重定向到域名

但是,我发现还是没有解决我的这个生产环境问题,依然报错账号密码错误

  • 对比了线上另外一个正常访问的grafana,在容器中 /etc/grafana/grafana.ini 有一项允许嵌入:

    [security]
    allow_embedding = true
    

我尝试添加也没有解决登陆(允许嵌入是不是降低了安全要求?)

诡异的是,用户账号密码正确,而且也有一瞬间能够正常登陆,但是立即退出系统

乌龙了

我发现原来是一个业务流程申请错误导致的:

  • 我们在两个集群分别部署了 kube-prometheus-stack ,公司的负载均衡申请流程非常繁杂,完全是一个黑盒: 需要按照应用绑定,我现在回想起来,实际上就是按照 应用名 构建了 Nginx反向代理upstream 配置

    • 但是公司的负载均衡将一个简单的 Nginx反向代理 配置拆解成多个申请流程,没有任何原理说明,导致我将两个集群的 kube-prometheus-stack 申请为一个应用名,实际上反向带来会将 upstream 随机发给两个集群中的两套 grafana ,相互间数据踩踏

我是怎么发现这个问题的呢:

我发现有时候Grafana登陆时显示认证正确,直接登陆进去但是立即被系统推出(实际上是负载均衡恰好将认证发送到符合正确密码的那套 grafana ,但是认证后后续操作发送给错误的那套 grafana ,由于session不匹配直接被系统强制登出),我突然想到会不会搞混了后端 grafana 系统,所以在登陆的时候同时打开了两个集群的 grafana pods日志观察,果然发现在登陆瞬间,两个不同集群的 grafana 都出现访问日志,而且从日志观察,错误接收数据的 grafana 系统出现了不应该出现的域名记录。

唉,真是一个惨痛的教训,折腾了好几天…

参考