Русский
Русский
English
Статистика
Реклама

Configuration management

VaultPydantic продолжение саги, локальная разработка

16.12.2020 02:21:17 | Автор: admin


Предыстория


Предыдущая статья


В предыдущей статье я писал о том, как сконфигурировать ваше приложение с помощью Sitri, однако, упустил момент с локальной разработкой, так как согласитесь, что не очень удобно разворачивать vault локально, да и хранить локальный конфиг в общем vault, особенно, если над проектом работают несколько человек неудобнее вдвойне.


В Sitri данная проблема решается достаточно просто с помощью локального режима для ваших классов настроек, то есть вам не придётся даже переписывать ничего или дублировать код, да и структура json файла для локального режима будет почти полностью повторять структуру секретов.


Итак, ну а теперь, давайте добавим в наш проектик буквально пару строк кода + я покажу, как со всем этим можно работать, если ваш проект локально запускается в docker-compose.


Готовим код


Для начала, давайте договоримся, что local_mode действует тогда, когда ENV = "local" :) Далее, я предлагаю немного отредактировать наш provider_config.py и создать там класс BaseConfig от которого мы будем наследовать в наших настройках Config классы. Мы сделаем это для того, чтобы не дублировать код, то есть в самих классах настроек будет только то, что специфично для них.


import hvacfrom sitri.providers.contrib.system import SystemConfigProviderfrom sitri.providers.contrib.vault import VaultKVConfigProviderfrom sitri.settings.contrib.vault import VaultKVSettingsconfigurator = SystemConfigProvider(prefix="superapp")ENV = configurator.get("env")is_local_mode = ENV == "local"local_mode_file_path = configurator.get("local_mode_file_path")def vault_client_factory() -> hvac.Client:    client = hvac.Client(url=configurator.get("vault_api"))    client.auth_approle(        role_id=configurator.get("role_id"),        secret_id=configurator.get("secret_id"),    )    return clientprovider = VaultKVConfigProvider(    vault_connector=vault_client_factory,    mount_point=f"{configurator.get('app_name')}/{ENV}",)class BaseConfig(VaultKVSettings.VaultKVSettingsConfig):    provider = provider    local_mode = is_local_mode    local_provider_args = {"json_path": local_mode_file_path}

Немного про local_provider_args в этом поле мы указываем аргументы для создания экземпляра JsonConfigProvider, они будут провалидированы и данный словарь должен соотвествовать схеме, поэтому не волнуйтесь это не какой-то грязный приём. Однако, если вы хотите создать экземпляр локального провайдера сами, то вы просто положить его в опциональное поле local_provider.


Теперь, мы можем спокойно наследовать конфиг классы от базового. Например, класс настроек для соединения с Kafka будет выглядеть так:


from typing import Any, Dictfrom pydantic import Fieldfrom sitri.settings.contrib.vault import VaultKVSettingsfrom superapp.config.provider_config import BaseConfig, configuratorclass KafkaSettings(VaultKVSettings):    mechanism: str = Field(..., vault_secret_key="auth_mechanism")    brokers: str = Field(...)    auth_data: Dict[str, Any] = Field(...)    class Config(BaseConfig):        default_secret_path = "kafka"        default_mount_point = f"{configurator.get('app_name')}/common"        local_mode_path_prefix = "kafka"

Как видите требуемые изменения минимальны. local_mode_path_prefix мы указываем, чтобы в нашем json файле сохранялась структура общего конфига. А теперь, давайте напишем этот самый json файл для локальной конфигурации:


{    "db":    {        "host": "testhost",        "password": "testpassword",        "port": 1234,        "user": "testuser"    },    "faust":    {        "agents":        {            "X":            {                "concurrency": 2,                "partitions": 5            }        },        "app_name": "superapp-workers",        "default_concurrency": 5,        "default_partitions_count": 10    },    "kafka":    {        "auth_data":        {            "password": "testpassword",            "username": "testuser"        },        "brokers": "kafka://test",        "mechanism": "SASL_PLAINTEXT"    }}

Ну, или просто скопипастим его из конца прошлой статьи. Как видите, тут всё весьма просто. Для наших дальнейших изысканий, переименуем main.py в корне проекта на __main__.py, чтобы можно было запустить пакет командой из docker-compose.


Запихиваем приложение в контейнер и наслаждаемся сборкой


Первое, что мы должны сделать это написать небольшой Dockerfile:


FROM python:3.8.3-busterENV PYTHONUNBUFFERED=1 \    POETRY_VIRTUALENVS_CREATE=false \    POETRY_VIRTUALENVS_IN_PROJECT=false \    POETRY_NO_INTERACTION=1RUN pip install poetryRUN mkdir /superapp/WORKDIR /superapp/COPY ./pyproject.toml ./poetry.lock /superapp/RUN poetry install --no-ansiWORKDIR /

Здесь мы просто устанавливаем зависимости и всё, так как он для локальной разработки, то код проекта мы не копируем.


Далее, нам понадобиться env-файл с необходимыми для local-mode переменными:


SUPERAPP_ENV=localSUPERAPP_LOCAL_MODE_FILE_PATH=/config.jsonSUPERAPP_APP_NAME=superapp

Как видите, ничего лишнего, никакой конфигурационной информации для Vault не нужно, так как при локальном режиме, приложение даже не попытается постучаться в Vault.


Ну и последнее, что мы должны написать сам docker-compose.yml файл:


# docker-compose config for local developmentversion: '3'services:  superapp:    command: python3 -m superapp    restart: always    build:      context: ./      dockerfile: Dockerfile    volumes:      - ./superapp:/superapp      - ./config.json:/config.json    env_file:      - .env.local

Как видите, тут тоже всё просто. Наш json-файл мы кладём в корень, как и указали выше в переменной окружения для контейнера.


Ну а теперь, запуск:


docker-compose up

Creating article_sitri_vault_pydantic_superapp_1 ... doneAttaching to article_sitri_vault_pydantic_superapp_1superapp_1  | db=DBSettings(user='testuser', password='testpassword', host='testhost', port=1234) faust=FaustSettings(app_name='superapp-workers', default_partitions_count=10, default_concurrency=5, agents={'X': AgentConfig(partitions=5, concurrency=2)}) kafka=KafkaSettings(mechanism='SASL_PLAINTEXT', brokers='kafka://test', auth_data={'password': 'testpassword', 'username': 'testuser'})superapp_1  | {'db': {'user': 'testuser', 'password': 'testpassword', 'host': 'testhost', 'port': 1234}, 'faust': {'app_name': 'superapp-workers', 'default_partitions_count': 10, 'default_concurrency': 5, 'agents': {'X': {'partitions': 5, 'concurrency': 2}}}, 'kafka': {'mechanism': 'SASL_PLAINTEXT', 'brokers': 'kafka://test', 'auth_data': {'password': 'testpassword', 'username': 'testuser'}}}

Как видите, всё успешно запустилось и информация из нашего json-фала успешно прошла все проверки и стала настройками для локальной версии приложения, юхху!


Код этой статьи я положил в отдельную ветку, так что можете зайти, склонить и протестировать всё самолично :)
branch

Подробнее..

Конфигурация проекта внутри и вне Kubernetes

17.10.2020 20:10:41 | Автор: admin

Недавно я написал ответ о жизни проекта в Докерах и отладке кода вне него, где мельком упомянул о том, что можно сделать свою систему конфигурирования, чтобы сервис и в Кубере хорошо работал, подтягивал секреты, и локально удобно запускался, в том числе вообще вне Докера. Ничего сложного, но описанный "рецепт" может кому-то пригодится :) Код на Питоне, но логика к языку не привязана.



Предыстория вопроса такова: жил-был один проект, сначала он был маленьким монолитом с утилитами и скриптами, но со временем рос, делился на сервисы, которые в свою очередь стали делиться на микросервисы, а потом ещё и скейлиться. Поначалу это всё выполнялось на голых VPS, процессы настройки и разворачивания кода на которых были автоматизированны с помощью Ansible, и каждому сервису составлялся YAML-конфиг с нужными настройками и ключами, и аналогичный конфиг-файл использовался для локальных запусков, что было очень удобно, т.к этот конфиг грузится в глобальный объект, доступный из любого места в проекте.


Однако рост числа микросервисов, их связей, а также потребность в централизованном логировании и мониторинге, предвещали переезд в Кубер, который до сих пор ещё в процессе. Вместе с помощью в решении упомянутых задач, Kubernetes предлагает свои подходы к управлению инфраструктурой, в том числе т.н Секреты и способы работы с ними. Механизм стандартный и надёжный, поэтому в буквальном смысле грех им не воспользоваться! Но при этом хотелось бы сохранить свой текущий формат работы с конфигом: во-первых, единообразно использовать его в разных микросервисах проекта, а во-вторых, иметь возможность запускать код на локальной машине используя один простой конфиг-файл.


В связи с этим, механизм построения объекта-конфигурации был доработан так, чтобы уметь работать как с нашим классическим конфиг-файлом, так и с секретами из Кубера. Также была задана более жёсткая структура конфига, говоря языком третьего Питона, такая:


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: 26379smtp:  server: smtp.gmail.com  port: 465  email: info@test.com  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-mainvolumes:  - name: db-main-secret-volume    secret:      secretName: db-main-secret

То есть, в каждом секрете описана одна секция. Сами секреты создаются так:


apiVersion: v1kind: Secretmetadata:  name: db-main-secrettype: OpaquestringData:  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 osimport 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 Falsedef 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 Truedef 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 configdef 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 configconfig = build_config()

Логика здесь довольно простая: объединяем крупные конфиги из директории проекта и пути по переменной окружения, и небольшие конфиги-секции из секретов Кубера, а затем немного их предобрабатываем. Плюс некоторые переменные. Замечу, что при поиске файлов из секретов используется ограничение глубины, т.к K8s в каждом секрете создаёт ещё скрытую папку, где сами секреты и хранится, а уровнем выше находится просто ссылка.


Надеюсь, описанное окажется кому-нибудь полезным :) Принимаются любые комментарии и рекомендации касательно безопасности или других моментов на улучшение. Также интересно мнение сообщества, возможно стоит добавить поддержку ConfigMaps (в нашем проекте они пока не используется) и оформить код на ГитХабе / PyPI? Лично я думаю, что такие вещи слишком индивидуальные для проектов, чтобы быть универсальными, и достаточно небольшого подглядывания на чужие реализации, вроде приведённой здесь, да обсуждения нюансов, советов и best practices, которое я надеюсь увидеть в комментариях ;)

Подробнее..

Конфигурируем сервис с помощью Vault и Pydantic

09.12.2020 04:19:27 | Автор: admin

image


Предисловие


В данной статье я расскажу о конфигурации для вашей сервисов с помощью связки Vault (KV и пока только первой версии, т.е. без версионирования секретов) и Pydantic (Settings) под патронажем Sitri.


Итак, допустим, что у нас есть приложение superapp с заведёнными конфигами в Vault и аутентификацией с помощью approle, примерно так настроим (настройку policies для доступа к секрет-энжайнам и к самим секретам я оставлю за кадром, так как это достаточно просто и статья не об этом):


Key                        Value---                        -----bind_secret_id             truelocal_secret_ids           falsepolicies                   [superapp_service]secret_id_bound_cidrs      <nil>secret_id_num_uses         0secret_id_ttl              0stoken_bound_cidrs          []token_explicit_max_ttl     0stoken_max_ttl              30mtoken_no_default_policy    falsetoken_num_uses             50token_period               0stoken_policies             [superapp_service]token_ttl                  20mtoken_type                 default

Прим.: естественно, что если у вас есть возможность и приложение выходит в боевой режим, то secret_id_ttl лучше делать не бесконечным, выставляя 0 секунд.


SuperApp требует конфигурации: подключения к базе данных, подключение к kafka и faust конфигурации для работы кластера воркеров.


Подготовим Sitri


