Vagrant Pt.2 – Customising our VMs

In Part 1, we looked at how to spin up a Centos 7 box using Vagrant, Hyper-V and PowerShell. However, to make this virtual machine test-worthy as soon as it is booted, we need to configure our VM, i.e. update the system, install necessary packages, and so on and so forth. In short, we want to automate as much of what would have otherwise been a completely manual installation of the new environment we intend to work on.

There are multiple ways to achieve this with tools like Ansible, Chef, Docker or Puppet, but in the following steps I will be keeping it simple and just use the Shell Provisioner.

There are 2 main ways we can leverage the shell provisioner from within our Vagrantfile:

  • inline –  Specifies a shell command inline to execute on the remote machine
  • path – Path to a shell script to upload and execute

I will be using both methods simultaneously, as some are better suited than others depending on the task at hand.

If you followed along from Part 1, we created a Centos 7 box under D:\Vagrant\centos7-vm.
Let’s see if we can provision this same VM with a few modifications of our own. I would like to:

  • Update the OS and install a few packages
  • Change the VM’s hostname
  • Disable Selinux
  • Edit the /etc/hosts file
  • Set up authentication using my own pubkey
  • Add my user to the sudoers file
  • Install Docker

With the exception of the VM’s hostname, all of the above will be configured using a simple shell script, leveraging the path option
The VM hostname will be set up with the inline option, just so you can get an example of what this looks like in the Vagrantfile.

Picking up from the Part 1, out Vagrantfile file should look like this:

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "generic/centos7"
  config.vm.network "public_network"
  config.vm.synced_folder ".", "/vagrant_data", disabled: true
  config.vm.provider "hyperv" do |h|
    h.enable_virtualization_extensions = true
    h.linked_clone = true
    h.memory = 2048
    h.cpus = 2
    h.vmname = "centos7-vm"
  end
  
end

This whole block relates to the VM creation, and it is good practice to create a separate block for VM-specific configurations.

💡 It is important to note that any methods applied to config.vm will apply to ALL your machines (even if you put it in a specific machine block) so its better to put all your config.vm properties outside of any machine-specific block.

During my tests with Vagrant, I found that mixing config and the machine-specific variables causes Vagrant to make multiple passes of the VM configuration. This is a problem, especially when dealing with multi-machine scenarios, where a single Vagrantfile can be used to spin up several VMs.

VM Hostname change with the inline option

Let’s create a new block and leverage the shell provisioner’s inline option in order to set the VM’s hostname first:

    config.vm.define "centos7vm" do |centos7vm|
      centos7vm.vm.provision "shell", inline: "hostnamectl set-hostname centos7-vm"
    end

💡 Notice how I just provided a generic variable “centos7vm” – You can use any name, provided it does not contain any special characters within the |xxx|block.
(i.e using |centos7-vm| will generate an error)

Our Vagrantfile should now look like this:

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "generic/centos7"
  config.vm.network "public_network"
  config.vm.synced_folder ".", "/vagrant_data", disabled: true
  config.vm.provider "hyperv" do |h|
    h.enable_virtualization_extensions = true
    h.linked_clone = true
    h.memory = 2048
    h.cpus = 2
    h.vmname = "centos7-vm"
  end
  
  config.vm.define "centos7vm" do |centos7vm|
    centos7vm.vm.provision "shell", inline: "hostnamectl set-hostname centos7-vm"
  end

end


Notice how we are “defining” the machine-specific configuration and how the inline option just contains the hostnamectl command, directly in the Vagrantfile. You can leverage the inline option to run any command on the guest VM.

Let’s test this out by spinning up our VM and then SSHing into it:

PS D:\Vagrant\centos7-vm> vagrant up

Bringing machine 'centos7vm' up with 'hyperv' provider...
==> centos7vm: Verifying Hyper-V is enabled...
==> centos7vm: Verifying Hyper-V is accessible...
==> centos7vm: Importing a Hyper-V instance
    centos7vm: Creating and registering the VM...
    centos7vm: Successfully imported VM
    centos7vm: Please choose a switch to attach to your Hyper-V instance.
    centos7vm: If none of these are appropriate, please open the Hyper-V manager
    centos7vm: to create a new virtual switch.
    centos7vm:
    centos7vm: 1) External
    centos7vm: 2) Default Switch
    centos7vm: 3) WSL
    centos7vm:
    centos7vm: What switch would you like to use? 1
    centos7vm: Configuring the VM...
    centos7vm: Setting VM Enhanced session transport type to disabled/default (VMBus)
