Микросервисная архитектура с развертыванием PM2

Pm2 - это менеджер процессов для Node.js, хотя и не ограниченный им, и он дает нам много возможностей для настройки конвейера развертывания, который позже мы можем использовать для автоматизации процесса развертывания.

В этом руководстве мы собираемся создать распределенную архитектуру приложения Javascript и развернуть его с помощью нескольких команд.

Базовые требования:

  • Базовые знания JavaScript и его технологий
  • Знание команд и сценариев Linux для начинающих
  • Облачный / локальный сервер с доступом по ssh. (или любой другой Linux-компьютер, чтобы поиграть с ssh-доступом, vm и т. д.)
  • Учебник будет проводиться в среде Linux, вы можете использовать консоль git-scm (https://git-scm.com/) для Windows.
  • В руководстве предполагается, что на удаленном сервере предварительно установлены NodeJS (18.04, 16.04) и MongoDB (18.04, 16.04).

Инструменты, которые мы собираемся использовать:

  • NodeJS 10
  • ExpressJS 4
  • MongoDB 3.6 / 4
  • PM2

Часть 1. Настройка проекта:

Для простоты в нашем pm2 будет 3 сервиса.

  • pm2-auth
  • pm2-api
  • pm2-mongodb

Архитектура приложения также может быть очень сложной, но рекомендуется перейти на более надежные варианты инфраструктуры, такие как контейнеры (докеры). Но вплоть до приложений среднего уровня эту архитектуру можно было использовать очень плавно.

Мы создадим 2 сервиса, а именно pm2-api и pm2-auth, префикс «pm2-» указывает на то, что наши сервисы связаны между собой. И у нас также будет процесс подготовки mongodb внутри pm2, также называемый pm2-mongodb.

Быстрый старт:

Если вы собираетесь быстро начать работу, пропустите следующие шаги и перейдите к шагу № 2.

Если вы хорошо разбираетесь в создании сервисов на узле js, вы можете пропустить их создание и сразу перейти к процессу развертывания, вам нужно будет разветвить этот репозиторий https://github.com/imixtron/pm2-microservices.git Затем продолжайте и клонируйте этот репозиторий для быстрый старт:

$ git clone https://github.com/<your_username>/pm2-microservices.git

Подробно:

Если вы новичок в JavaScript и Node Scene, я бы порекомендовал этот Учебник для начинающих, который поможет вам начать работу.

Начнем с создания папки, в которой будет находиться наша кодовая база.

$ mkdir pm2-microservices
$ cd pm2-microservices

Теперь мы будем создавать отдельные папки для каждой службы с префиксом «pm2-» со следующей структурой папок, так что продолжайте создавать файлы и папки.

•
├── pm2-api
│   ├── .env
│   ├── index.js
│   └── package.json
│
├── pm2-auth
│   ├── .env
│   ├── index.js
│   └── package.json
│
└── pm2-mongodb
    ├── .gitkeep

Мы оставим папку pm2-mongodb пустой, так как она будет содержать наши файлы данных, мы также можем позже подключить ее к постоянному хранилищу, чтобы она не зависела от нашего сервера приложений или полностью изменила местоположение по своему усмотрению. Поскольку проекты похожи, мы продолжим и заполним package.json следующим содержимым:

// pm2-api/package.json
{
  "name": "pm2-api",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "build": "npm install && npm start",
    "start": "node index.js",
  },
  "dependencies": {
    "body-parser": "^1.19.0",
    "dotenv": "^8.0.0",
    "express": "^4.17.0",
    "mongoose": "^5.5.11"
  }
}
// pm2-auth/package.json
{
  "name": "pm2-auth",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "build": "npm install && npm start",
    "start": "node index.js",
  },
  "dependencies": {
    "body-parser": "^1.19.0",
    "dotenv": "^8.0.0",
    "express": "^4.17.0",
    "mongoose": "^5.5.11"
  }
}

Теперь, когда у нас есть наши зависимости в порядке, мы можем продолжить и нажать npm install в наших основных папках pm2-api и pm2-auth.. Наш файл .env будет иметь общее содержимое с небольшим изменением PORT, для pm2-api мы будем использовать 9999, тогда как для pm2-auth мы будем использовать port4000.

.env

PORT = 9999
# global environment
MONGO_URI = 'mongodb://localhost:27017'
MONGO_DB_NAME = 'serviceDB'

pm2-api (index.js)

const dotenv      = require('dotenv').config();
const express     = require('express');
const app         = express();
const bodyParser  = require('body-parser');
const mongoose    = require('mongoose');
const Schema      = mongoose.Schema;
const mongoUri    = process.env.MONGO_URI;
const mongoDbName = process.env.MONGO_DB_NAME;
// connect to mongoose instance (default config)
mongoose.connect(`${mongoUri}/${mongoDbName}`, { useNewUrlParser: true });
// user model
const userSchema = new Schema({
  name: String,
  email: String,
  password: String
});
const User = mongoose.model('users', userSchema);
// configuring bodyParser for POST data
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
const port = process.env.PORT || 9999;
// API Routes
const router = express.Router();
router.post('/user', (req, res) => {
  const userData = req.body;
  const user = new User(userData);
  user.save((error) => {
    if (!!error)
      return res.status(500).send({error: error});
    return res.status(500).send({
      message: 'user created successfully'
    });
  });
});
app.use('/api', router);
app.listen(port);
console.log(`port ${port}`);

pm2-auth (index.js)

const dotenv      = require('dotenv').config();
const express     = require('express');
const app         = express();
const bodyParser  = require('body-parser');
const mongoose    = require('mongoose');
const Schema      = mongoose.Schema;
const mongoUri    = process.env.MONGO_URI;
const mongoDbName = process.env.MONGO_DB_NAME;
// connect to mongoose instance (default config)
mongoose.connect(`${mongoUri}/${mongoDbName}`, { useNewUrlParser: true });
// user model
const userSchema = new Schema({
  name: String,
  email: String,
  password: String
});
const User = mongoose.model('users', userSchema);
// configuring bodyParser for POST data
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
const port = process.env.PORT || 4000;
// API Routes
const router = express.Router();
router.post('/login', (req, res) => {
  const {email, password} = req.body;
  User.findOne({ email: email}, (error, user) => {
    if (error)
      return res.status(500).send({ 
        error: true, 
        message: 'Invalid credentials' 
      });
    if (user.password === password)
      return res.status(200).send({
        token: Buffer.from(user.password).toString('base64'),
        ...user.toObject()
      });
    return res.status(200).send({
      message: 'Invalid Credentials'
    });
  });
});
app.use('/auth', router);
app.listen(port);
console.log(`port ${port}`);

Как вы можете ясно видеть, чтобы не усложнять руководство, обе наши службы в точности похожи, за исключением функциональности конечной точки. У нас может быть несколько сервисов, которые работают независимо друг от друга.

Обратите внимание, что мы используем dotenv для синтаксического анализа файла .env для локальной разработки. Когда мы развертываем это на сервере, естественно, у нас будут собственные переменные среды, установленные через pm2 для каждого процесса.

pm2-mongodb

мы собираемся пока оставить эту папку пустой и создать пустой файл .gitkeep

Часть 2. Настройка экосистемы PM2

Теперь, что касается магии развертывания, мы собираемся создать файл процесса, содержащий выполнение наших приложений. Для этого в основной папке (pm2-microservices) выполните следующую команду, чтобы запустить файл экосистемы.

$ pm2 ecosystem

pm2 должен сгенерировать файл ecosystem.config.js со следующим содержимым, имеющим больше содержимого, чем ниже (показывающее меньше содержимого):

module.exports = {
  apps : [{...}],
  deploy : {
    production : {...}
  }
};

