OpenShift, Rancher и другие зарубежные Kubernetes-платформы официально больше не поддерживаются в России. Многим компаниям приходится искать альтернативные решения для управления контейнеризированными приложениями — например, «ванильный» Kubernetes или российские платформы.

Хотя у Kubernetes-платформ одинаковая технологическая база, перейти с одной на другую непросто: миграция неизбежно сопряжена с различными трудностями, связанными с особенностями реализации компонентов. В этой статье рассмотрен пример переезда приложения из OpenShift в «ванильный» кластер Kubernetes. В конце статьи приведена таблица соответствия примитивов OpenShift и Kubernetes — с информацией о том, какие из этих примитивов требуют замены, а какие нет.

Есть инструменты, которые автоматизируют процесс миграции — например, move2kube. Однако они требуют отдельного рассмотрения и, соответственно, отдельной статьи. Здесь же мы сосредоточимся именно на «ручном» переносе приложения.

Исходные данные

Рассмотрим template OpenShift с простым веб-сервисом:

apiVersion: template.openshift.io/v1
kind: Template
labels:
  nginx: master
metadata:
  annotations:
    description: example-template
    iconClass: icon-nginx
    tags: web,example
  name: web-app-example
objects:
- apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: ${NAME}
  spec:
    replicas: ${{REPLICAS}}
    revisionHistoryLimit: 3
    selector:
      matchLabels:
        app: ${NAME}
    template:
      metadata:
        labels:
          app: ${NAME}
      spec:
        containers:
        - image: camunda/camunda-bpm-platform:run-7.15.0
          imagePullPolicy: Always
          name: camunda
          ports:
          - containerPort: 8080
            name: http
            protocol: TCP
          resources:
            limits:
              memory: ${BACK_MEMORY}
            requests:
              cpu: ${BACK_CPU}
              memory: ${BACK_MEMORY}
        - command:
          - /usr/sbin/nginx
          - -g
          - daemon off;
          image: nginx:stable-alpine
          imagePullPolicy: Always
          lifecycle:
            preStop:
              exec:
                command:
                - /bin/bash
                - -c
                - sleep 5; kill -QUIT 1
          name: nginx
          ports:
          - containerPort: 9000
            name: http
            protocol: TCP
          resources:
            limits:
              memory: ${FRONT_MEMORY}
            requests:
              cpu: ${FRONT_CPU}
              memory: ${FRONT_MEMORY}
          volumeMounts:
          - mountPath: /etc/nginx/nginx.conf
            name: configs
            subPath: nginx.conf
        volumes:
        - configMap:
            name: ${NAME}-config
          name: configs
- apiVersion: v1
  kind: Service
  metadata:
    annotations:
      description: Exposes and load balances the application pods
    name: ${NAME}-service
  spec:
    ports:
    - name: http
      port: 9000
      targetPort: 9000
    selector:
      app: ${NAME}
- apiVersion: v1
  kind: ConfigMap
  metadata:
    name: ${NAME}-config
  data:
    nginx.conf: |
      user nginx;
      worker_processes 1;
      pid /run/nginx.pid;

      events {
        worker_connections 1024;
      }

      http {
        include /etc/nginx/mime.types;
        default_type application/octet-stream;

        upstream backend {
          server 127.0.0.1:8080 fail_timeout=0;
        }

        server {
          listen 9000;
          server_name _;

          root /www;

          client_max_body_size 100M;
          keepalive_timeout 10s;

          location / {
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header Host $http_host;
            proxy_redirect off;

            proxy_pass http://backend;
          }
        }
      }

- apiVersion: route.openshift.io/v1
  kind: Route
  metadata:
    name: ${NAME}-route
  spec:
    host: ${DOMAIN}.apps-crc.testing
    port:
      targetPort: http
    to:
      kind: Service
      name: ${NAME}-service
      weight: 100
    wildcardPolicy: None
parameters:
- description: Name for application
  from: '[A-Z]{8}'
  generate: expression
  name: NAME
- description: Domain for application
  from: '[A-Z]{8}'
  generate: expression
  name: DOMAIN
- description: Number of replicas
  from: '[0-9]{1}'
  generate: expression
  name: REPLICAS
- description: Memory request and limit for frontend container
  from: '[A-Z0-9]{4}'
  generate: expression
  name: FRONT_MEMORY
- description: CPU request for frontend container
  from: '[A-Z0-9]{3}'
  generate: expression
  name: FRONT_CPU
- description: Memory request and limit for backend container
  from: '[A-Z0-9]{4}'
  generate: expression
  name: BACK_MEMORY
- description: CPU request for backend container
  from: '[A-Z0-9]{3}'
  generate: expression
  name: BACK_CPU

Что внутри этого template:

  • Deployment приложения. В качестве примера использованы nginx, который выступает в роли фронтенда, и демонстрационная stateless-версия camunda в качестве бэкенда.

  • ConfigMap с конфигурацией для nginx, подключаемый в контейнер.

  • Route — принимает трафик на целевой домен снаружи кластера.

  • Service — направляет трафик непосредственно к Pod'ам с приложением.

Также есть возможность параметризации ряда настроек.

Значения параметров, используемых в template, находятся в файле values.env:

NAME=example-application
DOMAIN=example
REPLICAS=1
FRONT_MEMORY=128Mi
FRONT_CPU=50m
BACK_MEMORY=512Mi
BACK_CPU=50m

Переменные подставляются в раздел parameters.

Миграция кластера

В качестве целевого может выступать любой кластер, в основе которого лежит оригинальный Kubernetes. Для этой статьи миграция выполнялась в кластер, развернутый с помощью платформы Deckhouse.

Чтобы переехать из OpenShift в Kubernetes-кластер необходимо:

  1. Вынести описания всех сущностей из template в отдельные YAML-файлы, так как template — это специфичный для OpenShift объект.

  2. Изменить параметризацию c помощью файла values.yaml вместо values.env.

  3. Заменить Route на Ingress.

Deployment, Service и ConfigMap требуют меньше всего изменений. Для каждого из них нужно создать свой файл с описанием. Начнем с приложения в файле app.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Chart.Name }}
spec:
  replicas: {{ .Values.app.replicas }}
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: {{ .Chart.Name }}
  template:
    metadata:
      labels:
        app: {{ .Chart.Name }}
    spec:
      containers:
      - image: camunda/camunda-bpm-platform:run-7.15.0
        imagePullPolicy: Always
        name: camunda
        ports:
        - containerPort: 8080
          name: http
          protocol: TCP
        resources:
          limits:
            memory: {{ .Values.app.backend.memory }}
          requests:
            cpu: {{ .Values.app.backend.cpu }}
            memory: {{ .Values.app.backend.memory }}
      - command:
        - /usr/sbin/nginx
        - -g
        - daemon off;
        image: nginx:stable-alpine
        imagePullPolicy: Always
        lifecycle:
          preStop:
            exec:
              command:
              - /bin/bash
              - -c
              - sleep 5; kill -QUIT 1
        name: nginx
        ports:
        - containerPort: 9000
          name: http
          protocol: TCP
        resources:
          limits:
            memory: {{ .Values.app.frontend.memory }}
          requests:
            cpu: {{ .Values.app.frontend.cpu }}
            memory: {{ .Values.app.frontend.memory }}
        volumeMounts:
        - mountPath: /etc/nginx/nginx.conf
          name: configs
          subPath: nginx.conf
      volumes:
      - configMap:
          name: {{ .Chart.Name }}-config
        name: configs

Service разместим в файле service.yaml:

apiVersion: v1
kind: Service
metadata:
  annotations:
    description: Exposes and load balances the application pods
  name: {{ .Chart.Name }}
spec:
  ports:
  - name: http
    port: 9000
    targetPort: 9000
  selector:
    app: {{ .Chart.Name }}

ConfigMap — в файле configmap.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Chart.Name }}-config
data:
  nginx.conf: |
    user nginx;
    worker_processes 1;
    pid /run/nginx.pid;

    events {
      worker_connections 1024;
    }

    http {
      include /etc/nginx/mime.types;
      default_type application/octet-stream;

      upstream backend {
        server 127.0.0.1:8080 fail_timeout=0;
      }

      server {
        listen 9000;
        server_name _;

        root /www;

        client_max_body_size 100M;
        keepalive_timeout 10s;

        location / {
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-Proto $scheme;
          proxy_set_header Host $http_host;
          proxy_redirect off;

          proxy_pass http://backend;
        }
      }
    }