В базовой документации библиотеки есть простой пример, конфигурирования через vault-провайдер, однако, он не охватывает все возможности и может быть полезным, если ваше приложение конфигурируется достаточно легко.


Итак, для начала сконфигурируем vault-провайдер в условном файле provider_config.py:


import hvac  from sitri.providers.contrib.vault import VaultKVConfigProvider  from sitri.providers.contrib.system import SystemConfigProvider  configurator = SystemConfigProvider(prefix="superapp")  ENV = configurator.get("env")  def vault_client_factory() -> hvac.Client:      client = hvac.Client(url=configurator.get("vault_api"))      client.auth_approle(          role_id=configurator.get("role_id"),    secret_id=configurator.get("secret_id"),    )      return client  provider = VaultKVConfigProvider(      vault_connector=vault_client_factory, mount_point=f"{configurator.get('app_name')}/{ENV}"  )

В данном случае мы достаём из среды с помощью системного провайдера несколько переменных для конфигурирования подключения к vault, т.е. изначально должны быть экспортированы следующие переменные:


export SUPERAPP_ENV=devexport SUPERAPP_APP_NAME=superappexport SUPERAPP_VAULT_API=https://your-vault-host.domainexport SUPERAPP_ROLE_ID=535b268d-b858-5fb9-1e3e-79068ca77e27 # Примерexport SUPERAPP_SECRET_ID=243ab423-12a2-63dc-3d5d-0b95b1745ccf # Пример

В примере предполагается, что базовый mount_point к вашим секретам для определённой среды будет содержать имя приложения и имя среды, поэтому мы и экспортировали SUPERAPP_ENV. Путь до секретов отдельных частей приложения мы будем определять в settings-классах далее, поэтому в провайдере secret_path мы оставляем пустым.


Классы настроек


Начнём по пунктам и разнесём три класса настроек (БД, Kafka, Faust) по трём разным файлам.


Настройки БД


from pydantic import Field  from sitri.settings.contrib.vault import VaultKVSettings  from superapp.config.provider_config import provider  class DBSettings(VaultKVSettings):      user: str = Field(..., vault_secret_key="username")      password: str = Field(...)      host: str = Field(...)      port: int = Field(...)      class Config:          provider = provider          default_secret_path = "db"

Итак, как видите, конфиг. данные для базы у нас достаточно простые. Этот класс будет по-умолчанию смотреть в секрет superapp/dev/db, так, как мы указали в config классе, в остальном здесь простые pydantic поля, но в одном из них присутствует extra-аргумент vault_secret_key он нужен тогда, когда ключ в секрете не совпадает по имени с pydantic полем в нашем классе, если его не указывать, то провайдер будет искать ключ по имени поля.


Например, в нашем тестовом приложении, предполагается, что в секрете superapp/dev/db, есть ключи password и username, но мы хотим, чтобы последний был помещён в поле user для удобства и краткости.


Поместим в вышеозначенный секрет следующие данные для примера:


{  "host": "testhost",  "password": "testpassword",  "port": "1234",  "username": "testuser"}

Для первого класса из тройки, я покажу, как легко можно всё это запустить, чтобы данные собрались сами:


db_settings = DBSettings()pprint(db_settings.dict())# -> # {#     "host": "testhost",#     "password": "testpassword",#     "port": 1234,#     "user": "testuser"# }

Настройки Kafka


from typing import Dict, Any  from pydantic import Field  from sitri.settings.contrib.vault import VaultKVSettings  from superapp.config.provider_config import provider, configurator  class KafkaSettings(VaultKVSettings):      mechanism: str = Field(..., vault_secret_key="auth_mechanism")      brokers: str = Field(...)      auth_data: Dict[str, Any] = Field(...)      class Config:          provider = provider          default_secret_path = "kafka"          default_mount_point = f"{configurator.get('app_name')}/common"

В данном случае, представим, что инстанс кафки для разных сред нашего сервиса один, поэтому секрет хранится по пути superapp/common/kafka


{  "auth_data": "{\"password\": \"testpassword\", \"username\": \"testuser\"}",  "auth_mechanism": "SASL_PLAINTEXT",  "brokers": "kafka://test"}