Давайте сначала обсудим приложения, json будет содержать конфигурацию нашего процесса, который будет запускать наше соответствующее приложение. рассмотрим pm2-api в качестве примера, pm2-auth будет точно таким же:

{
  cwd: 'pm2-api',
  name: 'pm2-api',
  script: 'npm',
  args: 'run build',
  restartDelay: 1000,
  instances: 1,
  autorestart: true,
  watch: false,
  max_memory_restart: '200M',
  env: {
    SERVICE_NAME: 'pm2-api',
    PORT: 9999,
    NODE_ENV: 'development'
  },
  env_production: {
    SERVICE_NAME: 'pm2-api',
    PORT: 9999,
    NODE_ENV: 'production'
  }
}

Обратите внимание на некоторые важные атрибуты: cwd, script, args.

cwd - текущий рабочий каталог, который будет определять местоположение файла из нашего основного файла. Рекомендуется оставить его таким же, как и в папке проекта, если вы не знаете, что делаете.

script может быть файлом bash или командой, которую мы выполняем, в нашем случае мы используем сценарий запуска npm для запуска нашего сервера, когда он запускается из нашей экосистемы.

args служат аргументами для нашего сценария. поэтому, например, мы должны запустить команду npm run build, которая установит зависимости и запустит сервер. поэтому мы разделим npm и поместим его в наш атрибут скрипта, а run build - в наш args атрибут.

Более подробную информацию об атрибутах вы можете найти в основной документации.

А для pm2-mongodb у нас будет только наш процесс, выполняющий команду bash, monogod --dbpath pm2-mongodb (при условии, что mongodb уже установлен на нашем сервере и присутствует в наших переменных пути).

Скомпилировав все, у нас будет следующая структура, скопируйте ее в свой ecosystem.config.js

// global environment
const commonEnv = {
  dev: {
    MONGO_URI: 'mongodb://localhost:27017',
    MONGO_DB_NAME: 'serviceDB'
  },
  prod: {
    MONGO_URI: 'mongodb://localhost:27017',
    MONGO_DB_NAME: 'serviceDB'
  }
}
module.exports = {
  apps: [{
    cwd: 'pm2-api',
    name: 'pm2-api',
    script: 'npm',
    args: 'run build',
    restartDelay: 1000,
    instances: 1,
    autorestart: true,
    watch: false,
    max_memory_restart: '200M',
    env: {
      ...commonEnv.dev,
      SERVICE_NAME: 'pm2-api',
      PORT: 9999,
      NODE_ENV: 'development'
    },
    env_production: {
      ...commonEnv.prod,
      SERVICE_NAME: 'pm2-api',
      PORT: 9999,
      NODE_ENV: 'production'
    }
  },
  {
    cwd: 'pm2-auth',
    name: 'pm2-auth',
    script: 'npm',
    args: 'run build',
    restartDelay: 1000,
    instances: 1,
    autorestart: true,
    watch: false,
    max_memory_restart: '200M',
    env: {
      ...commonEnv.dev,
       SERVICE_NAME: 'pm2-auth',
      PORT: 4000,
      NODE_ENV: 'development'
    },
    env_production: {
      ...commonEnv.prod,
      SERVICE_NAME: 'pm2-auth',
      PORT: 4000,
      NODE_ENV: 'production'
    }
  },
  {
    name: 'pm2-mongodb',
    script: 'mongod',
    args: '--dbpath pm2-mongodb',
    instances: 1,
    autorestart: true,
    watch: false
  }]
};

Как видите, у нас есть переменная commonEnv, которая имеет общие переменные среды, общие для приложений, в нашем случае - строка Uri MongoDB.

Теперь, когда у нас есть наш файл процесса, нажмите следующую команду, чтобы проверить, все ли работает:

$ pm2 start ecosystem.config.js

Если все в порядке, мы собираемся зафиксировать это изменение и отправить его в наш репозиторий.

