Docker the Vagrant Way - part 2
Using Vagrant to Orchestrate Docker Containers
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.