Класс настройки поймёт комплексный тип данных Dict[str, Any] и распарсит его в словарь, то есть при заполнении наших настроек будут следующие данные:


{    "auth_data":    {        "password": "testpassword",        "username": "testuser"    },    "brokers": "kafka://test",    "mechanism": "SASL_PLAINTEXT"}

Так же, если секрет будет задан напрямую в json, например так:


{  "auth_data": {    "password": "testpassword",    "username": "testuser"  },  "auth_mechanism": "SASL_PLAINTEXT",  "brokers": "kafka://test"}

То класс настроек тоже сможет правильно разложить данные.


P.S.
Так же, secret_path и mount_point можно задавать на уровне полей, чтобы провайдер запросил конкретные значения из разных секретов (если это требуется). Приведу цитату с приоритезацией пути секрета и точки монтирования из документации:


Secret path prioritization:
  1. vault_secret_path (Field arg)
  2. default_secret_path (Config class field)
  3. secret_path (provider initialization optional arg)


Mount point prioritization:
  1. vault_mount_point (Field arg)
  2. default_mount_point (Config class field)
  3. mount_point (provider initialization optional arg)


Настройки Faust и отдельных воркеров


from typing import Dict  from pydantic import Field, BaseModel  from sitri.settings.contrib.vault import VaultKVSettings  from superapp.config.provider_config import provider  class AgentConfig(BaseModel):      partitions: int = Field(...)      concurrency: int = Field(...)  class FaustSettings(VaultKVSettings):      app_name: str = Field(...)      default_partitions_count: int = Field(..., vault_secret_key="partitions_count")      default_concurrency: int = Field(..., vault_secret_key="agent_concurrency")      agents: Dict[str, AgentConfig] = Field(default=None, vault_secret_key="agents_specification")      class Config:          provider = provider          default_secret_path = "faust"

superapp/dev/faust:


{  "agent_concurrency": "5",  "app_name": "superapp-workers",  "partitions_count": "10"}

В данном случае, по-умолчанию у нас есть глобальные значения кол-ва партиций в кафке и concurrency для агентов. Таким образом, по-умолчанию наши настройки будут выгружены так:


{  "agents": None,  "app_name": "superapp-workers",  "default_concurrency": 5,  "default_partitions_count": 10}

Например, у нас есть агент X с настройками:


{  "partitions": 5,  "concurrency": 2}

Наш секрет в связи с этим должен выглядеть следующим образом:


{  "agent_concurrency": "5",  "agents_specification": {    "X": {      "concurrency": "2",      "partitions": "5"    }  },  "app_name": "superapp-workers",  "partitions_count": "10"}

Как и ожидалось данные корректно смапились и типы значений были преобразованы так, как указано в модели AgentConfig:


{    "agents":    {        "X":        {            "concurrency": 2,            "partitions": 5        }    },    "app_name": "superapp-workers",    "default_concurrency": 5,    "default_partitions_count": 10}

Совмещаем в единый конфиг класс


from pydantic import BaseModel, Field  from superapp.config.database_settings import DBSettings  from superapp.config.faust_settings import FaustSettings  from superapp.config.kafka_settings import KafkaSettings  class AppSettings(BaseModel):      db: DBSettings = Field(default_factory=DBSettings)      faust: FaustSettings = Field(default_factory=FaustSettings)      kafka: KafkaSettings = Field(default_factory=KafkaSettings)

Совместим наши классы настроек в одну модель, применив default_factory для автоматического сбора при инициализации модели всех наших данных.


Давайте запустим наш код и проверим, как всё сработается вместе:


from superapp.config import AppSettings  config = AppSettings()  print(config)  print(config.dict())

Получаем общий вывод всей конфигурации приложения:


db=DBSettings(user='testuser', password='testpassword', host='testhost', port=1234) faust=FaustSettings(app_name='superapp-workers', default_partitions_count=10, default_concurrency=5, agents={'X': AgentConfig(partitions=5, concurrency=2)}) kafka=KafkaSettings(mechanism='SASL_PLAINTEXT', brokers='kafka://test', auth_data={'password': 'testpassword', 'username': 'testuser'})

