company_banner

Лучшие практики для деплоя высокодоступных приложений в Kubernetes. Часть 1

    Развернуть в Kubernetes приложение в минимально рабочей конфигурации нетрудно. Но когда вы захотите обеспечить своему приложению максимальную доступность и надежность в работе, вы неизбежно столкнётесь с немалым количеством подводных камней. В этой статье мы попытались систематизировать и ёмко описать самые важные правила для развертывания высокодоступных приложений в Kubernetes.

    Функциональность, которая не доступна в Kubernetes «из коробки», здесь почти не будет затрагиваться. Также мы не будем привязываться к конкретным CD-решениям и опустим вопросы шаблонизации/генерации Kubernetes-манифестов. Рассмотрены только общие правила, касающиеся того, как Kubernetes-манифесты могут выглядеть в конечном итоге при деплое в кластер.

    1. Количество реплик

    Вряд ли получится говорить о какой-либо доступности, если приложение не работает по меньшей мере в двух репликах. Почему при запуске приложения в одной реплике возникают проблемы? Многие сущности в Kubernetes (Node, Pod, ReplicaSet и др.) эфемерны, т. е. при определенных условиях они могут быть автоматически удалены/пересозданы. Соответственно, кластер Kubernetes и запущенные в нём приложения должны быть к этому готовы.

    К примеру, при автомасштабировании узлов вниз, какие-то узлы вместе с запущенными на них Pod'ами будут удалены. Если в это время на удаляемом узле работает ваше приложение в одном экземпляре, то неизбежна полная — хотя обычно и непродолжительная — недоступность приложения. В целом, при работе в одной реплике любое нештатное завершение работы приложения будет означать простой. Таким образом, приложение должно быть запущено по меньшей мере в двух репликах.

    При этом, если реплика экстренно завершает работу, то чем больше рабочих реплик было изначально, тем меньше просядет вычислительная способность всего приложения. К примеру, если у приложения всего две реплики и одна из них перестала работать из-за сетевых проблем на узле, то приложение теперь сможет выдержать только половину первоначальной нагрузки (одна реплика доступна, одна — недоступна). Конечно, через некоторое время новая реплика приложения будет поднята на новом узле, и работоспособность полностью восстановится. Но до тех пор увеличение нагрузки на единственную рабочую реплику может приводить к перебоям в работе приложения, поэтому количество реплик должно быть с запасом.

    Рекомендации актуальны, если не используется HorizontalPodAutoscaler. Лучший вариант для приложений, у которых будет больше нескольких реплик, — настроить HorizontalPodAutoscaler и забыть про указание количества реплик вручную. О HorizontalPodAutoscaler мы поговорим в следующей статье.

    2. Стратегия обновления

    Стратегия обновления у Deployment'а по умолчанию такая, что почти до конца обновления только 75% Pod'ов старого+нового ReplicaSet'а будут в состоянии Ready. Таким образом, при обновлении приложения его вычислительная способность может падать до 75%, что может приводить к частичному отказу. Отвечает за это поведение параметр strategy.rollingUpdate.maxUnavailable. Поэтому убедитесь, что приложение не теряет в работоспособности при отказе 25% Pod'ов, либо уменьшите maxUnavailable. Округление maxUnavailable происходит вниз.

    Также у стратегии обновления по умолчанию (RollingUpdate) есть нюанс: приложение некоторое время будет работать не только в несколько реплик, но и в двух разных версиях — разворачивающейся сейчас и развернутой до этого. Поэтому, если приложение не может даже непродолжительное время работать в нескольких репликах и нескольких разных версиях, то используйте strategy.type: Recreate. При Recreate новые реплики будут подниматься только после того, как удалятся старые. Очевидно, здесь у приложения будет небольшой простой.

    Альтернативные стратегии деплоя (blue-green, canary и др.) часто могут быть гораздо лучшей альтернативой RollingUpdate, но здесь мы не будем их рассматривать, так как их реализация зависит от того, какое ПО вы используете для деплоя. Это выходит за рамки текущей статьи. (См. также статью «Стратегии деплоя в Kubernetes: rolling, recreate, blue/green, canary, dark (A/B-тестирование)» в нашем блоге.)

    3. Равномерное распределение реплик по узлам

    Очень важно разносить Pod'ы приложения по разным узлам, если приложение работает в нескольких репликах. Для этого рекомендуйте планировщику не запускать несколько Pod'ов одного Deployment'а на одном и том же узле:

          affinity:
            podAntiAffinity:
              preferredDuringSchedulingIgnoredDuringExecution:
              - podAffinityTerm:
                  labelSelector:
                    matchLabels:
                      app: testapp
                  topologyKey: kubernetes.io/hostname

    Предпочитайте preferredDuringScheduling вместо requiredDuringScheduling, который может привести к невозможности запустить новые Pod'ы, если доступных узлов окажется меньше, чем новым Pod'ам требуется. Тем не менее, requiredDuringScheduling может быть полезен, когда количество узлов и реплик приложения точно известно и необходимо быть уверенным, что два Pod'а не смогут оказаться на одном и том же узле.

    4. Приоритет

    priorityClassName влияет на то, какие Pod'ы будут schedule'иться в первую очередь, а также на то, какие Pod'ы могут быть «вытеснены» (evicted) планировщиком, если места для новых Pod'ов на узлах не осталось.

    Потребуется создать несколько ресурсов типа PriorityClass и ассоциировать их с Pod'ами через priorityClassName. Набор PriorityClass'ов может выглядеть примерно так:

    • Cluster. Priority > 10000. Критичные для функционирования кластера компоненты, такие как kube-apiserver.

    • Daemonsets. Priority: 10000. Обычно мы хотим, чтобы Pod'ы DaemonSet'ов не вытеснялись с узлов обычными приложениями.

    • Production-high. Priority: 9000. Stateful-приложения.

    • Production-medium. Priority: 8000. Stateless-приложения.

    • Production-low. Priority: 7000. Менее критичные приложения.

    • Default. Priority: 0. Приложения для окружений не категории production.

    Это предохранит нас от внезапных evict'ов важных компонентов и позволит более важным приложениям вытеснять менее важные при недостатке узлов.

    5. Остановка процессов в контейнерах

    При остановке контейнера всем процессам в нём отправляется сигнал, указанный в STOPSIGNAL (обычно это TERM). Но не все приложения умеют правильно реагировать на него и делать graceful shutdown, который бы корректно отработал и для приложения, запущенного в Kubernetes.

    Например, чтобы сделать корректную остановку nginx, нам понадобится preStop-хук вроде этого:

    lifecycle:
      preStop:
        exec:
          command:
          - /bin/sh
          - -ec
          - |
            sleep 3
            nginx -s quit
    1. sleep 3 здесь для страховки от race conditions, связанных с удалением endpoint. 

    2. nginx -s quit инициирует корректное завершение работы для nginx. Хотя в свежих образах nginx эта строка больше не понадобится, т. к. там STOPSIGNAL: SIGQUIT установлен по умолчанию.

    (Более подробно про graceful shutdown для nginx в связке с PHP-FPM вы можете узнать из другой нашей статьи.)

    Корректно ли ваше приложение обработает STOPSIGNAL, зависит только от него. На практике для большинства приложений приходится гуглить, как оно обрабатывает указанный для него STOPSIGNAL. И если оказывается, что не так, как надо, то делается preStop-хук, который эту проблему решает, либо же STOPSIGNAL меняется на тот, который приложение сможет обработать корректно и штатно завершиться.

    Ещё один важный параметр, связанный с остановкой приложения, — terminationGracePeriodSeconds. Он отвечает за то, сколько времени будет у приложения на корректное завершение. Если приложение не успеет завершиться в течение этого времени (30 секунд по умолчанию), то приложению будет послан сигнал KILL. Таким образом, если вы ожидаете, что выполнение preStop-хука и/или завершение работы приложения при получении STOPSIGNAL могут занять более 30 секунд, то terminationGracePeriodSeconds нужно будет увеличить. Например, такое может потребоваться, если некоторые запросы у клиентов веб-сервиса долго выполняются (вроде запросов на скачивание больших файлов).

    Стоит заметить, что preStop-хук выполняется блокирующе, т. е. STOPSIGNAL будет послан только после того, как preStop-хук отработает. Тем не менее, отсчет terminationGracePeriodSeconds идёт и в течение работы preStop-хука. А процессы, запущенные в хуке, равно как и все процессы в контейнере, получат сигнал KILL после того, как terminationGracePeriodSeconds закончится.

    Также у некоторых приложений встречаются специальные настройки, регулирующие время, в течение которого приложение должно завершить свою работу (к примеру, опция --timeout у Sidekiq). Оттого для каждого приложения надо убеждаться, что если у него есть подобная настройка, то она выставлена в значение немного меньшее, чем terminationGracePeriodSeconds.

    6. Резервирование ресурсов

    Планировщик на основании resources.requests Pod'а принимает решение о том, на каком узле этот Pod запустить. К примеру, Pod не будет schedule'иться на узел, на котором свободных (т. е. non-requested) ресурсов недостаточно, чтобы удовлетворить запросам (requests) нового Pod'а. А resources.limits позволяют ограничить потребление ресурсов Pod'ами, которые начинают расходовать ощутимо больше, чем ими было запрошено через requests. Лучше устанавливать лимиты равные запросам, так как если указать лимиты сильно выше, чем запросы, то это может лишить другие Pod'ы узла выделенных для них ресурсов. Это может приводить к выводу из строя других приложений на узле или даже самого узла. Также схема ресурсов Pod'а присваивает ему определенный QoS class: например, он влияет на порядок, в котором Pod'ы будут вытесняться (evicted) с узлов.

    Поэтому необходимо выставлять и запросы, и лимиты и для CPU, и для памяти. Единственное, что можно/нужно опустить, так это CPU-лимит, если версия ядра Linux ниже 5.4 (для EL7/CentOS7 версия ядра должна быть ниже 3.10.0-1062.8.1.el7).

    (Подробнее о том, что такое requests и limits, какие бывают QoS-классы в Kubernetes, мы рассказывали в этой статье.)

    Также некоторые приложения имеют свойство бесконтрольно расти в потреблении оперативной памяти: к примеру, Redis, использующийся для кэширования, или же приложение, которое «течёт» просто само по себе. Чтобы ограничить их влияние на остальные приложения на том же узле, им можно и нужно устанавливать лимит на количество потребляемой памяти. Проблема только в том, что, при достижении этого лимита приложение будет получать сигнал KILL. Приложения не могут ловить/обрабатывать этот сигнал и, вероятно, не смогут корректно завершаться. Поэтому очень желательно использовать специфичные для приложения механизмы контроля за потреблением памяти в дополнение к лимитам Kubernetes, и не доводить эффективное потребление памяти приложением до limits.memory Pod'а.

    Конфигурация для Redis, которая поможет с этим:

    maxmemory 500mb   # если данные начнут занимать 500 Мб...
    maxmemory-policy allkeys-lru   # ...Redis удалит редко используемые ключи

    А для Sidekiq это может быть Sidekiq worker killer:

    require 'sidekiq/worker_killer'
    Sidekiq.configure_server do |config|
      config.server_middleware do |chain|
        # Корректно завершить Sidekiq при достижении им потребления в 500 Мб
        chain.add Sidekiq::WorkerKiller, max_rss: 500
      end
    end

    Понятное дело, что во всех этих случаях limits.memory должен быть выше, чем пороги срабатывания вышеуказанных механизмов.

    В следующей статье мы также рассмотрим использование VerticalPodAutoscaler для автоматического выставления ресурсов.

    7. Пробы

    В Kubernetes пробы (healthcheck'и) используются для того, чтобы определить, можно ли переключить на приложение трафик (readiness) и не нужно ли приложение перезапустить (liveness). Они играют большую роль при обновлении Deployment'ов и при запуске новых Pod'ов в целом.

    Сразу общая рекомендация для всех проб: выставляйте высокий timeoutSeconds. Значение по умолчанию в одну секунду — слишком низкое. Особенно критично для readinessProbe и livenessProbe. Слишком низкий timeoutSeconds будет приводить к тому, что при увеличении времени ответов у приложений в Pod'ах (что обычно происходит для всех Pod'ов сразу благодаря балансированию нагрузки с помощью Service) либо перестанет приходить трафик почти во все Pod'ы (readiness), либо, что ещё хуже, начнутся каскадные перезапуски контейнеров (liveness).

    7.1 Liveness probe

    На практике вам не так часто нужна liveness probe (дословно: «проверка на жизнеспособность»), насколько вы думаете. Её предназначение — перезапустить контейнер с приложением, когда livenessProbe перестаёт отрабатывать, например, если приложение намертво зависло. На практике подобные deadlock’и скорее исключение, чем правило. Если же приложение работает, но не полностью (например, приложение не может само восстановить соединение с БД, если оно оборвалось), то это нужно исправлять в самом приложении, а не накручивать «костыли» с livenessProbe.

    И хотя как временное решение можно добавить в livenessProbe проверку на подобные состояния, по умолчанию livenessProbe лучше либо совсем не использовать, либо делать очень простую liveness-пробу, вроде проверки возможности TCP-соединения (обязательно выставьте большой таймаут). В таком случае это поможет приложению перезапуститься при возникновении очевидного deadlock'а, но при этом приложение не подвергнется риску войти в цикл перезапусков, когда перезапуск не может помочь.

    И риски, которые плохая livenessProbe несёт, весьма серьезные. Самые частые случаи: когда livenessProbe перестаёт отрабатывать по таймауту из-за повышенной нагрузки на приложение, а также когда livenessProbe перестаёт работать, т. к. проверяет (прямо или косвенно) состояние внешних зависимостей, которые сейчас отказали. В последнем случае последует перезагрузка всех контейнеров, которая при лучшем раскладе ни к чему не приведет, а при худшем — приведет к полной (и, возможно, длительной) недоступности приложения. Полная длительная недоступность приложения может происходить, если при большом количестве реплик контейнеры большинства Pod'ов начнут перезагружаться в течение короткого промежутка времени. При этом какие-то контейнеры, скорее всего, поднимутся быстрее других, и на это ограниченное количество контейнеров теперь придется вся нагрузка, которая приведет к таймаутам у livenessProbe и заставит контейнеры снова перезапускаться.

    Также, если все-таки используете livenessProbe, убедитесь, что она не перестает отвечать, если у вашего приложения есть лимит на количество установленных соединений и этот лимит достигнут. Чтобы этого избежать, обычно требуется зарезервировать под livenessProbe отдельный тред/процесс самого приложения. Например, запускайте приложение с 11 тредами, каждый из которых может обрабатывать одного клиента, но не пускайте извне в приложение более 10 клиентов, таким образом гарантируя для livenessProbe отдельный незанятый тред.

    И, конечно, не стоит добавлять в livenessProbe проверки внешних зависимостей.

    (Подробнее о проблемах с liveness probe и рекомендациях по предотвращению таких проблем рассказывалось в этой статье.)

    7.2 Readiness probe

    Дизайн readinessProbe (дословно: «проверка на готовность [к обслуживанию запросов]»), пожалуй, оказался не очень удачным. Она сочетает в себе две функции: проверять, что приложение в контейнере стало доступным при запуске контейнера, и проверять, что приложение остаётся доступным уже после его запуска. На практике первое нужно практически всегда, а второе примерно настолько же часто, насколько оказывается нужной livenessProbe. Проблемы с плохими readinessProbe примерно те же самые, что и с плохими livenessProbe, и в худшем случае также могут приводить к длительной недоступности приложения.

    Когда readinessProbe перестаёт отрабатывать, то на Pod перестаёт приходить трафик. В большинстве случаев такое поведение мало помогает, т. к. трафик обычно балансируется между Pod'ами более-менее равномерно. Таким образом, чаще всего readinessProbe либо работает везде, либо не работает сразу на большом количестве Pod'ов. Есть ситуации, когда подобное поведение readinessProbe может понадобиться, но в моей практике это скорее исключение.

    Тем не менее, у readinessProbe есть другая очень важная функция: определить, когда только что запущенное в контейнере приложение стало способно принимать трафик, чтобы не пускать трафик в ещё не доступное приложение. Эта же функция readinessProbe, напротив, нужна нам почти всегда.

    Получается странная ситуация, что одна функция readinessProbe обычно очень нужна, а другая очень не нужна. Эта проблема была решена введением startupProbe, которая появилась в Kubernetes 1.16 и перешла в Beta в 1.18. Таким образом, рекомендую для проверки готовности приложения при его запуске в Kubernetes < 1.18 использовать readinessProbe, а в Kubernetes >= 1.18 — использовать startupProbe. readinessProbe всё ещё можно использовать в Kubernetes >= 1.18, если у вас есть необходимость останавливать трафик на отдельные Pod'ы уже после старта приложения.

    7.3 Startup probe

    startupProbe (дословно: «проверка на запуск») реализует первоначальную проверку готовности приложения в контейнере для того, чтобы пометить текущий Pod как готовый к приёму трафика, или же для того, чтобы продолжить обновление/перезапуск Deployment'а. В отличие от readinessProbe, startupProbe прекращает работать после запуска контейнера. Проверять внешние зависимости в startupProbe не лучшая идея, потому что если startupProbe не отработает, то контейнер будет перезапущен, что может приводить к переходу Pod'а в состояние CrashLoopBackOff. При этом состоянии между попытками перезапустить неподнимающийся контейнер будет делаться задержка до пяти минут. Это может означать простой в том случае, когда приложение уже может подняться, но контейнер всё ещё выжидает CrashLoopBackOff перед тем, как снова попробовать запуститься.

    Обязательна к использованию, если ваше приложение принимает трафик и у вас Kubernetes >= 1.18.

    Также предпочитайте увеличение failureTreshold вместо использования initialDelaySeconds. Это позволит контейнеру становиться доступным настолько быстро, насколько это возможно.

    8. Проверка внешних зависимостей

    Часто можно встретить совет проверять внешние зависимости вроде баз данных в readinessProbe. И хотя такой подход имеет право на существование, предпочтительно разделять проверку внешних зависимостей и проверку на то, не стоит ли остановить идущий на Pod трафик, когда приложение в нём полностью утилизировано.

    С помощью initContainers можно проверять внешние зависимости до того, как начнут запускаться startupProbe/readinessProbe основных контейнеров. В readinessProbe, соответственно, проверки внешних зависимостей уже не понадобится. Подобные initContainers не требуют изменений в коде приложения, не требуют собирать контейнеры приложения с дополнительными утилитами для проверок внешних зависимостей, а также в целом довольно просты в реализации:

          initContainers:
          - name: wait-postgres
            image: postgres:12.1-alpine
            command:
            - sh
            - -ec
            - |
              until (pg_isready -h example.org -p 5432 -U postgres); do
                sleep 1
              done
            resources:
              requests:
                cpu: 50m
                memory: 50Mi
              limits:
                cpu: 50m
                memory: 50Mi
          - name: wait-redis
            image: redis:6.0.10-alpine3.13
            command:
            - sh
            - -ec
            - |
              until (redis-cli -u redis://redis:6379/0 ping); do
                sleep 1
              done
            resources:
              requests:
                cpu: 50m
                memory: 50Mi
              limits:
                cpu: 50m
                memory: 50Mi

    Полный пример

    Резюмируя, привёдем полный пример того, как уже с учётом всех вышеописанных рекомендаций может выглядеть Deployment stateless-приложения при его боевом развертывании.

    Требования: Kubernetes >= 1.18, на узлах Ubuntu/Debian с версией ядра >= 5.4.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: testapp
    spec:
      replicas: 10
      selector:
        matchLabels:
          app: testapp
      template:
        metadata:
          labels:
            app: testapp
        spec:
          affinity:
            podAntiAffinity:
              preferredDuringSchedulingIgnoredDuringExecution:
              - podAffinityTerm:
                  labelSelector:
                    matchLabels:
                      app: testapp
                  topologyKey: kubernetes.io/hostname
          priorityClassName: production-medium
          terminationGracePeriodSeconds: 40
          initContainers:
          - name: wait-postgres
            image: postgres:12.1-alpine
            command:
            - sh
            - -ec
            - |
              until (pg_isready -h example.org -p 5432 -U postgres); do
                sleep 1
              done
            resources:
              requests:
                cpu: 50m
                memory: 50Mi
              limits:
                cpu: 50m
                memory: 50Mi
          containers:
          - name: backend
            image: my-app-image:1.11.1
            command:
            - run
            - app
            - --trigger-graceful-shutdown-if-memory-usage-is-higher-than
            - 450Mi
            - --timeout-seconds-for-graceful-shutdown
            - 35s
            startupProbe:
              httpGet:
                path: /simple-startup-check-no-external-dependencies
                port: 80
              timeoutSeconds: 7
              failureThreshold: 12
            lifecycle:
              preStop:
                exec:
                  ["sh", "-ec", "#command to shutdown gracefully if needed"]
            resources:
              requests:
                cpu: 200m
                memory: 500Mi
              limits:
                cpu: 200m
                memory: 500Mi

    В следующий раз

    Осталось ещё немало важных вещей, о которых обязательно надо рассказать, таких как PodDisruptionBudget, HorizontalPodAutoscaler и VerticalPodAutoscaler, чем мы непременно займемся во второй части этой статьи. А пока предлагаем вам поделиться своими лучшими практиками по деплою, либо исправить/дополнить уже описанные.

    ОБНОВЛЕНО: Вторая часть статьи вышла и доступна здесь.

    P.S.

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

    Флант
    DevOps-as-a-Service, Kubernetes, обслуживание 24×7

    Комментарии 18

      +2

      Спасибо, статья в целом полезна.


      Есть сомнения по поводу проверки внешних зависимостей в initContainers, а именно — как сделано в вашем примере — с использованием образов бд. Идея скачать и запустить целый постгрес, чтобы проверить соединение с постгрес через его cli клиент, выглядит мягко говоря избыточно. Мы у себя обычно делаем это внутри самого приложения.

        +3
        Действительно, можно реализовать проверку внутри приложения. Инит-контейнеры не использовать. Стартап-проба только в таком случае по-хорошему не должна проверять внешние зависимости, иначе проба начнет падать и Pod может войти в CrashLoopBackOff, что приведет к лишнему простою.

        При этом само приложение не должно падать, когда внешняя зависимость становится недоступной, иначе опять же Pod может войти в CrashLoopBackOff. Вместо этого надо ретраить внешнюю зависимость, пока она не поднимется.

        Если всё это получается реализовать, то соглашусь, такой подход предпочтительнее инит-контейнеров. Это у меня профдеформация видимо уже малехонькая, в нашем случае «ребят, ваше приложение надо переписать» нечасто работает :)

        Идея скачать и запустить целый постгрес

        Там образ весит всего 50Мб, на практике это ничего особо не аффектит. Сам postgresql-сервер в данном случае конечно не запускается. Если всё же образ беспокоит, то можно либо самому собрать минимальный образ только с pg_isready, либо засунуть pg_isready в контейнер с приложением и поэкспериментировать с чем-то вроде

        readinessProbe:
          exec:
            command:
            - sh
            - -ec
            - |
              pg_isready ....
              redis-cli ping ....


        И всё же, ваш вариант мне видится самым предпочтительным. Остается только вопрос, стоит ли в вашем варианте вынести проверку внешних зависимостей на какой-нибудь /readiness, где проверять этот эндпоинт в readinessProbe.
        +4

        Спасибо, все по делу. Задумался о пробах и их корректности в очередной раз.

          +1

          То чувство, когда cicd проекта выполнен почти по статье))


          • в хорошие стандартные практики — явное проставление конкретной версии собираемого образа, не использовать latest
            0

            И pull always ))) вместе с зависимыми проверками в readness probe это просто катастрофа

              0

              у pull always (точнее его неиспользования) есть очень интересный сайд эффект.
              Сам удивлен был. Кратко — если PullAlways не стоит, то пользователь кластера может инстанцировать под на произвольной ноде из любого образа, который на ней лежит. Проверки на то, что пользователь знает секрет, с помощью которого образ можно скачать из регистри, не производится. Вот так вот.
              Так что помимо здравого смысла PullAlways надо еще ставить по соображениям ИБ.
              Ссылка на CIS стандарт для кубернетеса

                0
                Так надо не latest качать, а что-то более точное. Вроде бы этого хватает, чтобы скачать обновленные версии. У нас сборки тегируются хешем коммита в git. Вы сталкивались с проблемами (кроме проблем с secrets) даже в таком случае?
                  0
                  Так надо не latest качать, а что-то более точное.

                  только это "точное" должно быть каждый раз новое.


                  У нас сборки тегируются хешем коммита в git.

                  это вопрос к CI/CD, но вообще — вы не фиксируете зависимости на уровне пакетов внутри Dockerfile. Следовательно, две последовательные сборки образа из одного sha могут быть разные, причем существенно. Следовательно, возникает большое желание либо идти в сторону reproducible build, но соответствующая инфра для докера пока отсутствует, либо в сторону идеи, что если у нас уже есть собранный образ, то мы не имеем права его менять == иммутабельность тегов. Если нужна новая сборка — будьте добры сделайте новый коммит -> новый Sha -> новая пачка образов.


                  Так что, в целом, да — у вас верный подход.

                    0

                    Immutable образы в этом контексте выглядят самой подходящее практикой имхо. И решается не сложно, как уже упоминали, версионировать образы по sha коммита, плюс запрет на перезапись артифактов в хранилке. По крайней мере для дев окружений, в продакшн с семвер версинированием как-то удобнее )

                      0
                      Так можно же не sha git commit, а sha контейнера деплоить:
                      $ docker inspect alpine:latest -f "{{.RepoDigests}}"
                      $ kubectl set image deployment/test alpine=alpine@sha256:08d6ca16c60fe7490c03d10dc339d9fd8ea67c6466dea8d558526b1330a85930 --record


                      А если лень потом искать sha коммита в пайплане, можно commit sha в метаданные лейбла при docker build засовывать:
                      $ docker build -t ${DOCKER_IMG}:${MOVING_TAG} --build-arg=COMMIT=$(git rev-parse --short HEAD) .
                      соответсвенно в Dockerfile должно быть
                      LABEL commit=${COMMIT}

                        0
                        Так можно же не sha git commit, а sha контейнера деплоить:

                        докопаюсь — не "контейнера", а "образа"
                        И все равно это плохая практика… потому что иммутабельность образов ломается.


                        $ docker build -t ${DOCKER_IMG}:${MOVING_TAG} --build-arg=COMMIT=$(git rev-parse --short HEAD).
                        соответсвенно в Dockerfile должно быть
                        LABEL commit=${COMMIT}

                        а это — одобрям-с, запекать служебную инфу в LABELs и ENV'ы — хорошая практика.

                +1
                И, с учётом лимита на Docker Hub — задуматься о складывание образов в свой приватный registry.
                  0
                  Мы просто купили платный docker hub на год на одного юзера и вопрос исчез
                  +1
                  в хорошие стандартные практики — явное проставление конкретной версии собираемого образа, не использовать latest

                  и использование иммутабельных тегов по возможности (но там тоже большие нюансы).


                  n_bogdanov


                  И, с учётом лимита на Docker Hub — задуматься о складывание образов в свой приватный registry.

                  это нужно ВНЕ ЗАВИСИМОСТИ от лимитов Docker Hub. Это вопрос безопасности, т.к. в докерхабе образ подменят или удалят — и все, твой проект встанет

                  +3

                  я чего подумал еще — здесь речь идет о практиках деплоя "высокодоступных" приложений, но по ходе нам еще нужно добавить несколько grains of salt (щепоток соли):


                  1. безопасность — потому что все в целом на нее забивают и хотя бы минимальная ее имплементация может быть конкурентным преимуществом и shift left позволяет избежать больших трат, когда выясняется, что чтобы "сделать безопасно" приходится перелопатить пол-проекта. Заодно безопасность часто имеет отношение к доступности и целостности систем (но иногда вступает с доступностью и быстродействием в противоречие).
                  2. не хватает отказоустойчивости связанных компонентов — тех же БД, хранилищ и прочего. И паттернов реализации этого на уровне микросервиса — retry, circuit breaker etc. Но это больше архитектурные паттерны, чем особенности кубернетеса самого, хотя они могут быть реализованы на уровне service mesh...
                    0
                    Всё это интересно и мы подумаем, что из этого мы можем захватить, хотя возможно уже в рамках других статей. Тут хотелось ограничить охват того, что описываем, а то как видите, даже несмотря на то, что опустили шаблонизацию и опустили какие-то слишком уж очевидные (на мой взгляд) вещи, вроде пина тегов, а итак вышло довольно объёмно. Ну и вторая часть статьи сейчас в работе.
                    +2
                    Эх, эти бы заметки да два года назад, когда все эти грабли были собраны на собственном опыте. Но для тех кто только вступает на эту стезю — будет очень полезно.
                      +1
                      Тут как в анекдоте: всё знаем, всё умеем, но уже поздно.

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое