Project configuration inside and outside of Kubernetes

Recently, I wrote a response about the project's life in Docker and debugging code outside of it, where I briefly mentioned that it's possible to create your own configuration system so that the service works well in Kubernetes, retrieves secrets, and runs conveniently locally, even completely outside of Docker. It's nothing complicated, but the described "recipe" might be useful to someone 🙂 The code is in Python, but the logic is not tied to the language.

Project configuration inside and outside of Kubernetes

The backstory of the question is as follows: there once was a project, initially a small monolith with utilities and scripts, but over time it grew, dividing into services that, in turn, became microservices, and then began to scale. Initially, everything was run on bare VPSs, and the processes of setting up and deploying code were automated using Ansible. Each service had a YAML config created with the necessary settings and keys, and a similar config file was used for local runs, which was very convenient as this config is loaded into a global object accessible from anywhere in the project.

However, the growth of the number of microservices, their interconnections, and the need for centralized logging and monitoring, forewarned a move to Kubernetes, which is still in process. Together with addressing the mentioned challenges, Kubernetes offers its approaches to infrastructure management, including so-called Secrets and and ways to work with them. The mechanism is standard and reliable, so it’s literally a shame not to take advantage of it! However, I would like to maintain my current format of working with the config: firstly, to use it uniformly across different microservices of the project, and secondly, to be able to run code on a local machine using one simple config file.

In this regard, the mechanism for constructing the configuration object was refined to work with both our classic config file and the Kubernetes secrets. A stricter config structure was also defined, in Python 3 terms, like this:

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

That is, the final config is a dictionary with named sections, each of which is a dictionary with values of simple types. The sections describe the configuration and access to resources of a certain kind. Here’s an example snippet from our config:

admin panel:
  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"

At the same time, the field engine database can be set to SQLite, while redis configured to mock, specifying the filename for saving – these parameters are correctly recognized and processed, allowing for easy local code execution for debugging, unit testing, and various other needs. This is particularly relevant for us, as there are many such needs – part of our code is designed for various analytical calculations, running not only on orchestrated servers but also via different scripts, and on analysts' computers, who need to develop and debug complex data processing pipelines without worrying about backend issues. By the way, it’s worth sharing that our main tools, including the configuration code assembler, are installed via setup.py – together, this unites our code into a single ecosystem, independent of the platform and method of use.

The pod description in Kubernetes looks like this:

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

That is, each secret describes one section. The secrets themselves are created like this:

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

Together, this leads to the creation of YAML files at the path /etc/secrets/db-main/section_name.yaml

And for local launches, a config located in the root directory of the project or at the path specified in the environment variable is used. The code responsible for these conveniences can be found in the 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()

The logic here is quite simple: we combine large configs from the project directory and the path from the environment variable, along with small config sections from Kubernetes secrets, and then preprocess them a bit. Plus some variables. I should note that when searching for files from secrets, a depth restriction is used, as K8s creates another hidden folder for each secret where the secrets are actually stored, while a level above contains just the link.

I hope what I've described will be useful to someone 🙂 Any comments and recommendations regarding security or other areas for improvement are welcome. I'm also interested in the community's opinion on whether we should add support for ConfigMaps (which are not currently used in our project) and whether to showcase the code on GitHub / PyPI? Personally, I think these aspects are too individual for projects to be universal, and a brief look at others' implementations, like the one mentioned here, along with discussions on nuances, advice, and best practices, which I hope to see in the comments 😉

Only registered users can participate in the survey. Please log in, please.

Should it be published as a project/library?

  • 0,0%Yes, I would use it / contribute

  • 33,3%Yes, that sounds great

  • 41,7%No, those who need it will do it themselves in their own format and for their own needs

  • 25,0%I will refrain from answering

12 users voted. 3 users abstained.

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster