WEB应用

APISIX运维优化之配置文件自动化生成方案

Jager · 11月14日 · 2021年 · · 3323次已读

在这个容器化技术盛行的时代,大家都习惯采用 Docker 或者 K8S 来运行 APISIXAPISIX 的配置参数非常多,因此很多介绍文章都采用挂载文件或者 K8S Configmap 的方式来配置 APISIX。最开始我们就采用 Configmap 的方式在腾讯云 TKE 上部署 APISIX,当网络区域越开越多时,每个 TKE 集群都需要去定义一套 config.yaml 对应的 Configmap,管理非常繁琐。因此,这里我们利用 Python 的 Jinja2 插件来自动化渲染 APISIX 的配置文件,整体非常方便!

一、Jinja2 模板

熟悉 Jinja2 的同学都很清楚,要通过 Jinja2 生成所需文件,需要先定制一个渲染模板,Jinja2 的原理就是将动态的内容填充到模板中,最终渲染成所需文件。因此,这里参考 APISIX 官方最新 2.10.0 版本config-default.yaml配置文件制作了 Jinja2 的配置模板如下:

apisix:
  node_listen:
    - ip: {{ http_listen_ip | default("0.0.0.0") }}
      port: {{ http_listen_port | default(9080) | int }}
      enable_http2: {{ http_enable_http2 | default("false") }}

    {% if multi_http_ports: -%}
    # supports more listen ports
    {% for port in multi_http_ports | regex_split -%}
    {% if port: -%}
    - {{port}}
    {% endif -%}
    {% endfor -%}
    {% endif %}

  enable_admin: {{ enable_admin | default("true") }}
  enable_admin_cors: {{ enable_admin_cors | default("true") }}
  enable_dev_mode: {{ enable_dev_mode | default("false") }}
  enable_reuseport: {{ enable_reuseport | default("true") }}
  enable_ipv6: {{ enable_ipv6 | default("false") }}
  config_center: {{ config_center | default("etcd") }}
  enable_server_tokens: {{ enable_server_tokens | default("true") }}

  extra_lua_path: {{ extra_lua_path | default("") }}
  extra_lua_cpath: {{ extra_lua_cpath | default("") }}

  proxy_cache:
    cache_ttl: {{ cache_ttl | default("3600s") }}
    zones:
      - name: {{ proxy_cache_zones | default("disk_cache_one") }}
        memory_size: {{ proxy_cache_memory_size | default("50m") }}
        disk_size: {{ proxy_cache_disk_size | default("1G") }}
        disk_path: {{ proxy_cache_disk_path | default("/tmp/disk_cache_one") }}
        cache_levels: {{ proxy_cache_cache_levels | default("1:2") }}

  allow_admin:
    {% if allow_admin_subnet: -%}  
    {%- for item in allow_admin_subnet | regex_split -%}
    {% if item: -%}
    - {{item}}
    {% endif -%}
    {% endfor %}
    {%- endif -%}

  admin_key:
    -
      name: {{ admin_key_name | default("admin") }}
      key: {{ admin_key_secret | default("d208uj44fnd2yk6quczd6szkytvoi0x1") }}
      role: admin

    -
      name: {{ viewer_key_name | default("viewer") }}
      key: {{ viewer_key_secret | default("4054f7cf07e344346cd3f287985e76a2") }}
      role: viewer

  delete_uri_tail_slash: {{ delete_uri_tail_slash | default("false") }}
  global_rule_skip_internal_api: {{ global_rule_skip_internal_api | default("true") }}

  router:
    http: {{ router_http | default("radixtree_uri") }}
    ssl: {{ router_ssl | default("radixtree_sni") }}

  resolver_timeout: {{ resolver_timeout | default(3) | int }}
  enable_resolv_search_opt: {{ enable_resolv_search_opt | default("true") }}
  ssl:
    enable: {{ ssl_enable | default("true") }}
    enable_http2: {{ ssl_enable_http2 | default("true") }}
    listen:  
      - ip: {{ https_listen_ip | default("0.0.0.0") }}
        port: {{ https_listen_port | default(9443) | int }}
        enable_http2: {{ https_enable_http2 | default("true") }}
      
      {% if multi_https_ports: -%}
      # supports more listen ports
      {% for port in multi_https_ports | regex_split -%}
      {% if port: -%}
      - {{port}}
      {% endif -%}
      {% endfor -%}
      {% endif %}
    
    ssl_protocols: {{ ssl_protocols | default("TLSv1 TLsV1.1 TLSv1.2 TLSv1.3") }}
    ssl_ciphers: {{ ssl_ciphers | default("ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384") }}
    ssl_session_tickets: {{ ssl_session_tickets | default("true") }}
    key_encrypt_salt: {{ key_encrypt_salt | default("edd1c9f0985e76a2") }}

  enable_control: {{ enable_control | default("true") }}
  control:
    ip: {{ control_ip | default("127.0.0.1") }}
    port: {{ control_port | default(9090) | int }}

  disable_sync_configuration_during_start: {{ disable_sync_configuration_during_start | default("false") }}

nginx_config:
  user: {{ nginx_user | default("root") }}
  error_log: {{ error_log | default("/dev/stdout") }}
  error_log_level:  {{ error_log_level | default("warn") }}
  worker_processes: {{ worker_processes | default(1) | int }}
  enable_cpu_affinity: {{ enable_cpu_affinity | default("true") }}
  worker_rlimit_nofile: {{ worker_rlimit_nofile | default(20480) | int }}
  worker_shutdown_timeout: {{ worker_shutdown_timeout | default("240s") }}
  event:
    worker_connections: {{ worker_connections | default(20480) | int }}

  envs:
    {% if not nginx_config_env: -%}
    {%- set nginx_config_env = "POD_IP" -%}
    {%- endif -%}
    {%- for item in nginx_config_env | regex_split -%}
    {% if item: -%}
    - {{item}}
    {% endif -%}
    {% endfor %}

  stream:
    lua_shared_dict:
      etcd-cluster-health-check-stream: 10m
      lrucache-lock-stream: 10m
      plugin-limit-conn-stream: 10m
      {% if not stream_lua_shared_dicts: -%}
      {%- set stream_lua_shared_dicts = "" -%}
      {%- endif -%}
      {%- for item in stream_lua_shared_dicts | regex_split -%}
      {% if item: -%}
      {{item}}
      {%- endif -%}
      {% endfor %}

  main_configuration_snippet: |
    {% if not main_configuration_snippet: -%}
    {%- set main_configuration_snippet = "" -%}
    {%- endif -%}
    {%- for item in main_configuration_snippet | regex_split -%}
    {% if item: -%}
    {{item}}
    {% endif -%}
    {% endfor %}

  http_configuration_snippet: |
    {% if not http_configuration_snippet: -%}
    {%- set http_configuration_snippet = "" -%}
    {%- endif -%}
    {%- for item in http_configuration_snippet | regex_split -%}
    {% if item: -%}
    {{item}}
    {% endif -%}
    {% endfor %}

  http_server_configuration_snippet: |
    {% if not http_server_configuration_snippet: -%}
    {%- set http_server_configuration_snippet = "" -%}
    {%- endif -%}
    {%- for item in http_server_configuration_snippet | regex_split -%}
    {% if item: -%}
    {{item}}
    {% endif -%}
    {% endfor %}

  http_admin_configuration_snippet: |
    {% if not http_admin_configuration_snippet: -%}
    {%- set http_admin_configuration_snippet = "" -%}
    {%- endif -%}
    {%- for item in http_admin_configuration_snippet | regex_split -%}
    {% if item: -%}
    {{item}}
    {% endif -%}
    {% endfor %}

  http_end_configuration_snippet: |
    {% if not http_end_configuration_snippet: -%}
    {%- set http_end_configuration_snippet = "" -%}
    {%- endif -%}
    {%- for item in http_end_configuration_snippet | regex_split -%}
    {% if item: -%}
    {{item}}
    {% endif -%}
    {% endfor %}

  stream_configuration_snippet: |
    {% if not stream_configuration_snippet: -%}
    {%- set stream_configuration_snippet = "" -%}
    {%- endif -%}
    {%- for item in stream_configuration_snippet | regex_split -%}
    {% if item: -%}
    {{item}}
    {% endif -%}
    {% endfor %}

  http:
    enable_access_log: {{ enable_access_log | default("false") }}
    access_log: {{ access_log | default("/dev/stdout") }}
    access_log_format: {{ access_log_format | default('\"$remote_addr - $remote_user [$time_local] $http_host \\"$request\\" $status $body_bytes_sent $request_time \\"$http_referer\\" \\"$http_user_agent\\" $upstream_addr $upstream_status $upstream_response_time \\"$upstream_scheme://$upstream_host$upstream_uri\\"\"') }}
    access_log_format_escape: {{ access_log_format_escape | default("default") }}
    keepalive_timeout: {{ keepalive_timeout | default("60s") }}
    client_header_timeout: {{ client_header_timeout | default("60s") }}
    client_body_timeout: {{ client_body_timeout | default("60s") }}
    client_max_body_size: {{ client_max_body_size | default(0) }}
    send_timeout: {{ send_timeout | default("10s") }}
    underscores_in_headers: {{ underscores_in_headers | default("on") }}
    real_ip_header: {{ real_ip_header | default("X-Real-IP") }}
    real_ip_recursive: {{ real_ip_recursive | default("off") }}
    real_ip_from:
      - 127.0.0.1
      - "unix:"
      {% if not real_ip_from: -%}
      {%- set real_ip_from = "" -%}
      {%- endif -%}
      {%- for item in real_ip_from | regex_split -%}
      {% if item: -%}
      - {{item}}
      {% endif -%}
      {% endfor %}

    custom_lua_shared_dict:
      {% if not custom_lua_shared_dict: -%}
      {%- set custom_lua_shared_dict = "" -%}
      {%- endif -%}
      {%- for item in custom_lua_shared_dict | regex_split -%}
      {{ item }}
      {% endfor %}

    proxy_ssl_server_name: {{ proxy_ssl_server_name | default("true") }}
    upstream:
      keepalive: {{ upstream_keepalive | default(320) | int }}
      keepalive_requests: {{ upstream_keepalive_requests | default(1000) | int }}
      keepalive_timeout: {{ upstream_keepalive_timeout | default("60s") }}
    charset: {{ charset | default("utf-8") }}

    variables_hash_max_size: {{ variables_hash_max_size | default(2048) | int }}

    lua_shared_dict:
      internal-status: 10m
      plugin-limit-req: 10m
      plugin-limit-count: 10m
      prometheus-metrics: 32m
      plugin-limit-conn: 10m
      upstream-healthcheck: 10m
      worker-events: 10m
      lrucache-lock: 10m
      balancer-ewma: 10m
      balancer-ewma-locks: 10m
      balancer-ewma-last-touched-at: 10m
      plugin-limit-count-redis-cluster-slot-lock: 1m
      tracing_buffer: 10m
      plugin-api-breaker: 10m
      etcd-cluster-health-check: 10m
      discovery: 1m
      jwks: 1m
      introspection: 10m
      access-tokens: 1m

