Vagrant Multi-Node

В данной практике рассмотрим такие функции vagrant как создание собственного box из развернутой машины, а также создание нескольких машин в одном Vagrantfile.

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

Package

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

Front

Создадим подготовленный бокс для виртуальной машины front, за базовый бокс возьмем ubuntu/lunar64 и напишем Vagrantfile для установки nginx, который будет отдавать статическую страницу:

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/lunar64"
  config.vm.provision "shell", inline: <<-SHELL
    apt-get update
    apt-get install -y nginx
  SHELL
end

Запустим вм:

$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'ubuntu/lunar64'...
==> default: Matching MAC address for NAT networking...
==> default: Setting the name of the VM: vagrant2_default_1694453192956_9259
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: Forwarding ports...
    default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Running 'pre-boot' VM customizations...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
    default: SSH address: 127.0.0.1:2222
    default: SSH username: vagrant
    default: SSH auth method: private key
==> default: Machine booted and ready!

Дополнительно сохраним себе приватный ключ для доступа по ssh к виртуальной машине - он нам понадобится в дальнейшем, путь к нему указан в параметре IdentityFile в выводе команды vagrant ssh-config:

$ vagrant ssh-config | grep IdentityFile
  IdentityFile /home/alex/infra-course/example/vagrant2/vagrant_insecure_key
# сохранить в текущую директорию по именем "key" можно следующей командой
$ vagrant ssh-config | awk '/IdentityFile/{print $2}' | xargs -i cp {} ./key

Сохраним бокс на файловую систему и добавим в локальное хранилище:

$ vagrant package --output front.box
==> default: Attempting graceful shutdown of VM...
==> default: Clearing any previously set forwarded ports...
==> default: Exporting VM...
==> default: Compressing package to: /home/alex/infra-course/example/vagrant2/front.box
$ vagrant box add --name front front.box
==> box: Box file was not detected as metadata. Adding it directly...
==> box: Adding box 'front' (v0) for provider:
    box: Unpacking necessary files from: file:///home/alex/infra-course/example/vagrant2/front.box
==> box: Successfully added box 'front' (v0) for 'virtualbox'!

После чего уничтожим вм и удалим бокс из файловой системы:

$ vagrant destroy -f
==> default: Forcing shutdown of VM...
==> default: Destroying VM and associated drives...
$ rm front.box

Back

Для виртуальной машины back подготовим среду для работы приложения, для бэкенда будем использовать язык golang, так что нам понадобится установить пакеты для сборки. В качестве базового образа также возьмем ubuntu/lunar64, напишем Vagrantfile:

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/lunar64"
  config.vm.provision "shell", inline: <<-SHELL
    apt-get update
    apt-get install -y golang
  SHELL
end

Проделаем те же действия, что и в предыдущем пункте:

$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'ubuntu/lunar64'...
==> default: Matching MAC address for NAT networking...
==> default: Setting the name of the VM: vagrant2_default_1694457463073_44923
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: Forwarding ports...
    default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Running 'pre-boot' VM customizations...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
    default: SSH address: 127.0.0.1:2222
    default: SSH username: vagrant
    default: SSH auth method: private key
==> default: Machine booted and ready!
$ vagrant package --output back.box
==> default: Attempting graceful shutdown of VM...
==> default: Clearing any previously set forwarded ports...
==> default: Exporting VM...
==> default: Compressing package to: /home/alex/infra-course/example/vagrant2/back.box
$ vagrant box add --name back back.box
==> box: Box file was not detected as metadata. Adding it directly...
==> box: Adding box 'back' (v0) for provider:
    box: Unpacking necessary files from: file:///home/alex/infra-course/example/vagrant2/back.box
==> box: Successfully added box 'back' (v0) for 'virtualbox'!
$ vagrant destroy -f
==> default: Destroying VM and associated drives...
$ rm back.box

DB

Для виртуальной машины с базой данных также используем в качестве базового образа ubuntu/lunar64, в качестве субд возьмем postgresql. Также при подготовке добавим конфигурацию, которая даст возможность удаленного подключения из любой доступной сети. Таким образом получим следующее содержание для Vagrantfile:

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/lunar64"
  config.vm.provision "shell", inline: <<-SHELL
    apt-get update
    apt-get install -y postgresql
    echo "listen_addresses = '*'" >> /etc/postgresql/15/main/conf.d/listen.conf
    echo "host all all 0.0.0.0/0 trust" >> /etc/postgresql/15/main/pg_hba.conf
  SHELL
