Лучший подход к непрерывному развертыванию Google Cloud
Опубликовано: 2022-03-11Непрерывное развертывание (CD) — это практика автоматического развертывания нового кода в рабочей среде. Большинство систем непрерывного развертывания проверяют работоспособность кода, подлежащего развертыванию, путем выполнения модульных и функциональных тестов, и, если все выглядит хорошо, развертывание развертывается. Само развертывание обычно происходит поэтапно, чтобы можно было выполнить откат, если код ведет себя не так, как ожидалось.
В блогах нет недостатка в сообщениях о том, как реализовать собственный конвейер CD с использованием различных инструментов, таких как стек AWS, стек Google Cloud, конвейер Bitbucket и т. д. Но я обнаружил, что большинство из них не соответствуют моему представлению о том, что такое хороший конвейер CD. должен выглядеть так: тот, который сначала строит, а затем тестирует и развертывает только этот единственный созданный файл.
В этой статье я собираюсь создать управляемый событиями конвейер непрерывного развертывания, который сначала выполняет сборку, а затем запускает тесты на нашем окончательном артефакте развертывания. Это не только делает результаты наших тестов более надежными, но и упрощает расширение конвейера CD. Это будет выглядеть примерно так:
- Коммит сделан в наш исходный репозиторий.
- Это запускает сборку связанного образа.
- Тесты выполняются на построенном артефакте.
- Если все выглядит хорошо, образ развертывается в рабочей среде.
В этой статье предполагается, по крайней мере, поверхностное знакомство с Kubernetes и технологией контейнеров, но если вы незнакомы или хотели бы освежить знания, см. Что такое Kubernetes? Руководство по контейнеризации и развертыванию.
Проблема с большинством настроек компакт-диска
Вот моя проблема с большинством конвейеров CD: они обычно делают все в файле сборки. Большинство сообщений в блогах, которые я читал об этом, будут иметь некоторые варианты следующей последовательности в любом файле сборки, который у них есть ( cloudbuild.yaml
для Google Cloud Build, bitbucket-pipeline.yaml
для Bitbucket).
- Запустить тесты
- Построить образ
- Отправить изображение в репозиторий контейнера
- Обновить среду с новым изображением
Вы не проводите тесты на своем окончательном артефакте.
Выполняя действия в этом порядке, вы запускаете свои тесты. Если они успешны, вы создаете образ и продолжаете работу с остальной частью конвейера. Что произойдет, если процесс сборки изменил ваш образ таким образом, что тесты больше не будут проходить? На мой взгляд, вы должны начать с создания артефакта (конечного образа контейнера), и этот артефакт не должен меняться между сборкой и моментом его развертывания в рабочей среде. Это гарантирует, что имеющиеся у вас данные об указанном артефакте (результаты тестирования, размер и т. д.) всегда действительны.
В вашей среде сборки есть «ключи от королевства».
Используя среду сборки для развертывания вашего образа в производственном стеке, вы фактически позволяете ему изменить вашу производственную среду. Я считаю это очень плохой вещью, потому что любой, у кого есть доступ на запись к вашему исходному репозиторию, теперь может делать все, что захочет, с вашей производственной средой.
Вы должны перезапустить весь конвейер, если последний шаг не удался.
Если последний шаг завершится неудачно (например, из-за проблемы с учетными данными), вам придется перезапустить весь конвейер, что займет время и другие ресурсы, которые лучше было бы потратить на что-то другое.
Это подводит меня к моему последнему пункту:
Ваши шаги не являются независимыми.
В более общем смысле наличие независимых шагов позволяет вам иметь большую гибкость в конвейере. Допустим, вы хотите добавить функциональные тесты в свой пайплайн. Имея свои шаги в одном файле сборки, вам нужно, чтобы ваша среда сборки запускала среду функционального тестирования и запускала в ней тесты (скорее всего, последовательно). Если бы ваши шаги были независимыми, вы могли бы запускать как модульные, так и функциональные тесты с помощью события «построение образа». Затем они будут работать параллельно в своей среде.
Моя идеальная установка компакт-диска
На мой взгляд, лучшим способом решения этой проблемы было бы наличие ряда независимых шагов, связанных между собой механизмом событий.
Это имеет ряд преимуществ по сравнению с предыдущим методом:
Вы можете предпринять несколько независимых действий по разным событиям.
Как указано выше, успешное создание нового образа просто опубликует событие «успешная сборка». В свою очередь, мы можем запускать несколько вещей, когда это событие срабатывает. В нашем случае мы бы запустили модульные и функциональные тесты. Вы также можете подумать о таких вещах, как оповещение разработчика, когда запускается событие сбоя сборки или если тесты не проходят.
Каждая среда имеет свой собственный набор прав.
Благодаря тому, что каждый шаг выполняется в своей собственной среде, мы устраняем необходимость в одной среде, чтобы иметь все права. Теперь среда сборки может только строить, тестовая среда может только тестировать, а среда развертывания может только развертывать. Это позволяет вам быть уверенным, что после того, как ваш образ будет создан, он не изменится. Произведенный артефакт — это тот, который окажется в вашем производственном стеке. Это также позволяет упростить аудит того, какой шаг вашего конвейера что делает, поскольку вы можете связать один набор учетных данных с одним шагом.
Существует больше гибкости.
Хотите отправить электронное письмо кому-нибудь о каждой успешной сборке? Просто добавьте что-то, что реагирует на это событие и отправляет электронное письмо. Это просто — вам не нужно менять свой код сборки и вам не нужно жестко кодировать чью-то электронную почту в исходном репозитории.
Повторные попытки проще.
Наличие независимых шагов также означает, что вам не нужно перезапускать весь конвейер в случае сбоя одного шага. Если состояние сбоя является временным или было исправлено вручную, вы можете просто повторить неудачный шаг. Это позволяет создать более эффективный конвейер. Если этап сборки занимает несколько минут, лучше не перестраивать образ только потому, что вы забыли предоставить среде развертывания доступ на запись в кластер.
Внедрение непрерывного развертывания Google Cloud
Google Cloud Platform имеет все инструменты, необходимые для создания такой системы за короткий промежуток времени и с очень небольшим количеством кода.
Наше тестовое приложение представляет собой простое приложение Flask, которое просто обслуживает фрагмент статического текста. Это приложение развертывается в кластере Kubernetes, который обслуживает его в более широком Интернете.
Я буду реализовывать упрощенную версию конвейера, который я представил ранее. Я в основном удалил тестовые шаги, поэтому теперь это выглядит так:
- Новая фиксация сделана в исходном репозитории
- Это запускает сборку образа. В случае успеха оно помещается в репозиторий контейнеров, а событие публикуется в теме Pub/Sub.
- На эту тему подписывается небольшой скрипт, который проверяет параметры образа — если они совпадают с тем, что мы просили, он развертывается в кластере Kubernetes.
Вот графическое представление нашего пайплайна.
Поток выглядит следующим образом:
- Кто-то делает коммит в наш репозиторий.
- Это запускает облачную сборку, которая создает образ Docker на основе исходного репозитория.
- Облачная сборка отправляет образ в репозиторий контейнеров и публикует сообщение в облачной публикации/подписке.
- Это запускает облачную функцию, которая проверяет параметры опубликованного сообщения (статус сборки, имя построенного образа и т. д.).
- Если параметры в порядке, облачная функция обновляет развертывание Kubernetes новым образом.
- Kubernetes развертывает новые контейнеры с новым образом.
Исходный код
Наш исходный код представляет собой очень простое приложение Flask, которое просто обслуживает статический текст. Вот структура нашего проекта:

├── docker │ ├── Dockerfile │ └── uwsgi.ini ├── k8s │ ├── deployment.yaml │ └── service.yaml ├── LICENSE ├── Pipfile ├── Pipfile.lock └── src └── main.py
Каталог Docker содержит все необходимое для создания образа Docker. Образ основан на образе uWSGI и Nginx и просто устанавливает зависимости и копирует приложение по правильному пути.
Каталог k8s содержит конфигурацию Kubernetes. Он состоит из одной службы и одного развертывания. Развертывание запускает один контейнер на основе образа, созданного из Dockerfile . Затем служба запускает балансировщик нагрузки с общедоступным IP-адресом и перенаправляет его в контейнеры приложений.
Облачная сборка
Саму настройку облачной сборки можно выполнить через облачную консоль или командную строку Google Cloud. Я решил использовать облачную консоль.
Здесь мы создаем образ для любого коммита в любой ветке, но, например, у вас могут быть разные образы для разработки и производства.
Если сборка прошла успешно, облачная сборка самостоятельно опубликует образ в реестре контейнеров. Затем он опубликует сообщение в теме публикации/подраздела cloud-builds.
Облачная сборка также публикует сообщения, когда сборка выполняется и когда происходит сбой, поэтому вы также можете реагировать на эти сообщения.
Документация по уведомлениям pub/sub в облачной сборке находится здесь, а формат сообщения можно найти здесь.
Облачный паб/саб
Если вы посмотрите на вкладку облачной публикации/подписки в облачной консоли, вы увидите, что облачная сборка создала тему под названием облачные сборки. Здесь облачная сборка публикует обновления своего статуса.
Облачная функция
Что мы сейчас сделаем, так это создадим облачную функцию, которая срабатывает при любом сообщении, опубликованном в теме облачных сборок. Опять же, вы можете использовать облачную консоль или утилиту командной строки Google Cloud. Что я сделал в моем случае, так это то, что я использую облачную сборку для развертывания облачной функции каждый раз, когда в нее вносятся изменения.
Исходный код облачной функции находится здесь.
Давайте сначала посмотрим на код, который развертывает эту облачную функцию:
steps: - name: 'gcr.io/cloud-builders/gcloud' id: 'test' args: ['functions', 'deploy', 'new-image-trigger', '--runtime=python37', '--trigger-topic=cloud-builds', '--entry-point=onNewImage', '--region=us-east1', '--source=https://source.developers.google.com/projects/$PROJECT_ID/repos/$REPO_NAME']
Здесь мы используем образ Google Cloud Docker. Это позволяет легко запускать команды GCcloud. То, что мы выполняем, эквивалентно запуску следующей команды напрямую из терминала:
gcloud functions deploy new-image-trigger --runtime=python37 --trigger-topic=cloud-builds --entry-point=onNewImage --region=us-east1 --source=https://source.developers.google.com/projects/$PROJECT_ID/repos/$REPO_NAME
Мы просим Google Cloud развернуть новую облачную функцию (или заменить ее, если функция с таким именем в этом регионе уже существует), которая будет использовать среду выполнения Python 3.7 и будет активироваться новыми сообщениями в теме облачных сборок. Мы также сообщаем Google, где найти исходный код этой функции (здесь PROJECT_ID и REPO_NAME — это переменные среды, которые устанавливаются в процессе сборки). Мы также сообщаем ему, какую функцию вызывать в качестве точки входа.
В качестве побочного примечания: чтобы это работало, вам необходимо предоставить вашей учетной записи службы cloudbuild как «разработчика облачных функций», так и «пользователя учетной записи службы», чтобы он мог развертывать облачную функцию.
Вот несколько закомментированных фрагментов кода облачной функции.
Данные точки входа будут содержать сообщение, полученное в теме публикации/подписки.
def onNewImage(data, context):
Первый шаг — получить переменные для этого конкретного развертывания из среды (мы определили их, изменив облачную функцию в облачной консоли.
project = os.environ.get('PROJECT') zone = os.environ.get('ZONE') cluster = os.environ.get('CLUSTER') deployment = os.environ.get('DEPLOYMENT') deploy_image = os.environ.get('IMAGE') target_container = os.environ.get('CONTAINER')
Мы пропустим часть, где мы проверяем, что структура сообщения соответствует нашим ожиданиям, и подтверждаем, что сборка прошла успешно и создала один артефакт изображения.
Следующий шаг — убедиться, что созданный образ — это тот, который мы хотим развернуть.
image = decoded_data['results']['images'][0]['name'] image_basename = image.split('/')[-1].split(':')[0] if image_basename != deploy_image: logging.error(f'{image_basename} is different from {deploy_image}') return
Теперь мы получаем клиент Kubernetes и извлекаем развертывание, которое хотим изменить.
v1 = get_kube_client(project, zone, cluster) dep = v1.read_namespaced_deployment(deployment, 'default') if dep is None: logging.error(f'There was no deployment named {deployment}') return
Наконец, мы исправляем развертывание с помощью нового образа; Kubernetes позаботится об этом.
for i, container in enumerate(dep.spec.template.spec.containers): if container.name == target_container: dep.spec.template.spec.containers[i].image = image logging.info(f'Updating to {image}') v1.patch_namespaced_deployment(deployment, 'default', dep)
Заключение
Это очень простой пример того, как мне нравится строить архитектуру в конвейере CD. У вас может быть больше шагов, просто изменив, какое событие pub/sub запускает что.
Например, вы можете запустить контейнер, который запускает тесты внутри образа и публикует событие в случае успеха, а другое — в случае сбоя, и реагировать на них либо путем обновления развертывания, либо с помощью предупреждения в зависимости от результата.
Конвейер, который мы построили, довольно прост, но вы можете написать другие облачные функции для других частей (например, облачную функцию, которая отправит электронное письмо разработчику, который зафиксировал код, который сломал ваши модульные тесты).
Как видите, наша среда сборки не может ничего изменить в нашем кластере Kubernetes, а наш код развертывания (облачная функция) не может изменить созданный образ. Наше разделение привилегий выглядит хорошо, и мы можем спать спокойно, зная, что мошеннический разработчик не разрушит наш производственный кластер. Также мы можем предоставить нашим более ориентированным на эксплуатацию разработчикам доступ к коду облачных функций, чтобы они могли его исправить или улучшить.
Если у вас есть какие-либо вопросы, замечания или улучшения, не стесняйтесь обращаться в комментариях ниже.