Автор оригинала: Agustin Chiappe Berrini.
Недавно я начал работать в новом проекте для нового клиента. Идея состоит в том, чтобы сделать приложение, которое будет перечислять все счастливые часы, которые происходят рядом с вами. Я решил использовать такой же стек, который я использую в своих других проектах, потому что я не люблю изменения. Это SCALA и PostgreSQL для бэкэнда и Teadercript с реагированием на интерфейс. Мы развернули приложение, используя контейнеры через Heroku.
Пока я готовил контейнеры, я сделал замечательное открытие: SBT-Docker Отказ Это плагин для SBT
Инструмент сборки я использую, когда я программирую в SCALA, чтобы создавать контейнеры из Scala Projects. Это приятно по двум причинам:
- Я могу построить новое изображение, просто набрав
SBT Docker
Отказ - Я могу написать спецификации моего образа Docker (I.E. Dockerfile), используя Scala. Это давайте избегаем необходимости запоминать детали синтаксиса DockerFile и одновременно у всех проектов завершится интегрированы весь проект.
Я наслаждался так много, что плагин, что решил искать что-то похожее на узел, и не было ничего, что полностью удовлетворено моими ожидаемыми. Итак, я решил, что я напишу своими.
Требования
Это то, что я хочу:
- Я хочу быть в состоянии покрыть 90% наиболее обычных случаев без необходимости записи любого дополнительного кода. Я хочу сделать что-то вроде этого в главном каталоге:
Название-AWESOME-TOOL TOOL - имя контейнера --Таг --таг-контейнер-тег - порт 8080
и получить новое изображение, помеченное какКонтейнер-имя: контейнер-тег
Это будет запускать проект на порту 8080. - Я хочу, чтобы это было интегрировано в экосистему узла. Я Я хочу быть в состоянии установить его, используя
NPM
и создать сценарий вPackage.json
Файл, который автоматически запустит команду, упомянутую выше. - Для других 10% случаев, в котором мне нужно что-то более конкретное, я хочу иметь возможность обрабатывать их с помощью JavaScript (без синтаксиса Dockerfile, пожалуйста, мне достаточно необходимости запоминать все языки, которые я использую) и в выразительной основе способ. Как можно похоже на то, что вы бы делаете с
SBT-Docker
Отказ
Базовый докерф
Обычно большинство моих приложений узла имеют DockerFile, который выглядит что-то подобное:
FROM node RUN mkdir -p /app WORKDIR /app COPY package.json /app RUN npm install COPY . /app EXPOSE 8080 CMD [ "npm", "start" ]
Некоторые заметки об этом подходе:
- Как видите, я не определяющую версию узла на изображении, от которого я тяну. Я делаю это в моих приложениях, но для этого случая я буду игнорировать это. Это не так просто, как может показаться. На данный момент я буду использовать последнюю версию. В будущем я разбираю раздел Двигатели решить, какой из них уточнить.
- Я копирую первый
Package.json
а затем остальная часть каталога. Причина очень проста: таким образом, я могу кэшировать шаг запускаNPM установить
Отказ Преимущество не имеет необходимости запуска каждый раз, когда я изменяю что-то в коде, который не связан с моими зависимостями. - Я использую Конвенцию
NPM начать
Отказ По той же причине, чем 1. Я предпочитаю делегировать как можно больше логики к файлу конфигурации узла. Если вы хотите увидеть, как начать приложение, вы должны посмотреть наPackage.json
, не у докера.
В идеале мое новое приложение создаст что-то очень похожее на это по умолчанию.
Базовое использование
Давайте начнем кодировать. Первое, что я хочу, – это класс для создания докерфайлов. Указанный класс должен принять имя контейнера в конструкторе, и он должен иметь разные методы для описания того, как создать DockerFile.
Я хочу использовать этот класс, чтобы быть чем-то вроде этого:
new Dockering(imagename) .from('ubuntu') .run(command) .env(envVariableList) // Etc
Я Я хочу иметь возможность цепи разные инструкции Docker в одном утверждении. Я также хочу, чтобы класс был неизменно:
const dockerfile = new Dockering(imagename) .from('ubuntu'); // this is one dockerfile const otherDockerfile = dockerfile .run(command); // this is a different one, based on the previous one
Я решил написать это, используя Typescript, потому что я люблю иметь компилятор, который обнаружит мои ошибки. Используя TeampScript, я бы создал новый интерфейс под названием Инструкция
и новый класс под названием Приклеенность
Отказ Класс будет иметь список Инструкция
и набор методов, которые определит инструкции Docker и вернуть новый Приклеенность
класс согласно этому. Я хочу также метод построить
Это будет скомпилировать все инструкции в образе докера.
import * as instructions from './instructions'; export default class Dockering { constructor( public name: string = path.basename(__dirname), public instructions: Array= [], public docker: Docker = new Docker({ socketPath: '/var/run/docker.sock' })) { } run(command: string | Array ): Dockering { return this.withNewInstruction(new instructions.Run(command)); } cmd(command: string | Array ): Dockering { return this.withNewInstruction(new instructions.Cmd(command)); } expose(ports: Array ): Dockering { return this.withNewInstruction(new instructions.Expose(ports)); } add(srcs: Array , dst: string): Dockering { return this.withNewInstruction(new instructions.Add(srcs, dst)); } shell(cmd: Array ): Dockering { return this.withNewInstruction(new instructions.Shell(cmd)); } // Etc. build(project: string = '.'): Promise<{}> { const tarStream = tar .pack(project, { ignore: (name: string) => name.indexOf('node_modules') === 0 }); const dockerfileContent = this.instructions.map(i => i.toString()).join('\n'); tarStream.entry({ name: 'Dockerfile' }, dockerfileContent) return this.docker.image.build(tarStream, { t: this.name }) .then((stream: Stream) => promisifyStream(stream)); } private withNewInstruction(newInstruction: instructions.Instruction): Dockering { return new Dockering(this.name, this.instructions.concat([newInstruction]), this.docker); } }
Вы можете увидеть полный пример этого здесь Отказ
Я думаю, что большая часть этого кода объясняет сам по себе, за исключением построить
метод. Некоторые комментарии там:
- Я использую два внешних библиотеках:
tar-fs
иметь дело с TAR-файлами иDocker-Node-API
иметь дело с докера API. - Я не включаю в Docker File Содержание папки Node_Modules.
- Я на самом деле не создаю DockerFile в пользовательской файловой системе, но добавьте его в поток TAL, который будет передан в Docker. Не нужно медлевать вещи вниз, сохраняя на диск.
- Функция
promisifyStream
Получает поток и возвращаетОбещание
Это удается после мероприятияконец
уволен и отклоняется, если событиеОшибка
. получилось. Наданные
Он печатает содержимое в терминале, поэтому вы можете увидеть прогресс сборки. - Вы можете увидеть содержимое файла инструкций здесь Хотя это не нужно понимать, как это работает.
Использование этого класса именно то, что мы искали.
Интерфейс командной строки
Теперь мы должны определить использование интерфейса командной строки, чтобы покрыть большую часть обычных случаев. Для этого я буду использовать библиотеку Минимист
и создать функцию, которая принимает аргументы, проанализированные этой структурой для обработки поведения командной строки.
export interface Args { cmd?: string; project?: string; name?: string; tag?: string port?: string; }; export interface Package { name: string; }; const getConfig = (path: string): Package => { try { return require(path); } catch (e) { console.log('error', path, e); throw 'could not find package.json'; } }; export default function (args: Args): Promise<{}> { const startCmd = args.cmd || 'npm start'; const projectPath = args.project || process.cwd(); const port = parseInt(args.port) || 8080; const confFile = `${projectPath}/package.json`; const configuration = getConfig(confFile); const name = args.name || configuration.name; const tag = args.tag || 'latest'; return (new Dockering(`${name}:${tag}`)) .fromImage('node') .run('mkdir /app') .workdir('/app') .copy(['package.json'], '/app') .run('npm install') .copy(['.'], '/app') .expose([port]) .cmd(startCmd.split(' ')) .build(projectPath); }
Вы можете увидеть файл здесь
Как видите, наша командная строка инструмент создаст докер, очень похоже на то, что она была раньше! Что именно то, что мы хотели.
С этим мы можем создать новый исполняемый файл в проекте. Например, под Bin/Dockering
:
#!/usr/bin/env node const minimist = require('minimist'), command = require('../lib/command').default, Dockering = require('../lib/index').default; if (require.main === module) { const args = minimist(process.argv.slice(2)) command(args) .catch((err) => console.log('An error happened!', err)) }
Что более чем достаточно.
Использование этого инструмента командной строки, было бы чем-то похожее на это:
dockering --name newimagename --tag tag --cmd "npm start" --port 8080 --project .
Таким образом, мы можем создать новый сценарий в Package.json
Чтобы завершить наши узлы на одном очень простом шаге:
{ "scripts": { "docker": "dockering" } }
И это все!
Вы можете проверить полный пример кода здесь и пакет NPM здесь Отказ