Sử dụng Vagrant để build local server một cách tự động

Hoàn cảnh lịch sử:

Một ngày đẹp trời, sếp giao cho bạn 1 task mới – sửa và phát triển thêm vài states và modules trong Saltstack. Vì Saltstack hiện tại của dự án có quá nhiều dependencies, nên bạn làm biếng xây dựng 1 cụm saltstack local để test các module bạn vừa làm, mà bay vào thẳng con Salt-master đang chạy để test và debug.

Và rồi chuyện gì đến cũng sẽ đến, với kinh nghiệm phá đảo hàng chục con server trước đó, con Salt-master cũng ra đi nhưng bao con server khác, bạn lỡ clean hết toàn bộ pillar data hiện tại, và rồi các states tất nhiên sẽ không work được nữa. Cũng may là có backup cái volume chứa đống pillar data này, nên bạn cũng thở phào nhẹ nhõm !! 😛

Bạn chợt suy nghĩ lại, dù gì mình cũng sẽ làm dự án này lâu dài, mà test trên con Salt-master đang chạy thì nguy hiểm quá, với lại có nhiều pipeline đang sử dụng nó, nên tỉ lệ bạn bị chửi nếu lỡ may làm gì đó cũng khá cao. Thế là anh bạn siêng năng viết ra một cái Vagrantfile, để có thể mô phỏng salt cluster này dưới local và vọc vạch một cách thoải mái.

Vagrant là gì?

Giới thiệu

Vagrant là một công cụ dùng để xây dựng và phát triển máy ảo ở môi trường cá nhân.

Tương tự như Terraform, nhưng Vagrant không phục vụ cho các Provider thuộc nền tảng, hạ tầng và dịch vụ cloud, mà hướng tới các Provider có thể cài đặt được trên local như VirtualBox, VMWare, Hyper-V, Libvirt, Docker,…

Cách cài đặt và sử dụng của Vagrant cũng khá chi tiết, bạn có thể tham khảo trên trang chủ của Vagrant.

Vagrantfile

Vagrant sẽ đọc cấu hình từ Vagrantfile để build máy ảo.

Vagrantfile là một ruby script, nên nó phụ thuộc vào syntax của Ruby và tất nhiên sẽ thừa hưởng tất cả các modules của Ruby, vì vậy Vagrantfile có độ tùy biến rất cao, hoàn toàn có thể tích hợp vào Ruby program. Tuy nhiên DevOps chúng ta ít ai dùng Ruby để viết script cả, nên cũng là một điểm trừ cùa Vagrant.

Cấu trúc chung của một Vagrant file.

Vagrant.configure("2") do |config|
  # ...
end

Vagrant Box

Vì không phải một nền tảng cloud nên các provider như VirtualBox, VMWare, Hyper-V,… chắc chắn sẽ không có các machine images được xây dựng sẵn như AWS, GCP, Azure,..

Tuy nhiên, Vagrant đã xây dựng một Vagrant Cloud có lưu trữ các images cần thiết cho các provider trên. Các images này này đa số được cộng đồng xây dựng và upload lên.

Bạn cũng có thể build một image riêng cho mình theo hướng dẫn sau, tuy nhiên, bài viết này sẽ chú trọng vào việc sử dụng các images có sẵn nhé.

Cách sử dụng một box cụ thể (ở đây mình sẽ sử dụng Centos 7):

Vagrant.configure("2") do |config|
  config.vm.box = "centos/7"
end

Vagrant CLI

Có rất nhiều Vagrant commands sử dụng cho các mục đích khác nhau. Bài viết này sẽ sử dụng 2 command chính đó là vagrant up (build máy ảo) và vagrant destroy (xóa máy ảo)

Xây dựng Salt-stack cluster bằng Vagrant và VirtualBox Provider

Yêu cầu

