问题的背景

k8s集群部署在海外,且集群内的Ingress节点从国内访问网络速度较慢,加带宽换优质线路又太贵,因此开始选择曲线救国的道路。😂

问题的初次想法

  • 买一台线路好但配置很低的Linux机器(1C2G)
  • 部署OpenResty来反代Ingress的域名,从而使客户端访问代理后的域名来加速

把想法落地吧

访问慢的问题解决了,但又产生了另外一个问题: 代理机器的配置太低,且又与k8s集群不在一个网络内,无法加入k8s集群。 从而又又又又带来了另外一个繁琐的问题:

k8s的Ingress配置更新后,还需手动更新OpenResty配置,我太懒。且手动更新配置比较繁琐且容易出错

再优化优化

因为起初本身需求也比较简单,只是将繁琐、手动的工作做成自动化的方式。减少人工的参与或仅仅是人工来复核应用生效。因此,较为理想的方案是:

  • 使用python的kubernetes库来连接k8s
  • 程序持续监听k8s api对新增或修改的Ingress规则自动将nginx配置渲染出来
  • 对有变化的规则进行关键字检索,只有符合关键字的规则才会渲染
  • 渲染后的配置,人工进行复核;并进行 nginx -s reload

程序编写

配置变更的监听

使用kubernetes库中的watch模块,我们可以监听Kubernetes集群中Ingress的变更。通过比对前后的Ingress规则,我们可以得知哪些规则发生了变化。

下面是一个例子,动态获取指定namespace下的ingress元数据名称

from kubernetes import client, config, watch

def watch_k8s_config(namespace):
    """kube config认证文件"""
    config.load_kube_config('~/.kube/config')
    networking_v1 = client.NetworkingV1Api()

    # 监听k8s资源变化方法
    w = watch.Watch()
    print("开始持续监听中...")
    for events in w.stream(networking_v1.list_namespaced_ingress, namespace=namespace):
        ingress = events['object']
         # 获取元数据名称
        rules_name = ingress.metadata.name
        print(f"已经捕获到: {rules_name}")

 # 调用方法传入default命名空间
watch_k8s_config("default")

运行后,会立即打印出来已有的ingress规则,如果有在新增或修改ingress规则,也会同步打印

功能的完善

规划在kubectl apply ingress的规则时,捕获想要的字段,并以json格式返回,比如下面这个格式的数据类型:

{
    'name': 'ingress名称',
    'rules': [
        {
            'domain_name': '前置代理域名',
            'https_enable': False
        }
    ],
    'source_domain': '后置源域名',
    'source_path': '后置源路径',
    'secret_name': '证书名称'
}

Nginx配置渲染方法

在渲染配置这方面,我们使用JinJa2模板引擎,现将配置模板规划出来。后面通过传入json数据来生成模板。

JinJa2模板(template.j2):

