Автоматическое развёртывание контейнерных приложений с помощью AWS Copilot

Оригинал статьи: ссылка (Nathan Peck, Developer Advocate)

Развитие идеи приложения до работающего продукта, с которым могут взаимодействовать люди, это многоступенчатый процесс. После того, как дизайн закончен, а исходный код написан, следующая проблема – как развернуть приложение, чтобы оно было доступно пользователям. Один из способов сделать это – использовать контейнер Docker и утилиту AWS Copilot, для автоматической подготовки инфраструктуры для запуска этого контейнера. Если вы ещё не знакомы с AWS Copilot, вы можете почитать о нём в статье Introducing AWS Copilot.

Copilot можно использовать для сборки и развёртывания приложений через командную строку с помощью таких команд как, например, copilot svc deploy. Однако это не лучший способ в долгосрочной перспективе, особенно при росте количества разработчиков и количества сервисов. В этой статье мы покажем, как использовать Copilot для автоматизации выпуска приложений. Мы начнём с базового конвейера (pipeline), который будет собирать контейнер, загружать его в репозиторий образов, и запускать его автоматически каждый раз, когда вы добавляете изменения в репозиторий кода. Затем мы обновим наш конвейер, чтобы он следовал лучшим практикам и состоял из нескольких этапов, включающих в себя тестирование, с помощью которого можно убедиться, что приложение работает корректно, до выпуска его в производственную среду. В конце мы рассмотрим реальный сценарий, в котором найдём проблему в производственной среде и выпустим её исправление.

Знакомство с приложением

Представьте, что вы работаете в стартапе, который называется «String Services» и который стремится стать ведущим поставщиком онлайн-API для работы со строками. Ваш работодатель решил начать предоставлять сервис под названием «reverse». Он будет принимать любую строку в качестве ввода, а на выходе выдавать её «перевёрнутую» версию, то есть, эту же строку с обратным порядком символов. Ваша задача – развернуть этот сервис для ваших нетерпеливых клиентов. Для начала давайте посмотрим на его исходный код, написанный на Node.js:

var getRawBody = require("raw-body");
var http = require("http");

var server = http.createServer(function (req, res) {
getRawBody(req)
.then(function (buf) {
res.statusCode = 200;
res.end(buf.toString().split("").reverse().join(""));
})
.catch(function (err) {
res.statusCode = 500;
res.end(err.message);
})
});

server.listen(3000);

К коду приложения прилагается простой Dockerfile из нескольких шагов, который используется для установки зависимостей приложения, а также создания минимального образа Docker для него:

FROM node:14 AS build
WORKDIR /srv
RUN npm install raw-body

FROM node:14-slim
WORKDIR /srv
COPY --from=build /srv .
ADD . .
EXPOSE 3000
CMD ["node", "index.js"]

С этими двумя файлами у вас есть всё необходимое, чтобы развернуть приложение для переворачивания строк с помощью Copilot. Как описано в статье Introducing AWS Copilot, вы можете использовать команду copilot init, чтобы запустить мастер, который обнаружит приложение и автоматически развернёт его. В конце процесса Copilot вернёт URL приложения.

Предположим, что компания String Services владеет доменом https://string.services и хочет развернуть приложение на него. В таком случае, вы можете использовать следующие команды, чтобы запустить приложение в двух средах с использованием этого домена:

copilot app init --domain string.services
copilot env init --name test
copilot env init --name prod
copilot svc init --name reverse
copilot svc deploy --name reverse --env test
copilot svc deploy --name reverse --env prod

В итоге приложение будет запущено в двух средах, как мы и планировали. Теперь вы можете отправить строку на URL сервиса и получить её перевёрнутую версию назад:

$ curl -d "Hello" https://reverse.prod.std.string.services
olleH

Автоматизация выпуска приложения

Установка приложения с помощью интерфейса командной строки Copilot была не слишком сложной, но String Services создаёт большую библиотеку различных сервисов по работе со строками. В итоге у них могут быть сотни таких сервисов, которые необходимо будет регулярно обновлять. Есть несколько путей решения этой задачи:

Централизованные развёртывания: любое развёртывание любого сервиса должно пройти через определённого человека или группу людей, которые запустят Copilot для установки каждого сервиса. Такой способ лишает разработчиков прямого доступа к установленному приложению, а также серьёзно ограничивает пропускную способность компании по развёртыванию сервисов.

Децентрализованные развёртывания: вы учите всех разработчиков использованию Copilot и передаёте им ответственность за установку и обновление собственных сервисов. Это рабочий способ, однако все разработчики будут разворачивать свои изменения отдельно друг от друга, а иногда такие изменения могут приводить к поломке сервисов.

Автоматизация с защитными механизмами: лучшее из обоих предыдущих способов – все разработчики могут производить развёртывание, однако процесс проходит через централизованный автоматический конвейер, который проверяет, что сервис работает корректно до того, как новая версия будет доступна для ваших клиентов.

При использовании третьего подхода разработчики могут, если требуется, использовать Copilot, но могут и просто использовать git push, чтобы отправлять код в репозиторий, откуда уже начнётся автоматический процесс развёртывания кода.

С помощью всего нескольких команд вы можете запустить автоматический конвейер для вашего приложения, который соберет и запустит приложение после отправки кода в Git:

copilot pipeline init
git push
copilot pipeline update

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

