
Часто возникает ситуация, когда нужно развернуть приложение одновременно в нескольких облаках, совместить облачную инфраструктуру и управляемый 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 devmain.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. Узнать подробнее о курсе и зарегистрироваться на бесплатный урок можно по ссылке ниже.
 
          