Self-hosted 'vagrant cloud'

A tutorial on how to set up a self-hosted 'vagrant cloud' with versioned, self-packaged vagrant boxes

Preamble

Before we start setting things up, I assume this is what you know / what you have:

The tutorial uses an installation of Ubuntu 14.04.2 LTS as the guest machine, VirtualBox at version 5.0.0 as provider and Vagrant at version 1.7.4.

1. Install the tools

2. Prepare your virtual machine

2.1 Import an Ubuntu image to VirtualBox

2.2 Setup the virtual machine

Before you boot the vm for the first time:

Configure the guest

$ sudo apt-get update
$ sudo apt-get dist-upgrade -y
$ sudo nano /root/.profile
# ~/.profile: executed by Bourne-compatible login shells.

if [ "$BASH" ]; then
  if [ -f ~/.bashrc ]; then
    . ~/.bashrc
  fi
fi

mesg n # replace this line by "tty -s && mesg n"

Note: This avoids an annoying warning, when you vagrant up later.

$ sudo nano /etc/hostname
ubuntu-amd64 # replace this by "devops-template"
$ sudo nano /etc/hosts
127.0.0.1   localhost
127.0.1.1   ubuntu-amd64    # replace this by "127.0.1.1 devops-template"

# The following lines are desirable for IPv6 capable hosts
::1         localhost ip6-localhost ip6-loopback
ff02::1     ip6-allnodes
ff02::2     ip6-allrouters
$ sudo locale-gen en_US.UTF-8 # Or whatever language you want to use
$ sudo dpkg-reconfigure locales
$ sudo nano /etc/default/locale
LANG="en_US.UTF-8"
LANGUAGE="en_US"
$ sudo adduser vagrant
# Set the password to "vargrant" too!
# Set the Full Name to "Vagrant", leave the rest blank
# Add a ssh config folder and authorized_keys file
$ sudo mkdir /home/vagrant/.ssh
$ sudo touch /home/vagrant/.ssh/authorized_keys
# Set owner and permissions
$ sudo chown -R vagrant /home/vagrant/.ssh
$ sudo chmod 0700 /home/vagrant/.ssh
$ sudo chmod 0600 /home/vagrant/.ssh/authorized_keys
# Add the insecure public key, see https://github.com/mitchellh/vagrant/tree/master/keys
$ su vagrant
$ curl 'https://raw.githubusercontent.com/mitchellh/vagrant/master/keys/vagrant.pub' >> /home/vagrant/.ssh/authorized_keys
$ exit
$ sudo nano /etc/sudoers.d/vagrant
vagrant ALL=(ALL) NOPASSWD: ALL
$ sudo chmod 0440 /etc/sudoers.d/vagrant
$ sudo nano /etc/ssh/sshd_config
# Add the following line at the end of the file:
UseDNS no

Hint: Do not install the guest additions with apt-get install virtualbox-guest-additions-iso as this release is mostly outdated. I said above that I'm using VirtualBox in Version 5.0.0, so the guest additions should be of the same version.

# prepare
$ sudo apt-get install -y linux-headers-generic build-essential dkms
# get the right ISO from http://download.virtualbox.org/virtualbox/
$ wget http://download.virtualbox.org/virtualbox/5.0.0/VBoxGuestAdditions_5.0.0.iso
# create a mount folder
$ sudo mkdir /media/VBoxGuestAdditions
# mount the ISO
$ sudo mount -o loop,ro VBoxGuestAdditions_5.0.0.iso /media/VBoxGuestAdditions
# install the guest additions
$ sudo sh /media/VBoxGuestAdditions/VBoxLinuxAdditions.run
# remove the ISO
$ rm VBoxGuestAdditions_5.0.0.iso
# unmount the ISO
$ sudo umount /media/VBoxGuestAdditions
# remove the mount folder
$ sudo rmdir /media/VBoxGuestAdditions
$ rm -rf /etc/motd
$ sudo nano /etc/motd
--
Welcome to devops-template version 0.1.0!
--
$ sudo shutdown -r now