==> centos7vm: Starting the machine...
==> centos7vm: Waiting for the machine to report its IP address...
    centos7vm: Timeout: 120 seconds
    centos7vm: IP: 10.10.5.211
==> centos7vm: Waiting for machine to boot. This may take a few minutes...
    centos7vm: SSH address: 192.168.1.27:22
    centos7vm: SSH username: vagrant
    centos7vm: SSH auth method: private key
    centos7vm:
    centos7vm: Vagrant insecure key detected. Vagrant will automatically replace
    centos7vm: this with a newly generated keypair for better security.
    centos7vm:
    centos7vm: Inserting generated public key within guest...
    centos7vm: Removing insecure key from the guest if it's present...
    centos7vm: Key inserted! Disconnecting and reconnecting using new SSH key...
==> centos7vm: Machine booted and ready!
==> centos7vm: Running provisioner: shell...
    centos7vm: Running: inline script

Notice the last 3 lines: After the machine booted and was ready, the shell provisioner kicked in and ran our inline “script”.
Also notice how every line is prefixed with “centos7vm”. This is particularly useful in multi-machine scenarios.

Let’s verify the hostname change by SSHing into our guest VM:

PS D:\Vagrant\centos7-vm> vagrant ssh
[vagrant@centos7-vm ~]$

🎉 The machine’s hostname has been successfully changed to centos7-vm, as defined by the hostnamectl command

Additional configuration from script using the path option

Now let’s configure the rest. As stated at the beginning of this article, I want to update the system, install a bunch of apps, disable selinux, edit my hosts file, create a user and copy my pubkey so I can authenticate directly to my new virtual machine & install Docker.

To achieve this, I will create a simple bash script named bootstrap.sh (who can call it anything you want) containing all the required commands and then I’ll use the shell provisioner’s path option to call it from the Vagrantfile.

#!/bin/bash

DEFAULT_APPS="curl wget git vim htop net-tools yum-utils zsh tmux netcat"

#* Disable Selinux
sed -i 's/enforcing/disabled/g' /etc/selinux/config

#* Add hosts
echo "192.168.1.20    sqldb" >> /etc/hosts
echo "192.168.1.150   ubuntu01" >> /etc/hosts
echo "192.168.1.100   ansible" >> /etc/hosts

#* Update resolv.conf
echo "nameserver 192.168.1.200" >> /etc/resolv.conf

#* Update system
yum update -y
yum install -y epel-release
yum install -y $DEFAULT_APPS

#* Pubkey authentication for dodz
groupadd dodz -g 1002
adduser dodz -u 1002 -g 1002
install -o dodz -g dodz -m 700 -d /home/dodz/.ssh
echo "ssh-ed25519 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx me@mylaptop" > /home/dodz/.ssh/authorized_keys
chmod 600 /home/dodz/.ssh/authorized_keys
chown dodz:dodz /home/dodz/.ssh/authorized_keys
echo "dodz   ALL=(ALL)   NOPASSWD: ALL" >> /etc/sudoers
echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config
systemctl restart sshd

#* Install Docker
yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
yum install -y docker-ce docker-ce-cli containerd.io
systemctl enable docker.service
systemctl enable containerd.service
systemctl start docker.service
systemctl start containerd.service
usermod -aG docker dodz

You can place this script anywhere you want but you will need to provide the path inside the Vagrantfile. For ease of use, I will place this script inside D:\Vagrant\centos7-vm, same folder as the Vagrantfile just so that I won’t need to define the path. So my machine-specific block now looks like this:

  config.vm.define "centos7vm" do |centos7vm|
    centos7vm.vm.provision "shell", inline: "hostnamectl set-hostname centos7-vm"
    centos7vm.vm.provision "shell", path: "bootstrap.sh"
  end

Let’s test this out. To properly test, I like destroying the VM completely (vagrant destroy), just so I can make sure everything works from start to finish, however, you could use vagrant reload to reload the config. This will shutdown the VM and re-run the Vagrantfile.

I edited the output below, so as not to make this too big to read:

PS D:\Vagrant\centos7-vm> vagrant up

