Система контейнерной оркестрации 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 - используется для управления ресурсами облака Amazon

  • provider-gcp - для облака Google Cloud Platform

  • provider-azure - для ресурсов Microsoft Azure

  • provider-digitalocean - для ресурсов Digital Ocean

  • provider-alibaba - для Alibaba Cloud

  • provider-ibm-cloud - для IBM Cloud

  • provider-jet-yc - для управления ресурсами Яндекс Облака

  • provider-cloudflare - управление CloudFlare

  • provider-linode - управление ресурсами провайдера Linode

  • provider-terraform - управление ресурсами с использованием terraform

  • provider-kubernetes - управление удаленным кластером Kubernetes

  • provider-sql - используется для управления базами данных и схемой данных в реляционных СУБД

  • provider-kafka - управление ресурсами kafka

  • provider-influxdb - управление ресурсами influxdb

  • provider-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 - ссылка на соответствующего провайдера (по названию), если не указано, будет использован провайдер с названием default

  • publishConnectionDetailsTo - конфигурация для сохранения информации о подключении

  • 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" и зарегистрироваться на бесплатный урок можно по ссылке ниже.

Узнать подробнее о курсе.

Комментарии (0)