etcd:
  host:
    - "{{ etcd_host }}"
  prefix: {{ etcd_prefix | default("/apisix") }}
  timeout: {{ etcd_timeout | default(30) }}
  resync_delay: {{ etcd_resync_delay | default(5) | int }}
  health_check_timeout: {{ etcd_health_check_timeout | default(10) | int }}
  user: {{ etcd_user | default("tapisix") }}
  password: {{ etcd_password | default("") }}
  tls:
    verify:  {{ etcd_tls_verify | default("false") }}

graphql:
  max_size: 1048576

plugins:
  - client-control
  - ext-plugin-pre-req
  - zipkin
  - request-id
  - fault-injection
  - serverless-pre-function
  - batch-requests
  - cors
  - ip-restriction
  - ua-restriction
  - referer-restriction
  - uri-blocker
  - request-validation
  - openid-connect
  - wolf-rbac
  - hmac-auth
  - basic-auth
  - jwt-auth
  - key-auth
  - consumer-restriction
  - authz-keycloak
  - proxy-mirror
  - proxy-cache
  - proxy-rewrite
  - api-breaker
  - limit-conn
  - limit-count
  - limit-req
  - gzip
  - server-info
  - traffic-split
  - redirect
  - response-rewrite
  - grpc-transcode
  - prometheus
  - echo
  - http-logger
  - skywalking-logger
  - sls-logger
  - tcp-logger
  - kafka-logger
  - syslog
  - udp-logger
  - serverless-post-function
  - ext-plugin-post-req
  {% if not custom_plugins: -%}
  {%- set custom_plugins = "" -%}
  {%- endif -%}
  {%- for item in custom_plugins | regex_split -%}
  {% if item: -%}
  - {{item}}
  {% endif -%}
  {% endfor %}