Now you should see a login terminal like this:

Ubuntu 14.04.2 LTS devops-template tty1

devops-template login: _

Login and check the message of the day (motd) we set up:

Last login: ...
Welcome to Ubuntu ...

 * Documentation: ...

 [...]

--
Welcome to devops-template version 0.1.0!
--
ubuntu@devops-template:~$ _
$ sudo shutdown -h now

Got it? Yeah, preparation is done!

3. Package the vagrant box

Remember, we said in the message of the day, that this is version 0.1.0.

$ mkdir ~/VagrantBoxes
$ mkdir ~/VagrantTest

3.1 Create the box out of the vm named devops-template in VirtualBox

$ cd ~/VagrantBoxes
$ vagrant package --base 'devops-template' --output 'devops_0.1.0.box'

Note: Because we will build multiple versions of the devops-template vm, we will put the version number in the name of the box file.

Packaging will take some time, you may get your next coffee!

When packaging is done, you should see an output like this:

==> devops-template: Exporting VM...
==> devops-template: Compressing package to: ~/VagrantBoxes/devops_0.1.0.box

3.2 Test the box

$ cd ~/VagrantTest
$ vagrant box add 'devops' file://~/VagrantBoxes/devops_0.1.0.box

If successful, you should see output like this:

==> box: Adding box 'devops' (v0) for provider:
    box: Downloading: file://~/VagrantBoxes/devops_0.1.0.box
==> box: Successfully added box 'devops' (v0) for 'virtualbox'!

Note the v0. There is no version specified yet.

$ vagrant init

Now there is a Vagrantfile with default settings in your current directory.

# Change the following line from
config.vm.box = "base"
# to
config.vm.box = "devops"
# save the file

Note: base is vagrant's default name for a box. But we told vagrant box add the name devops.

$ vagrant up

If done, you should see output like this:

Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'devops'...
==> default: Matching MAC address for NAT networking...
==> default: Setting the name of the VM: VagrantTest_default_1406634147824_25052
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: Forwarding ports...
    default: 22 => 2222 (adapter 1)
==> 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: Warning: Connection timeout. Retrying...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
==> default: Mounting shared folders...
    default: /vagrant => ~/VagrantTest

Note: Vagrant's default behaviour is to boot the vm in headless mode (without a GUI in background). While booting the vm you can see the new vm in your VirtualBox GUI as "-> Running"

$ vagrant ssh

Now you should see our previously added message of the day.

$ cd /vagrant/
$ ls -la

And you should see something like this:

drwxr-xr-x  1 vagrant vagrant  136 Jul 29 13:38 .
drwxr-xr-x 23 root    root    4096 Jul 29 13:42 ..
drwxr-xr-x  1 vagrant vagrant  102 Jul 29 13:38 .vagrant
-rw-r--r--  1 vagrant vagrant 4813 Jul 29 13:39 Vagrantfile

Okay, your box works fine, well done!

$ exit # On guest
$ vagrant destroy # On host
$ vagrant box remove 'devops'
Removing box 'devops' (v0) with provider 'virtualbox'...

4. Using a box catalog for versioning

In order to serve multiple versions of a vagrant box and enable update notifications we need to set up a box catalog. This catalog is written in JSON code to a single file.

To keep things simple at this point we will carry on in the local filesystem of your host.

4.1 Set up the catalog for the devops box

cd ~/VagrantBoxes
{
    "name": "devops",
    "description": "This box contains Ubuntu 14.04.2 LTS 64-bit.",
    "versions": [{
        "version": "0.1.0",
        "providers": [{
                "name": "virtualbox",
                "url": "file://~/VagrantBoxes/devops_0.1.0.box",
                "checksum_type": "sha1",
                "checksum": "d3597dccfdc6953d0a6eff4a9e1903f44f72ab94"
        }]
    }]
}

What is going on here?

$ openssl sha1 ~/VagrantBoxes/devops_0.1.0.box
SHA1(~/VagrantBoxes/devops_0.1.0.box)= d3597dccfdc6953d0a6eff4a9e1903f44f72ab94

Note: This is done on a linux based system. On Windows there will be another way.

Your catalog is finished!

4.2 Link the catalog to vagrant instead of the box

$ cd ~/VagrantTest
# Under ...
config.vm.box = "devops"
# ... add the line
config.vm.box_url = "file://~/VagrantBoxes/devops.json"
# save the file
$ vagrant up

When done, you should see output like this:

Bringing machine 'default' up with 'virtualbox' provider...
==> default: Box 'devops' could not be found. Attempting to find and install...
    default: Box Provider: virtualbox
    default: Box Version: >= 0
==> default: Loading metadata for box 'file://~/VagrantBoxes/devops.json'
    default: URL: file://~/VagrantBoxes/devops.json
==> default: Adding box 'devops' (v0.1.0) for provider: virtualbox
    default: Downloading: file://~/VagrantBoxes/devops_0.1.0.box
    default: Calculating and comparing box checksum...
==> default: Successfully added box 'devops' (v0.1.0) for 'virtualbox'!

Note the line Loading metadata for box 'file://~/VagrantBoxes/devops.json' that confirms the catalog is read. And note the line Adding box 'devops' (v0.1.0) for provider: virtualbox that confirms that version 0.1.0 is added to VirtualBox. The last two lines confirm that our checksum in the devops.json file was correct.

==> default: Importing base box 'devops'...
==> default: Matching MAC address for NAT networking...
==> default: Checking if box 'devops' is up to date...
==> default: Setting the name of the VM: VagrantTest_default_1406640770074_13210
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: Forwarding ports...
    default: 22 => 2222 (adapter 1)
==> 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: Warning: Connection timeout. Retrying...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
==> default: Mounting shared folders...
    default: /vagrant => ~/VagrantTest

Again, our machine is up and running. If you wish you now can re-check by ssh into the machine, reading the message of the day and listing the content of /vagrant on the guest.

Okay, now we have built a vagrant box with an initial version. Let's add another version.

$ vagrant halt
==> default: Attempting graceful shutdown of VM...

4.3 Raise the box version by changing the template vm

$ sudo nano /etc/motd
--
Welcome to devops-template version 0.1.1!
--
$ sudo shutdown -h now

To be clear: Changing the version number in /etc/motd has nothing to do with the version itself. It just simulates a minor but visible change to the vm template. Instead of changing the content of a file, you'll be installing software or editing configs on a real-world vm.

$ cd ~/VagrantBoxes
$ vagrant package --base 'devops-template' --output 'devops_0.1.1.box'
==> devops-template: Exporting VM...
==> devops-template: Compressing package to: ~/VagrantBoxes/devops_0.1.1.box

Note the raised version number 0.1.1 in the output filename!

Your directory listing of ~/VagrantBoxes should look like this now:

$ ls -a1 ~/VagrantBoxes
.
..
devops.json
devops_0.1.0.box
devops_0.1.1.box

If you wish you can test your new box like done under chapter 3.2.

4.4 Add the new version to the catalog

{
    "name": "devops",
    "description": "This box contains Ubuntu 14.04.1 LTS 64-bit.",
    "versions": [{
        "version": "0.1.0",
        "providers": [{
                "name": "virtualbox",
                "url": "file:///Users/hollodotme/VagrantBoxes/devops_0.1.0.box",
                "checksum_type": "sha1",
                "checksum": "d3597dccfdc6953d0a6eff4a9e1903f44f72ab94"
        }]
    },{
        "version": "0.1.1",
        "providers": [{
                "name": "virtualbox",
                "url": "file:///Users/hollodotme/VagrantBoxes/devops_0.1.1.box",
                "checksum_type": "sha1",
                "checksum": "0b530d05896cfa60a3da4243d03eccb924b572e2"
        }]
    }]
}

Don't forget to determine the checksum of the newly created box devops_0.1.1.box!

4.5 Check for outdated vagrant box and update

$ cd ~/VagrantTest
$ vagrant box outdated
Checking if box 'devops' is up to date...
A newer version of the box 'devops' is available! You currently
have version '0.1.0'. The latest is version '0.1.1'. Run
`vagrant box update` to update.

Suprise, suprise - a new version is available!

Note: Instead of manually asking for outdated boxes, vagrant will notify you automatically when you use the Vagrant commands like vagrant up, vagrant reload, vagrant resume, etc.!

$ vagrant box update
==> default: Checking for updates to 'devops'
    default: Latest installed version: 0.1.0
    default: Version constraints:
    default: Provider: virtualbox
==> default: Updating 'devops' with provider 'virtualbox' from version
==> default: '0.1.0' to '0.1.1'...
==> default: Loading metadata for box 'file://~/VagrantBoxes/devops.json'
==> default: Adding box 'devops' (v0.1.1) for provider: virtualbox
    default: Downloading: file://~/VagrantBoxes/devops_0.1.1.box
    default: Calculating and comparing box checksum...
==> default: Successfully added box 'devops' (v0.1.1) for 'virtualbox'!

Note: The box with version 0.1.0 still exists. Vagrant will never prune your boxes automatically because of potential data loss.

$ vagrant box remove 'devops'
You requested to remove the box 'devops' with provider
'virtualbox'. This box has multiple versions. You must
explicitly specify which version you want to remove with
the `--box-version` flag. The available versions for this
box are:

 * 0.1.0
 * 0.1.1

Okay, so...

$ vagrant box remove 'devops' --box-version '0.1.0'
~/VagrantTest/Vagrantfile:5: warning: already initialized constant VAGRANTFILE_API_VERSION
~/VagrantTest/Vagrantfile:5: warning: previous definition of VAGRANTFILE_API_VERSION was here
Box 'devops' (v0.1.0) with provider 'virtualbox' appears
to still be in use by at least one Vagrant environment. Removing
the box could corrupt the environment. We recommend destroying
these environments first:

default (ID: 3206d9d1a427459daac770f2e7e81f1b)

Are you sure you want to remove this box? [y/N]

No! Let's destroy it first.

$ vagrant destroy
    default: Are you sure you want to destroy the 'default' VM? [y/N] y
==> default: Destroying VM and associated drives...

Now remove it, please!

$ vagrant box remove 'devops' --box-version '0.1.0'
Removing box 'devops' (v0.1.0) with provider 'virtualbox'...

Hell yeah!

4.6 Check for the change

$ vagrant up
$ vagrant ssh

Now you should see the previously changed version number 0.1.1 in the message of the day after login.

--
Welcome to devops-template version 0.1.1!
--

So we are up-to-date!

What we did so far:

What we will do now:

So cleanup your desk: exit your SSH session, destroy the vm and remove it from vagrant.

$ exit # On guest
$ vagrant destroy # On host
$ vagrant box remove 'devops'

5. Hosting

As I mentioned at the beginning, I assume that you have private/public webserver and access to its config and filesystem.

For explanation I'll use the domain www.example.com targeting to this webserver. Furthermore www.example.com points to /var/www/ on the webserver's filesystem (document root).

5.1 Suggested directory structure

To keep things easy I prefer to separate the catalog and the box files physically in the filesystem. Keep on reading and you'll understand why.

- /var/www                              # document root
        `- vagrant
           `- devops                    # box name folder
              |- boxes                  # contains all available box files
              |  |- devops_0.1.0.box    # version 0.1.0
              |  `- devops_0.1.1.box    # version 0.1.1
              `- devops.json            # box catalog

Translated to URLs we have three targets to care about (we will use these later):

5.2 Webserver configuration

I want to explain the basic webserver configuration with nginx on a linux server, because this is my favorite software. The configuration can be ported to apache and/or windows as well.

