Мы все знаем Slack, правда? Вы когда-нибудь задумывались об использовании его для развертывания кода, просто набрав /deploy
? Я сделал, и это то, что я спроектировал и разработал для команды, которой надоело иметь дело с плохо спроектированным пользовательским интерфейсом AWS Codedeploy.
Контекст
Наш стек жил на AWS, и у нас была учетная запись для каждой среды, это означает, что Staging
и Production
жили в двух полностью разделенных и изолированных учетных записях; каждый пользователь должен был войти в систему, которую мы назвали учетной записью Main
, а затем взять на себя роль среды, с которой он хотел взаимодействовать. Чтобы лучше понять, о чем я говорю, вот как выглядел этот процесс от git push
до фактического развертывания:
- Разработчик отправляет код в git
- CircleCI создает код, упаковывает все в
tar.gz
файл и помещает его в корзину S3 (в учетной записиMain
), используя следующую структуру папок:prefix/productname/branchname/commitid.tar.gz
- Разработчик заходит на Github и копирует Commit ID.
- вход в учетную запись
Main
AWS - Принимает на себя роль
Staging
- Открывает консоль Codedeploy и выбирает имя приложения.
- Вводит URL-адрес корзины S3 вручную, вставляет идентификатор фиксации и добавляет
.tar.gz
в конец строки. - Нажимает кнопку развертывания в надежде, что он / она не допустил ошибки при составлении URL-адреса корзины S3 (скорее всего, он / она это сделали!)
Моя идея заключалась в том, чтобы заменить что-либо после шага № 2 простой Slack-командой.
Команда косой черты
Когда вызывается команда с косой чертой, Slack отправляет HTTP POST на указанный вами URL-адрес запроса. Этот запрос содержит полезные данные с заголовком Content-type
, установленным как application/x-www-form-urlencoded
. Тело запроса выглядит так:
token=gIkuvaNzQIHg97ATvDxqgjtO
team_id=T0001
team_domain=example
enterprise_id=E0001
enterprise_name=Globular%20Construct%20Inc
channel_id=C2147483705
channel_name=test
user_id=U2147483697
user_name=Steve
command=/weather
text=94070
response_url=https://hooks.slack.com/commands/1234/5678
trigger_id=13345224609.738474920.8088930838d88f008e0
Перед тем, как начать кодирование, я создал следующие команды с косой чертой через панель управления Slack и сохранил их токены в надежном месте:
Приложение
Теперь, когда у меня готовы команды с косой чертой, мне нужно поразить несколько конечных точек, и вот здесь начинается самое интересное.
Я разработал приложение Express NodeJS под названием node-slack-deployer (ранее называвшееся codedeploy-helper
), которое выполняет эту работу. Сервер имеет две конечные точки для этих команд с косой чертой (/slack/get-deployments
и /slack/deploy
) и одну, используемую для проверки работоспособности (/
, которая возвращает сообщение OK
).
Но как мы узнаем, что нам нужно искать или развертывать на основе тела POST
? Это довольно просто: все, что добавлено после самого /command
, передается как text
тела в формате string
, нам просто нужно разделить его.
Аргументы команды развертывания:
if (req.body.text.split(' ').length === 2) { // Store product name and branch in product object res.locals.product = { service: req.body.text.split(' ')[0].toLowerCase(), env: req.body.text.split(' ')[1].toLowerCase(), description: 'Deploying via Slack' }; } else { // Store body information in product object res.locals.product = { service: req.body.text.split(' ')[0].toLowerCase(), env: req.body.text.split(' ')[1].toLowerCase(), branch: req.body.text.split(' ')[2], hash: req.body.text.split(' ')[3], description: 'Deploying via Slack' }; }
Аргументы команды артефактов:
//Params length must have at least 2 elements if (params.length < 2) { res.json( slackHandler.payloadToSlack( 'failure', 'Code Artifacts', 400, 'Invalid number of arguments\nUsage: `/artifact [service-name] [branch]`' ) ); return; } res.locals.bucketPath = awsHandler.generateBucketPath({ service: params[0], branch: params[1], hash: params[2] });
И, по соображениям безопасности, мы также проверяем, совпадает ли токен, отправленный Slack, с токеном, который мы передаем приложению, используя переменные среды:
//Check if the deployment token passed matches the env vars one exports.checkDeploymentToken = token => { return token === process.env.SLACK_DEPLOYMENT_TOKEN; };
Выглядит хорошо, что теперь? Ах да, давайте посмотрим, что могут сделать для меня отдельные конечные точки!
Команда Артефакты
Команда /artifacts
возвращает пользователю список из 5 последних созданных артефактов данного продукта и его ветви:
/artifacts api master
Поскольку мы сказали, что сегмент S3 находится в Main
аккаунте, нам здесь не требуется никакого STS
волшебства, только запросить сегмент S3!
exports.getDeployments = async (req, res) => { const data = await awsHandler.s3GetObjects(res.locals.bucketPath); let max = 5; let arr = []; // Check if data is empty if (data.length < 1) { res.json( slackHandler.payloadToSlack( 'warning', 'Code Artifacts', 404, 'Elements not found' ) ); return; } // Check length of the object and returns max 5 items if (data.length < 5) { max = data.length; } for (let i = 0; i < max; i++) { arr.push(utilsHandler.getHash(data[i].Key)); } res.json( slackHandler.payloadToSlack( 'success', 'Code Artifacts', 'Artifacts List:', slackHandler.generateHashesText(arr, res.locals.params[0]) ) ); };
Вы можете заметить, что data
получает результат метода awsHandler.s3GetObjects
, который представляет собой не что иное, как этот вызов API AWS:
exports.s3GetObjects = async bucketPath => { const listObjects = promisify(S3.listObjects, S3); const data = await listObjects({ Bucket: this.bucketParams.Bucket, Prefix: bucketPath }); //Order data from newest to oldest if (data.Contents.length > 1) { data.Contents.sort((a, b) => { return new Date(b.LastModified) - new Date(a.LastModified); }); } return data.Contents; };
И, наконец, slackHandler.generateHashesText
выглядит так:
//Generate a formatted string of hashes with ordered list exports.generateHashesText = (data, service) => { let string = ''; let max = 5; if (data.length < 5) { max = data.length; } for (let i = 0; i < max; i++) { string += `${i + 1}. <https://github.com/${ process.env.GITHUB_USER }/${service}/commit/${data[i]}|${data[i]}>`; if (i < max - 1) { string += '\n'; } } return string; };
Сортировано: наше приложение возвращается к резервной полезной нагрузке, включая в своем теле список последних 5 артефактов, которые связывают пользователя с его URL-адресом фиксации Github, если вы хотите убедиться, что просматриваете правильно совершить!
Команда развертывания
Команда /deploy
запускает развертывание Codedeploy и принимает 4 аргумента:
- наименование товара
- окружающая обстановка
- филиал
- идентификатор фиксации
/deploy api production master 5d0e80e4152eef31f64c7b8026277894a541f166
Здесь все немного сложнее, поскольку нам нужно:
- Проверьте, была ли команда вызвана из правого канала резервирования;
- Проверить, существует ли артефакт;
- Возьмем на себя роль AWS; и
- Наконец, разверните код.
Начнем с пунктов №1 и №2, а затем:
// ... if (!this.isDeploymentChannel(req.body.channel_id)) { console.log('Invalid Slack channel'); res.json( slackHandler.payloadToSlack( 'failure', 'Slack Deploy', 401, "Oooops! It looks like you can't run this command from this channel!" ) ); return; } // ... const isS3Element = await awsHandler.s3GetSingleObject(bucketPath); if (!isS3Element) { res.json( slackHandler.payloadToSlack( 'warning', 'Slack Deploy', 404, 'Element not found' ) ); return; } // ...
Где isDeploymentChannel
просто:
//Check Slack Channel ID exports.isDeploymentChannel = channelId => { return channelId === process.env.SLACK_DEPLOYMENTS_CHANNEL_ID; };
Тогда мы готовы взять на себя роль аккаунта:
exports.assumeRole = async (req, res, next) => { params.RoleSessionName = res.locals.product.env; params.RoleArn = awsHandler.awsRoles[res.locals.product.env]; // If the environment received doesn't exist, throw an error if (!params.RoleArn) { res.json( slackHandler.payloadToSlack( 'failure', 'Slack Deploy', 400, `${params.RoleSessionName} is not a valid environment` ) ); return; } res.locals.awsRole = await awsHandler.stsAssumeRole(params); next(); };
Где stsAssumeRole
:
exports.stsAssumeRole = async params => { const assumeRole = promisify(STS.assumeRole, STS); return await assumeRole(params); };
И последнее, но не менее важное: давайте развернем код - это методы, вызываемые контроллером развертывания:
// Initialise codedeploy object exports.codedeployInit = params => { return new AWS.CodeDeploy({ accessKeyId: params.Credentials.AccessKeyId, secretAccessKey: params.Credentials.SecretAccessKey, sessionToken: params.Credentials.SessionToken }); }; // Deploy using codedeploy exports.codedeployDeploy = async (awsRole, product, bucketPath) => { const CodeDeploy = this.codedeployInit(awsRole); const params = { applicationName: product.service, deploymentGroupName: product.service, description: product.description, revision: { revisionType: 'S3', s3Location: { bucket: this.bucketParams.Bucket, bundleType: process.env.AWS_CODEDEPLOY_BUNDLE_TYPE, key: bucketPath } } }; const codedeploy = promisify(CodeDeploy.createDeployment, CodeDeploy); return await codedeploy(params); };
Готово: пользователь в Slack получит приятное сообщение о том, что Codedeploy начал развертывание и предоставит идентификатор развертывания.
Наблюдения
Это должно было быть быстрым прогоном простого приложения Express, вставляющего случайные фрагменты туда и сюда, просто чтобы дать больше контекста. Он был разработан и написан в соответствии с потребностями нашей команды, поэтому я понимаю, что его может быть трудно применить к любой команде / компании, которая развертывает свой код с помощью Codedeploy. Однако учтите, что его разработка еще не закончена. Я недавно открыл его исходный код и собираюсь потратить некоторое время на его улучшение и добавление / удаление некоторых функций, которые могут или не могут иметь отношение к любому другому варианту использования.
Взносы
Мы очень ценим любой вклад, и я буду более чем счастлив просмотреть ваши PR или обсудить улучшения в разделе Github Проблемы. Проект, опять же, можно найти здесь.
Первоначально опубликовано на странице https://www.darkraiden.com/blog/slack-codedeploy-love/ 13 августа 2018 г.