stream_plugins:
  - ip-restriction
  - limit-conn
  - mqtt-proxy
  {% if not custom_stream_plugins: -%}
  {%- set custom_stream_plugins = "" -%}
  {%- endif -%}
  {%- for item in custom_stream_plugins | regex_split -%}
  {% if item: -%}
  - {{item}}
  {% endif -%}
  {% endfor %}

plugin_attr:
  prometheus:
    export_uri: {{ prometheus_export_uri | default("/apisix/prometheus/metrics") }}
    enable_export_server: {{ prometheus_enable_export_server | default("false") }}
    export_addr:
      ip: 0.0.0.0
      port: {{ prometheus_export_port | default(9091) | int}}

  server-info:
    report_interval: {{ serveir_info_report_interval | default(60) | int }}
    report_ttl: {{ serveir_info_report_ttl | default(3600) | int }}

discovery:
  eureka:
    host:
      {% if not eureka_host: -%}
      {%- set eureka_host = "http://eureka.svc.local" -%}
      {%- endif -%}
      {%- set host_list = eureka_host | regex_split -%}
      {%- for item in host_list -%}
      {% if item: -%}
      - {{item}}
      {% endif -%}
      {% endfor %}
    prefix: "/eureka/"
    fetch_interval: {{ eureka_fetch_interval | default(5) | int }}
    weight: {{ eureka_weight | default(100) | int }}
    timeout:
      connect: {{ eureka_connect_timeout | default(2000) | int }}
      send: {{ eureka_send_timeout | default(2000) | int }}
      read: {{ eureka_read_timeout | default(5000) | int }}

  polaris:
     cache_size: {{ polaris_cache_size | default(1000) | int }}
     update_time: {{ polaris_update_time | default(3) | int }}
     max_cache_time: {{ polaris_max_cache_time | default(5) | int }}

将上述代码保存为 config-template.yaml,即 Jinja2 的渲染模板。这个模板基本覆盖到了每一个 APISIX 配置文件的内容,能够默认的就都设置了默认值,减少配置工作量。对于行数可变的多行配置,比如http_configuration_snippet 和plugins 等,我们也是通过 Jinja2 里面的遍历+英文逗号分隔的方法来支持动态配置。

二、Python 脚本

简单写一个从环境变量中提取 APISIX 变量、然后通过 Jinja2 渲染成实际配置文件的脚本:

# -*- coding:utf-8 -*-
"""APISIX 配置文件生成工具
功能描述:通过获取环境变量生成 APISIX 的配置文件。
"""
import sys
import os
import requests
from jinja2 import Environment, FileSystemLoader
reload(sys)
sys.setdefaultencoding('utf-8')


class Utils():
    def __init__(self):
        self.path = os.path.dirname(os.path.abspath(__file__))
        self.template_environment = Environment(
            autoescape=False,
            loader=FileSystemLoader(os.path.join(self.path, '')),
            trim_blocks=False)
        self.template_environment.filters["regex_split"] = self.regex_split

    def render_template(self, template_filename, context):
        return self.template_environment.get_template(
            template_filename).render(context)

    def gen_yaml_content(self, template, context):
        yaml = self.render_template(template, context)
        return yaml
    
    def regex_split(self, input):
        return re.split(r"[,|\n]", input)

    def get_env_list(self, prefix=None, replace=True):
        """ 获取环境变量
        :param prefix: 指定目标变量的前缀
        :param replace:指定前缀后,键名是否去掉前缀
        """
        env_dict = os.environ

        if prefix:
            env_list = {}
            for key in env_dict:
                if prefix in key:
                    if replace:
                        env_list[key.replace(prefix, "")] = env_dict[key]
                    else:
                        env_list[key] = env_dict[key]

            return env_list

        else:
            return dict(env_dict)


if __name__ == "__main__":
    utils = Utils()

    try:
        config_list = utils.get_env_list(prefix="apisix_")
        content = utils.gen_yaml_content("config-template.yaml", config_list)

        with open("/usr/local/apisix/conf/config.yaml", "w") as f:
            f.write(content)

    except Exception as error:  # pylint: disable=broad-except
        exit("Failed to generate configuration file: {}".format(error))

脚本会从运行系统的环境变量中提取前缀为 apisix_ 的环境变量列表, 然后通过 Jinja2 填充到配置模板中,最终生成 APISIX 的配置文件 config.yaml,整体非常简单。

我们在公司内部其实是有配置中心的,所以在实际使用中,我们是从配置中心去拉取配置然后来渲染的,这里只是分享一个方案,因此就用环境变量简单示范一下了。确实需要使用的朋友,可以将脚本改成从配置中心拉取,比如 Apollo、Zookeeper、Consul、DB 等,难度也非常小。

三、Docker 镜像

上面展示了通过执行 Python 脚本提取环境变量,快速生成 APISIX 配置文件的方案。接下来,我们将这个机制集成到 APISIX 的 Docker 镜像中,实现一个自动化配置的镜像。

1、Dockerfile 配置

Jinja2 需要 Python 环境的支持,所以这里选择 APISIX 官方的 Centos 镜像,默认自带了 Python2.7.5,只需要在这个基础上安装一下 Jinja2 插件即可。

FROM apache/apisix:2.10.0-centos
LABEL maintainer="Jager", description="支持环境变量设置任意配置的 APISIX 镜像。"

RUN yum install -y python-jinja2

# 自定义插件可以放到 plugins 目录,一并集成
COPY plugins /usr/local/apisix/apisix/plugins

COPY auto_conf /opt/auto_conf

COPY docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]

CMD ["/usr/local/openresty/bin/openresty", "-p", "/usr/local/apisix", "-g", "daemon off;"]

2、docker-entrypoint.sh

因为渲染时需要执行 Python 脚本的,因此需要在 ENTRYPOINT 这里插入相关执行命令,脚本内容如下:

#!/bin/bash
set -e
# 启动前先进行 Jinja2 渲染
cd /opt/auto_conf && \
    python make_conf.py >/dev/stderr 2>&1 || exit 1

# APISIX 初始化
/usr/bin/apisix init >/dev/stderr 2>&1 && \
/usr/bin/apisix init_etcd >/dev/stderr 2>&1 || exit 1

# 执行真正的启动命令
exec "$@"

3、自定义插件

在实际使用场景中,我们可能还有一些自定义的 APISIX 插件,也可以在制作这个 Docker 镜像过程中一并集成进去,比如张戈博客前两篇文章分享的 2 个实用插件:

整个镜像配置我已经上传到 github,有需要的同学可以自行 fork 改造:https://github.com/jagerzhang/apisix-docker

四、运行示例

看懂了前面的同学应该已经对如何运行是没什么疑问了。这里还是简单贴一下使用方法,方便第一次接触的同学快速上手。

其实非常简单,需要配置 APISIX 的哪个参数,只需要在 config-template.yaml 这个模板中去找对应的变量名,比如需要配置 etcd 地址,我们在 config-template.yaml 找到对应的变量名称是 etcd_host,而且支持通过英文逗号分隔来配置多条。

因此,启动命令如下:

docker run --name=apisix_test -d \
    -e apisix_etcd_host=http://127.0.0.1:2379,http://127.0.0.2:2379,http://127.0.0.3:2379
    <apisix 镜像名>

总之,需要改啥配置就去 config-template.yaml 找对应的变量名,然后在指定系统环境变量 apisix_<变量名>的值,如果是多行则用英文逗号分隔即可。如果在 config-template.yaml 没找到,那么就参考官方config-default.yaml来修改 Jinja2 模板:config-template.yaml。

五、其他

本文分享的方法虽然非常实用,实际上还需要安装 jinja2 然后跑 Python 脚本, 并非最优雅的方案。如果是会写 lua 脚本的朋友,可以通过lua-resty-template改造下,那就完美了!有实现的朋友记得给张戈留言分享一下成果。

0 条回应
  1. 欧乐安 2021-11-16 · 11:32

    受益了,这个方式好

  2. 自媒体运营 2021-12-8 · 18:45

    不错,必须顶一下!