$ ssh user@example.com
$ sudo apt-get install nginx-full
# Create folders
$ sudo mkdir -p /var/www/vagrant/devops/boxes
# Set owner to www-data
$ sudo chown -R www-data:www-data /var/www
# Set permissions
$ sudo chmod -R 0751 /var/www
$ sudo rm -rf /etc/nginx/sites-enabled/default
sudo nano /etc/nginx/sites-available/example.com

... with the following content:

server {
    listen   80 default_server;
    listen   [::]:80 ipv6only=on default_server;

    server_name example.com www.example.com;

    root /var/www;

    # Match the box name in location and search for its catalog
    # e.g. http://www.example.com/vagrant/devops/ resolves /var/www/vagrant/devops/devops.json  
    location ~ ^/vagrant/([^\/]+)/$ {
        index $1.json;
        try_files $uri $uri/ $1.json =404;
        autoindex off;
    }

    # Enable auto indexing for the folder with box files
    location ~ ^/vagrant/([^\/]+)/boxes/$ {
        try_files $uri $uri/ =404;
        autoindex on;
        autoindex_exact_size on;
        autoindex_localtime on;
    }

    # Serve json files with content type header application/json
    location ~ \.json$ {
        add_header Content-Type application/json;
    }

    # Serve box files with content type application/octet-stream
    location ~ \.box$ {
        add_header Content-Type application/octet-stream;
    }

    # Deny access to document root and the vagrant folder
    location ~ ^/(vagrant/)?$ {
        return 403;
    }
}
$ sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/000-example.com
$ service nginx restart
$ exit

5.3 Change the box catalog

Now, that our boxes won't be stored any longer on the local filesystem, we have to change their locations in the box catalog.

{
    "name": "devops",
    "description": "This box contains Ubuntu 14.04.1 LTS 64-bit.",
    "versions": [{
        "version": "0.1.0",
        "providers": [{
                "name": "virtualbox",
                "url": "http://www.example.com/vagrant/devops/boxes/devops_0.1.0.box",
                "checksum_type": "sha1",
                "checksum": "d3597dccfdc6953d0a6eff4a9e1903f44f72ab94"
        }]
    },{
        "version": "0.1.1",
        "providers": [{
                "name": "virtualbox",
                "url": "http://www.example.com/vagrant/devops/boxes/devops_0.1.1.box",
                "checksum_type": "sha1",
                "checksum": "0b530d05896cfa60a3da4243d03eccb924b572e2"
        }]
    }]
}

If you open the URL http://www.example.com/vagrant/devops/ in your browser you should see your JSON box catalog.

5.4 Upload your boxes

If you open the URL http://www.example.com/vagrant/devops/boxes/ in your browser you should see a directory listing with both box files listed.

5.5 Change the Vagrantfile

$ cd ~/VagrantTest
# Change the line
config.vm.box_url = "file://~/VagrantBoxes/devops.json"
# to
config.vm.box_url = "http://www.example.com/vagrant/devops/"
# save the file

5.6 Get finally up and running

$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Box 'devops' could not be found. Attempting to find and install...
    default: Box Provider: virtualbox
    default: Box Version: >= 0
==> default: Loading metadata for box 'http://www.example.com/vagrant/devops/'
    default: URL: http://www.example.com/vagrant/devops/
==> default: Adding box 'devops' (v0.1.1) for provider: virtualbox
    default: Downloading: http://www.example.com/vagrant/devops/boxes/devops_0.1.1.box
    default: Calculating and comparing box checksum...
==> default: Successfully added box 'devops' (v0.1.1) for 'virtualbox'!
==> default: Importing base box 'devops'...
==> default: Matching MAC address for NAT networking...
==> default: Checking if box 'devops' is up to date...
==> default: Setting the name of the VM: VagrantTest_default_1406660957112_34972
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
==> default: Forwarding ports...
    default: 22 => 2222 (adapter 1)
==> 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: Warning: Connection timeout. Retrying...
    default: Warning: Remote connection disconnect. Retrying...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
==> default: Mounting shared folders...
    default: /vagrant => ~/VagrantTest

And here it is: Your own vagrant cloud!

Epilog

Further reading

09/27/2016