Bringing machine 'centos7vm' up with 'hyperv' provider...
==> centos7vm: Verifying Hyper-V is enabled...
==> centos7vm: Verifying Hyper-V is accessible...
==> centos7vm: Importing a Hyper-V instance
    centos7vm: Creating and registering the VM...
    centos7vm: Successfully imported VM
    centos7vm: Please choose a switch to attach to your Hyper-V instance.
    centos7vm: If none of these are appropriate, please open the Hyper-V manager
    centos7vm: to create a new virtual switch.
    centos7vm:
    centos7vm: 1) External
    centos7vm: 2) Default Switch
    centos7vm: 3) WSL
    centos7vm:
    centos7vm: What switch would you like to use? 1
    centos7vm: Configuring the VM...
    centos7vm: Setting VM Enhanced session transport type to disabled/default (VMBus)
==> centos7vm: Starting the machine...
==> centos7vm: Waiting for the machine to report its IP address...
    centos7vm: Timeout: 120 seconds
==> centos7vm: Waiting for machine to boot. This may take a few minutes...
    centos7vm: SSH address: 192.168.1.27:22
    centos7vm: SSH username: vagrant
    centos7vm: SSH auth method: private key
    centos7vm:
    centos7vm: Vagrant insecure key detected. Vagrant will automatically replace
    centos7vm: this with a newly generated keypair for better security.
    centos7vm:
    centos7vm: Inserting generated public key within guest...
    centos7vm: Removing insecure key from the guest if it's present...
    centos7vm: Key inserted! Disconnecting and reconnecting using new SSH key...
==> centos7vm: Machine booted and ready!
==> centos7vm: Running provisioner: shell...
    centos7vm: Running: inline script
