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