Recientemente escribí , donde mencioné brevemente que se puede crear un sistema de configuración propio para que el servicio funcione bien en Kubernetes, acceda a secretos y se ejecute de manera conveniente localmente, incluso completamente fuera de Docker. No es complicado, pero la "receta" descrita puede ser útil para alguien 🙂 El código está en Python, pero la lógica no está atada al lenguaje.

La historia del problema es la siguiente: había un proyecto que comenzó como un pequeño monolito con utilidades y scripts, pero con el tiempo creció, se dividió en servicios, que a su vez comenzaron a dividirse en microservicios, y luego también se escaló. Al principio, todo esto se ejecutaba en VPS desnudos, donde los procesos de configuración y despliegue de código estaban automatizados con Ansible, y a cada servicio se le asignaba un archivo de configuración YAML con las configuraciones y claves necesarias, y un archivo de configuración similar se utilizaba para ejecuciones locales, lo cual era muy conveniente, ya que esta configuración se carga en un objeto global accesible desde cualquier lugar del proyecto.
Sin embargo, el crecimiento en el número de microservicios, sus interconexiones y la , presagiaban una migración a Kubernetes, que aún está en proceso. Junto con la ayuda en la resolución de las tareas mencionadas, Kubernetes ofrece sus propios enfoques para la gestión de la infraestructura, incluyendo y . El mecanismo es estándar y fiable, ¡por lo que, en sentido literal, es un pecado no aprovecharlo! Pero al mismo tiempo, me gustaría conservar mi formato actual de trabajo con la configuración: en primer lugar, utilizarlo de manera uniforme en diferentes microservicios del proyecto, y en segundo lugar, poder ejecutar el código en la máquina local utilizando un único archivo de configuración simple.
En consecuencia, se mejoró el mecanismo de construcción del objeto-configuración para que pudiera trabajar tanto con nuestro archivo de configuración clásico como con los secretos de Kubernetes. También se estableció una estructura más rígida para la configuración, hablando en términos de Python 3, así:
Dict[str, Dict[str, Union[str, int, float]]]
Es decir, la configuración final es un diccionario con secciones nombradas, cada una de las cuales es un diccionario con valores de tipos simples. Las secciones describen la configuración y accesos a recursos de un tipo específico. Un ejemplo de un fragmento de nuestra configuración:
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"Sin embargo, el campo engine de bases de datos se puede establecer en SQLite, y redis configurarse para mock, especificando además el nombre del archivo para guardar, estos parámetros son correctamente reconocidos y procesados, lo que permite ejecutar el código localmente para depuración, pruebas unitarias y otras necesidades. Esto es especialmente relevante para nosotros, ya que hay muchas necesidades diferentes, parte de nuestro código está destinado a diversos cálculos analíticos, se ejecuta no solo en servidores con orquestación, sino también a través de varios scripts, y en las computadoras de los analistas que necesitan procesar y depurar complejos flujos de trabajo de datos sin preocuparse por cuestiones de backend. Por cierto, no estaría de más compartir que nuestras principales herramientas, incluido el código de configuración del compilador, se instalan a través de setup.py – juntos, esto une nuestro código en un ecosistema único, independiente de la plataforma y del modo de uso.
La descripción del pod en Kubernetes se ve así:
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-secretEs decir, cada secreto describe una sección. Los secretos en sí se crean así:
apiVersion: v1
kind: Secret
metadata:
name: db-main-secret
type: Opaque
stringData:
db_main.yaml: |
engine: sqlite
filename: main.sqlite3Esto da como resultado la creación de archivos YAML en la ruta /etc/secrets/db-main/section_name.yaml
Y para ejecuciones locales se utiliza una configuración ubicada en el directorio raíz del proyecto o en la ruta especificada en la variable de entorno. El código responsable de estas comodidades se puede ver en el 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 sections
'''
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 lógica aquí es bastante simple: combinamos configuraciones grandes del directorio del proyecto y del camino a través de la variable de entorno, y pequeñas secciones de configuraciones de secretos de K8s, y luego hacemos un poco de preprocesamiento. Además, algunas variables. Notaré que cuando buscamos archivos de secretos, se utiliza un límite de profundidad, ya que K8s crea en cada secreto una carpeta oculta adicional, donde se almacenan los propios secretos, y un nivel arriba hay simplemente un enlace.
Espero que lo descrito sea útil para alguien 🙂 Se aceptan comentarios y recomendaciones sobre seguridad u otros aspectos de mejora. También me interesa escuchar la opinión de la comunidad, quizás debería agregar soporte para ConfigMaps (en nuestro proyecto todavía no se utilizan) y publicar el código en GitHub / PyPI? Personalmente, creo que estas cosas son demasiado individuales para los proyectos como para ser universales, y lo suficiente con echar un vistazo a las implementaciones de otros, como la que se presenta aquí, y discutir matices, consejos y buenas prácticas, que espero ver en los comentarios 😉
Solo los usuarios registrados pueden participar en la encuesta. , por favor.
¿Deberíamos publicarlo como un proyecto / biblioteca?
0,0%Sí, lo utilizaría / contribuiría.
33,3%Sí, suena genial.
41,7%No, quien lo necesite lo hará por sí mismo en su propio formato y para sus propias necesidades.
25,0%Me abstendré de responder.
12 usuarios han votado. 3 usuarios se han abstenido.
Fuente: habr.com
