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

Récemment, j'ai écrit une réponse sur la vie d'un projet dans des Docker et le débogage de code en dehors de celui-ci, où j'ai brièvement mentionné qu'on peut créer son propre système de configuration, pour que le service fonctionne bien dans Kubernetes, récupère les secrets et se lance facilement en local, y compris totalement en dehors de Docker. Rien de compliqué, mais la "recette" décrite pourra peut-être servir à quelqu'un 🙂 Le code est en Python, mais la logique n'est pas liée à un langage.

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

L'historique de la question est le suivant : il était une fois un projet, au départ un petit monolithe avec des utilitaires et des scripts, mais avec le temps, il a évolué, s'est divisé en services qui, à leur tour, se sont scindés en microservices, puis ont également dû être mis à l'échelle. Au début, tout cela était exécuté sur des VPS nus, et les processus de configuration et de déploiement du code étaient automatisés grâce à Ansible, pour chaque service un fichier de configuration YAML était élaboré avec les paramètres et clés nécessaires, et un fichier de configuration similaire était utilisé pour les lancements locaux, ce qui était très pratique, car cette configuration est chargée dans un objet global, accessible de n'importe où dans le projet.

Cependant, la croissance du nombre de microservices, leurs interconnexions, ainsi que le besoin de journalisation et de surveillance centralisées, annonçaient un déménagement vers Kubernetes, qui est encore en cours. En plus d'aider à résoudre les tâches mentionnées, Kubernetes propose ses propres approches pour gérer l'infrastructure, y compris les dits Secrets et et les façons de travailler avec eux. Le mécanisme est standard et fiable, donc, littéralement, il serait dommage de ne pas en profiter ! Mais en même temps, je voulais conserver mon format de travail actuel avec la configuration : d'une part, l'utiliser de manière cohérente dans différents microservices du projet, et d'autre part, avoir la possibilité de lancer le code sur ma machine locale en utilisant un seul fichier de configuration simple.

À cet égard, le mécanisme de construction de l'objet de configuration a été adapté pour pouvoir travailler à la fois avec notre fichier de configuration classique et avec les secrets de Kubernetes. Une structure de configuration plus stricte a également été définie, pour parler le langage de Python 3, celle-ci :

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

C'est-à-dire que la configuration finale est un dictionnaire avec des sections nommées, chacune étant un dictionnaire contenant des valeurs de types simples. Les sections décrivent la configuration et les accès aux ressources d'un certain type. Voici un extrait 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: info@test.com
  pw: "SuperHardPassword"

Dans ce cas, le champ moteur de base de données peut être configuré sur SQLite, alors que redis configurer sur mock, en indiquant également le nom du fichier pour la sauvegarde, – ces paramètres sont correctement reconnus et traités, ce qui permet d'exécuter facilement le code localement pour le débogage, les tests unitaires et d'autres besoins. Cela nous est particulièrement pertinent car il existe de nombreux autres besoins – une partie de notre code est destinée à divers calculs analytiques, et est exécutée non seulement sur des serveurs avec orchestration, mais aussi par différents scripts, et sur les ordinateurs des analystes, qui ont besoin de travailler et de déboguer des pipelines de traitement de données complexes sans se soucier des questions backend. D'ailleurs, il serait utile de partager que nos principaux outils, y compris le code de configuration, sont installés via setup.py – ensemble, cela unit notre code dans un écosystème intégré, indépendant de la plateforme et du mode d'utilisation.

La description du pod dans Kubernetes est la suivante :

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

C'est-à-dire que chaque secret décrit une section. Les secrets eux-mêmes sont créés comme suit :

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

Ensemble, cela conduit à la création de fichiers YAML au chemin /etc/secrets/db-main/section_name.yaml

Et pour les exécutions locales, un fichier de configuration est utilisé, situé dans le répertoire racine du projet ou au 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 fusionnons les configurations volumineuses du répertoire du projet et de l'emplacement défini par la variable d'environnement, et les petites sections de configuration des secrets de K8s, puis nous les prétraitons un peu. De plus, certaines variables. Je note que lors de la recherche de fichiers à partir des secrets, une restriction 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 au niveau supérieur se trouve simplement un lien.

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 aspects d'amélioration sont les bienvenus. Je suis aussi curieux de l'avis de la communauté, peut-être devrions-nous ajouter le support des ConfigMaps (dans notre projet, ils ne sont pas encore utilisés) et publier le code sur GitHub / PyPI ? Personnellement, je pense que ces choses sont trop spécifiques aux projets pour être universelles, et un simple coup d'œil aux réalisations des autres, comme celle présentée ici, ainsi qu'une discussion sur les détails, conseils et meilleures pratiques, que j'espère voir dans les commentaires 😉

Seuls les utilisateurs enregistrés peuvent participer au sondage. Connectez-vous, s'il vous plaît.

Faut-il publier en tant que projet / bibliothèque ?

  • 0,0%Oui, je l'utiliserais / contribuerais.

  • 33,3%Oui, ça sonne bien.

  • 41,7%Non, ceux qui en ont besoin le feront eux-mêmes dans leur propre format et selon leurs propres besoins.

  • 25,0%Je m'abstiendrai de répondre.

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

Source : habr.com

Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS 🔥 Acheter un hébergement fiable pour les sites avec protection DDoS, serveurs VPS VDS | ProHoster