Мы все знаем Slack, правда? Вы когда-нибудь задумывались об использовании его для развертывания кода, просто набрав /deploy? Я сделал, и это то, что я спроектировал и разработал для команды, которой надоело иметь дело с плохо спроектированным пользовательским интерфейсом AWS Codedeploy.

Контекст

Наш стек жил на AWS, и у нас была учетная запись для каждой среды, это означает, что Staging и Production жили в двух полностью разделенных и изолированных учетных записях; каждый пользователь должен был войти в систему, которую мы назвали учетной записью Main, а затем взять на себя роль среды, с которой он хотел взаимодействовать. Чтобы лучше понять, о чем я говорю, вот как выглядел этот процесс от git push до фактического развертывания:

  1. Разработчик отправляет код в git
  2. CircleCI создает код, упаковывает все в tar.gz файл и помещает его в корзину S3 (в учетной записи Main), используя следующую структуру папок: prefix/productname/branchname/commitid.tar.gz
  3. Разработчик заходит на Github и копирует Commit ID.
  4. вход в учетную запись Main AWS
  5. Принимает на себя роль Staging
  6. Открывает консоль Codedeploy и выбирает имя приложения.
  7. Вводит URL-адрес корзины S3 вручную, вставляет идентификатор фиксации и добавляет .tar.gz в конец строки.
  8. Нажимает кнопку развертывания в надежде, что он / она не допустил ошибки при составлении 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

Здесь все немного сложнее, поскольку нам нужно:

  1. Проверьте, была ли команда вызвана из правого канала резервирования;
  2. Проверить, существует ли артефакт;
  3. Возьмем на себя роль AWS; и
  4. Наконец, разверните код.

Начнем с пунктов №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 г.