Вы также можете увидеть этот конвейер в консоли AWS CodePipeline. На этом шаге у вас уже есть базовая автоматизация: разработчик может развернуть приложение в тестовой и производственной среде, просто изменив необходимые строки кода, а затем запустив git commit и git push. Разработчикам даже не нужно устанавливать Copilot или знать, как им пользоваться: всё, что им нужно – это знать, как использовать Git.

Добавляем несколько интеграционных тестов

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

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

var superagent = require('superagent');
var expect = require('expect');

const url = process.env.APP_URL;

if (!url) {
throw new Error('Test process requires that env variable `APP_URL` is set');
}

test('should be able to reverse a simple string', async () => {
const res = await superagent.post(url).send('Hello');
expect(res.text).toEqual('olleH');
});

Copilot позволяет легко добавить этот тест в конвейер. В файле pipeline.yml указан список стадий развёртывания, куда можно добавить команды тестирования. Таким образом, всё, что вам нужно сделать, это установить зависимости для теста и вызвать его, предоставив URL тестовой среды в переменной окружения:

# The deployment section defines the order the pipeline will deploy
# to your environments.
stages:
- # The name of the environment to deploy to.
name: test
# Optional: use test commands to validate this stage of your build.
test_commands:
- npm install --prefix test
- APP_URL=https://reverse.test.std.string.services npm test --prefix test
- # The name of the environment to deploy to.
name: prod

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

git push
copilot pipeline update

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

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

Выпуск исправления ошибки

После выпуска сервиса всё идёт хорошо пару недель, но затем приходит первое сообщение об ошибке от клиента: «Я попытался перевернуть строку, но в результате получил несколько странных вопросительных знаков!» К счастью, клиент прислал пример воспроизведения проблемы:

$ curl -d "Hello ?" https://reverse.prod.std.string.services
�� olleH

Вы исследуете проблему и находите ответ. Операция по переворачиванию строки просто разделяет её по границе байтов и затем меняет порядок каждого байта. Для простых ASCII-строк, где каждый символ представлен одним байтом, это работает успешно. Однако большинство современных систем используют Unicode. Unicode – это набор символов, которые могут добавляться в строку, используя такой стандарт кодирования, как UTF-8. Это позволяет использовать различные дополнительные символы, например, эмодзи, которые представлены последовательностями от двух до четырёх байтов. Когда приложение переворачивает такие последовательности байтов, оно меняет порядок для каждого отдельного байта, таким образом превращая последовательность в непереводимую мешанину, которая отображается в виде символов вопроса.

Время исправить проблему в сервисе. Один из лучших способов обеспечить непрерывное улучшение надёжности и стабильности вашего сервиса – это рассматривать любую найденную клиентом проблему как проблему в двух местах. Во-первых, в самом коде приложения, а во-вторых, в тестах. Если бы тесты имели достаточное покрытие, то они бы выявили эту проблему.

Таким образом, первый шаг – добавить новый тест в набор тестов и убедиться, что он не проходит и блокирует выпуск приложения в производственную среду:

test('should be able to reverse a string containing UTF-8 characters', async () => {
const res = await superagent.post(url).send('Hello ?');
expect(res.text).toEqual('? olleH');
});

После выполнения git push вы увидите, что конвейер запустил процесс выпуска новой версии, но затем остановился с ошибкой до того, как эта версия попала в производственную среду. Мы можем увидеть больше информации в консоли AWS CodePipeline.

При нажатии на кнопку «Details» в шаге TestCommands становится ясно, что ошибка была вызвана тестом, который мы только что добавили:

Это хорошо! Значит, тест выявил проблему. Теперь, когда вы её исправите, вы будете уверены, что тесты предотвратят её возможное повторное появление в производственной среде.

Теперь исправим проблему в самом приложении – это достаточно легко. Пакет с открытым исходным кодом runes осуществляет разбивку строк с учётом Unicode. Если мы используем этот пакет в нашем приложении, мы можем разделять строку на подстроки, которые выравниваются с реальными границами байтов, представляющих каждый символ.

var getRawBody = require("raw-body");
var runes = require('runes');
var http = require("http");

var server = http.createServer(function (req, res) {
getRawBody(req)
.then(function (buf) {
res.statusCode = 200;
let stringRunes = runes(buf.toString());
res.end(stringRunes.reverse().join(""));
})
.catch(function (err) {
res.statusCode = 500;
res.end(err.message);
})
});

server.listen(3000);

process.once('SIGTERM', function () {
server.close();
});

После повторного запуска git commit и git push, изменение отправляется в конвейер. На этот раз все тесты проходят успешно и обновление развёртывается в производственной среде.

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

$ curl -d "Hello ?" https://reverse.prod.std.string.services
? olleH

Заключение

В этой статье мы рассмотрели шаги, которые необходимы для автоматизации развёртывания приложения с помощью Copilot:

Создание простого конвейера для развёртывания приложения;

Улучшение конвейера с помощью интеграционных тестов в тестовой среде перед тем, как исходный код будет развёрнут в производственную среду;

Исправление приложения путём добавления нового теста с последующим развёртыванием кода с исправленной ошибкой.

Надеемся, что этот придуманный сценарий поможет вам в следующий раз, когда вы захотите автоматизировать развёртывание ваших контейнерных приложений. Вы можете найти полный исходный код приложения и его конвейера в Github. Этот репозиторий до сих пор связан с конвейером Copilot, поэтому вы можете открыть PR, и если он будет объединён в код (merge), то пройдёт через такой же конвейер и будет доступен по адресу https://reverse.prod.std.string.services.