==> centos7vm: Running provisioner: shell...
    centos7vm: Running: C:/Users/nonda/AppData/Local/Temp/vagrant-shell20210823-12332-1s4wg0t.sh
    centos7vm: Loaded plugins: fastestmirror
    centos7vm: Determining fastest mirrors
    centos7vm: Package epel-release-7-13.noarch already installed and latest version
    centos7vm: Nothing to do
    centos7vm: Loaded plugins: fastestmirror
    centos7vm: Loading mirror speeds from cached hostfile
    centos7vm: Package curl-7.29.0-59.el7_9.1.x86_64 already installed and latest version
    centos7vm: Package wget-1.14-18.el7_6.1.x86_64 already installed and latest version
    centos7vm: Package 2:vim-enhanced-7.4.629-8.el7_9.x86_64 already installed and latest version
    centos7vm: Package net-tools-2.0-0.25.20131004git.el7.x86_64 already installed and latest version
    centos7vm: Package yum-utils-1.1.31-54.el7_8.noarch already installed and latest version
    centos7vm: Resolving Dependencies
    centos7vm: --> Finished Dependency Resolution
    centos7vm:
    centos7vm: Dependencies Resolved
    centos7vm:
    centos7vm: ================================================================================
    centos7vm:  Package                Arch         Version                   Repository  Size
    centos7vm: ================================================================================
    centos7vm: Installing:
    centos7vm:  git                    x86_64       1.8.3.1-23.el7_8          base       4.4 M
    centos7vm:  htop                   x86_64       2.2.0-3.el7               epel       103 k
    centos7vm:  netcat                 x86_64       1.217-3.el7               epel        29 k
    centos7vm:  tmux                   x86_64       1.8-4.el7                 base       243 k
    centos7vm:  zsh                    x86_64       5.0.2-34.el7_8.2          base       2.4 M
    centos7vm: Installing for dependencies:
    centos7vm:  libbsd                 x86_64       0.8.3-1.el7               epel        85 k
    centos7vm:  libevent               x86_64       2.0.21-4.el7              base       214 k
    centos7vm:  libretls               x86_64       3.3.3p1-1.el7             epel        38 k
    centos7vm:  openssl11-libs         x86_64       1:1.1.1g-3.el7            epel       1.5 M
    centos7vm:  perl-Error             noarch       1:0.17020-2.el7           base        32 k
    centos7vm:  perl-Git               noarch       1.8.3.1-23.el7_8          base        56 k
    centos7vm:  perl-TermReadKey       x86_64       2.30-20.el7               base        31 k
    centos7vm:  rsync                  x86_64       3.1.2-10.el7              base       404 k
    centos7vm:
    centos7vm: Transaction Summary
    centos7vm: ================================================================================
    centos7vm: Install  5 Packages (+8 Dependent packages)
    centos7vm:
    centos7vm: Installed:
    centos7vm:   git.x86_64 0:1.8.3.1-23.el7_8            htop.x86_64 0:2.2.0-3.el7
    centos7vm:   netcat.x86_64 0:1.217-3.el7              tmux.x86_64 0:1.8-4.el7
    centos7vm:   zsh.x86_64 0:5.0.2-34.el7_8.2
    centos7vm:
    centos7vm: Dependency Installed:
    centos7vm:   libbsd.x86_64 0:0.8.3-1.el7             libevent.x86_64 0:2.0.21-4.el7
    centos7vm:   libretls.x86_64 0:3.3.3p1-1.el7         openssl11-libs.x86_64 1:1.1.1g-3.el7
    centos7vm:   perl-Error.noarch 1:0.17020-2.el7       perl-Git.noarch 0:1.8.3.1-23.el7_8
    centos7vm:   perl-TermReadKey.x86_64 0:2.30-20.el7   rsync.x86_64 0:3.1.2-10.el7
    centos7vm:
    centos7vm: Complete!
    centos7vm: Loaded plugins: fastestmirror
    centos7vm: adding repo from: https://download.docker.com/linux/centos/docker-ce.repo
    centos7vm: grabbing file https://download.docker.com/linux/centos/docker-ce.repo to /etc/yum.repos.d/docker-ce.repo
    centos7vm: repo saved to /etc/yum.repos.d/docker-ce.repo
    centos7vm: --> Finished Dependency Resolution
    centos7vm:
    centos7vm: Dependencies Resolved
    centos7vm:
    centos7vm: ================================================================================
    centos7vm:  Package                Arch   Version                   Repository        Size
    centos7vm: ================================================================================
    centos7vm: Installing:
    centos7vm:  containerd.io          x86_64 1.4.9-3.1.el7             docker-ce-stable  30 M
    centos7vm:  docker-ce              x86_64 3:20.10.8-3.el7           docker-ce-stable  23 M
    centos7vm:  docker-ce-cli          x86_64 1:20.10.8-3.el7           docker-ce-stable  29 M
    centos7vm: Installing for dependencies:
    centos7vm:  audit-libs-python      x86_64 2.8.5-4.el7               base              76 k
    centos7vm:  checkpolicy            x86_64 2.5-8.el7                 base             295 k
    centos7vm:  container-selinux      noarch 2:2.119.2-1.911c772.el7_8 extras            40 k
    centos7vm:  docker-ce-rootless-extras
    centos7vm:                         x86_64 20.10.8-3.el7             docker-ce-stable 8.0 M
    centos7vm:  docker-scan-plugin     x86_64 0.8.0-3.el7               docker-ce-stable 4.2 M
    centos7vm:  fuse-overlayfs         x86_64 0.7.2-6.el7_8             extras            54 k
    centos7vm:  fuse3-libs             x86_64 3.6.1-4.el7               extras            82 k
    centos7vm:  libcgroup              x86_64 0.41-21.el7               base              66 k
    centos7vm:  libsemanage-python     x86_64 2.5-14.el7                base             113 k
    centos7vm:  policycoreutils-python x86_64 2.5-34.el7                base             457 k
    centos7vm:  python-IPy             noarch 0.75-6.el7                base              32 k
    centos7vm:  setools-libs           x86_64 3.3.8-4.el7               base             620 k
    centos7vm:  slirp4netns            x86_64 0.4.3-4.el7_8             extras            81 k
    centos7vm:
    centos7vm: Complete!
    centos7vm: Created symlink from /etc/systemd/system/multi-user.target.wants/docker.service to /usr/lib/systemd/system/docker.service.
    centos7vm: Created symlink from /etc/systemd/system/multi-user.target.wants/containerd.service to /usr/lib/systemd/system/containerd.service.

🎉. Success! Things seem like they ran correctly: my VM is renamed, my apps are there, I can directly SSH to this machine, I can sudo, Docker is installed as well and all other configs applied! Just note that commands that do not usually provide an output will not be shown above.

In Part 3 we will have a look at a multi-machine scenario.