Tất nhiên là bạn phải có một máy tính cá nhân rồi, dùng Windows hay Linux hay MacOS gì cũng được (tuy nhiên phải enable VT (virtuallization) trong Bios nhé.

Vagrant (nên sử dụng version 1.8.0 trở về sau). Download: Vagrant

VirtualBox (nên sử dụng version 4.0 trở về sau). Download: VirtualBox

Một Code Editor để bạn có thể viết code nhé. Recommend: Visual Studio Code

Một chút kiến thức về Ruby để viết Vagrantfile

Structure

vagrant
├─ config.json
├─ files
│  └─ id_rsa.pub
├─ scripts
│  ├─ add_user.sh
│  ├─ base.sh
│  ├─ master.sh
│  └─ minion.sh
└─ Vagrantfile

Vagrantfile configuration

Giả sử khi bạn đã viết được một Vagrantfile, nếu bạn hardcode quá nhiều thì việc tái sử dụng Vagrantfile vô cùng khó khăn. Nên đơn giản hơn là bạn viết một configuration cho Vagrantfile này, khi cần thay đổi một vài giá trị thì bạn chỉ cần thay đổi config file, hạn chế hết mức có thể việc thay đổi Vagrantfile.

Mình sẽ sử dụng 1 json file có tên là configs.json chứa các thông tin sau:

{
    "provider": "virtualbox",
    "os": "centos/7",
    "username": "thachanpy",
    "vm": [
        {
            "name": "salt-minion",
            "count": 2,
            "cpu": 1,
            "memory": 1024,
            "custom_scripts": [
                "minion.sh"
            ],
            "first_ip": "172.16.1.21"
        },
        {
            "name": "salt-master",
            "count": 1,
            "cpu": 1,
            "memory": 512,
            "custom_scripts": [
                "master.sh"
            ],
            "first_ip": "172.16.1.11"
        }
    ],
    "linked_clones": true,
    "network": {
        "network_type": "private_network",
        "netmask": "255.255.255.0"
    },
    "files": {
        "ssh_public_key": "id_rsa.pub"
    },
    "scripts": {
        "base": "base.sh",
        "add_user": "add_user.sh"
    }
}
  • Ý nghĩa sơ bộ của config file này:
    • provider: provider mình muốn sử dụng, ở đây mình dùng VirtualBox
    • os: hệ điều hành bạn muốn cài đặt cho các máy ảo của bạn, tương ứng với name trên Vagrant Cloud Box nhé.
    • username: user bạn muốn init trong máy ảo
    • vm: cấu hình của các máy ảo bạn muốn xây dựng, vì salt-cluster bắt buộc phải có salt-master và salt-minion nên mình cấu hình riêng cho 2 máy ảo này nhé.
      • name: tên máy ảo (sau này sẽ gắn liền với hostname, vm name).
      • count: số lượng máy ảo cho cấu hình này (để tránh sự trùng lặp, mình sẽ thêm index vào tên máy ảo)
      • cpu: số cpu cho máy ảo
      • memory: lượng memory cấp cho máy ảo (tính bằng MB)
      • custom_scripts: một vài script chỉ chạy trong máy ảo này khi cài đặt
      • first_ip: ip address sẽ cấp phát cho máy ảo tính từ ip này (nếu count = 1 thì coi như máy ảo đó là ip này luôn)
    • linked_clones: bản chất là các máy ảo sẽ được clone từ template có sẵn, nên mình dùng linked clones để tiết kiệm disk 🙁
    • network:
      • network_type: ở đây mình dùng private network. Tham khảo thêm ở đây
      • netmask: vì mình muốn các máy ảo này cùng 1 lớp mạng nên netmask mình cấu hình chung ở đây
    • files: đơn giản là tạo các key cho các file mình cần provision vào máy ảo (các file này sẽ đặt trong thư mục files). Hiện tại trong bài viết này thì mình có tạo 1 file ssh_public_key để thêm vào trong VM để có thể truy cập ssh từ bên ngoài thông qua private key tương ứng
    • scripts: như trên, tạo các key cho các special script có trong Vagrantfile. Các script này sẽ được đặt trong thư mục scripts)
  • Xong, với file config này, mình sẽ tùy biến Vagrantfile theo hắn để sau này có thể chỉnh sửa cấu hình tùy thích.

Viết vài shell scripts

Base script: cài những tools cơ bản

#!/bin/sh

sudo yum update -y
sudo yum install -y curl git vim net-tools

Add user script: tạo username từ config file

#!/bin/sh

USER=$1

useradd -m -s /bin/bash -U $USER -u 666 --group wheel
cp -pr /home/vagrant/.ssh /home/${USER}/.ssh
mv /tmp/id_rsa.pub /home/${USER}/.ssh/authorized_keys
chown -R ${USER}:${USER} /home/${USER}
echo "%${USER} ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/${USER}

For salt-master:

#!/bin/sh

yum install -y https://repo.saltstack.com/py3/redhat/salt-py3-repo-latest.el7.noarch.rpm
yum clean expire-cache

yum install -y salt-minion

systemctl start salt-minion
systemctl enable salt-minion

yum install -y salt-master
systemctl start salt-master
systemctl enable salt-master

For salt-minion:

#!/bin/sh

yum install -y https://repo.saltstack.com/py3/redhat/salt-py3-repo-latest.el7.noarch.rpm
yum clean expire-cache
yum install salt-minion

systemctl start salt-minion
systemctl enable salt-minion

Viết Vagrantfile

Vì Vagrantfile viết theo cấu trúc của Ruby, nên bạn tìm hiểu sơ sơ về Ruby syntax để hiểu thêm nhé.

Vagrantfile dựa trên file cấu hình trên đây rồi:

require 'json'

work_dir        = File.expand_path(File.dirname(__FILE__))
input_configs   = JSON.parse(File.read(File.join(work_dir, "configs.json")))
scripts_dir     = File.join(work_dir, "scripts")
files_dir       = File.join(work_dir, "files")

# Define salt-master IP Address
master_ip_address = ""

# Fetch new IP Address from first_ip and index function
def fetch_ip_address(first_ip, i)
    array = first_ip.split('.')
    change = (array.first 3).push(array[-1].to_i + i)
    new_ip = ""
    for i in change
        new_ip += "." + i.to_s
    end
    return new_ip[1..-1]
end

Vagrant.configure(2) do |config|
    # Define box
    config.vm.box = input_configs.fetch('os')

    # Define custom VM
    input_configs.fetch('vm').each do |vms|
        # Define salt-master IP Address, add to /etc/hosts on all salt VMs 
        if vms.fetch('name') == "salt-master"
            master_ip_address = vms.fetch('first_ip')
        end

        # Define VM by count
        (1..vms.fetch('count')).each do |i|

            # Define Server name => ID for Vagrant VM define, hostname and provider VM name also
            server_name = "#{vms.fetch('name')}-#{i}"

            config.vm.define server_name do |server|

                # Define VM IP Address
                ip_address = fetch_ip_address(vms.fetch('first_ip'), i - 1)

                # Get VMs configuration
                server.vm.provider input_configs.fetch('provider') do |v|
                    v.name = server_name
                    v.cpus = vms.fetch('cpu')
                    v.memory = vms.fetch('memory')
                    v.linked_clone = input_configs.fetch('linked_clones')
                end
                server.vm.network input_configs.fetch('network').fetch('network_type'), ip: ip_address, netmask: input_configs.fetch('network').fetch('netmask')
                server.vm.hostname = server_name

                # Add salt-master IP Address to all salt-minion (Use `salt` prefix)
                if vms.fetch('name').start_with?("salt")
                    server.vm.provision "shell", inline: "sudo echo #{master_ip_address} salt >> /etc/hosts"
                end

                # Run custom scripts for each VM type
                if vms.key?("custom_scripts")
                    vms.fetch("custom_scripts").each do |script|
                        server.vm.provision "shell", path: File.join(scripts_dir, script)
                    end
                end
                # Post message after provison all VMs to get IP Address for each VM
                server.vm.post_up_message = "IP Address: #{ip_address} - CPUs: #{vms.fetch('cpu')} - Memory: #{vms.fetch('memory')} MB"
            end
        end
    end

    # Run scripts to all created VM
    config.vm.provision "shell", path: File.join(scripts_dir, input_configs.fetch('scripts').fetch('base'))
    config.vm.provision "file", source: File.join(files_dir, input_configs.fetch('files').fetch('ssh_public_key')), destination: "/tmp/id_rsa.pub"
    config.vm.provision "shell" do |s|
        s.path = File.join(scripts_dir, input_configs.fetch('scripts').fetch('add_user'))
        s.args = input_configs.fetch('username')
    end
end

Code thì lằn nhằn, nhưng mình cứ hiểu đơn giản là Vagrant file sẽ duyệt cái configs.json ra các VM config, sau đó build VM này với cấu hình tương ứng, một vài cấu hình sẽ apply lên tất cả các VM nên mình không để trong loop.

Tạo máy ảo từ Vagrantfile

Cái này thì đến con nít cũng làm được, đơn giản là mình chạy lệnh vagrant up thôi.

Output thì dài lắm, mình chỉ note ra phần Post Up Message nhé:

==> salt-minion-1: Machine 'salt-minion-1' has a post `vagrant up` message. This is a message
==> salt-minion-1: from the creator of the Vagrantfile, and not from Vagrant itself:
==> salt-minion-1: 
==> salt-minion-1: IP Address: 172.16.1.21 - CPUs: 1 - Memory: 1024 MB

==> salt-minion-2: Machine 'salt-minion-2' has a post `vagrant up` message. This is a message
==> salt-minion-2: from the creator of the Vagrantfile, and not from Vagrant itself:
==> salt-minion-2: 
==> salt-minion-2: IP Address: 172.16.1.22 - CPUs: 1 - Memory: 1024 MB

==> salt-master-1: Machine 'salt-master-1' has a post `vagrant up` message. This is a message
==> salt-master-1: from the creator of the Vagrantfile, and not from Vagrant itself:
==> salt-master-1: 
==> salt-master-1: IP Address: 172.16.1.11 - CPUs: 1 - Memory: 512 MB

Nhìn vào output, bạn có thể thấy được IP Address và cấu hình của mỗi VM vừa được tạo.

Testing

  • Với mỗi VM, ta có sẽ được IP Address tương ứng, vậy làm thế nào để truy cập vào các VMs này? Sẽ có 2 cách sau:
    • Cách 1: sử dụng vagrant ssh command:
      • Truy cập vào đường dẫn chứa Vagrantfile đã chạy, bắt buộc phải có thư mục .vagrant chứa metadata của các VM đã được tạo theo Vagrantfile.
      • vagrant ssh <server_name># <servername> này chính là server.vm.provider server_name trong Vagrantfile
    • Cách 2: truy cập ssh dựa theo username và ssh public key đã cung cấp.
      • ssh toannd22 <ip_address>
  • Vì mình chỉ cài đặt saltstack trên master và minion, nên mình chỉ cần test trên salt-master là được
    • Test salt-key:
      • sudo salt-key --accept='salt-*' -y
      • sudo salt-key -L
Accepted Keys:
salt-master-1
salt-minion-1
salt-minion-2
Denied Keys:
Unaccepted Keys:
Rejected Keys:
  • Test ping to minions:
    • sudo salt 'salt-  *' test.ping
salt-minion-1:
    True
salt-minion-2:
    True
salt-master-1:
    True

Tổng kết:

Đây chỉ là một demo nhỏ về việc dùng Vagrant để provision một máy ảo trên VirtualBox nên mình chỉ chạy những đoạn script đơn giản.

Tùy thuộc vào công việc và nhu cầu của từng máy ảo mà bạn có thể thêm vào nhiều cấu hình và script khác nhau. Tuy nhiên vì đây chỉ là provision nên bạn không cần chi tiết quá về việc cài đặt các service và application, sau này có thể dùng các configuration management tools như ansible, chef, puppet, và saltstack như mình vừa demo 😀

Xem thêm source code ở Github

0 0 vote
Article Rating
guest
0 Comments
Inline Feedbacks
View all comments