Как я использую Node.js для сценариев DevOps

Как я использую Node.js для сценариев DevOps

7 февраля 2023 г.

Независимо от того, нужно ли вам создавать лямбда-код или увлажнять тестовую базу данных, большинство проектов, над которыми вы работаете, будут включать некоторые фрагменты сценариев DevOps. Если ваше приложение использует Node, нет причин не использовать его и в своих сценариях. Это облегчает другим разработчикам Node, работающим над проектом, поддержку поддерживающих его сценариев. С помощью всего нескольких пакетов вы можете создать отличный скрипт, который оценят другие разработчики.

Использование типов

Первый шаг при настройке любого скрипта – это запуск вашего кода. Я мог бы просто написать чистый JavaScript и использовать Node для его прямого выполнения, но мне нравится безопасность, которую я получаю с TypeScript. Кроме того, поскольку весь код моего приложения написан на TypeScript, мне часто приходится импортировать контроллер, который я написал для приложения, в свой скрипт.

Как я описал в своей предыдущей статье Запуск TypeScript без компиляции, ts-node долгое время был моим основным решением для этого. С тех пор я начал использовать esbuild для переноса всего моего TypeScript и обнаружил TypeScript Execute, который похож на ts-node, за исключением того, что внутри него используется esbuild.

Теперь я глобально устанавливаю tsx:

npm i -g tsx

тогда я могу просто добавить shebang вверху скрипта

#!/usr/bin/env tsx

и, наконец, пометить файл как исполняемый

chmod +x ./script.ts

Параметры анализа

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

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

Определение параметров

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

import { program } from 'commander';

program
    .option('--someFlag')
    .option('--foo <foo>',
                        'A foo value',
                        process.env.FOO_VALUE);

Действие

Теперь, когда параметры определены, пришло время определить, что происходит с этими параметрами. Это делается путем вызова action в построителе. Обычно я просто добавляю действие в конец своих определений опций, поэтому приведенный выше пример будет таким:

import { program } from 'commander';
import { inspect } from 'util';

interface CommandOptions {
    someFlag: boolean;
    foo: string;
}

program
    .option('--someFlag')
    .option('--foo <foo>',
                        'A foo value',
                        process.env.FOO_VALUE)
    .action(async (options: CommandOptions) => {
            console.info(inspect(options));

            await someCommand(options);
    });

С чего начать

Теперь, когда все определено, нужно запустить саму программу. По умолчанию используется функция parse(). Однако если вы хладнокровны и возвращаете Promise из функции action, вам нужно будет вызвать parseAsync(). Эта функция получит значения из process.argv, проанализирует их, вызовет соответствующую функцию action, которую вы определили, и дождется ее результата.

program.parseAsync()
    .then(() => console.info('🎉 Done!'));

Наблюдение за работой

Есть два типа сценариев, которые я ненавижу: те, которые перегружены вызовами console.info(), и те, которые не показывают никакого прогресса. Если сценарий завершится за короткий промежуток времени (менее пары секунд), то я думаю, что можно просто вывести результат в конце. Однако для большинства сценариев существует несколько шагов, и это может занять некоторое время.

Чтобы убедиться, что все идет успешно, я определяю шаги сценария с помощью listr. Я знаю, что на момент написания этой статьи он не обновлялся 4 года, но он просто работает. Он отображает список шагов со счетчиком и флажками при локальном запуске и распечатывает журналы при запуске в среде CI/CD.

Определение задач

Чтобы использовать listr, вы предоставляете ему список шагов. Задача может возвращать Promise или новый экземпляр Listr с подзадачами. Чтобы определить список, создайте новый экземпляр Listr и передайте массив объектов задач, каждый из которых имеет как минимум свойство title и task.

Функция task принимает параметр context, который передается на каждом этапе. Начальные значения для контекста будут переданы, когда мы также выполним список задач, поэтому сейчас мы можем предположить, что это будут просто CommandOptions из предыдущего.

const tasks = new Listr<CommandOptions>([
    {
        title: 'First Step',
        async task(ctx) {
            // TODO: Actually do something 🙄
        }
    }
]);

Пропуск задач

Что делать, если в некоторых случаях вы хотите пропустить задачу? Вы можете предусмотреть функцию пропустить в определении задачи. Функция skip получает параметр context и возвращает boolean (или Promise<boolean>), который сообщает listr если задача должна быть пропущена или нет. Имеет смысл.

Поэтому, если вы хотите пропустить этот первый шаг, если переменная someFlag имеет значение true, вы просто вернете ее из функции skip.

const tasks = new Listr<CommandOptions>([
    {
        title: 'First Step',
        skip: (ctx) => ctx.someFlag, // Stop 🛑
        async task(ctx) {
            // TODO: Actually do something 🙄 or not? 🤷
        }
    }
]);

Параллельная обработка

Одна из моих любимых функций Listr заключается в том, что я могу отображать массив элементов для создания списка подзадач, а затем запускать их параллельно. Чтобы запускать задачи Listr параллельно, передайте объект параметров после списка задач и установите для свойства concurrent значение true.

const tasks = new Listr([
    {
        title: 'Get Items',
        async task(ctx) {
            ctx.items = await getItems();
        }
    },
    {
        title: 'Process Items',
        task: (ctx) => new Listr(
            ctx.items.map(processItem),
            { concurrent: true }
        )
    }
]);

Выполнение задач

После того, как список задач определен, вам нужно указать listr запустить его и передать начальное значение контекста. Это не может быть проще, просто вызовите метод run в вашем экземпляре listr и передайте в контексте. Он возвращает Promise с окончательным контекстом, поэтому вам нужно ожидать результата.

// Imagine all the options calls from before... 
.action(async (options: CommandOptions) => {
    const result = await tasks.run(options);
    displayResult(result);
});

Обзор

Теперь у вас есть несколько необходимых вещей, которые вы можете использовать. В следующий раз, когда вы в 15-й раз обнаружите, что вам приходится в 15-й раз вручную создавать код, попробуйте собрать сценарий вместе.


Фото на обложке Джефферсона Сантоса на Unsplash

Также опубликовано здесь


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE