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-кластер необходимо:
- Вынести описания всех сущностей из template в отдельные YAML-файлы, так как template — это специфичный для OpenShift объект. 
- Изменить параметризацию c помощью файла - values.yamlвместо- values.env.
- Заменить 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: configsService разместим в файле 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: 50mRoute — тоже объект 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.
- apiVersion: apps.openshift.io/v1заменить на- apiVersion: apps/v1.
- kind: DeploymentConfigзаменить на- kind: Deployment.
- spec.selectorsзаменить с- selector: name: ...на- selector: matchLabels: name: ...
- Убедиться, что секция - spec.template.spec.containers.imageописана для каждого контейнера.
- Удалить секции - 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.
Читайте также в нашем блоге:
 
           
 
olga_ryabukhina
Что значит "ванильный"?
WellsBart
Ванильная — оригинальная, немодифицированная версия ПО. В случае Kubernetes это, грубо говоря, бесплатная версия ПО с базовой функциональностью, которую нужно настраивать самому. В «ванильном» Kubernetes нет интеграции с инструментами мониторинга, безопасности, логирования и пр. и пр. — всем тем, что важно для production. Отсюда — популярность Kubernetes-платформ и managed-сервисов, у которых всё это есть «из коробки».
olga_ryabukhina
Спасибо!
shurup
https://en.wikipedia.org/wiki/Vanilla_software