Kubernetes 内外的项目配置

我最近写的 回答有关 Docker 中的项目生命以及在其外部调试代码的问题,他在其中简要提到,您可以创建自己的配置系统,以便该服务在 Kuber 中正常运行、提取机密并在本地(甚至完全在 Docker 之外)方便地运行。 没什么复杂的,但所描述的“食谱”可能对某人有用:)代码是用Python编写的,但逻辑与语言无关。

Kubernetes 内外的项目配置

这个问题的背景是这样的:从前有一个项目,起初它是一个带有实用程序和脚本的小型整体,但随着时间的推移,它不断增长,分为服务,服务又开始分为微服务,然后扩大规模。 起初,所有这些都是在裸 VPS 上完成的,设置和部署代码的过程是使用 Ansible 自动完成的,每个服务都使用带有必要设置和密钥的 YAML 配置进行编译,并且使用类似的配置文件本地启动,这非常方便,因为 .k 此配置被加载到全局对象中,可以从项目中的任何位置访问。

然而,微服务数量、连接数和连接数的增长 需要集中记录和监控,预示着迁移到 Kuber,目前仍在进行中。 除了帮助解决上述问题之外,Kubernetes 还提供了基础设施管理方法,包括 所谓的秘密 и 与他们合作的方式。 该机制是标准且可靠的,所以不使用它简直是一种罪过! 但同时,我想保持当前使用配置的格式:首先,在项目的不同微服务中统一使用它,其次,能够使用一个简单的方法在本地计算机上运行代码配置文件。

在这方面,构建配置对象的机制被修改为能够使用我们的经典配置文件和来自 Kuber 的秘密。 在第三种 Python 语言中还指定了更严格的配置结构,如下所示:

Dict[str, Dict[str, Union[str, int, float]]]

也就是说,最终的 cogfig 是一个具有命名部分的字典,每个部分都是一个包含来自简单类型的值的字典。 各节描述了某种类型资源的配置和访问。 我们的配置的一个示例:

adminka:
  django_secret: "ExtraLongAndHardCode"

db_main:
  engine: mysql
  host: 256.128.64.32
  user: cool_user
  password: "SuperHardPassword"

redis:
  host: 256.128.64.32
  pw: "SuperHardPassword"
  port: 26379

smtp:
  server: smtp.gmail.com
  port: 465
  email: [email protected]
  pw: "SuperHardPassword"

与此同时,场 engine 数据库可以安装在 SQLite 上,并且 redis 设置 mock,还指定要保存的文件的名称 - 这些参数被正确识别和处理,这使得在本地运行代码以进行调试、单元测试和任何其他需求变得容易。 这对我们来说尤其重要,因为还有很多其他需求——我们的部分代码是用于各​​种分析计算的,它不仅运行在具有编排的服务器上,还运行在各种脚本上,以及需要在分析人员的计算机上运行并调试复杂的数据处理管道,而无需担心后端问题。 顺便说一下,分享一下我们的主要工具(包括配置布局代码)是通过安装的 setup.py – 这将我们的代码统一到一个生态系统中,独立于平台和使用方法。

Kubernetes pod 的描述如下所示:

containers:
  - name : enter-api
    image: enter-api:latest
    ports:
      - containerPort: 80
    volumeMounts:
      - name: db-main-secret-volume
        mountPath: /etc/secrets/db-main

volumes:
  - name: db-main-secret-volume
    secret:
      secretName: db-main-secret

也就是说,每个秘密描述一个部分。 秘密本身是这样创建的:

apiVersion: v1
kind: Secret
metadata:
  name: db-main-secret
type: Opaque
stringData:
  db_main.yaml: |
    engine: sqlite
    filename: main.sqlite3

这将导致沿路径创建 YAML 文件 /etc/secrets/db-main/section_name.yaml

对于本地启动,使用位于项目根目录中或沿着环境变量中指定的路径的配置。 负责这些便利的代码可以在剧透中看到。

配置文件

__author__ = 'AivanF'
__copyright__ = 'Copyright 2020, AivanF'

import os
import yaml

__all__ = ['config']
PROJECT_DIR = os.path.abspath(__file__ + 3 * '/..')
SECRETS_DIR = '/etc/secrets'
KEY_LOG = '_config_log'
KEY_DBG = 'debug'

def is_yes(value):
    if isinstance(value, str):
        value = value.lower()
        if value in ('1', 'on', 'yes', 'true'):
            return True
    else:
        if value in (1, True):
            return True
    return False

def update_config_part(config, key, data):
    if key not in config:
        config[key] = data
    else:
        config[key].update(data)

def parse_big_config(config, filename):
    '''
    Parse YAML config with multiple section
    '''
    if not os.path.isfile(filename):
        return False
    with open(filename) as f:
        config_new = yaml.safe_load(f.read())
        for key, data in config_new.items():
            update_config_part(config, key, data)
        config[KEY_LOG].append(filename)
        return True

def parse_tiny_config(config, key, filename):
    '''
    Parse YAML config with a single section
    '''
    with open(filename) as f:
        config_tiny = yaml.safe_load(f.read())
        update_config_part(config, key, config_tiny)
        config[KEY_LOG].append(filename)

def combine_config():
    config = {
        # To debug config load code
        KEY_LOG: [],
        # To debug other code
        KEY_DBG: is_yes(os.environ.get('DEBUG')),
    }
    # For simple local runs
    CONFIG_SIMPLE = os.path.join(PROJECT_DIR, 'config.yaml')
    parse_big_config(config, CONFIG_SIMPLE)
    # For container's tests
    CONFIG_ENVVAR = os.environ.get('CONFIG')
    if CONFIG_ENVVAR is not None:
        if not parse_big_config(config, CONFIG_ENVVAR):
            raise ValueError(
                f'No config file from EnvVar:n'
                f'{CONFIG_ENVVAR}'
            )
    # For K8s secrets
    for path, dirs, files in os.walk(SECRETS_DIR):
        depth = path[len(SECRETS_DIR):].count(os.sep)
        if depth > 1:
            continue
        for file in files:
            if file.endswith('.yaml'):
                filename = os.path.join(path, file)
                key = file.rsplit('.', 1)[0]
                parse_tiny_config(config, key, filename)
    return config

def build_config():
    config = combine_config()
    # Preprocess
    for key, data in config.items():
        if key.startswith('db_'):
            if data['engine'] == 'sqlite':
                data['filename'] = os.path.join(PROJECT_DIR, data['filename'])
    # To verify correctness
    if config[KEY_DBG]:
        print(f'** Loaded config:n{yaml.dump(config)}')
    else:
        print(f'** Loaded config from: {config[KEY_LOG]}')
    return config

config = build_config()

这里的逻辑非常简单:我们通过环境变量组合项目目录和路径中的大型配置,以及 Kuber 机密中的小型配置部分,然后对它们进行一些预处理。 加上一些变量。 我注意到,当从秘密中搜索文件时,会使用深度限制,因为 K8s 在每个秘密中创建一个隐藏文件夹,其中存储秘密本身,并且只有一个链接位于更高级别。

我希望所描述的内容对某人有用:) 任何有关安全性或其他需要改进的领域的评论和建议都会被接受。 社区的意见也很有趣,也许值得添加对 ConfigMaps 的支持(我们的项目还没有使用它们)并在 GitHub / PyPI 上发布代码? 就我个人而言,我认为这样的事情对于项目来说太个体化了,无法通用,并且稍微看看其他人的实现,就像这里给出的那样,以及对细微差别、技巧和最佳实践的讨论,我希望在评论中看到,就够了😉

只有注册用户才能参与调查。 登录拜托

我应该作为项目/库发布吗?

  • 0,0%是的,我会使用 /contribution0

  • 33,3%是的,听起来不错4

  • 41,7%不,谁需要以自己的格式自己做并满足自己的需求5

  • 25,0%我不会回答3

12 位用户投票。 3 名用户弃权。

来源: habr.com

添加评论