$ git add ecosystem.config.js
$ git commit -a -m "add ecosystem file for pm2"
$ git push

Часть 3. Развертывание

Настройка сервера:

Для простоты давайте рассмотрим имя хоста сервера как pm2-microservices, вы можете заменить его на IP-адрес, и мы собираемся использовать root в качестве нашего пользователя. Не рекомендуется использовать root для развертываний.

Все основные поставщики облачных услуг предлагают ограниченное количество ресурсов бесплатно, которых будет достаточно для начала работы с этим руководством. Вы можете использовать любой, я лично предпочитаю Digital Ocean, и для этого урока у меня есть дроплет за 5 долларов. Вы можете использовать мою реферальную ссылку, чтобы получить 10 долларов США на счет при регистрации.

Для настройки сервера вы можете следовать этому руководству по Digital Ocean, которое поможет вам настроить ssh-ключи на сервере и в вашей учетной записи GitHub для беспроблемного и беспроблемного развертывания.

Сначала мы сгенерируем ключ ssh и скопируем его на наш сервер. Если у вас уже есть ключ ssh, который вы используете, игнорируйте этот бит. Если вы этого не сделаете, то запустите следующую команду в своем терминале bash: (введите адрес электронной почты в ‹email›)

$ ssh-keygen -t rsa -b 4096 -C "<email>"

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

$ ssh-copy-id -i ~/.ssh/id_rsa.pub <username>@<ip_address>

После этого нам также нужно будет сгенерировать ssh-ключ на нашем сервере, который мы скопируем в нашу учетную запись GitHub, чтобы он распознавался, когда наш сервер загружает репозиторий на сервер. Здесь пусть адрес электронной почты будет таким же, как и в нашей учетной записи Github, чтобы не было конфликтов.

$ ssh root@<hostname>
...
root@pm2-microservices:~# ssh-keygen -t rsa -b 4096 -C "<email>"
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:f+Z2fzSC/Y4BD+1xHXtvpAzKGvtHA7a5ErMFXLS6wUQ <email>
The key's randomart image is:
+---[RSA 4096]----+
|        E..      |
|       .  ..     |
|       ....    . |
|       oo.o .   +|
|        So *+o o+|
|        o=+oB=o++|
|        o=+oo==.+|
|        o+.+o +o.|
|        oo.o.o.oo|
+----[SHA256]-----+
root@pm2-microservices:~# cat ~/.ssh/id_rsa.pub
ssh-rsa AAAAB3AAADAQABAAACAQCqOPsoWFJkNwahquJx6mr/H0TPEBqcaZKqGkB7wIHTB2gvJ36aZhfTFg5dSfknluMZa6nt1KOSVIhCBciEaRvObLxI2NwRYdtYP3zEl3SjwCNg1G0gFUJ8+uIobD2AIM1g9WURMRDDmuPZTakyyPsu5qqS2TvoezzzsJUBu/1Ul3grZ8am0SAeJYGjm1l/ajgRdlemn3m/GQMJTxMyR0Q/yPrvXmC/Z3jhs24km9n9b+/2gXmzeQdXpxYyl/R7iM52cZNC7cFukYcZL8Zw6JZ5fs9zv0mxOK4uQEeIrUldxAezaD9IsFWtYtVr9QpjxTRnZRJ7CrsBSnZttQgVig0FjlDkYSFmkfeQkfJe9BvrYYi3VO1piaAdRjlxTwud74GZK3vwz9TIN7MbF7kVOvxnwXW19n6ZJe9xsnrDBRaFHvjjd6tXTUn4p0fz1BIuHQPGPFOvW/I+/IKAtNBpxAumGZ6JMAvYBwacP8sKalROzmSe2Z8BQt8nJKBDeLHJbDPAQn4ghyqcsR0s0UH+NV3Oci1h3JkMV9310VSdytpjRQKOl98uniaOZWXINzaC1yc2EABdHBsdKh7cMSBbM1BDb5dcwuy76OJn/SXQy8VY39a6AqHMS19IjHDhr1zzNuhLI9HrxIooBOtof9egthyU5gRBLi8rUNauD5Bgfscp+ZT+Dufdbxew== <email>
root@pm2-microservices:~#