{    "db":    {        "host": "testhost",        "password": "testpassword",        "port": 1234,        "user": "testuser"    },    "faust":    {        "agents":        {            "X":            {                "concurrency": 2,                "partitions": 5            }        },        "app_name": "superapp-workers",        "default_concurrency": 5,        "default_partitions_count": 10    },    "kafka":    {        "auth_data":        {            "password": "testpassword",            "username": "testuser"        },        "brokers": "kafka://test",        "mechanism": "SASL_PLAINTEXT"    }}

Счастье, радость, восторг!


У нас получилась такая структура тест-проекта:


superapp config    app_settings.py    database_settings.py    faust_settings.py    __init__.py    kafka_settings.py    provider_config.py __init__.py main.py

Послесловие


Как видите настройка достаточно проста с Sitri, после неё мы получаем чёткую схему конфигурации с нужными нам типами данных у значений, даже если в vault по-умолчанию они хранились строками.


Пишите комментарии по поводу либы, кода или общие впечатления. Буду рад любому отзыву!


P.S. Код из статьи я залил на github https://github.com/Egnod/article_sitri_vault_pydantic

Подробнее..

Перевод Интеграция CICD для нескольких сред с Jenkins и Fastlane. Часть 1

04.11.2020 02:21:17 | Автор: admin

В преддверии старта курса "iOS Developer. Basic" традиционно подготовили для вас интересный перевод, а также приглашаем записаться на бесплатный вебинар, в рамках которого наши эксперты подробно расскажут о программе курса, а также ответят на интересующие вас вопросы.


Внедрение технологий непрерывной интеграции (Continuous Integration - CI) и непрерывного развертывания (Continuous Delivery - CD) в процесс разработки - бесспорно единственный способ отслеживать актуальность изменений кода и определять ошибки интеграции на самых ранних этапах. Это также еще и путь к отлаженным билдам, практически сразу же доступным для тестирования и готовым к отправке в продакшн даже после значительных изменений кода.

Интегрируя CI/CD в свою повседневную работу, разработчики могут достичь двух важных целей: во-первых, возможность запускать наборы тестов на сервере, который не является рабочим компьютером, чтобы инженер мог продолжать разрабатывать новый функционал без отвлекающих факторов и, во-вторых, возможность отправлять билды своему заказчику или инженеру по обеспечению качества, чтобы продемонстрировать/протестировать/проанализировать новую фичу, даже если они не могут собрать и запустить проект на своих собственных компьютерах.

Но что происходит, когда у нас есть несколько промежуточных сред, и тесты должны выполняться на разных серверах или функции должны тестироваться в разных средах? Вот где на помощь приходят Jenkins и Fastlane - инструменты, которые позволяют автоматизировать процесс под различные конфигурации, и именно на этом фокусируется данная статья.

Существует множество отличных статей и руководств, в которых подробно описывается, как использовать Jenkins и Fastlane для настройки CI для мобильного проекта. Однако я хочу сосредоточиться на том, как мы можем настроить соответствующие скрипты для работы с несколькими средами.

Jenkins и Fastlane

Чтобы понять, как мы можем интегрировать CI в проект, мы должны понимать, как Jenkins и Fastlane работают. Вкратце, Jenkins - это сервер непрерывной интеграции с открытым исходным кодом, написанным на Java, и это один из наиболее широко используемых инструментов для управления сборками непрерывной интеграции. Jenkins предоставляет возможность создавать пайплайны (Pipelines), которые по сути являются жизненными циклами процессов, называемых задачами (Jobs), которые включают сборку, документирование, тестирование, упаковку, стейджинг, развертывание, статический анализ и многое другое. Задачи связаны друг с другом в последовательности, образующей конвейер (пайплайн), и именно здесь в игру вступает Fastlane.

