DevOps с Kubernetes и VSTS. Часть 1: Локальная история +16

- такой же как Forbes, только лучше.

Последнее время я часто рассказываю про контейнеры, Docker и Kubernetes. На фоне этого коллеги всё чаще стали спрашивать о том, а где же здесь технологи Microsoft? Чтобы объяснить, я нашёл несколько материалов, в том числе этот набор из пары статей от Colin Dembovsky. В них есть всё: Docker, Kubernetes и наши технологии. Думаю, что для читателей Хабры это тоже должно быть интересно. Итак, встречайте, перевод первой части.



Если вы читаете мой блог, то знаете, что я фанат контейнеров в целом и Docker в частности. Когда вы в последний раз ставили софт на «голое железо»? Может быть, только на ноутбук, но и то шансы невелики. Виртуализация кардинально изменила наше отношение к ресурсам центра обработки данных, значительно увеличив их плотность и эффективность использования. Следующим этапом повышения плотности стали контейнеры, только ВМ размещаются на физических серверах, а контейнеры — в самих ВМ. Очень скоро большинство из нас не будет работать не только на уровне серверов, но даже на уровне ВМ, все рабочие нагрузки переместятся в контейнеры. Но это в перспективе.

Несмотря на все преимущества контейнеров с точки зрения упаковки приложений, многим до сих пор непонятно, как запускать контейнеры в производственной среде. Установка одного контейнера станет интересным и воодушевляющим опытом для любого разработчика, но как насчет масштабирования контейнеров или объединения их в кластер? Как вы будете наблюдать за своими контейнерами? Как выявлять и устранять сбои? Здесь мы плавно переходим к проблеме оркестрации контейнеров.

В этой статье мы рассмотрим подходы к локальной разработке с использованием Kubernetes и minikube. Часть 2 посвящена вопросам создания конвейеров CI/CD для кластера Kubernetes в Azure.

Поле битвы — оркестрация


Существуют три популярные системы оркестрации контейнеров — Mesos, Kubernetes и Docker Swarm Mode. Я не буду призывать вас встать под чей-то флаг (по крайней мере пока), концептуально они все похожи. Они все используют концепцию «конфигурация как код» для развертывания множества контейнеров на множество узлов. Kubernetes предлагает ряд возможностей, которые, на мой взгляд, станут настоящим прорывом в области DevOps: карты конфигурации (ConfigMaps), секреты (Secrets) и пространства имен (namespaces).

Не вдаваясь в подробности, скажу, что пространства имен позволяют создавать различные логические среды в одном кластере. В качестве примера приведу пространство имен DEV, где вы сможете запускать небольшие копии своей среды PROD в целях тестирования. Пространства имен также применяются для реализации мультитенантности или различных контекстов безопасности. Карты конфигурации (ConfigMaps) и секреты (Secrets) позволяют хранить конфигурацию за пределами контейнеров, то есть вы сможете запускать один образ в различных контекстах без встраивания специфичного кода для конкретной среды в сами образы.

Kubernetes Workflow (рабочий процесс) и Pipeline (конвейер)


В этой статье я продемонстрирую подход к разработке с ориентацией на Kubernetes. В первой части мы рассмотрим рабочий процесс разработки, а во второй — конвейер DevOps. К счастью, благодаря MiniKube (кластеру с одним узлом Kubernetes, который запускается на ВМ) мы можем работать с полноценным кластером на ноутбуке! Это означает, что вам доступны преимущества кластерной технологии (вроде ConfigMaps) без подключения к производственному кластеру.

Итак, рассмотрим рабочий процесс разработчика. Это будет что-то вроде:

  1. Разработать код.
  2. Создать образ на основе файла Dockerfile или пакета файлов, сформированного с помощью команды docker-compose.
  3. Запустить службу в MiniKube (запускаем контейнеры из ваших образов).

Как показывает практика, благодаря Visual Studio 2017 (и (или) VS Code), Docker и MiniKube на этом пути вам не встретятся подводные камни.

Затем вы перейдете к конвейеру DevOps, начиная с его создания. На основе ваших исходных файлов и файлов Dockerfile создаются образы, которые регистрируются в приватном реестре контейнеров. Далее необходимо передать конфигурацию в кластер Kubernetes, чтобы запустить/развернуть новые образы. Благодаря Azure и VSTS мы создадим конвейер DevOps буквально за пару-тройку кликов! Но это тема второй части нашей статьи, сейчас же мы изучаем рабочий процесс разработчика.

Подготовка среды разработки


Для демонстрации я буду использовать Windows, но в Mac или Linux настройки аналогичные. Для развертывания локальной среды разработки вам нужно установить:

  1. Docker
  2. Kubectl
  3. MiniKube

Можете воспользоваться ссылками и выполнить установку. В процессе я столкнулся с небольшой проблемой при запуске MiniKube на Hyper-V — по умолчанию команда MiniKube start, создающая ВМ для MiniKube, подключается к первой найденной виртуальной сети Hyper-V. Сетей у меня было несколько, и MiniKube подключился ко внутренней, что привело к сбою. Я создал новую виртуальную сеть с именем minikube в консоли Hyper-V и убедился, что это внешняя сеть. Для создания ВМ MiniKube я воспользовался следующей командой:

c:
cd minikube start --vm-driver hyperv --hyperv-virtual-switch minikube

Мне пришлось выполнить команду cd для перехода в корневой каталог c:\, без этого MiniKube не смог бы создать ВМ.

Внешняя сеть подключена к моей точке доступа Wi-Fi. Это означает, что когда я подключаюсь к новой сети, ВМ minikube получает новый IP-адрес. Вместо того чтобы каждый раз обновлять kubeconfig, я просто добавил строку хоста в свой файл hosts (в Windows это c:\windows\system32\drivers\etc\hosts): kubernetes, где IP — это IP-адрес ВМ minikube, полученный с помощью команды minikube ip. Для обновления kubeconfig используйте следующую команду:

kubectl config set-cluster minikube --server=https://kubernetes:8443 --certificate-authority=c:/users/<user>/.minikube/ca.crt

где <user> — ваше имя пользователя; таким образом, cert указывает на файл ca.crt, созданный в вашем каталоге .minikube.

Теперь при подключении к новой сети вы просто обновите IP-адрес в файле hosts, и команда kubectl по-прежнему будет работать. Сертификат генерируется для узла с именем kubernetes, поэтому используйте это имя.

Если все работает нормально, вы получите лаконичный ответ на команду kubectl get nodes:

PS:\> kubectl get nodes
NAME       STATUS    AGE       VERSION
minikube   Ready     11m       v1.6.4

Чтобы запустить UI Kubernetes, просто введите команду minikube dashboard. В браузере откроется следующее окно:



Наконец, для «повторного использования» контекста minikube docker выполните следующую команду:

minikube docker-env | Invoke-Expression

Таким образом обеспечивается совместное использование сокета minikube docker. Выполнив команду docker ps, вы получите информацию о нескольких работающих контейнерах, это базовые системные контейнеры Kubernetes. Это также означает возможность создания здесь образов, которые могут запускаться кластером minikube.

Теперь у вас есть кластер с одним узлом. Можете приступать к разработке!

Переходим к коду


Не так давно я опубликовал в блоге статью Разработка проекта Aurelia с помощью Azure и VSTS. И поскольку у меня уже была пара готовых сайтов .NET Core, я решил попробовать запустить их в кластере Kubernetes. Выполните клонирование этого репозитория и проверьте ветку docker. Я добавил несколько файлов в репозиторий, чтобы обеспечить возможность создания образов Docker и указать конфигурацию Kubernetes. Давайте посмотрим, как это выглядит.

Файл docker-compose.yml определяет составное приложение из двух образов: api и frontend:

version: '2'

services:
  api:
    image: api
    build:
      context: ./API
      dockerfile: Dockerfile

frontend:
    image: frontend
    build:
      context: ./frontend
      dockerfile: Dockerfile

Файл Dockerfile для каждой службы максимально прост: запуск из образа ASP.NET Core 1.1, копирование файлов приложения в контейнер, открытие порта 80 и запуск dotnet app.dll (frontend.dll и api.dll для каждого сайта) в качестве точки входа для каждого контейнера:

FROM microsoft/aspnetcore:1.1
ARG source
WORKDIR /app
EXPOSE 80
COPY ${source:-obj/Docker/publish} .
ENTRYPOINT ["dotnet", "API.dll"]

Чтобы подготовиться к созданию образов, выполните команды dotnet restore, build и publish, произойдёт сборка и публикация проектов. Теперь можно переходить к созданию образов. При наличии готовых образов мы можем настроить службу Kubernetes на их запуск в нашем кластере minikube.

Создание образов


Проще всего для создания образов использовать Visual Studio. Настройте проект docker-compose как стартовый и запустите его. Образы будут созданы. Если вы не работаете с Visual Studio, создавайте образы, выполняя следующие команды из корневого каталога репозитория:

cd API
dotnet restore
dotnet build
dotnet publish -o obj/Docker/publish
cd ../frontend
dotnet restore
dotnet build
dotnet publish -o obj/Docker/publish
cd ..
docker-compose -f docker-compose.yml build

Теперь после выполнения команды docker images вы увидите контейнеры minikube, а также образы для frontend и api:



Объявление служб — конфигурация как код


Теперь мы можем указать, какие службы запускать в кластере. На мой взгляд, одно из преимуществ Kubernetes заключается в том, что вы должны объявлять свою среду, вместо того чтобы запускать скрипт. Такая декларативная модель намного лучше императивной, и в настоящее время она распространяется все шире благодаря Chef, Puppet и PowerShell DSC. Kubernetes позволяет нам указывать запускаемые службы, а также определять методы их развертывания. Различные объекты Kubernetes можно определять с помощью простого файла yaml. Мы объявляем две службы: api и frontend. Серверные службы (backend) обычно недоступны за пределами кластера, однако в данном случае наш демонстрационный код представляет собой одностраничное приложение, поэтому служба api должна быть доступна извне.

Перечень служб будет меняться очень редко, это службы, доступные в кластере. Однако базовые контейнеры (в Kubernetes их называют подами), из которых состоит служба, будут меняться. Они меняются при обновлении, а также при масштабировании. Для управления контейнерами, из которых состоит служба, используется конструкция Deployment. Поскольку служба и развертывание довольно тесно связаны, я поместил их в один файл. То есть у нас есть файл для службы/развертывания frontend (k8s/app-demo-frontend-minikube.yml) и файл для службы/развертывания api (k8s/app-demo-backend-minikube.yml). Если вы посчитаете нужным, можете поместить определения служб и развертываний в отдельные файлы. Изучим содержимое файла app-demo-backend.yml:

apiVersion: v1
kind: Service
metadata:
  name: demo-backend-service
  labels:
    app: demo
spec:
  selector:
    app: demo
    tier: backend
  ports:
    - protocol: TCP
      port: 80
      nodePort: 30081
  type: NodePort
---
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: demo-backend-deployment
spec:
  replicas: 2
  template:
    metadata:
      labels:
        app: demo
        tier: backend
    spec:
      containers:
      - name: backend
        image: api
        ports:
        - containerPort: 80
        imagePullPolicy: Never

Примечания:

  • Строки 1–15 объявляют службу.
  • В строке 4 указано имя службы.
  • В строках 8–10 описан селектор для этой службы. Любой под с метками app=demo и tier=frontend будет использоваться для балансировки нагрузки этой службы. Служба будет знать, как перенаправить на свои базовые модули трафик, связанный с запросами для этой службы, которые попадают в кластер. Это упрощает добавление, удаление и обновление контейнеров, поскольку все, что нам нужно сделать, — изменить селектор. Служба получит статический IP-адрес, а ее базовые модули — динамические адреса, которые будут меняться на разных этапах жизненного цикла модулей. Тем не менее этот процесс абсолютно прозрачен для нас, потому мы будем просто посылать запросы службе, и все должно работать.
  • Строка 14 — мы хотим, чтобы эта служба была доступна через порт 30081 (сопоставленные с портом 80 на подах, как указано в строке 13).
  • Строка 15 — NodePort указывает, что мы хотим, чтобы Kubernetes предоставлял службе порт на том же IP-адресе, который использует кластер. Для «реальных» кластеров (на ресурсах поставщика облачных услуг, например Azure) мы изменили бы эту настройку, чтобы получить IP-адрес от облачного хоста.
  • В строках 17–34 объявляется конструкция Deployment, которая обеспечит наличие контейнеров (подов) для этой службы. Если под неработоспособен, Deployment автоматически запускает новый под. Эта конструкция гарантирует нормальную работу службы.
  • Строка 22 указывает, что нам постоянно требуются два экземпляра контейнера для этой службы.
  • Строки 26 и 27 важны, они должны соответствовать меткам селектора из службы.
  • В строке 30 указано имя контейнера в поде (в нашем случае в этом поде всего один контейнер, как мы и планировали).
  • В строке 31 указано имя запускаемого образа — это же имя мы указали в файле docker-compose для образа backend.
  • В строке 33 мы открываем порт 80 на этом контейнере для кластера.
  • Строка 34 указывает, что мы не хотим, чтобы Kubernetes извлекал образ, поскольку собираемся создавать образы в контексте minikube docker. В производственном кластере мы планируем указать другие политики, чтобы кластер мог получать обновленные образы из реестра контейнеров (мы увидим это во второй части).