К настоящему времени сертификат и сервер были бы удалены, вывод оболочки должен дать вам, ребята, представление о том, как это будет выглядеть. У вас будет вся эта информация, уникальная для вашей системы, когда вы ее сгенерируете.

Когда у вас есть открытый ключ ssh на консоли, как показано выше. внимательно скопируйте его. На Github перейдите в Настройки › Ключи SSH и GPG и нажмите Новый ключ SSH. Перед вами должен быть следующий экран:

Также обратите внимание, что вы будете генерировать ssh-ключ на своем сервере (а не на локальном компьютере), который будет установлен внутри учетной записи Github.

Дайте название вашему ключу, чтобы вы могли распознать его позже, и в текстовое поле Key вставьте ключ, который мы скопировали ранее с консоли.

Пока вы подключены к серверу, вы также должны добавить github.com к своим известным хостам, чтобы при развертывании pm2 у него не было проблем с получением репо из github.

root@pm2-microservices:~# ssh-keyscan -H github.com >> ~/.ssh/known_hosts

Подробная информация о развертывании в файле экосистемы

Теперь, когда это сделано, мы можем безопасно подключаться к нашему серверу и репозиторию, не беспокоясь о паролях. Это необходимо, поскольку развертывание на сервере поддерживается только с помощью ключа ssh. Мы добавим единственное, чего не хватает в нашем Equity.config.js, - инструкции по развертыванию. Мы будем добавлять следующий контент в module.exports

...
module.exports = {
  apps: [...],
  deploy: {
    development: {
      user: 'root',
      host: '<remote host ip>',
      ref: 'origin/master', // branch to be deployed could be diff
      ssh_options: "StrictHostKeyChecking=no",
      repo: '[email protected]:<github_user>/pm2-microservices.git',
      path: '/opt/pm2-microservices/production',
      'pre-setup': 'npm install -g pm2',
      'post-deploy': 'npm install && pm2 reload ecosystem.config.js --env development'
    }
  }
}

Наконец, наш ecosystem.config.js будет выглядеть примерно так:

// ecosystem.config.js
// global environment
const commonEnv = {
  dev: {
    MONGO_URI: 'mongodb://localhost:27017',
    MONGO_DB_NAME: 'serviceDB'
  },
  prod: {
    MONGO_URI: 'mongodb://localhost:27017',
    MONGO_DB_NAME: 'serviceDB'
  }
}
module.exports = {
  apps: [
    {
      cwd: 'pm2-api',
      name: 'pm2-api',
      script: 'index.js',
      restartDelay: 1000,
      instances: 1,
      autorestart: true,
      watch: false,
      max_memory_restart: '200M',
      env: {
        ...commonEnv.dev,
        SERVICE_NAME: 'pm2-api',
        PORT: 9999,
        NODE_ENV: 'development'
      },
      env_production: {
        ...commonEnv.prod,
        SERVICE_NAME: 'pm2-api',
        PORT: 9999,
        NODE_ENV: 'production'
      }
    },
    {
      cwd: 'pm2-auth',
      name: 'pm2-auth',
      script: 'index.js',
      restartDelay: 1000,
      instances: 1,
      autorestart: true,
      watch: false,
      max_memory_restart: '200M',
      env: {
        ...commonEnv.dev,
        SERVICE_NAME: 'pm2-auth',
        PORT: 4000,
        NODE_ENV: 'development'
      },
      env_production: {
        ...commonEnv.prod,
        SERVICE_NAME: 'pm2-auth',
        PORT: 4000,
        NODE_ENV: 'production'
      }
    },
    {
       name: 'pm2-mongodb',
       script: 'mongod',
       args: '--dbpath pm2-mongodb',
       instances: 1,
       autorestart: true,
       watch: false,
    }],
  deploy: {
    development: {
      user: 'root',
      host: '<hostname>',
      ref: 'origin/master',
      ssh_options: "StrictHostKeyChecking=no",
      repo: '[email protected]:<github_user>/pm2-microservices.git',
      path: '/opt/pm2-microservices/production',
      'pre-setup': 'npm install pm2',
      'post-deploy': 'npm install && pm2 reload ecosystem.config.js --env production'
    }
  }
};

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

