Универсальное описание инфраструктуры на Pulumi. Раскатываемся в Kubernetes, AWS, GCP с одной конфигурацией

от автора

Часто возникает ситуация, когда нужно развернуть приложение одновременно в нескольких облаках, совместить облачную инфраструктуру и управляемый Kubernetes-кластер или предусмотреть возможную миграцию сервиса в будущем. Одним из возможных решений для создания универсальной конфигурации может быть использование проекта Pulumi, который позволяет публиковать приложения в разные облака (GCP, Amazon, Azure, AliCloud), Kubernetes, провайдеры (например, Linode, Digital Ocean), системы управления виртуальной инфраструктурой (OpenStack) и в локальный Docker. В этой статье мы рассмотрим основные идеи проекта, создадим универсальную конфигурацию для простого Python-приложения.

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

  • Windows — choco install pulumi или winget install pulumi

  • Linux — curl -fsSL https://get.pulumi.com | sh

  • MacOS — brew install pulumi/tap/pulumi

После установки выполним конфигурацию стека (на этом этапе будем использовать локальный Docker, запущенный на одной машине).

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

  • Go

  • Python

  • JavaScript

  • TypeScript

  • C#

  • Java / Kotlin

  • Yaml

При генерации кода создается заготовка с использованием библиотек Pulumi для регистрации ресурсов развертывания nginx. В варианте сборки yaml в Pulumi.yaml добавляются дополнительные секции:

  • variables — определение переменных для подстановки в конфигурации;

  • resources — определение ресурсов:

    • type — определение типа объекта для плагина;

    • deleteBeforeReplace — удалить перед созданием объекта;

    • dependsOn — список ресурсов, от которых зависит этот (для определения последовательности создания и модификации);

    • ignoreChanges — список полей, которые будут игнорироваться при определении обновлений;

    • parent — родительский ресурс;

    • provider / providers — определение, какие провайдеры будут использоваться с этим ресурсом;

    • customTimeouts — определение таймаутов при управлении ресурсов:

      • create — таймаут при операциях создания;

      • delete — таймаут при удалении;

      • update — таймаут при обновлении ресурса.

    • properties — свойства ресурса (для Kubernetes повторяет схему yaml-ресурсов, при этом может использоваться интерполяция переменных из variables, в том числе составные yaml-объекты).

  • configuration — настройки, которые могут быть использованы в сценарии развертывания;

  • outputs — артефакты сборки, созданные на основе сценария.

Также конфигурация развертывания может быть определена через манипуляцию объектами в коде (в Go и Java запускаются внутри функции pulumi.run, в остальных языках в точке входа в приложение).

Начнем с использования провайдера Docker и создадим простой веб-сервер на Python. Прежде всего создадим заготовку для проекта (она также зарегистрируется в облаке Pulumi, для чего может потребоваться выполнить авторизацию через pulumi config).

pulumi new yaml -n helloworld -d "Sample application" -s dev
main.py ===================  from flask import Flask app = Flask(__name__)  @app.route("/") def hello():     return "Hello World!"  if __name__ == "__main__":     app.run(host='0.0.0.0')

и создадим соответствующий Dockerfile:

FROM python WORKDIR /opt RUN pip install flask ADD main.py /opt CMD python main.py

Мы можем определить конфигурацию инфраструктуры через yaml-манифест:

name: helloworld runtime: yaml description: Hello World resources:   backend-image:     type: docker:index:RemoteImage     properties:       name: dmitriizolotov/helloworld

При запуске Docker на Windows нужно дополнительно изменить конфигурацию Docker-провайдера (используется провайдер от Terraform):

pulumi config set docker:host npipe:////.//pipe//docker_engine

Соберем образ, отправим его в Docker hub и запустим наш проект:

docker build -t dmitriizolotov/helloworld . docker push dmitriizolotov/helloworld pulumi up -y

В результате получим план выполнения:

Изменения в стеке
Изменения в стеке

Теперь добавим конфигурацию для определения порта публикации контейнера:

name: helloworld runtime: yaml description: Hello World configuration:   publishingPort:     type: Number variables:   internalPort: 5000 resources:   backend-image:     type: docker:index:Container     properties:       image: dmitriizolotov/helloworld        name: helloworld       ports:       - internal: ${internalPort}         external: ${publishingPort}

Переменные конфигурации должны быть заданы в определении стека (Pulumi.dev.yaml) или через pulumi config set:

pulumi config set publishingPort 8080

и применим изменение конфигурации через pulumi up (или можем запустить pulumi watch для непрерывного отслеживания изменений). Это становится возможным благодаря тому, что pulumi сохраняет текущее состояние стека и определяет миграции для перехода в новое ожидаемое состояние, описанное конфигурацией. Теперь можно убедиться, что сервер работает и опубликован на порт 8080 (http://localhost:8080).

Этот подход очень похож на концепцию Terraform, ресурсы определяются в плагинах, которые можно посмотреть в реестре. Например, можно выполнять произвольные системные команды (command), управлять репозиториями (Gitlab, Github), управлять ресурсами серверов (RabbitMQ, Kafka, MySQL, PostgreSQL) и облачных провайдеров (AWS, Digital Ocean, Google Cloud Platform, Yandex Cloud, Microsoft Azure) и Kubernetes.

Но все же Pulumi более полезен при использовании описания инфраструктуры через код. Благодаря поддержки разных runtime конфигурация может быть описана на языке, привычном для разработчика (например, Java, Python, C#, JS/TS или Go). Каждый плагин экспортирует набор классов и структур данных для определения ожидаемого состояния стека. Рассмотрим пример ранее обозначенной конфигурации на Python:

import pulumi from pulumi_docker import Image, DockerBuild, Container, ContainerPortArgs  config = pulumi.Config() publishing_port = config.require_int("publishingPort")  internal_port = 5000 image = "dmitriizolotov/helloworld"  img = Image(     "helloworld",     image_name=image,     build=DockerBuild(),     skip_push=False, )  container = Container(   resource_name="helloworld",   name="helloworld",   image=image,   ports=[ContainerPortArgs(internal=internal_port, external=publishing_port)] )

Для корректной работы необходимо установить модули pulumi и pulumi_docker:

pip install pulumi pulumi_docker

Также изменим тип runtime в Pulumi.yaml:

name: helloworld runtime: python description: Hello World

И пересоздадим стек:

pulumi down pulumi up -y

Аналогично можно определить конфигурацию для используемых ресурсов и определения для облачных провайдеров (например, создание S3-бакетов), реализация может быть выполнена на одном из поддерживаемых runtime. Но основной вопрос для нас — как создать универсальное определение, которое можно было бы использовать на нескольких облаках. Здесь существует несколько возможных решений.

Наиболее простое — получить идентификатор облака из конфигурации и использовать возможности языка программирования для выбора корректной реализации. Например, для регистрации S3-бакета в облаках aws, digitalocean или yandex можно использовать следующий код:

import pulumi_aws as aws import pulumi_digitalocean as digitalocean from pulumi_gcp import storage  bucket_name = "bucket" cloud = config.require("cloud") if cloud == "aws":     bucket = aws.s3.Bucket(bucket_name) elif cloud == "do":     bucket = digitalocean.SpacesBucket(bucket_name) elif cloud == "gcp":     bucket = storage.Bucket(bucket_name)

Для переключения облака в стеке достаточно будет изменить конфигурацию:

pulumi config set cloud gcp

Если нужно реализовать сложную логику для управления ресурсами, можно создать собственный провайдер через создание класса-наследника ResourceProvider и реализацию методов create (создание ресурса), diff (определение разности), update (обновление), delete (удаление ресурса), read (получение состояния ресурса). Провайдер получает входы (inputs), которые могут быть произвольным объектом (заполняются через properties) и может вернуть данные (outputs), которые возвращаются в CreateResult (outs).

Поскольку конфигурация размещается вместе с кодом сервиса, можно использовать возможности инструментов CI/CD. Например, для CircleCI существует orb pulumi/pulumi@1.0.0, для Github Actions — pulumi/actions@v3, для остальных инструментов можно организовать установку pulumi через пакетный менеджер выбранной технологии разработки в рамках конвейера CI/CD.

Таким образом, Pulumi позволяет описывать сложные сценарии для развертывания ресурсов на основе кода и сохраняя все преимущества Terraform существенно повышает гибкость конфигурирования благодаря использованию возможностей языков программирования по реализации алгоритмов создания и модификации ресурсов, которые могут учитывать параметры конфигурации, а также выполнять итеративные операции (при необходимости запуска группы ресурсов).

Статья подготовлена в преддверии старта курса Infrastructure as a code. Узнать подробнее о курсе и зарегистрироваться на бесплатный урок можно по ссылке ниже.


ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/674818/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *