![](https://habrastorage.org/getpro/habr/upload_files/eb1/8f2/d1b/eb18f2d1bf45a7408a6adb06559b8778.png)
Система контейнерной оркестрации Kubernetes де факто стала золотым стандартом в DevOps, как универсальный инструмент управления конфигурацией и автоматической поддержки ожидаемого состояния системы. Но, даже при использовании Managed Kubernetes решений (например, Amazon EKS, GKE или решений от Яндекс Облака или Mail.Ru), некоторые сервисы провайдера размещаются за пределами Kubernetes (например, S3-совместимое хранилище, брокеры очередей или базы данных) и хотелось бы иметь возможность управлять с использованием общей конфигурации. В статье мы обсудим возможности использования проекта Crossplane (входит в Incubating CNCF), а также посмотрим на код провайдера на примере управления базами данных MySQL.
Crossplane устанавливается как оператор в кластер Kubernetes и может управляться через расширение kubectl (подкоманда kubectl crossplane) или через создание Custom Resource в кластере. Поставщиком дополнительных ресурсов и их преднастроенных комбинаций (которые называются CombinedResource или XR) являются провайдеры (Provider), представляющие из себя контроллер для регистрации типов ресурсов и реализации логики по их созданию, обновления, согласования состояния с внешним ресурсом и удаления. Провайдер может быть установлен через kubectl crossplane install provider crossplane/provider-<название> (также может быть установлен внешний пакет из Docker Hub или любого другого OCI-совместимого реестра), либо через создание ресурса с типом ClusterPackageInstall
. Провайдер может быть сделан или самостоятельно (можно использовать шаблон https://github.com/crossplane/provider-template) или сгенерирован из Terraform провайдера с помощью Terrajet (например, https://github.com/yandex-cloud/provider-jet-yc)
Например, из коробки доступны следующие провайдеры:
provider-aws
- используется для управления ресурсами облака Amazonprovider-gcp
- для облака Google Cloud Platformprovider-azure
- для ресурсов Microsoft Azureprovider-digitalocean
- для ресурсов Digital Oceanprovider-alibaba
- для Alibaba Cloudprovider-ibm-cloud
- для IBM Cloudprovider-jet-yc
- для управления ресурсами Яндекс Облакаprovider-cloudflare
- управление CloudFlareprovider-linode
- управление ресурсами провайдера Linodeprovider-terraform
- управление ресурсами с использованием terraformprovider-kubernetes
- управление удаленным кластером Kubernetesprovider-sql
- используется для управления базами данных и схемой данных в реляционных СУБДprovider-kafka
- управление ресурсами kafkaprovider-influxdb
- управление ресурсами influxdbprovider-rook
- управление ресурсами оператора rook (используется для развертывания распределенного хранилища, например на основе ceph)
Рассмотрим использование провайдера на примере Яндекс Облака (сейчас это наиболее доступный ресурс в условиях проблем с оплатой иностранных сервисов). Начнем с установки оператора Crossplane в кластер (будем использовать minikube, также заранее должен быть установлен helm):
minikube start
kubectl create namespace crossplane-system
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
helm install crossplane --namespace crossplane-system crossplane-stable/crossplane
Создадим ключ для доступа к Yandex Cloud (после создания сервисной учетной записи):
yc iam service-account create --name crossplane
yc resource-manager folder add-access-binding <folder_id> --service-account-name crossplane --role admin
yc iam service-account get crossplane
yc iam key create --service-account-id service_account_id --output key.json
kubectl create secret generic yc-creds -n "crossplane-system" --from-file=credentials=./key.json
И создадим ресурс для подключения провайдера yandex-cloud и конфигурацию, которая определяет расположение секрета для доступа к облаку:
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: provider-yandex-cloud
spec:
package: "cr.yandex/crp0kch415f0lke009ft/crossplane/provider-jet-yc:v0.1.28"
---
apiVersion: yandex-cloud.jet.crossplane.io/v1alpha1
kind: ProviderConfig
metadata:
name: default
spec:
credentials:
source: Secret
secretRef:
name: yc-creds
namespace: crossplane-system
key: credentials
И проверим доступность провайдера: kubectl get provider
NAME INSTALLED HEALTHY PACKAGE AGE
provider-yandex-cloud True True cr.yandex/crp0kch415f0lke009ft/crossplane/provider-jet-yc:v0.1.28 28m
Также можем убедиться, что провайдер зарегистрировал дополнительные типы ресурсов (фрагмент вывода):
buckets storage.yandex-cloud.jet.crossplane.io/v1alpha1 false Bucket
objects storage.yandex-cloud.jet.crossplane.io/v1alpha1 false Object
defaultsecuritygroups vpc.yandex-cloud.jet.crossplane.io/v1alpha1 false DefaultSecurityGroup
networks vpc.yandex-cloud.jet.crossplane.io/v1alpha1 false Network
При создании провайдера можно использовать Vault для доступа к секретам. Теперь мы можем использовать созданные ресурсы для управления облаком, например можем создать бакет в S3:
apiVersion: iam.yandex-cloud.jet.crossplane.io/v1alpha1
kind: ServiceAccountStaticAccessKey
metadata:
name: bucket-creds
spec:
forProvider:
description: "static access key for object storage"
serviceAccountIdRef:
name: crossplane
providerConfigRef:
name: provider-yandex-cloud
writeConnectionSecretToRef:
name: bucket-creds
namespace: crossplane-system
---
apiVersion: storage.yandex-cloud.jet.crossplane.io/v1alpha1
kind: Bucket
metadata:
name: example-bucket
spec:
forProvider:
accessKeyRef:
name: bucket-creds
secretKeySecretRef:
name: bucket-creds
namespace: crossplane-system
key: attribute.secret_key
bucket: "example-test-bucket"
acl: "public-read"
providerConfigRef:
name: provider-yandex-cloud
На примере ресурсов видно, что спецификация определяется следующими ключами (внутри spec):
forProvider
- этот объект передается в провайдер без изменений и предназначен для определения конфигурации конкретного объектаproviderConfigRef.name
- ссылка на соответствующего провайдера (по названию), если не указано, будет использован провайдер с названием defaultpublishConnectionDetailsTo
- конфигурация для сохранения информации о подключенииwriteConnectionSecretToRef
- название секрета, куда может быть записана конфигурация после создания объекта (например, сгенерированные ключи доступа)
Важно отметить, что провайдер не только создает ресурсы, но и поддерживает их в актуальном состоянии, поэтому если через веб-интерфейс облака удалить бакет, он будет создан заново. Также в некоторых случаях возможно применение обновление конфигурации поверх существующего объекта. При удалении Bucket объект также будет удален из облака (кроме случая, если в spec.deletionPolicy был указан Orphan). Для получения подробной информации о структуре spec можно использовать механизм explain в kubernetes:
kubectl explain Bucket.spec
Аналогично выполняется манипуляция другими типами объектов (в том числе с другими провайдерами), включая виртуальные машины, очереди сообщений и иные управляемые сервисы (базы данных, сетевые балансировщики и др.). Также можно определять собственные композиции из объектов (своеобразные "макросы") для единообразного развертывания. Для этого нужно создать ресурс CompositeResourceDefinition в apiextensions.crossplane.io/v1, указать в спецификации group (группа для композиции, будет использоваться с versions.name как apiVersion), names.kind (название определения ресурса), names.plural (множественное число для названия определения ресурса), claimName.kind (название создаваемого ресурса), claimName.plurlal (множественное число для создаваемого ресурса). В versions перечисляется список определяемых версий ресурса со структурой:
name - название (например, v1alpha1, полное название apiVersion будет собираться из group/name)
served=true - если версия ресурса доступна через API
referenceable=true - если можно создавать экземпляры (должна быть установлена только у одной записи)
schema.openAPIV3Schema - определение схемы (определяют структуры spec для ресурса)
Например, мы можем определить ресурс для создания бакета, для которого будут передаваться два параметра: название секрета для сохранения ключей доступа и название бакета:
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xpostgresqlinstances.database.example.org
spec:
group: bucket.yandexcloud.ru
names:
kind: XBucketInstance
plural: xbucketinstances
claimNames:
kind: BucketInstance
plural: bucketinstances
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
parameters:
type: object
properties:
name:
type: string
secretName:
type: string
required:
- name
- secretName
required:
- parameters
Это определение создает новый тип ресурса BucketInstance с двумя параметрами, который может использоваться для выделения ресурса в провайдере:
apiVersion: bucket.yandexcloud.ru/v1alpha1
kind: BucketInstance
metadata:
name: images
spec:
parameters:
name: images
secretName: imagesSecret
compositionRef:
name: production
writeConnectionSecretToRef:
name: bucketSecret
Но для того, чтобы это работало корректно, нужно определить способ создания ресурса (из каких ресурсов он будет состоять, и в какие поля конфигурации будут применяться параметры):
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: example
labels:
crossplane.io/xrd: xbucketinstances.bucket.yandexcloud.ru
provider: provider-yandex-cloud
spec:
writeConnectionSecretsToNamespace: crossplane-system
compositeTypeRef:
apiVersion: database.example.org/v1alpha1
kind: XBucketInstance
resources:
- name: serviceAccountKey
base:
apiVersion: iam.yandex-cloud.jet.crossplane.io/v1alpha1
kind: ServiceAccountStaticAccessKey
metadata:
name: bucket-creds
spec:
forProvider:
description: "static access key for object storage"
serviceAccountIdRef:
name: crossplane
providerConfigRef:
name: provider-yandex-cloud
writeConnectionSecretToRef:
name: bucket-creds
namespace: crossplane-system
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.name
toFieldPath: metadata.name
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.secretName
toFieldPath: spec.writeConnectionSecretToRef.name
- name: bucket
base:
apiVersion: storage.yandex-cloud.jet.crossplane.io/v1alpha1
kind: Bucket
metadata:
name: example-bucket
spec:
forProvider:
accessKeyRef:
name: bucket-creds
secretKeySecretRef:
name: bucket-creds
namespace: crossplane-system
key: attribute.secret_key
bucket: "example-test-bucket"
acl: "public-read"
providerConfigRef:
name: provider-yandex-cloud
patches:
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.name
toFieldPath: metadata.name
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.name
toFieldPath: spec.forProvider.bucket
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.secretName
toFieldPath: spec.forProvider.accessKeyRef.name
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.secretName
toFieldPath: spec.forProvider.secretKeySecretRef.name
Определение включает в себя перечисление объектов, составленных из исходной конфигурации (в base) и замены значений полей в patches (в fromFieldPath указываются пути в конфигурации Claim-объекта, в toFieldPath - создаваемого объекта).
Теперь, когда мы разобрались с созданием простых и определением комбинированных ресурсов, можем перейти к созданию собственного провайдера. За основу возьмем репозиторий https://github.com/crossplane/provider-template. Проект по умолчанию регистрирует тип ресурса MyType в apiVersion: sample.template.crossplane.io/v1alpha1.
Для создания нового типа можно использовать сценарий hack/helpers/addtype.sh, при вызове которого нужно определить переменные окружения:
APIVERSION - версия API (по умолчанию v1alpha1)
GROUP - группа для apiVersion
KIND - тип ресурса
PROVIDER - название провайдера
Вызов сценария создаст структуры описания API и пустой контроллер. Структуры определяются в apis/sample (или в другой группе). groupversion_info.go определяет метаданные API (группа и версия) и подготавливает объекты SchemeGroupVersion (метаданные) и SchemeBuilder - который будет использоваться для регистрации схемы. Сама схема определяется с использованием kubebuilder в <type>_types.go (например, mytype_types.go). Более подробно использование Operator SDK мы рассматривали в этой статье.
Общая конфигурация провайдера (и регистрация ресурса для определения конфигурации) определяется в apis/v1alpha1 (providerconfig_types.go, providerconfigusage_types.go, storeconfig_types.go). В этих файлах может быть изменена структура конфигурации провайдера.
Поскольку оператор должен отслеживать внешний ресурс, в status.conditions.type необходимо указывать состояние синхронизации (Ready может быть установлен когда оператор подключился к серверу, Synced когда состояние внешнего ресурса согласовано с ожидаемой конфигурацией).
Сам контроллер определяется в internal/controller/<type>/<type>.go (например, internal/controller/mytype/mytype.go). В контроллере определяются несколько методов:
Setup
- регистрирует контроллер для управления ресурсам (используется ControllerManager)Connect
- создает managed.ExternalClient из подключения к внешнему ресурсу (например, к базе данных или к API управления облаком)Observe
- наблюдение за состоянием внешнего ресурса, возвращает структуру managed.ExternalObservation с полями:ResourceExists
- ресурс существует во внешней системеResourceUpToDate
- внешний ресурс в актуальном состоянииConnectionDetails
- структура managed.ConnectionDetails с описанием подробностей подключения к внешнему ресурсу
Create
- создание внешнего ресурса (возвращаетmanaged.ExternalCreation
с ConnectionDetails - подробностей подключения к созданному ресурсу)Update
- обновление конфигурации внешнего ресурса (возвращает managed.ExternalUpdate с ConnectionDetails - подробностей подключения к созданному ресурсу)Delete
- удаление внешнего ресурса (описывается структурой определяемого нами типа, например v1alpha1.MyType)
Минимально должен быть определен метод Setup, он используется для регистрации контроллера через ControllerManager (код можно заимствовать из примера контроллера). Контроллер создается для каждого определяемого типа ресурса (для конфигурации ресурс определен в internal/controller/config/config.go).
Кроме API и контроллера для корректной сборки провайдера нужно определить метаданные в package:
crossplane.yaml
содержит метаданные об авторе, исходном коде, лицензии и описании провайдера, а также в spec.controller.image расположение образа Docker-контейнераcrds/*.yaml
- определения создаваемых ресурсов (CustomResourceDefinition) - конфигурации провайдера и дополнительных типов ресурсов
Пример CRD yaml-файла для описания определения базы данных можно посмотреть в этом файле. Определение спецификации базы данных выполняется в файле. Реализация контроллера для управления базами данных доступна в reconciler.go.
Для создания пакета (образа провайдера) можно использовать расширение kubectl:
kubectl crossplane build provider
В результате будет выполнена сборка образа контейнера и загрузка его в OCI-совместимый реестр (например, Docker Hub) и в дальнейшем он может быть установлен в Kubernetes-кластер с созданием определенных в провайдере типов ресурсов. Более подробно можно посмотреть в документации по созданию провайдеров.
Таким образом, с использованием Crossplane могут быть созданы специализированные операторы (провайдеры), которые ориентированы на управление внешними ресурсами (как облачными сервисами, так и любыми другими системами, для которых возможно получение текущего состояния).
Количество модулей Ansible велико и возможности их разнообразны. Но иногда даже этого не хватает. И в этом случае мы можем разработать свой собственный модуль, использовать его в работе и, возможно, даже поделиться им с комьюнити. О том как это сделать, вы можете узнать на бесплатном уроке "Разработка модуля Ansible" от моих коллег из OTUS. Узнать подробнее о курсе "Infrastructure as a code" и зарегистрироваться на бесплатный урок можно по ссылке ниже.