$ pm2 deploy development setup
--> Deploying to development environment
--> on host /////hostip/////
  ○ executing pre-setup `npm install pm2`
...
○ running setup
  ○ cloning [email protected]:imixtron/pm2-microservices.git
  ○ full fetch
Cloning into '/opt/pm2-microservices/production/source'...
Warning: Permanently added the RSA host key for IP address '13.229.188.59' to the list of known hosts.
  ○ hook post-setup
  ○ setup complete
--> Success
$ pm2 deploy development --force
--> Deploying to development environment
--> on host /////hostip/////
  ○ deploying origin/complete
  ○ executing pre-deploy-local
  ○ hook pre-deploy
  ○ fast forward complete
Your branch is up to date with 'origin/complete'.
Already on 'complete'
From github.com:imixtron/pm2-microservices
 * branch            complete   -> FETCH_HEAD
Already up to date.
  ○ executing post-deploy `npm install && pm2 reload ecosystem.config.js --env production`
npm WARN saveError ENOENT: no such file or directory, open '/opt/pm2-microservices/production/source/package.json'
npm WARN enoent ENOENT: no such file or directory, open '/opt/pm2-microservices/production/source/package.json'
npm WARN source No description
npm WARN source No repository field.
npm WARN source No README data
npm WARN source No license field.
up to date in 0.586s
found 0 vulnerabilities
[PM2] Spawning PM2 daemon with pm2_home=/root/.pm2
[PM2] PM2 Successfully daemonized
[PM2][WARN] Applications pm2-api, pm2-auth, pm2-mongodb not running, starting...
[PM2] App [pm2-api] launched (1 instances)
[PM2][WARN] Environment [production] is not defined in process file
[PM2] App [pm2-auth] launched (1 instances)
[PM2] App [pm2-mongodb] launched (1 instances)
  ○ hook test
  ○ successfully deployed origin/complete
--> Success

Вот и все, вы развернули всю свою кодовую базу на сервере. Подготовка к этому моменту заняла некоторое время, как и «Игра престолов», но финал этого гораздо более удовлетворительный :). В следующий раз, когда вам нужно будет выполнить развертывание, вам нужно будет отправить свои изменения на сервер и выполнить одну команду, и никто не должен умирать:

$ pm2 deploy development update
# Pushed Bad Code? Revert in a single command
$ pm2 deploy production revert 1 

Маршруты тестирования

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

- Create User
ENDPOINT
  POST <hostname>:9999/api/user
PAYLOAD
  {
    "name": "user1",
    "email": "[email protected]",
    "password": "password123"
  }
# curl
curl -X POST -H "Content-type: application/json" -d '{ "name": "user1", "email": "[email protected]", "password": "password123" }' '<hostname>:9999/api/user'
- Login
ENDPOINT
  POST <hostname>:4000/auth/login
PAYLOAD
  {
    "email": "[email protected]",
    "password": "password123"
  }
# curl
curl -X POST -H "Content-type: application/json" -d '{ "email": "[email protected]", "password": "password123" }' '<hostname>:4000/auth/login'

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

Кроме того, с этой архитектурой мы можем легко перейти в среду docker, создав dockerfile для каждой службы и используя kubernetes для организации нашего развертывания и масштабирования.