end

Проделаем те же действия, что и в предыдущем пункте:

$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'ubuntu/lunar64'...
==> default: Matching MAC address for NAT networking...
==> default: Setting the name of the VM: vagrant2_default_1694458609907_72643
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: Forwarding ports...
    default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Running 'pre-boot' VM customizations...
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
    default: SSH address: 127.0.0.1:2222
    default: SSH username: vagrant
    default: SSH auth method: private key
==> default: Machine booted and ready!
$ vagrant package --output db.box
==> default: Attempting graceful shutdown of VM...
==> default: Clearing any previously set forwarded ports...
==> default: Exporting VM...
==> default: Compressing package to: /home/alex/infra-course/example/vagrant2/db.box
$ vagrant box add --name db db.box
==> box: Box file was not detected as metadata. Adding it directly...
==> box: Adding box 'db' (v0) for provider:
    box: Unpacking necessary files from: file:///home/alex/infra-course/example/vagrant2/db.box
==> box: Successfully added box 'db' (v0) for 'virtualbox'!
$ vagrant destroy -f
==> default: Destroying VM and associated drives...
$ rm db.box

Multi-Node

После проделанных действий у нас появилось еще 3 бокса:

$ vagrant box list
back           (virtualbox, 0)
db             (virtualbox, 0)
front          (virtualbox, 0)
ubuntu/lunar64 (virtualbox, 0)

Теперь имея боксы для разных виртуальных машин можно позаботиться их наполнением.

Code

Опишем программный код, который будет деплоиться на каждую машину.

Front

На виртуальной машине front мы будем отдавать статичную html страницу с простым js скриптом, сохраним его в файл index.html:

<!DOCTYPE html>
<html>
<body>

<h2>Users table:</h2>

<p id="users"></p>

<script>
var req = function() {
  var http = new XMLHttpRequest();
  http.onload = function() {
    const users = JSON.parse(this.responseText);
    let text = "<table border='1'>"
    for (let x in users) {
      text += "<tr><td>" + users[x].name + "</td>";
      text += "<td>" + users[x].email + "</td></tr>";
    }
    text += "</table>"
    document.getElementById("users").innerHTML = text;
  }

  http.open("GET", "http://localhost:8889");
  http.send();
}
setInterval(req, 1000);
</script>

</body>
</html>

Back

На виртуальной машине back соберем приложение на golang, которое будет подключаться к базе данных и отдавать список пользователей из базы в виде json по http. Опишем это в файле main.go:

package main

import (
        "context"
        "encoding/json"
        "fmt"
        "net/http"
        "os"

        "github.com/jackc/pgx/v5"
)

type users struct {
        ID    int    `json:"id"`
        Name  string `json:"name"`
        Email string `json:"email"`
}

func main() {
        ctx := context.Background()
        conn, err := pgx.Connect(ctx, "postgres://app:pass@db:5432/app?sslmode=disable")
        if err != nil {
                fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err)
                os.Exit(1)
        }
        defer conn.Close(ctx)

        http.ListenAndServe("0.0.0.0:80", http.HandlerFunc(
                func(w http.ResponseWriter, r *http.Request) {
                        rows, err := conn.Query(ctx, "select * from users")
                        if err != nil {
                                fmt.Printf("error db query: %s", err)
                                return
                        }

                        users, err := pgx.CollectRows(rows, pgx.RowToStructByName[users])
                        if err != nil {
                                fmt.Printf("error collect rows: %s", err)
                                return
                        }

                        jsonUsers, err := json.Marshal(users)
                        if err != nil {
                                fmt.Printf("error marshal json: %s", err)
                        }

                        w.Header().Add("Access-Control-Allow-Origin", "*")
                        w.Write(jsonUsers)
                }),
        )
}

DB

На виртуальной машине db подготовим пользователя, базу и таблицу. Для этого опишем файл users.sql:

create database app;
create user app;
grant all on database app to app;
\connect app;
create table users(
  id serial primary key,
  name varchar(50),
  email varchar(100)
);
grant all on all tables in schema public to app;

Vagrantfile

Опишем подготовку виртуальных машин в Vagrantfile:

Vagrant.configure("2") do |config|
  config.ssh.private_key_path = "key"
  config.vm.define "db" do |c|
    c.vm.box = "db"
    c.vm.network "private_network", ip: "192.168.56.30"
    c.vm.provision "shell", inline: <<-SHELL
      su - postgres -c 'psql -f /vagrant/users.sql'
      echo 192.168.56.10 front >> /etc/hosts
      echo 192.168.56.20 back >> /etc/hosts
      echo 192.168.56.30 db >> /etc/hosts
    SHELL
  end

  config.vm.define "front" do |c|
    c.vm.box = "front"
    c.vm.network "forwarded_port", guest: 80, host: 8888
    c.vm.network "private_network", ip: "192.168.56.10"
    c.vm.provision "shell", inline: <<-SHELL
      cat /vagrant/index.html > /var/www/html/index.html
      echo 192.168.56.10 front >> /etc/hosts
      echo 192.168.56.20 back >> /etc/hosts
      echo 192.168.56.30 db >> /etc/hosts
    SHELL
  end

  config.vm.define "back" do |c|
    c.vm.box = "back"
    c.vm.network "forwarded_port", guest: 80, host: 8889
    c.vm.network "private_network", ip: "192.168.56.20"
    c.vm.provision "shell", inline: <<-SHELL
      echo 192.168.56.10 front >> /etc/hosts
      echo 192.168.56.20 back >> /etc/hosts
      echo 192.168.56.30 db >> /etc/hosts
      cp /vagrant/main.go /home/vagrant/
      cd /home/vagrant/
      go mod init example
      go mod tidy
      go build main.go
      /home/vagrant/main &
    SHELL
  end
end

UP

После всех операций содержимое директории будет выглядеть так:

$ ls
index.html  key  main.go  users.sql  Vagrantfile

Теперь можем запустить команду vagrant up:

$ vagrant up
Bringing machine 'db' up with 'virtualbox' provider...
Bringing machine 'front' up with 'virtualbox' provider...
Bringing machine 'back' up with 'virtualbox' provider...
...
==> back: Running provisioner: shell...
    back: Running: inline script
    back: go: creating new go.mod: module example
    back: go: to add module requirements and sums:
    back:       go mod tidy
    back: go: finding module for package github.com/jackc/pgx/v5
    back: go: downloading github.com/jackc/pgx/v5 v5.4.3
    back: go: downloading github.com/jackc/pgx v3.6.2+incompatible
    back: go: found github.com/jackc/pgx/v5 in github.com/jackc/pgx/v5 v5.4.3
    back: go: downloading github.com/stretchr/testify v1.8.1
    back: go: downloading github.com/jackc/pgpassfile v1.0.0
    back: go: downloading github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a
    back: go: downloading golang.org/x/crypto v0.9.0
    back: go: downloading golang.org/x/text v0.9.0
    back: go: downloading github.com/davecgh/go-spew v1.1.1
    back: go: downloading github.com/pmezard/go-difflib v1.0.0
    back: go: downloading gopkg.in/yaml.v3 v3.0.1

После чего можно открыть страницу http://localhost:8888 и убедиться, что она работает:

Добавим пару пользователей в нашу базу:

$ vagrant ssh db -c 'sudo -u postgres psql'
could not change directory to "/home/vagrant": Permission denied
psql (15.4 (Ubuntu 15.4-0ubuntu0.23.04.1))
Type "help" for help.

postgres=# \c app
You are now connected to database "app" as user "postgres".
app=# insert into users (name,email) values ('alex', 'alex@mail.ru');
INSERT 0 1
app=# insert into users (name,email) values ('jo', 'jo@ya.ru');
INSERT 0 1
\q

После чего можно убедиться, что они попали на наш фронтенд:

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

$ vagrant destroy -f
==> back: Forcing shutdown of VM...
==> back: Destroying VM and associated drives...
==> front: Forcing shutdown of VM...
==> front: Destroying VM and associated drives...
==> db: Forcing shutdown of VM...
==> db: Destroying VM and associated drives...