server {
    listen 80;
    {% if https_enable %}listen 443 ssl; {% endif %}
    server_name {{domain_name}};
    client_max_body_size 4096m;
    {% if https_enable %}
    # ssl配置
    ssl_certificate     /etc/nginx/ssl/{{secret_name}}/ssl.pem;
    ssl_certificate_key /etc/nginx/ssl/{{secret_name}}/ssl.key;
    ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
    ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;

    if ($server_port !~ 443){
        rewrite ^(/.*)$ https://$host$1 permanent;
    }
    {% endif %}
    location / {
        {% if https_enable %}proxy_pass https://{{source_domain}}{{source_path}};{% else %}proxy_pass http://{{source_domain}}{{source_path}};{% endif %}
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    access_log  /etc/nginx/logs/{{domain_name}}.access.log;
    error_log   /etc/nginx/logs/{{domain_name}}.error.log;
}

渲染配置的代码片段

import os,sys
from jinja2 import Environment, FileSystemLoader

def generate_openresty_config(data):

    # 这里用前面的template.j2配置模板
    template = Environment(loader=FileSystemLoader(searchpath="./")).get_template("template.j2")

    for rule in data["rules"]:
        """
        渲染的字段含义:
            domain_name: 前置代理名称
            source_domain: 后置源域名
            source_path: 后置源路径
            https_enable: 是否启用HTTPS
            secret_name: HTTPS证书名称
        """

        domain_name = rule["domain_name"]
        source_domain = data["source_domain"]
        https_enable = rule["https_enable"]
        source_path = data["source_path"]
        secret_name = data["secret_name"]

        nginx_config = template.render(
            secret_name=secret_name,
            domain_name=domain_name,
            source_path=source_path,
            https_enable=https_enable,
            source_domain=source_domain,
        )

        # 进行渲染配置后的内容写入到当前路径下{domain_name}.conf文件内
        with open(f"./{domain_name}.conf", "w") as f:
            f.write(nginx_config)

组合程序代码

将以上所有的代码进行合并,组合为程序(main.py):

import os,sys
import logging
from kubernetes import client, config, watch
from jinja2 import Environment, FileSystemLoader

previous_rules = {}

"""
容易变动的配置通过环境变量来维护:

- keyword:  检索关键字
- namespace: 命名空间
- log_level: 日志输出等级
"""
keyword = os.environ.get('keyword') or ''
namespace = os.environ.get('namespace') or 'default'
log_level = os.environ.get('log_level') or 'INFO'

# 设置日志
logging.basicConfig(
    level=logging.getLevelName(log_level),
    datefmt="%d/%m/%Y %H:%M:%S",
    format="[%(asctime)s][%(levelname)s] ==> %(message)s",
    handlers=[logging.StreamHandler(stream=sys.stdout)],
)

logger = logging.getLogger(__name__)

# 检查是否启用了https
def tls_check(ingress, host):
    for tls_rule in ingress.spec.tls:
        if host in tls_rule.hosts:
            return True
    return False

# host关键字匹配
def keyword_matching(rule): 
    return keyword in rule.host

# 生成nginx配置规则
def generate_openresty_config(data):
    template = Environment(loader=FileSystemLoader(searchpath="./")).get_template("template.j2")

    for rule in data["rules"]:
        domain_name = rule["domain_name"]
        source_domain = data["source_domain"]
        https_enable = rule["https_enable"]
        source_path = data["source_path"]
        secret_name = data["secret_name"]

        nginx_config = template.render(
            secret_name=secret_name,
            domain_name=domain_name,
            source_path=source_path,
            https_enable=https_enable,
            source_domain=source_domain,
        )

        with open(f"./{domain_name}.conf", "w") as f:
            f.write(nginx_config)

# 监听配置变更方法
def watch_k8s_config(namespace):
    """
    input: kube confugration
    return: json data: new_ingress_info
    """

    # kube config的认证文件
    config.load_kube_config('~/.kube/config')
    networking_v1 = client.NetworkingV1Api()

    # 监听k8s资源变化方法
    w = watch.Watch()

    logger.info(f"事件监听中 namespace: {namespace}")
    if keyword != '':
        logger.info(f"匹配host关键字: {keyword}")

    for events in w.stream(networking_v1.list_namespaced_ingress, namespace=namespace):
        ingress = events['object']
        rules_name = ingress.metadata.name

        rules = []
        for rule in ingress.spec.rules:
            if keyword_matching(rule):
                tls_enabled = tls_check(ingress, rule.host)
                rule_info = {"domain_name": rule.host, "https_enable": tls_enabled}
                rules.append(rule_info)

        new_ingress_info = {"name": rules_name, "rules": rules, "source_domain": ingress.spec.tls[0].hosts[0],"source_path": rule.http.paths[0].path,"secret_name": ingress.spec.tls[0].secret_name}
        if rules_name in previous_rules:
            # 拿之前的配置和新配置进行对比
            if previous_rules[rules_name] != new_ingress_info:
                logger.info(f"配置规则{rules_name}已监听到更新")
                generate_openresty_config(new_ingress_info)
                if True:
                    logger.info(f"配置规则{rules_name}已生成配置文件")
                    logger.info(f"前置代理域名: {rule_info['domain_name']} 后置源域名: {new_ingress_info['source_domain']} 后置源路径: {new_ingress_info['source_path']} 是否启用TLS: {rule_info['https_enable']}")
                else:
                    logger.error(f"配置规则{rules_name}生成配置失败")

        previous_rules[rules_name] = new_ingress_info

if __name__ == "__main__":
    if  keyword == '':
        logger.warning("未指定匹配关键字,则生成全部规则配置") 
    watch_k8s_config(namespace)

需要将config.load_kube_config('~/.kube/config')的k8s客户端认证文件修改为具体的路径,接着运行程序,传入环境变量配置,在配置发生变更时来查看运行日志,下面是一个运行的日志例子:

$ export namespace=private # 监听private命名空间下的变更
$ export keyword=proxy     # 只有host配置的域名包含proxy关键字才会被捕获

$ python3 main.py
[01/02/2024 14:10:47][INFO] ==> 事件监听中 namespace: private
[01/02/2024 14:10:47][INFO] ==> 匹配host关键字: proxy
[01/02/2024 14:12:13][INFO] ==> 配置规则tele-stream-bot已监听到更新
[01/02/2024 14:12:13][INFO] ==> 配置规则tele-stream-bot已生成配置文件
[01/02/2024 14:12:13][INFO] ==> 前置代理域名: tele-xxxx 后置源域名: tele-xxxx 后置源路径: / 是否启用TLS: True

如果运行结果如上所示,那么在当前目录下,就会生成一个nginx的配置文件 xxx.conf

容器运行

我不喜欢在服务器内安装各种环境,只希望运行一个docker容器,即可完成我的需求。

如果在容器内运行,需要特别注意在main.py程序内的几个文件的路径:

    # 生成的nginx配置路径
    with open(f"/app/configs/{domain_name}.conf", "w") as f:
        f.write(nginx_config)

    # k8s认证文件的路径
    config.load_kube_config('/app/k8s_config')

准备dockerfile:

FROM python:3.7-alpine

WORKDIR /app/
RUN pip3 install --no-cache-dir kubernetes jinja2

ADD template.j2 main.py /app/

CMD ["python3", "main.py"]

构建容器镜像

docker build . -t svc_to_openresty:1.0

运行容器

docker rm -f svc_to_openresty
docker run -itd --name=svc_to_openresty \
-e keyword=检索关键字 \
-e namespace=命名空间 \
-v /app/openresty/conf:/app/configs/ \
-v /root/.kube/config:/app/k8s_config \
-v /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime \
svc_to_openresty:1.0
docker logs -f --tail=200 svc_to_openresty
最后修改:2024 年 02 月 02 日
如果觉得我的文章对你有用,请随意赞赏