Docker the Vagrant Way - part 2

Using Vagrant to Orchestrate Docker Containers

Joaquín Menchaca (智裕)
5 min readMay 28, 2019

--

This will document the overall solution from previous article that defined the problem for orchestrating two Docker containers (WordPress app and dependent MySQL 5.7 database) on a virtual guest (Ubuntu 16.04), as well as InSpec script to validate the final result.

Previous Article

Solution Details

The Vagrantfile configuration file is a Ruby script that is evaluated at runtime. Vagrant has a many provisioners, including Docker, that we can use to provision our system.

The Docker Provisioner is capable of pulling down images and running containers, but if we want more advance features like volumes and network resources, then we have to use the Shell Provisioner, and later pass in custom argument list to the Docker Provisioner for options not directly supported, such as using the network resource and volume resource.

Global Variables

Recall in supplied Vagrantfile given to us in the previous article, we declared some global variables that will be used throughout our script:

@wordpress_port = ENV['WORDPRESS_PORT'] || '8080'
@mysql_volume = "db_data"
@network_name = "wordpress_net"

Network and Volume Resources

You can add these lines to the script to add the required network and volumes resources:

config.vm.provision :shell, name: "docker_volume.sh",  
inline: "docker volume create -d local \
--name #{@mysql_volume}"
config.vm.provision :shell, name: "docker_network.sh",
inline: "docker network list | grep -q #{@network_name} || \
docker network create #{@network_name}"

Containers

For the containers, we will use the Docker Provisioner, so we need to create a block of commands we’ll pass to the provisioner that will contain the WordPress and MySQL container run instructions:

config.vm.provision :docker do |d|
docker.run "mysql:5.7", ...
docker.run "wordpress:latest", ...
end

Detailing out the MySQL line, we’ll have the following:

docker.run "mysql:5.7",
dameonize: true,
restart: "always",
args: %W[
-v #{@mysql_volume}:/var/lib/mysql:rw
--network=#{@network_name}
-e MYSQL_ROOT_PASSWORD=wordpress
-e MYSQL_PASSWORD=wordpress
-e MYSQL_USER=wordpress
-e MYSQL_DATABASE=wordpress
--name db].join(' ')

And then detailing WordPress container:

docker.run "wordpress:latest",
dameonize: true,
restart: "always",
args: %W[
--network=#{@network_name}
-p #{@wordpress_port}:80
-e WORDPRESS_DB_HOST=db:3306
-e WORDPRESS_DB_PASSWORD=wordpress
--name wordpresss].join(' ')

For those unfamiliar with Ruby, we specify our argument list as an array divided by white space with this notation: %W[ words words words ] , and then join() the list together into a complete string.

This way we can format it all nice and neat, but have it render as a single long string when eventaully passed to the docker run command.

The full block should like this when combined:

config.vm.provision :docker do |docker|
docker.run "mysql:5.7",
dameonize: true,
restart: "always",
args: %W[
-v #{@mysql_volume}:/var/lib/mysql:rw
--network=#{@network_name}
-e MYSQL_ROOT_PASSWORD=wordpress
-e MYSQL_PASSWORD=wordpress
-e MYSQL_USER=wordpress
-e MYSQL_DATABASE=wordpress
--name db].join(' ')
docker.run "wordpress:latest",
dameonize: true,
restart: "always",
args: %W[
--network=#{@network_name}
-p #{@wordpress_port}:80
-e WORDPRESS_DB_HOST=db:3306
-e WORDPRESS_DB_PASSWORD=wordpress
--name wordpress].join(' ')
end

Full Source Solution

Below is a full working solution. You can download this to a directory as Vagrantfile, and then run vagrant up to test the solution:

Verifying the Solution

You can verify the solution using InSpec to remote into the system from the host workstation, or use Vagrant to remote into the guest system and run InSpec locally on the guest. Of course you’ll need to download and install InSpec first.

Verifying From Host Workstation

In the previous article I documented how to use InSpec to remote into the system and run the tests. I showed how to use a helper file and also how to support supplying alternative user specified port from the WORDPRESS_PORT environment variable:

echo "port: $WORDPRESS_PORT" > attributes.yml
inspec exec $(./inspec_helper) \
container_test.rb \
--attrs=attributes.yml

This is the best way to run it, because Vagrant itself is orchestrating Docker containers, so we need to tell our test script specifically which port is correct.

Advanced: Verifying From the Guest Workstation

Should however you wish to run the whole solution on the virtual guest instead, and then just call out command remotely using this:

# default port of 8080
vagrant ssh -c 'inspec exec container_test.rb'
# using alternative port
vagrant ssh -c "echo 'port: $WORDPRESS_PORT' > port.yml"
vagrant ssh -c 'inspec exec container_test.rb --attrs=port.yml'

You would need to install InSpec on the Ubuntu 16.04 guest. Here’s how you could do it with rvm.

Add these inline scripts somewhere in the top of your Vagrantfile below.

RVM install snippet

@rvm_setup = <<SCRIPT
if [[ ! -d $HOME/.rvm ]]; then
gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys \
409B6B1796C275462A1703113804BB82D39DC0E3 \
7D2BAF1CF37B13E2069D6956105BD0E739499BDB
curl -sSL https://get.rvm.io | bash -s stable
else
echo "'$HOME/.rvm' already exists, skipping"
fi
SCRIPT
@ruby_setup = <<SCRIPT
source ${HOME}/.rvm/scripts/rvm
rvm install 2.5.3
rvm use 2.5.3
echo 'gem: --no-document' >> ~/.gemrc
SCRIPT
@inspec_setup = <<SCRIPT
source ${HOME}/.rvm/scripts/rvm
gem install inspec
gem install inspec-bin
SCRIPT

Now in the main configuration block, i.e. Vagrant.configure, add the following after the docker install line:

config.vm.provision :shell, inline: @rvm_setup, privileged: false
config.vm.provision :shell, inline: @ruby_setup, privileged: false
config.vm.provision :shell, inline: @inspec_setup, privileged: false

Language Notes

The Vagrantfile configuration file is essentially a Ruby script that is eval by Vagrant tool.

For those not familiar with the Ruby language, here are some snippets and explanation for things used in this script. Otherwise, just ignore this section.

Parameter Lists

In Ruby, you can omit parenthesis () of a parameter list. There for you can take a method like the one below.

config.vm.provision(:shell, { :name => runit.sh, :inline => @scrpt})

And change it to the following, as long as you have a space between the method and parameter list:

config.vm.provision :shell, { :name => runit.sh, :inline => @scrpt}

Anonymous Hash in Parameter List

Also, if you have an anonymous hash pass as a parameter, you can also omit the curly braces {}:

config.vm.provision :shell, :name => runit.sh, :inline => @scrpt

Symbols As Keys in a Hash

Ruby has this type called symbols that are more efficient than strings, and are represented by the colon : before the variable name, e.g. :var.

If the symbol is used as a key in a hash, you can write it at the end of the variable name, e.g. var: and then omit the =>. Thus the above

config.vm.provision :shell, name: runit.sh, inline: @scrpt

Blocks

In Ruby, you also have code blocks, which can be passed into a method as well. A code block can be written either using curly braces {} or between do and end.

Using curly braces for single line code block:

config.vm.provision :docker, { |dkr| ... code ... }

Using do…end for multi-line code block:

config.vm.provision :docker, do |dkr| 
... code ...
end

Conclusion

There you have it, in the previous article I detailed the problem and in this article I give detail out the Vagrant solution with full Vagrantfile source code, expanded upon the automated verification using InSpec, and provided some notes (gotchas) in Ruby that may be confusing to new comers.

--

--

Joaquín Menchaca (智裕)

DevOps/SRE/PlatformEng — k8s, o11y, vault, terraform, ansible