Configuration du projet à l'intérieur et à l'extérieur de Kubernetes

J'ai récemment écrit réponse sur la vie du projet dans Docker et le code de débogage en dehors de celui-ci, où il a brièvement mentionné que vous pouvez créer votre propre système de configuration afin que le service fonctionne bien dans Kuber, récupère les secrets et s'exécute facilement localement, même en dehors de Docker. Rien de compliqué, mais la « recette » décrite peut être utile à quelqu'un :) Le code est en Python, mais la logique n'est pas liée au langage.

Configuration du projet à l'intérieur et à l'extérieur de Kubernetes

Le contexte de la question est le suivant : il était une fois un projet, au début c'était un petit monolithe avec des utilitaires et des scripts, mais au fil du temps, il s'est développé, divisé en services, qui à leur tour ont commencé à être divisés en microservices, et puis intensifié. Au début, tout cela a été fait sur un VPS nu, les processus de configuration et de déploiement de code sur lesquels ont été automatisés à l'aide d'Ansible, et chaque service a été compilé avec une configuration YAML avec les paramètres et les clés nécessaires, et un fichier de configuration similaire a été utilisé pour des lancements locaux, ce qui était très pratique, car .k cette configuration est chargée dans un objet global, accessible depuis n'importe où dans le projet.

Cependant, la croissance du nombre de microservices, de leurs connexions et nécessité d'une journalisation et d'une surveillance centralisées, préfigurait un passage à Kuber, qui est toujours en cours. En plus d'une assistance pour résoudre les problèmes mentionnés, Kubernetes propose ses approches de gestion de l'infrastructure, notamment les soi-disant secrets и façons de travailler avec eux. Le mécanisme est standard et fiable, c'est donc littéralement un péché de ne pas l'utiliser ! Mais en même temps, j'aimerais conserver mon format actuel pour travailler avec la configuration : d'une part, l'utiliser de manière uniforme dans les différents microservices du projet, et d'autre part, pouvoir exécuter le code sur la machine locale en utilisant un simple fichier de configuration.

À cet égard, le mécanisme de construction d'un objet de configuration a été modifié pour pouvoir fonctionner à la fois avec notre fichier de configuration classique et avec les secrets de Kuber. Une structure de configuration plus rigide a également été spécifiée, dans le langage du troisième Python, comme suit :

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

Autrement dit, le rouage final est un dictionnaire avec des sections nommées, chacune étant un dictionnaire avec des valeurs de types simples. Et les sections décrivent la configuration et l'accès aux ressources d'un certain type. Un exemple d'un morceau de notre configuration :

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"

En même temps, le terrain engine les bases de données peuvent être installées sur SQLite, et redis mis à mock, en spécifiant également le nom du fichier à enregistrer - ces paramètres sont correctement reconnus et traités, ce qui facilite l'exécution du code localement pour le débogage, les tests unitaires et tout autre besoin. Ceci est particulièrement important pour nous car il existe de nombreux autres besoins - une partie de notre code est destinée à divers calculs analytiques, il s'exécute non seulement sur des serveurs avec orchestration, mais également avec divers scripts, et sur les ordinateurs des analystes qui doivent travailler et déboguer des pipelines de traitement de données complexes sans vous soucier des problèmes de back-end. À propos, cela ne ferait pas de mal de partager que nos principaux outils, y compris le code de configuration, sont installés via setup.py – ensemble, cela unit notre code en un seul écosystème, indépendant de la plateforme et de la méthode d’utilisation.

La description d'un pod Kubernetes ressemble à ceci :

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

Autrement dit, chaque secret décrit une section. Les secrets eux-mêmes sont créés comme ceci :

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

Ensemble, cela aboutit à la création de fichiers YAML le long du chemin /etc/secrets/db-main/section_name.yaml

Et pour les lancements locaux, la configuration est utilisée, située dans le répertoire racine du projet ou le long du chemin spécifié dans la variable d'environnement. Le code responsable de ces commodités peut être vu dans le spoiler.

config.py

__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()

La logique ici est assez simple : nous combinons de grandes configurations du répertoire du projet et des chemins par variable d'environnement, et de petites sections de configuration des secrets Kuber, puis les prétraitons un peu. Plus quelques variables. Je note que lors de la recherche de fichiers à partir de secrets, une limitation de profondeur est utilisée, car K8s crée un dossier caché dans chaque secret, où les secrets eux-mêmes sont stockés, et juste un lien est situé à un niveau supérieur.

J'espère que ce qui est décrit sera utile à quelqu'un :) Tous les commentaires et recommandations concernant la sécurité ou d'autres domaines à améliorer sont acceptés. L'avis de la communauté est également intéressant, peut-être vaut-il la peine d'ajouter le support des ConfigMaps (notre projet ne les utilise pas encore) et de publier le code sur GitHub/PyPI ? Personnellement, je pense que de telles choses sont trop individuelles pour que les projets soient universels, et je jette un coup d'œil aux implémentations d'autres personnes, comme celle donnée ici, et une discussion sur les nuances, les astuces et les meilleures pratiques, que j'espère voir dans les commentaires , ça suffit 😉

Seuls les utilisateurs enregistrés peuvent participer à l'enquête. se connecters'il te plait.

Dois-je publier en tant que projet/bibliothèque ?

  • 0,0%Oui, j'utiliserais /contribution0

  • 33,3%Oui, ça a l'air génial4

  • 41,7%Non, qui doit le faire lui-même dans son propre format et selon ses besoins5

  • 25,0%je m'abstiendrai de répondre3

12 utilisateurs ont voté. 3 utilisateurs se sont abstenus.

Source: habr.com

Ajouter un commentaire