Теперь заменим values.env на values.yaml:

app:
  replicas: 1
  host: example.kubernetes.testing
  backend:
    memory: 512Mi
    cpu: 50m
  frontend:
    memory: 128Mi
    cpu: 50m

Route — тоже объект OpenShift. Заменим его на привычный Ingress (файл ingress.yaml):

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
  name: {{ .Chart.Name }}
spec:
  rules:
  - host: {{ .Values.app.host }}
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: {{ .Chart.Name }}
            port:
              number: 9000

На этом можно считать подготовку оконченной. 

Созданный Helm-чарт готов к деплою в «ванильный» Kubernetes. 

В нашем примере используется GitLab CI и werf, однако это не обязательное условие. Чарт совместим с любыми CI/CD-системами.

Запустим деплой командой werf converge:

Проверим, что все ресурсы действительно появились в кластере:

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

Немного о DeploymentConfig

В OpenShift часто используется ресурс DeploymentConfig, который также стоит упомянуть. Это версия Deployment, «расширенная» за счет ресурсов ImageStream и BuildConfig: они предназначены для сборки образов и деплоя приложения в кластер. 

Прямая замена DeploymentConfig на аналогичный ресурс в «ванильном» Kubernetes невозможна. Поэтому для сохранения функциональности, которую предоставляет связка DeploymentConfig + ImageStream + BuildConfig, потребуются дополнительные инструменты.

Чтобы перенести DeploymentConfig в Kubernetes, можно заменить его на Deployment, а неподдерживаемые функции реализовать сторонними инструментами — например, CI-системой, инструментом для сборки образов, а также внешним registry для их хранения.

 selector:
      name: ...

Ниже — примерный список действий для превращения DeploymentConfig в Deployment.

  1. apiVersion: apps.openshift.io/v1 заменить на apiVersion: apps/v1.

  2. kind: DeploymentConfig заменить на kind: Deployment.

  3. spec.selectors заменить с selector: name: ... на selector: matchLabels: name: ...

  4. Убедиться, что секция spec.template.spec.containers.image описана для каждого контейнера.

  5. Удалить секции spec.triggers, spec.strategy и spec.test.

Обратите внимание, что эта инструкция не универсальна. Каждый конкретный случай стоит рассмотреть отдельно. Рекомендуем ознакомиться с официальной документацией по DeploymentConfig.

Подытожим

Перенос приложения из OpenShift в «ванильный» Kubernetes требует декомпозиции templates на отдельные YAML-ресурсы, а также замены ряда специфичных для OpenShift объектов на сущности K8s.

Ниже — краткая таблица соответствия ресурсов OpenShift и K8s, которая поможет при миграции:

OpenShift

Kubernetes

Template

Отказываемся в пользу Helm chart

DeploymentConfig

Меняем на Deployment (не забывая об особенностях, связанных с ImageStream и BuildConfig)

Route

Меняем на Ingress

Deployment/Statefulset/Daemonset

Не требуют изменений (только замена параметризации)

Service/ConfigMap и т. д.

Не требуют изменений (только замена параметризации)

Рассмотренный в статье пример переезда с OpenShift в кластер под управлением Deckhouse актуален в том числе и для бесплатной версии платформы (community edition). Мы уже не раз переносили рабочие нагрузки наших клиентов с OpenShift, Rancher и других зарубежных решений, накопили лучшие практики и готовы помочь с миграцией на Deckhouse.

P.S.

Читайте также в нашем блоге:

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


  1. olga_ryabukhina
    00.00.0000 00:00

    Что значит "ванильный"?


    1. WellsBart
      00.00.0000 00:00

      Ванильная — оригинальная, немодифицированная версия ПО. В случае Kubernetes это, грубо говоря, бесплатная версия ПО с базовой функциональностью, которую нужно настраивать самому. В «ванильном» Kubernetes нет интеграции с инструментами мониторинга, безопасности, логирования и пр. и пр. — всем тем, что важно для production. Отсюда — популярность Kubernetes-платформ и managed-сервисов, у которых всё это есть «из коробки».


      1. olga_ryabukhina
        00.00.0000 00:00

        Спасибо!