Определение клиентской службы (службы frontend) практически аналогичное, за исключением того, что для настройки придется немного поколдовать. Давайте посмотрим, как это выглядит.

spec:
  containers:
    - name: frontend
      image: frontend
      ports:
      - containerPort: 80
      env:
      - name: "ASPNETCORE_ENVIRONMENT"
        value: "Production"
      volumeMounts:
        - name: config-volume
          mountPath: /app/wwwroot/config/
      imagePullPolicy: Never
  volumes:
    - name: config-volume
      configMap:
        name: demo-app-frontend-config

Примечания:

  • Строка 30: имя контейнера в поде.
  • Строка 31: укажите имя образа для этого контейнера, оно должно совпадать с именем в файле docker-compose.
  • Строки 34–36: пример указания переменных среды для службы.
  • Строки 37–39: ссылка для монтирования тома (указано ниже) для подключения файла конфигурации, из которого Kubernetes узнает, где в файловой системе контейнера должен быть смонтирован файл. В данном случае Kubernetes смонтирует том с именем config-volume по следующему пути в контейнере: /app/wwwroot/config.
  • Строки 41–44: они указывают том, в нашем случае — том configMap для конфигурации (подробнее об этом чуть ниже). Здесь мы просим Kubernetes создать том с именем config-volume (на который ссылается volumeMount контейнера) и использовать для тома данные из configMap с именем demo-app-frontend-config.

Управление конфигурацией


Теперь у нас есть несколько образов контейнеров и мы можем запускать их в minikube. Но прежде чем начать, давайте обсудим конфигурацию. Если вы слышали мои выступления или читали мой блог, то вы знаете, что я — один из главных пропагандистов концепции «создаем один раз, разворачиваем многократно». Это базовый принцип правильного DevOps. В случае с Kubernetes и контейнерами ситуация аналогичная.

Однако для этого вам нужно будет размещать конфигурацию за пределами своего скомпилированного кода, то есть понадобятся такие механизмы, как файлы конфигурации. Если вы выполняете развертывание в службах IIS или Azure App, то можете просто использовать файл web.config (для DotNet Core это будет appsettings.json) и указать разные значения для разных сред. Но как тогда быть с контейнерами? Весь код приложения находится в контейнере, поэтому вы не можете использовать разные версии конфигурационного файла, в противном случае вам понадобятся разные версии самого контейнера, то есть принцип однократного создания будет нарушен.

К счастью, мы можем использовать подключаемые тома (концепция контейнеров) в сочетании с секретами и (или) configMaps (концепция Kubernetes). Мы можем указать configMaps (по сути это пары ключ — значение) или секреты (маскированные или скрытые пары ключ — значение) в Kubernetes, а затем просто смонтировать их путем подключения томов в контейнерах. Это действительно мощный инструмент, поскольку определение пода остается прежним, но если у нас есть другой configMap, мы получаем другую конфигурацию! Мы увидим, как это работает, когда будем выполнять развертывание в облачном кластере и использовать пространства имен для изоляции среды разработки и производственной среды.

configMaps также можно указать с помощью метода «конфигурация как код». Вот конфигурация нашего configMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: demo-app-frontend-config
  labels:
    app: demo
    tier: frontend
data:
  config.json: |
    {
      "api": {
        "baseUri": "http://kubernetes:30081/api"
      }
    }

Примечания:

  • Строка 2: мы указываем, что это определение configMap.
  • Строка 4: имя этой карты.
  • Строка 9: мы указываем, что эта карта использует формат file format, и задаем имя файла — config.json.
  • Строки 10–14: содержимое конфигурационного файла.

Отступление: проблема символьных ссылок на статические файлы


Я действительно столкнулся с одной проблемой при монтировании конфигурационного файла с помощью configMaps: внутри контейнера путь /app/www/config/config.json оканчивается символической ссылкой. Идею использовать configMap в контейнере я подсмотрел в отличной статье Энтони Чу (Anthony Chu), где он описывает, как подключал файл application.json для использования в файле Startup.cs.

Очевидно, с проблемой символической ссылки в файле Startup он не сталкивался. Однако для своего демонстрационного клиентского приложения я создаю конфигурационный файл, который используется SPA-приложением, и поскольку он находится на стороне клиента, конфигурационный файл должен предоставляться из приложения DotNet Core, просто в виде html или js. Нет проблем. У нас уже есть вызов UseStaticFiles в Startup, поэтому он должен просто предоставлять файл, верно? К сожалению, это не так. Скорее всего, он перешлет только первые несколько байтов из этого файла.

Я потратил пару дней, чтобы разобраться с этим. На Github есть специальная тема, можете почитать, если вам интересно. Если коротко, длина символической ссылки — это не длина самого файла, а длина пути к файлу. Промежуточное ПО StaticFiles считывает FileInfo.Length байт при запросе файла, но поскольку эта длина не равна полной длине файла, то возвращаются только первые несколько байтов. Я создал средство FileProvider для обхода этой проблемы.

Запуск образов в Kubernetes


Чтобы запустить вновь созданные службы в minikube, мы можем просто использовать kubectl для применения конфигураций. Вот список команд (выделенные строки):

PS:\> cd k8s
PS:\> kubectl apply -f .\app-demo-frontend-config.yml
configmap "demo-app-frontend-config" created
 
PS:\> kubectl apply -f .\app-demo-backend-minikube.yml
service "demo-backend-service" created
deployment "demo-backend-deployment" created
 
PS:\> kubectl apply -f .\app-demo-frontend-minikube.yml
service "demo-frontend-service" created
deployment "demo-frontend-deployment" created

Теперь у нас есть службы! Вы можете открыть информационную панель командой minikube dashboard и убедиться, что статус служб — зеленый:



Чтобы подключиться к клиентской службе, введите адрес http://kubernetes:30080:


Список (value1 и value2) — это значения, возвращаемые службой API, то есть клиент смог успешно подключиться к серверной службе в minikube!

Обновление контейнеров


После обновления своего кода вам придется пересоздавать контейнер(ы). Обновив конфигурацию, снова запустите команду kubectl apply для обновления configMap. Затем, поскольку нам не нужна высокая доступность в среде разработки, мы можем просто удалить запущенные поды и дать возможность службе репликации перезапустить их, но на этот раз с обновленной конфигурацией и (или) кодом. Конечно, в производственной среде такие вольности недопустимы, и во второй части я покажу вам, как выполнять последовательные обновления, когда мы будем работать с CI/CD в кластере Kubernetes.

Но в среде разработки я получаю список подов, удаляю их все, а затем наблюдаю, как Kubernetes волшебным образом перезапускает контейнеры (с новыми идентификаторами). В итоге я получаю обновленные контейнеры.

PS:> kubectl get pods
NAME                                       READY     STATUS    RESTARTS   AGE
demo-backend-deployment-951716883-fhf90    1/1       Running   0          28m
demo-backend-deployment-951716883-pw1r2    1/1       Running   0          28m
demo-frontend-deployment-477968527-bfzhv   1/1       Running   0          14s
demo-frontend-deployment-477968527-q4f9l   1/1       Running   0          24s
 
PS:> kubectl delete pods demo-backend-deployment-951716883-fhf90 demo
-backend-deployment-951716883-pw1r2 demo-frontend-deployment-477968527-bfzhv demo-frontend-deployment-477968527-q4f9l
pod "demo-backend-deployment-951716883-fhf90" deleted
pod "demo-backend-deployment-951716883-pw1r2" deleted
pod "demo-frontend-deployment-477968527-bfzhv" deleted
pod "demo-frontend-deployment-477968527-q4f9l" deleted
 
PS:> kubectl get pods
NAME                                       READY     STATUS    RESTARTS   AGE
demo-backend-deployment-951716883-4dsl4    1/1       Running   0          3m
demo-backend-deployment-951716883-n6z4f    1/1       Running   0          3m
demo-frontend-deployment-477968527-j2scj   1/1       Running   0          3m
demo-frontend-deployment-477968527-wh8x0   1/1       Running   0          3m

Обратите внимание, что идентификаторы подов меняются, поскольку и сами поды уже другие. Если мы переключимся на клиентскую часть, то увидим обновленный код.

Заключение


Я действительно впечатлен возможностями Kubernetes и тем, как эта платформа способствует популяризации концепции «инфраструктура как код». Вы можете достаточно просто развернуть кластер локально на вашем ноутбуке с помощью minikube и начать разработку в среде, максимально приближенной к производственной, а это всегда хорошая идея. Вы можете использовать секреты и карты configMaps, аналогичные тем, что применяются в производственных контейнерах. В целом это отличный подход к разработке, позволяющий придерживаться лучших практик с самого начала.



P.s. Благодарим Константина Кичинского (Quantum Quintum) за иллюстрацию к этой статье.



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