Fastlane - это инструмент с открытым исходным кодом, который используется для автоматизации процесса развертывания и распространения мобильных проектов (iOS и Android), предлагающий широкий спектр функций автоматизации в рамках жизненного цикла мобильного приложения, таких как упаковка, подписание кода, распространение сборок и многое другое. Fastlane позволяет создавать задачи, именуемые Lanes, которые по сути представляют из себя скрипты, то есть серию команд, называемых Actions, которые описывают рабочий процесс, который мы хотим автоматизировать.

Наша цель

Итак, то, чего мы здесь хотим достичь, - это автоматизировать процесс распространения нашего iOS-приложения путем автоматизации загрузки нашего приложения в Testflight, откуда заинтересованные лица (заказчик, инженер по обеспечению качества, продавец, который хочет продемонстрировать приложение, и в том же духе) могут войти в систему и загрузить его без особых усилий. Более того, мы хотим продвинуться на один шаг дальше и реализовать возможность автоматически загружать билды в Testflight по одному нажатию кнопки:

a) для разных веток под разные фичи

b) для нескольких конфигураций, соответствующих разным средам

Такова будет наша конечная цель ?.

Предварительные требования

В этой статье мы считаем, что Jenkins и Fastlane уже были установлены и настроены. Существует множество руководств, в которых подробно описывается, как нужно выполнять настройку. Кроме того, для Jenkins потребуется установить некоторые плагины, чтобы позволить Jenkins запускаться из веб-хуков Github и запускать задание. Это плагины Github, Xcode, SCM (Source Control Management), которые будут использоваться для того, чтобы сделать чекаут нашего проекта с Github, и Credentials Plugin для того, чтобы связать учетные данные для переменных окружения.

Начнем

Для начала нам нужно создать новый пайплайн в Jenkins. Этот пайплайн будет описан созданным нами скриптом, который выполняет загрузку нашего приложения в Testflight.

В главном меню панели инструментов Jenkins (ниже) мы выбираем первый вариант => New Item (Pipeline) и создаем задачу (Job) Jenkins с именем Upload to Testflight.

Далее выбираем параметр Configure в левом меню и продолжаем, добавляя конфигурацию только что созданного пайплайна. Начнем с добавления небольшого описания.

В поле Definition ниже, в разделе Pipeline выбираем параметр Pipeline Script from SCM. Этот параметр указывает Дженкинсу получить пайплайн из системы управления исходным кодом (SCM), в роли которой будет наш клонированный Git-репозиторий.

Мы добавляем параметр - ветку, которую мы хотим каждый раз билдить и загружать в Testflight, а также добавляем ссылку на репозиторий github, предоставляя также параметры учетных данных github.

Наконец, мы определяем скрипт, который будет описывать весь процесс пайплайна.

Нажимаем кнопку Save и вуаля! Нам удалось создать собственную задачу в Jenkins. Теперь мы готовы приступить к написанию нашего скрипта!

В основном мы будем использовать подход Scripted Pipeline. Наш скрипт будет состоять из нескольких этапов, называемых стадиями (stages), которые описывают, что собирается делать наш пайплайн. В конечном итоге мы хотим закончить загрузкой в Testflight, но перед загрузкой мы хотим убедиться, что наши модульные тесты проходятся успешно. Таким образом, различные фазы скрипта автоматизации должны включать следующие стадии:

  1. Чекаут репозитория

  2. Установка зависимостей

  3. Сброс симуляторов

  4. Запуск тестов

  5. Сборка билда

  6. Загрузка в Testflight

  7. Очистка

После того, как мы написали наш скрипт, нам нужно выбрать опцию Build with Parameters из меню пайплайна, который мы только что создали, и указать ветку, которую мы хотим собрать:

После того, как мы нажмем кнопку Build, и конвейер будет успешно запущен, мы увидим следующее в Jenkins Stage view:

что будет означать, что мы успешно загрузили наше приложение в Testflight!

Сбой на одном из этих этапов будет автоматически означать, что вся работа завершится ошибкой, и мы увидим соответствующий этап, подсвеченный красным цветом. В этом случае мы можем перейти в Console Output в меню задачи, просмотреть логи и определить причину сбоя и, конечно, повторно запустить задачу после устранения проблемы.

Скрипт

В нашем скрипте, который мы назвали MyScript.groovy, мы определим функцию называемую deploy(), внутри которой мы намерены реализовать вышеперечисленные стадии следующим образом:

1. Чекаут репозитория

Мы делаем чекаут нашего репозитория с помощью команды checkout плагина SCM, которая будет запускать проверку проекта с использованием параметров конфигурации, которые мы указали в Jenkins Pipeline.

stage('Checkout') {checkout scm}

2. Установки зависимостей

stage('Install dependencies') {sh 'gem install bundler'sh 'bundle update'sh 'bundle exec pod repo update'sh 'bundle exec pod install'}

Наш проект использует в качестве менеджера зависимостей CocoaPods, поэтому нам нужно запустить pod install, чтобы загрузить зависимости проекта. Для выполнения этой команды и всех других шелл-команд мы используем Bundler, который представляет собой инструмент для управления гемом приложения Ruby. Выполнив первые 2 команды, нам удается установить Bundler, а затем Bundler загружает все гемы, указанные в Gemfile проекта. Это стадия, на которой мы можем указать набор инструментов, которые могут понадобиться нашему приложению, например Fastlane, который мы будем использовать следующим, или, если нам нужен линтер или Danger, это место как раз для определения их ruby гемов. Мы продолжаем дальше и запускаем pod repo update, чтобы всегда иметь последние версии pod'а, и, наконец, мы запускаем pod update, чтобы загрузить зависимости проекта.

3. Сброс симуляторов

stage('Reset Simulators') {sh 'bundle exec fastlane snapshot resetsimulators --force'}

Этот шаг не обязателен, но необходим, если мы хотим запускать модульные тесты на следующей стадии, не беспокоясь о потенциально устаревшем состоянии симулятора, которое может привести к сбою при модульном тестировании.

Это первый раз, когда Fastlane появляется в кадре. Как я упоминал ранее, Fastlane использует Lanes и уже имеет большое количество заготовленных лейнов, но мы можем создавать и свои собственные настраиваемые лейны внутри файла под именем Fastfile. Здесь мы используем заготовленный лейн, который делает именно то, что нам нужно.

Команда выше удалит и пересоздаст все iOS-симуляторы, что можно счесть чрезмерным, но иногда отчаянные времена требуют отчаянных мер .

4. Запуск тестов

stage('Run Tests') {sh 'bundle exec fastlane test'}

На этом этапе мы запускаем модульные тесты для нашего проекта. Теперь test представляет собой кастомный лейн, который мы реализовали внутри нашего Fastfile, и имеет следующую реализацию:

lane :test doscan(clean: true,devices: ["iPhone X"],workspace: "ourproject.xcworkspace",scheme: "productionscheme",codecoverage: true,outputdirectory: "./testoutput",outputtypes: "html,junit")slather(coberturaxml: true,proj: "ourproject.xcodeproj",workspace: "ourproject.xcworkspace",outputdirectory: "./testoutput",scheme: "productionscheme",jenkins: true,ignore: [arrayofdocstoignore])end

Здесь мы используем два основных экшена Fastlane: scan и slather.

Scan используется для фактического запуска модульных тестов и может быть настроен с несколькими параметрами, такими как workspace (рабочее пространство), scheme (схема), codecoverage (покрытие кода) и, что наиболее важно, devices (устройства), где можем указать симулятор, в котором мы хотим запускать наши модульные тесты.

Slather используется для сбора покрытия кода при выполнении модульных тестов, а также принимает несколько аргументов, таких как результирующий каталог для отчета о покрытии, а также массив документов, которые следует игнорировать при сборе покрытия кода (ignore).

Здесь и происходит Fastlane-магия . Определяя этот лейн всего двумя экшенами, нам удается легко запускать наши модульные тесты внутри Jenkins. Обратите внимание, что все они, очевидно, должны быть успешно пройдены, иначе эта стадия и вся задача закончатся неудачей.


На этом мы завершаем первую часть перевода, но уже в ближайшие дни опубликуем продолжение материала.

А если вас заинтересовал курс, предлагаем записаться на бесплатный вебинар по ссылке.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru