تكوين المشروع داخل وخارج Kubernetes

لقد كتبت مؤخرا إجابة حول عمر المشروع في Docker وتصحيح الأخطاء خارجه، حيث ذكر بإيجاز أنه يمكنك إنشاء نظام التكوين الخاص بك بحيث تعمل الخدمة بشكل جيد في Kuber، وتكشف الأسرار، وتعمل بشكل ملائم محليًا، حتى خارج Docker تمامًا. لا يوجد شيء معقد، ولكن "الوصفة" الموصوفة قد تكون مفيدة لشخص ما :) الكود موجود في بايثون، لكن المنطق غير مرتبط باللغة.

تكوين المشروع داخل وخارج Kubernetes

خلفية السؤال هي كما يلي: ذات مرة كان هناك مشروع واحد، في البداية كان عبارة عن كتلة صغيرة بها أدوات مساعدة ونصوص، ولكن مع مرور الوقت نمت، مقسمة إلى خدمات، والتي بدورها بدأت تنقسم إلى خدمات صغيرة، و ثم تم تحجيمها. في البداية، تم كل هذا على VPS، حيث تمت عمليات إعداد ونشر التعليمات البرمجية تلقائيًا باستخدام Ansible، وتم تجميع كل خدمة بتكوين YAML مع الإعدادات والمفاتيح اللازمة، وتم استخدام ملف تكوين مماثل لـ عمليات الإطلاق المحلية، والتي كانت مريحة للغاية، لأنه يتم تحميل هذا التكوين في كائن عام، ويمكن الوصول إليه من أي مكان في المشروع.

ومع ذلك، فإن النمو في عدد الخدمات الصغيرة واتصالاتها و الحاجة إلى التسجيل والمراقبة المركزية، ينذر بالانتقال إلى كوبر، الذي لا يزال قيد التقدم. إلى جانب المساعدة في حل المشكلات المذكورة، تقدم Kubernetes مناهجها لإدارة البنية التحتية، بما في ذلك ما يسمى بالأسرار и طرق العمل معهم. الآلية قياسية وموثوقة، لذا فإن عدم استخدامها يعد خطيئة! لكن في الوقت نفسه، أرغب في الحفاظ على التنسيق الحالي الخاص بي للعمل مع التكوين: أولاً، لاستخدامه بشكل موحد في الخدمات الصغيرة المختلفة للمشروع، وثانيًا، لكي أتمكن من تشغيل التعليمات البرمجية على الجهاز المحلي باستخدام أداة واحدة بسيطة ملف التكوين.

في هذا الصدد، تم تعديل آلية إنشاء كائن التكوين لتتمكن من العمل مع ملف التكوين الكلاسيكي الخاص بنا ومع الأسرار من Kuber. تم أيضًا تحديد بنية تكوين أكثر صرامة، بلغة بايثون الثالثة، على النحو التالي:

Dict[str، Dict[str، Union[str، int، float]]]

أي أن التكوين النهائي عبارة عن قاموس يحتوي على أقسام مسماة، كل منها عبارة عن قاموس يحتوي على قيم من أنواع بسيطة. وتصف الأقسام التكوين والوصول إلى الموارد من نوع معين. مثال على جزء من التكوين لدينا:

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 كما يلي:

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

وبالنسبة لعمليات الإطلاق المحلية، يتم استخدام التكوين الموجود في الدليل الجذر للمشروع أو على طول المسار المحدد في متغير البيئة. يمكن رؤية الكود المسؤول عن وسائل الراحة هذه في المفسد.

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

المنطق هنا بسيط للغاية: نقوم بدمج التكوينات الكبيرة من دليل المشروع والمسارات حسب متغير البيئة، وأقسام التكوين الصغيرة من أسرار Kuber، ثم نقوم بمعالجتها مسبقًا قليلاً. بالإضافة إلى بعض المتغيرات. ألاحظ أنه عند البحث عن الملفات من الأسرار، يتم استخدام قيود العمق، لأن K8s يقوم بإنشاء مجلد مخفي في كل سر، حيث يتم تخزين الأسرار نفسها، ويوجد رابط فقط على مستوى أعلى.

آمل أن يكون ما تم وصفه مفيدًا لشخص ما :) يتم قبول أي تعليقات وتوصيات بخصوص الأمان أو مجالات التحسين الأخرى. رأي المجتمع مثير للاهتمام أيضًا، ربما يستحق إضافة دعم لـ ConfigMaps (مشروعنا لا يستخدمها بعد) ونشر الكود على GitHub / PyPI؟ أنا شخصياً أعتقد أن مثل هذه الأشياء فردية جدًا بحيث لا يمكن أن تكون المشاريع عالمية، وإلقاء نظرة خاطفة قليلاً على تطبيقات الآخرين، مثل تلك المذكورة هنا، ومناقشة الفروق الدقيقة والنصائح وأفضل الممارسات، والتي آمل أن أراها في التعليقات ‎يكفي😉

يمكن للمستخدمين المسجلين فقط المشاركة في الاستطلاع. تسجيل الدخول، من فضلك.

هل يجب أن أنشر كمشروع/مكتبة؟

  • 0,0%نعم، سأستخدم /contribution0

  • 33,3%نعم، هذا يبدو عظيما4

  • 41,7%لا، من يحتاج إلى القيام بذلك بنفسه بالشكل الذي يناسبه وبما يتناسب مع احتياجاته5

  • 25,0%سأمتنع عن الرد 3

صوت 12 مستخدمًا. امتنع 3 مستخدما عن التصويت.

المصدر: www.habr.com

إضافة تعليق