Build a Docker Universal Plane Control (UCP) Demo Lab

This is a demo of Docker Universal Control Plane (UCP), as of v0.5.0 (December 2015), using Vagrant.

The needed code is on my GitHub project page: https://github.com/sjourdan/vagrant-docker-ucp

Requirements for this demo:

This demo will launch 2 machines with Ubuntu 14.04 LTS and a recent 4.2.x kernel:

  • ucp-master (192.168.100.10)
  • ucp-slave (192.168.100.11)

UCP main container will need to be explicitly named ucp on all nodes, master or slave.

UCP requires a kernel > 3.16.0 to work and a minimum of 1.5GB of RAM per node.

This vagrant setup installs a 4.2.x kernel on Ubuntu 14.04 LTS for better OverlayFS support if needed.

You can tweak a few settings in the config.rb file.

Deploy UCP master

Start it with the virtualbox provider and reboot the VM to activate the new kernel:

$ vagrant up ucp-master --provider virtualbox
$ vagrant reload ucp-master

Install UCP Master

You can choose to deploy the master either manually (interactive) or automatically (through environment variables accessed by Docker). I recommend a fully automatic deploy.

Access the master VM by issuing:

$ vagrant ssh ucp-master

Interactive Deploy

Bootstrap interactively the system, that will ask some questions:

  • Docker Hub username, password and email
  • UCP admin password (`admin:orca` by default)
$ docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock --name ucp dockerorca/ucp install -i
 INFO[0000] Verifying your system is compatible with UCP
 Please choose your initial Orca admin password:
 Confirm your initial password:
 INFO[0011] Pulling required images

Automated Deployment

Export or set a few environment variables about your Docker Hub account:

$ export REGISTRY_USERNAME=username
$ export REGISTRY_PASSWORD=password
$ export REGISTRY_EMAIL=email@

Then launch the container fully configured with the ucp name, as a fresh-install, and specifying our IP adress 192.168.100.10:

$ docker run --rm -it \
 -v /var/run/docker.sock:/var/run/docker.sock \
 -e REGISTRY_USERNAME=${REGISTRY_USERNAME} \
 -e REGISTRY_PASSWORD=${REGISTRY_PASSWORD} \
 -e REGISTRY_EMAIL=${REGISTRY_EMAIL} \
 --name ucp \
 dockerorca/ucp install \
 --fresh-install \
 --san 192.168.100.10 \
 --host-address 192.168.100.10

UCP will then be available at https://192.168.100.10 with credentials: admin:orca.

Deploy a UCP slave

Slave VM Deployment

Start the slave VM, and reboot it, to boot on the new kernel.

$ vagrant up ucp-slave --provider=virtualbox
$ vagrant reload ucp-slave

Vagrant now displays both ucp-master and ucp-slave machines:

$ vagrant status
machine states:
ucp-master                running (virtualbox)
ucp-slave                 running (virtualbox)

Access the slave VM by issuing:

$ vagrant ssh ucp-slave

UCP Container Deployment

To start this container, you need to grab the SHA1 fingerprint of the UCP master.

To do so, the tool has an option (fingerprint) here to help:

$ vagrant ssh ucp-master -c "docker run --rm -it --name ucp -v /var/run/docker.sock:/var/run/docker.sock dockerorca/ucp fingerprint"
SHA1 Fingerprint=E5:A2:45:C2:8B:B8:84:16:E3:F6:24:4F:49:44:3F:91:AC:FC:66:47

Store the SHA1 fingerprint somewhere.

Set your Docker Hub credentials in environment variables like the ucp-master (or replace the values directly from the command line):

$ export REGISTRY_USERNAME=username
$ export REGISTRY_PASSWORD=password
$ export REGISTRY_EMAIL=email@

Then launch the UCP slave fully configured by joining it to the cluster, with the UCP admin credentials correctly set, the master URL and SHA1 fingerprint and the node IP adresses (192.168.100.11):

$ docker run --rm -it \
 --name ucp \
 -e UCP_ADMIN_USER=admin \
 -e UCP_ADMIN_PASSWORD=orca \
 -e REGISTRY_USERNAME=${REGISTRY_USERNAME} \
 -e REGISTRY_PASSWORD=${REGISTRY_PASSWORD} \
 -e REGISTRY_EMAIL=${REGISTRY_EMAIL} \
 -v /var/run/docker.sock:/var/run/docker.sock \
 dockerorca/ucp join \
 --url https://192.168.100.10:443 \
 --san 192.168.100.11 \
 --host-address 192.168.100.11 \
 --fingerprint=<SHA1:CERT:FINGERPRINT>
 ```

Check cluster

Navigate to https://192.168.100.10/#/nodes and you’ll see your two nodes.

To talk directly to the docker cluster, you need to first download a “bundle”.

  • Login with your user to https://192.168.100.10
  • Navigate to your profile (https://192.168.100.10/#/user)
  • Generate a “Client Bundle” by clicking “Create a Client Bundle”, and/or download your “bundle” (a ucp-bundle-$username.zip file)
  • Then import it on your workstation to use it:
$ cd bundle
$ unzip ucp-bundle-admin.zip
$ eval $(<env.sh)

Now verify it’s talking to the cluster:

$ docker info
Containers: 11
Images: 18
Role: primary
Strategy: spread
Filters: health, port, dependency, affinity, constraint
Nodes: 2
[...]

You can now use the Docker UCP “Application” feature, or interact with the swarm cluster.

Install VirtualBox Additions on RHEL/CentOS 7.x

If you follow most of the documentation found here and there, your VirtualBox Additions installation will mostly be okay, excepted for OpenGL.

Errors like:

Building the OpenGL support module[FAILED]
(Look at /var/log/vboxadd-install.log to find out what went wrong. The module is not built but the others are.)

Inside the logfile, you’ll find something like:

echo >&2 "  ERROR: Kernel configuration is invalid.";        \
echo >&2 "         include/generated/autoconf.h or include/config/auto.conf are missing.";\
echo >&2 "         Run 'make oldconfig && make prepare' on kernel src to fix it.";    \
echo >&2 ;                            \
/bin/false)

Context

  • Red Hat 7 .x based OS (RHEL 7.2 here), like CentOS
  • VirtualBox 5.0.10

 

EPEL

If not already done, enable EPEL from https://fedoraproject.org/wiki/EPEL

rpm -ivh epel-release-latest-7.noarch.rpm

or CentOS:

yum install epel-release

Kernel Development Tools

We’ll need those to compile.

yum install kernel-devel kernel-headers gcc make

DKMS

If not already done, install Dynamic Kernel Module Support (DKMS):

yum install dkms

Build

export KERN_DIR=/usr/src/kernels/`uname -r`
export MAKE='/usr/bin/gmake -i'
cd /run/media/username/VBOXADDITIONS_5.0.10_104061/
./VBoxLinuxAdditions.run

Reboot and now you’re done.

Replace boot2docker with CoreOS and Vagrant to use Docker containers

The goal is to have a similar base use case with CoreOS than with boot2docker or docker-machine: I want to be able to launch on CoreOS a Docker container from my Mac’s command line and access it from the mac.

The test environment is:

  • Host: 10.10 Yosemite laptop
  • Docker 1.6
  • CoreOS alpha 675
  • Vagrant 1.7.2
  • VirtualBox 4.3.26

CoreOS-Vagrant

CoreOS is already providing a CoreOS-Vagrant repository, with a few useful files.

Clone it on your laptop:

git clone https://github.com/coreos/coreos-vagrant
cd coreos-vagrant

Vagrant Overview

If you’re accustomed to Vagrant, you’ll notice that the provided Vagrantfile includes more information than usual, especially the lines at the top:

CLOUD_CONFIG_PATH = File.join(File.dirname(__FILE__), "user-data")
CONFIG = File.join(File.dirname(__FILE__), "config.rb")

It looks like this Vagrantfile can read from a user-data file and a config.rb!

We’ll use just that.

CoreOS/Docker API TCP Configuration

Cloud-Config & Systemd

We want Docker API to be available on the Mac host, not just locally on the CoreOS VM, like it is with boot2docker.

So we need to start Docker with a socket listening on TCP/2375. The systemd unit would look like this:

[Unit]
Description=Docker Socket for the API

[Socket]
ListenStream=2375
Service=docker.service
BindIPv6Only=both

[Install]
WantedBy=sockets.target

Thankfully, there’s a provided user-data.sample file which is basically a cloud-config file configuring a few things and finally adding and starting Docker on TCP with exactly this systemd unit file. Copy it to user-data because that’s what’s expected by the Vagrantfile:

cp user-data.sample user-data

Vagrant Configuration

Now we have the user-data in place, we need to tell Vagrant to launch CoreOS with those settings.

So let’s look at this other config.rb and copy the sample:

cp config.rb.sample config.rb

There’s a bunch of commented out lines. To load our user-data, we’re interested in just the following:

if File.exists?('user-data') && ARGV[0].eql?('up')
 require 'open-uri'
 require 'yaml'

 data = YAML.load(IO.readlines('user-data')[1..-1].join)

 yaml = YAML.dump(data)
 File.open('user-data', 'w') { |file| file.write("#cloud-confignn#{yaml}") }
end

We’re done!

Start it up

Start CoreOS with Vagrant:

vagrant up --provider virtualbox

Get the IP (in our case it’s 172.17.8.101/24):

$ vagrant ssh -c "ip addr show dev eth1"

Local Docker Configuration

If you used boot2docker in the past, you probably have some environment variables set:

$ env | grep DOCKER

You’ll need to remove TLS cert verification:

unset DOCKER_TLS_VERIFY

And finally set the DOCKER_HOST variable like you would with boot2docker:

export DOCKER_HOST=tcp://172.17.8.101:2375

Launch a Docker container

If the communication works as expected, you can launch a container, say Consul for the test, and access its WebUI locally on your Mac:

$ docker run -p 8400:8400 -p 8500:8500 -p 8600:53/udp --hostname consul --name consul progrium/consul -server -bootstrap -ui-dir /ui

The WebUI should be available on http://a.b.c.d:8500 (a.b.c.d being CoreOS VM IP, 172.17.8.101 in this case).

Vault and Consul on CoreOS with Docker, using Terraform on Digital Ocean

Introduction

This time we’ll deploy Vault on CoreOS using my Vault Docker container with Terraform.

This initial version will make use of demo.consul.io as a backend, but using my docker-vault container, it can easily be extended to a private Consul backend.

Terraform’s role will be to start/manage the CoreOS infrastructure, cloud-init will give enough information to start/join the cluster and deploy required files. Then fleet will manage the containers.

You will need to generate a new etcd discovery token and enter it in the terraform.tf file for the demo to work.

The file cloud-config.yml contains:
* The Vault configuration file (/home/core/config/demo.hcl)
* The two fleet unit service files (/home/core/services/vault@.service and /home/core/services/vault-discovery@.service)
* enough to start etcd and fleet

The whole project is available on GitHub.

Terraform

Setup

Organize yourself as you wish, but we’ll need to store some variables and I like to declare them in a variables.tf file, then use them in a terraform.tfvars file.

It’s pretty easy to use DigitalOcean, so we need only 2 variables: the DigitalOcean token and the private SSH key file to use.

The variables.tf file:

variable "do_token" {}
variable "ssh_key_file" {}

The terraform.tfvars file:

do_token = "123456"
ssh_key_file = "~/.ssh/somekey"

Insert your values there.

Droplet(s)

The main terraform.tf file will do the following:

  • Authenticate on Digital Ocean
  • Create and register the SSH key
  • Launch a minimal CoreOS droplet, specifiying a cloud-config file.

To authenticate:

provider "digitalocean" {
  token = "${var.do_token}"
}

To create the new SSH key for this project:

resource "digitalocean_ssh_key" "default" {
  name = "DO SSH Key"
  public_key = "${file("${var.ssh_key_file}.pub")}"
}

To create a minimal CoreOS droplet, in Amsterdam, usable with the SSH key we just created:

resource "digitalocean_droplet" "coreos-1" {
  image = "coreos-beta"
  name = "core-1"
  region = "ams3"
  size = "512mb"
  ssh_keys = ["${digitalocean_ssh_key.default.id}"]
  private_networking = true
  user_data = "${file("cloud-config.yml")}"
}

Optionally output the public IPv4 address for easy reading:

output "core-1.ipv4_address" {
  value = "${digitalocean_droplet.coreos-1.ipv4_address}"
}

Cloud-Config 1

We’ll use Cloud Config for the following:

  • deploy Vault configuration file
  • deploy Vault systemd unit file (launching Docker container)
  • deploy Vault announcement systemd unit file (publishing in etcd the public IPv4 of the service)
  • all the CoreOS configuration (etcd, fleet and units configuration)

We’ll see in the next sections how this is split. In the end we’ll move everything back into this cloud-config.yml file.

CoreOS deployment

As said above, you will need to generate a new etcd discovery token and enter it in the terraform.tf file above for the demo to work. You’ll get an URL like https://discovery.etcd.io/fcc9c66458df3576daedffa38f0855f1.

We want to advertise the CoreOS cluster only internally, through our private IP (but it would work perfectly well with a public IP, so the cluster is accross continents): http://$private_ipv4:2379. We want to listen to any clients (http://$private_ipv4:2379) and peers locally (http://$private_ipv4:2380).

We want as well that fleet knows the public ip (public-ip: $private_ipv4) and that etcd and fleet units are started.

The whole CoreOS cloud-config file might look like this:

coreos:
  etcd:
  # generate a new token for each cluster: https://discovery.etcd.io/new
    discovery: https://discovery.etcd.io/fcc9c66458df3576daedffa38f0855f1
    # multi-region and multi-cloud deployments need to use $public_ipv4
    advertise-client-urls: http://$private_ipv4:2379
    listen-client-urls: http://0.0.0.0:2379
    listen-peer-urls: http://$private_ipv4:2380
  fleet:
    public-ip: $private_ipv4
  units:
    - name: etcd.service
      command: start
    - name: fleet.service
      command: start

Vault Configuration

Demo configuration

We’ll use the Vault docker container I created earlier (docker pull sjourdan/docker). By default it launches in the “dev” mode, which don’t use any backend at all (it all stays in memory), which is useless for our demo. We’ll then use the demo.consul.io Consul backend. It’s enough to show the power of the solution, and maybe we’ll go further in a later article.

A demo configuration file for Vault might look like this:

backend "consul" {
  address = "demo.consul.io:80"
  path = "demo_vault_changeme"
  advertise_addr = "http://127.0.0.1"
}

listener "tcp" {
  address = "0.0.0.0:8200"
  tls_disable = 1
}

Feel free to change the path value and use whatever you want.

Vault Systemd Unit

If we wanted to launch manually the Docker container, we’ll type something like this:

$ /usr/bin/docker pull sjourdan/vault
$ /usr/bin/docker run --cap-add IPC_LOCK --hostname vault --name vault --volume /home/core/config:/config -p 8200:8200 sjourdan/vault -config=/config/demo.hcl

To stop the container, we’ll do something like:

/usr/bin/docker stop vault

We might want to be sure the path is cleared before launching the container and not seeing it fail miserably, so we might want to check before if any container remains and remove it:

/usr/bin/docker kill vault
/usr/bin/docker rm vault

We may want to launch Vault on different ports, or at least be free to do it if needed, so we’ll add a variable at the end of each names (%i), the value being the TCP port (8200 by default).

We want as well to bind the container’s TCP 8200 port to the public IPv4 of the Core OS node it’s running on: ${COREOS_PUBLIC_IPV4} is the value to use in the Docker command line.

Here’s how a vault@.service file would look like with all of this:

[Unit]
Description=Vault Service
After=etcd.service
After=docker.service
Requires=vault-discovery@%i.service

[Service]
TimeoutStartSec=0
KillMode=none
EnvironmentFile=/etc/environment
ExecStartPre=-/usr/bin/docker kill vault%i
ExecStartPre=-/usr/bin/docker rm vault%i
ExecStartPre=/usr/bin/docker pull sjourdan/vault
ExecStart=/usr/bin/docker run --cap-add IPC_LOCK --hostname vault%i --name vault%i --volume /home/core/config:/config -p ${COREOS_PUBLIC_IPV4}:%i:8200 sjourdan/vault -config=/config/demo.hcl
ExecStop=/usr/bin/docker stop vault%i

[X-Fleet]
X-Conflicts=vault@*.service

Vault Discovery Unit

We want to announce, for other services, with etcd, the public IP of the service, with its port (1.2.3.4:8200). Manually that would look something like:

$ /usr/bin/etcdctl set /announce/services/vault 1.2.3.4:8200

To remove the announcement would simply look like:

$ /usr/bin/etcdctl rm /announce/services/vault

Combined, the systemd service file vault-discovery@.service might look like this:

[Unit]
Description=Announce Vault@%i service
BindsTo=vault@%i.service

[Service]
EnvironmentFile=/etc/environment
ExecStart=/bin/sh -c "while true; do /usr/bin/etcdctl set /announce/services/vault%i ${COREOS_PUBLIC_IPV4}:%i --ttl 60; sleep 45; done"
ExecStop=/usr/bin/etcdctl rm /announce/services/vault%i

[X-Fleet]
X-ConditionMachineOf=vault@%i.service

Cloud-Config Full

The whole cloud-config file, combined, might look like this:

#cloud-config
write_files:
  - path: /home/core/config/demo.hcl
    content: |
      backend "consul" {
        address = "demo.consul.io:80"
        path = "demo_vault_changeme"
        advertise_addr = "http://127.0.0.1"
      }

      listener "tcp" {
        address = "0.0.0.0:8200"
        tls_disable = 1
      }
  - path: /home/core/services/vault@.service
    content: |
      [Unit]
      Description=Vault Service
      After=etcd.service
      After=docker.service
      Requires=vault-discovery@%i.service

      [Service]
      TimeoutStartSec=0
      KillMode=none
      EnvironmentFile=/etc/environment
      ExecStartPre=-/usr/bin/docker kill vault%i
      ExecStartPre=-/usr/bin/docker rm vault%i
      ExecStartPre=/usr/bin/docker pull sjourdan/vault
      ExecStart=/usr/bin/docker run --cap-add IPC_LOCK --hostname vault%i --name vault%i --volume /home/core/config:/config -p ${COREOS_PUBLIC_IPV4}:%i:8200 sjourdan/vault -config=/config/demo.hcl
      ExecStop=/usr/bin/docker stop vault%i

      [X-Fleet]
      X-Conflicts=vault@*.service
  - path: /home/core/services/vault-discovery@.service
    content: |
      [Unit]
      Description=Announce Vault@%i service
      BindsTo=vault@%i.service

      [Service]
      EnvironmentFile=/etc/environment
      ExecStart=/bin/sh -c "while true; do /usr/bin/etcdctl set /announce/services/vault%i ${COREOS_PUBLIC_IPV4}:%i --ttl 60; sleep 45; done"
      ExecStop=/usr/bin/etcdctl rm /announce/services/vault%i

      [X-Fleet]
      X-ConditionMachineOf=vault@%i.service
coreos:
  etcd:
    # generate a new token for each cluster: https://discovery.etcd.io/new
    discovery: https://discovery.etcd.io/fcc9c66458df3576daedffa38f0855f1
    # multi-region and multi-cloud deployments need to use $public_ipv4
    advertise-client-urls: http://$private_ipv4:2379
    listen-client-urls: http://0.0.0.0:2379
    listen-peer-urls: http://$private_ipv4:2380
  fleet:
    public-ip: $private_ipv4
  units:
    - name: etcd.service
      command: start
    - name: fleet.service
      command: start

Start the CoreOS Infrastructure

We’ll now simply launch the infrastructure we just described with terraform on DigitalOcean:

$ terraform apply

Check that fleet does see all the cluster machines (in our case, only one):

$ fleetctl list-machines
MACHINE IP METADATA
6147c03d... 10.133.169.81 -
[...]

Confirm that no units are managed by fleet:

$ fleetctl list-units
UNIT MACHINE ACTIVE SUB
$ fleetctl list-unit-files
UNIT HASH DSTATE STATE TARGET

Load Vault Units

The cloud-config file pushed the unit files under services/. Let’s load them and verify we can see the two services:

$ fleetctl submit services/vault@.service services/vault-discovery@.service
$ fleetctl list-unit-files
UNIT HASH DSTATE STATE TARGET
vault-discovery@.service d15726b inactive inactive -
vault@.service de5c96e inactive inactive -

Use “fleet” to start Vault

We want to start a Vault service on TCP/8200, so let’s declare it before starting it:

fleetctl load vault@8200.service
Unit vault@8200.service loaded on 6147c03d.../10.133.169.81

fleetctl load vault-discovery@8200.service
Unit vault-discovery@8200.service loaded on 6147c03d.../10.133.169.81

Then finally, start the service:

fleetctl start vault@8200.service
Unit vault@8200.service launched on 6147c03d.../10.133.169.81

Check the containers service status:

fleetctl status vault@8200.service
● vault@8200.service - Vault Service
   Loaded: loaded (/run/fleet/units/vault@8200.service; linked-runtime; vendor preset: disabled)
   Active: active (running) since Tue 2015-05-05 21:04:15 UTC; 2s ago
May 05 21:04:15 core-1 docker[1628]: fdaa9c66787e: Download complete
May 05 21:04:15 core-1 docker[1628]: fdaa9c66787e: Download complete
May 05 21:04:15 core-1 docker[1628]: Status: Image is up to date for sjourdan/vault:latest
May 05 21:04:15 core-1 systemd[1]: Started Vault Service.
May 05 21:04:15 core-1 docker[1637]: ==> Vault server configuration:
May 05 21:04:15 core-1 docker[1637]: Log Level: info
May 05 21:04:15 core-1 docker[1637]: Mlock: supported: true, enabled: true
May 05 21:04:15 core-1 docker[1637]: Backend: consul (HA available)
May 05 21:04:15 core-1 docker[1637]: Listener 1: tcp (addr: "0.0.0.0:8200", tls: "disabled")
May 05 21:04:15 core-1 docker[1637]: ==> Vault server started! Log data will stream in below:

You can also tail the 100 last line of container’s logs:

fleetctl journal -lines=100 -f vault@8200.service
-- Logs begin at Tue 2015-05-05 17:13:23 UTC, end at Tue 2015-05-05 17:19:14 UTC. --
[...]

Retrieve “etcd” information

Get from etcd the public IP and port to use:

etcdctl get /announce/services/vault8200
188.166.87.74:8200

Use the Vault Service

On your workstation you can now use Vault:

$ export VAULT_ADDR='http://188.166.87.74:8200'
$ vault init
$ vault --help

Use Vault normally, or read the docs.

Termination

If you want to stop the service:

$ fleetctl stop vault@8200.service

Or if you need to destroy the units:

fleetctl destroy vault@8200.service
fleetctl destroy vault@.service

Ultimately you can destroy the whole infrastructure with Terraform:

$ terraform destroy

Misc

If you forgot what your etcd discovery address is:

$ grep DISCOVERY /run/systemd/system/etcd.service.d/20-cloudinit.conf

Use Vault with Consul on Docker

Following my previous article on Vault, here’s a little more.

We’re about to use Vault with our own Consul container, on Docker.

Our Docker Vault container is available on GitHub and available as an automated build on the Docker Hub.

You can pull it directly:

$ docker pull sjourdan/vault

Configuration lies under config/. Feel free to add your.

Consul Container

We’re using Progrium’s Consul container. It’s working great, no need to reinvent the wheel this time.

Launch it, as consul, with the WebUI:

$ docker run 
-p 8400:8400 
-p 8500:8500 
-p 8600:53/udp 
--hostname consul 
--name consul 
progrium/consul 
-server -bootstrap -ui-dir /ui

Consul is now available on this host.

Vault Container

Launch the Vault container, named vault, and linked to consul:

$ docker run -t -i 
  --cap-add IPC_LOCK 
  -p 8200:8200 
  --hostname vault 
  --name vault 
  --link consul:consul 
  --volume $PWD/config:/config 
  sjourdan/vault -config=/config/consul.hcl

Vault Usage

Now you can use your own Vault directly, with Consul backend:

$ export VAULT_ADDR='http://a.b.c.d:8200'
$ vault init

Note the different keys, and unseal the vault:

$ vault unseal
[...]

Authenticate against the vault:

$ vault auth e88396db-220f-e487-cdcb-4bf6790a6105

And create a simple secret to test the functionnality:

$ vault write secret/johndoe value=SuperSecretPassword
Success! Data written to: secret/johndoe

Read it to be sure:

$ vault read secret/johndoe
Key             Value
lease_id        secret/johndoe/090ab205-8158-0c43-9551-eb617fd408d9
lease_duration  2592000
value           SuperSecretPassword

Consul Backend Storage

To be sure Consul is really used as a backend, you can now destroy the vault container:

$ docker rm vault

and launch it again as above.

The vault will be sealed, so unseal it with the keys.

Try to read you previous secret:

$ vault read secret/johndoe
Key             Value
lease_id        secret/johndoe/163f71c6-7763-abcf-e3ce-90ba36f437c3
lease_duration  2592000
value           SuperSecretPassword

Works!

Vault Project (Complete Introduction)

Vault is the latest Hashicorp open-source project.

It’s a client/server tool to securely store & access any kind of secrets like API keys, passwords, certificates etc.

There’s a seal/unseal mechanism requiring a defined amount of keys, as well as user access management & control.

Various backends are available (like AWS dynamic access keys generation), and various authentication backends are available (like GitHub).

It means for example that in a complicated situation (laptop stolen, server compromised…), a single authorized person can seal the vault so the secrets can’t be accessed.

You can find a very nice Vault Interactive Tutorial here.

Those notes are mostly from the 9-parts-long Getting Started with Vault.

Installation

Vault is at its first version at the time of this writing: 0.1.0 – download it now.

Put it somewhere like /opt/vault:

$ sudo mkdir -p "/opt/vault" && cd "$_" && sudo unzip ~/Downloads/vault*.zip

Adapt your $PATH:

$ PATH=$PATH:/opt/vault

Does it work ?

$ vault version
Vault v0.1.0-dev (e9b3ad035308f73889dca383c8c423bb5939c4fc+CHANGES)

Yes it does.

Start a Vault Server

We’ll launch a dev Vault server. A dev server is 100% in memory, with no backed, and have a single unseal key.

$ vault server -dev
WARNING: Dev mode is enabled!

In this mode, Vault is completely in-memory and unsealed.
Vault is configured to only have a single unseal key. The root
token has already been authenticated with the CLI, so you can
immediately begin using the Vault CLI.

The only step you need to take is to set the following
environment variable since Vault will be taking without TLS:

    export VAULT_ADDR='http://127.0.0.1:8200'

The unseal key and root token are reproduced below in case you
want to seal/unseal the Vault or play with authentication.

Unseal Key: 6a72f9d9448bfe1a2df3c0d5665c7c0cfc8071c9b5cc9ffe58c8a1e4816814b1
Root Token: 8c40f7b4-1380-8a33-8d3b-5efd2dddfe51

==> Vault server configuration:

         Log Level: info
           Backend: inmem
        Listener 1: tcp (addr: "127.0.0.1:8200", tls: "disabled")

==> Vault server started! Log data will stream in below:

2015/04/30 16:33:30 [INFO] core: security barrier initialized
2015/04/30 16:33:30 [INFO] core: post-unseal setup starting
2015/04/30 16:33:30 [INFO] core: post-unseal setup complete
2015/04/30 16:33:30 [INFO] core: root token generated
2015/04/30 16:33:30 [INFO] core: pre-seal teardown starting
2015/04/30 16:33:30 [INFO] rollback: starting rollback manager
2015/04/30 16:33:30 [INFO] rollback: stopping rollback manager
2015/04/30 16:33:30 [INFO] core: pre-seal teardown complete
2015/04/30 16:33:30 [INFO] core: vault is unsealed
2015/04/30 16:33:30 [INFO] core: post-unseal setup starting
2015/04/30 16:33:30 [INFO] core: post-unseal setup complete
2015/04/30 16:33:30 [INFO] rollback: starting rollback manager

As said it the output:

$ export VAULT_ADDR='http://127.0.0.1:8200'

And don’t forget to temporarily store the Unseal Key and the Root Token somewhere.

Verify the server is running:

$ vault status
Sealed: false
Key Shares: 1
Key Threshold: 1
Unseal Progress: 0
High-Availability Enabled: false

This basically means that the vault is unsealed, with 1 key and a threshold of 1 to manipulate the vault.

Manipulate secret entries

Our dev server stores everything in memory without the need of a backend. In production, the backend might be on-disk or Consul.

Let’s store John Doe’s password under the secret/ path prefix:

$ vault write secret/johndoe value=SuperSecretPassword
Success! Data written to: secret/johndoe

Let’s store his secret token as well:

$ vault write secret/johndoe value=SuperSecretPassword token=ABCDE12345
Success! Data written to: secret/johndoe

Let’s read that information now:

$ vault read secret/johndoe
Key             Value
lease_id        secret/johndoe/8fe1128a-c25a-6a05-4409-ed12e4ac7b6f
lease_duration  2592000
token           ABCDE12345
value           SuperSecretPassword

or if you prefer a JSON output:

$ vault read --format=json secret/johndoe
{
        "lease_id": "secret/johndoe/1efe25c0-f4cc-2e66-c17a-dd901c443f19",
        "lease_duration": 2592000,
        "renewable": false,
        "data": {
                "token": "ABCDE12345",
                "value": "SuperSecretPassword"
        }
}

Let’s now delete our entry “token” from johndoe:

$ vault delete secret/johndoe/token
Success! Deleted 'secret/johndoe/token'

Or the whole thing:

$ vault delete secret/johndoe
Success! Deleted 'secret/johndoe'

Backends

We just used a generic backend: secret/. There’s others, like AWS.
Backends can be mounted, like a regular filesystem.

Let’s mount a generic backend:

$ vault mount generic
Successfully mounted 'generic' at 'generic'!

Inspect the mounts:

$ vault mounts
Path      Type     Description
generic/  generic
secret/   generic  generic secret storage
sys/      system   system endpoints used for control, policy and debugging

Let’s write an entry under generic/:

$ vault write generic/janedoe value=BetterSecretPassword
Success! Data written to: generic/janedoe

and read it:

$ vault read generic/janedoe
Key             Value
lease_id        generic/janedoe/b857ebfc-e6f5-6e19-a851-74d04494cf43
lease_duration  2592000
value           BetterSecretPassword

You can unmount the generic backend:

$ vault unmount generic/
Successfully unmounted 'generic/'!

This will erase all data from the backend. If you mount it again, it will be empty.

Dynamic Secrets

Dynamic Secrets are like regular secrets, except they don’t exist until they are accessed.

Here we’ll generate and revoke on-the-fly AWS IAM credentials. You just need your AWS access keys.

Mount the AWS backend:

$ vault mount aws

The AWS backend is mounted at aws/.

Let’s configure it by writing the root credentials values under it:

$ vault write aws/config/root 
  access_key=ABCDE1234 
  secret_key=1234ABCDE
Success! Data written to: aws/config/root

AWS IAM needs a role policy to work with, here’s one simple JSON policy file, that does allow to do everything inside EC2. Save it and name it policy.json.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1426528957000",
      "Effect": "Allow",
      "Action": [
        "ec2:*"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}

We’ll now create a new role: deploy with that policy file:

$ vault write aws/roles/deploy policy=@policy.json
Success! Data written to: aws/roles/deploy

Note that we used the syntax @filename to load a file and not a value.

Now we want to generate a dynamic secret access keypair. Just read the role name under aws/creds:

$ vault read aws/creds/deploy
Key             Value
lease_id        aws/creds/deploy/185e6910-6d36-e9a6-33b3-fc8dcfd4e97c
lease_duration  3600
access_key      ABCDE9876
secret_key      qwerty!@#$

Those keys are now real AWS keys. If you look under your IAM/Users page on the AWS Console, you’ll see it.

You’ll create new additional keys every time you read the entry.

For now, grab the lease_id, it will be useful to revoke the access.

To revoke the key:

$ vault revoke aws/creds/deploy/185e6910-6d36-e9a6-33b3-fc8dcfd4e97c
Key revoked with ID 'aws/creds/deploy/185e6910-6d36-e9a6-33b3-fc8dcfd4e97c'.

Check on the AWS console, they aren’t there anymore.

Vault Authentication

Tokens to authenticate always have a parent. It’s useful to delete a whole tree of credentials to be revoked at once.

You can create more child tokens with:

$ vault token-create
e0760b10-9cdb-ec78-39e2-480b4b806d55

Or revoke it later:

$ vault token-revoke
e0760b10-9cdb-ec78-39e2-480b4b806d55

Or login with it:

$ vault auth 8c40f7b4-1380-8a33-8d3b-5efd2dddfe51
Successfully authenticated! The policies that are associated
with this token are listed below:

root

Keep the original root token when playing with authentication and revokations.

Let’s create an authentication system with GitHub:

$ vault auth-enable github
Successfully enabled 'github' at 'github'!

Now, GitHub authentication backend is mounted under auth/.

Let’s say your GitHub organisation is CrazyInc, configure the auth to allow only users from this org:

$ vault write auth/github/config organization=CrazyInc
Success! Data written to: auth/github/config

And with the same role than ours (root):

$ vault write auth/github/map/teams/default value=root
Success! Data written to: auth/github/map/teams/default

Get your user’s GitHub personal access token and authenticate. If you’re not part of the above org, you’ll won’t be accepted:

$ vault auth -method=github token=zxc123
Error making API request.

URL: PUT http://127.0.0.1:8200/v1/auth/github/login
Code: 400. Errors:

* user is not part of required org

But if the user is, authentication will work:

 

$ vault auth -method=github token=asdqwe456789
Successfully authenticated! The policies that are associated
with this token are listed below:

root

If you want to revoke all GitHub tokens:

$ vault token-revoke -mode=path auth/github

You’ll now probably need to re-auth with a root token.

And you can now even disable fully all GitHub authentication:

$ vault auth-disable github

ACL

You can list available policies, as root:

$ vault policies
root

Only one is available at first.

Here’s an example ACL policy, save it to acl.hcl:

path "sys" {
  policy = "deny"
}

path "secret" {
  policy = "write"
}

path "secret/foo" {
  policy = "read"
}

It will deny access to sys/, default write access to secret/ except for secret/foo where read-only is applied.
Vault defaults to deny when not specified.

Apply it:

$ vault policy-write secret acl.hcl
Policy 'secret' written.

You can create another one just to delete it:

$ vault policy-write secret123 acl.hcl
Policy 'secret123' written.
$ vault policies
secret
secret123
root

Delete it:

$ vault policy-delete secret123
Policy 'secret123' deleted.

Create a new token using this policy:

$ vault token-create -policy="secret"
3c183cf2-822a-29de-ece2-393f9d964655

Authenticate with it:

$ vault auth 3c183cf2-822a-29de-ece2-393f9d964655
Successfully authenticated! The policies that are associated
with this token are listed below:

secret

See, no more root access, but secret level.

Try to write to secret/ and confirm you can only read from secret/foo as per the ACL:

$ vault write secret/bar value=yes
Success! Data written to: secret/bar
$ vault write secret/foo value=yes
Error writing data to secret/foo: Error making API request.

URL: PUT http://127.0.0.1:8200/v1/secret/foo
Code: 400. Errors:

* permission denied

Not having access to sys/ also prevents to use mounts:

$ vault mount generic
Mount error: Error making API request.

URL: POST http://127.0.0.1:8200/v1/sys/mounts/generic
Code: 500. Errors:

* permission denied

Notes on NFS, firewalld, SELinux Vagrant/VirtualBox

Maybe you tried to use Vagrant’s shared folders with VirtualBox on a ~ 2015 Fedora/CentOS system, with NFS.
And it’s tricky to make it work, with SELinux and Firewalld.

The Vagrant Synced Folder problem with NFS

You probably already have something like this in your Vagrantfile:

config.vm.synced_folder "../../myapp-code", "/var/www", :nfs =&gt; true

But when you vagrant up, you end up in a NFS related error:

==> default: Exporting NFS shared folders...
==> default: Preparing to edit /etc/exports. Administrator privileges will be required...
[sudo] password for sjourdan: 
Redirecting to /bin/systemctl status  nfs-server.service
● nfs-server.service - NFS server and services
   Loaded: loaded (/usr/lib/systemd/system/nfs-server.service; disabled)
   Active: inactive (dead)
Redirecting to /bin/systemctl start  nfs-server.service
==> default: Mounting NFS shared folders...
The following SSH command responded with a non-zero exit status.
Vagrant assumes that this means the command failed!

mount -o 'actimeo=1' 192.168.100.1:'/path/to/myapp-code' /var/www/

Stdout from the command:



Stderr from the command:

mount.nfs: access denied by server while mounting 192.168.100.1:/path/to/myapp-code

If you enabled NFS in Vagrant, you had to enable previously a host-only private network like this:

config.vm.network "private_network", ip: "192.168.100.101"

So you end up with your workstation having two IPs in two different networks (192.168.0.x and 192.168.100.x in this case), and your VM one, in the second network.

If you vagrant ssh into the failed box, we see that we can’t access the NFS shares with the host-only IP of our workstation:

$ showmount -e 192.168.100.1
clnt_create: RPC: Port mapper failure - Unable to receive: errno 113 (No route to host)

But if you request the same with the real IP of the workstation, it works:

$ showmount -e 192.168.0.104
Export list for 192.168.0.104:
/path/to/webapp-code 192.168.100.101

So, NFS works.

Let’s inspect the firewall.

Firewalld

If you disable the firewall, will you see the shares ?

$ sudo systemctl stop firewalld

Yes you do:

[vagrant@localhost ~]$ showmount -e 192.168.100.1
Export list for 192.168.100.1:
/path/to/webapp-code 192.168.100.101

So let’s work on the firewall (and start it again):

$ sudo systemctl start firewalld

What’s the current configuration for which firewall zone?

$ firewall-cmd --list-all
FedoraWorkstation (default, active)
  interfaces: docker0 vboxnet0 virbr0 wlp2s0b1
  sources: 
  services: dhcpv6-client mdns samba-client ssh
  ports: 1025-65535/udp 1025-65535/tcp
  masquerade: no
  forward-ports: 
  icmp-blocks: 
  rich rules: 

We see here that the current, default and active zone is named “FedoraWorkstation”, with our network interfaces.

Add the VirtualBox interface to the “FedoraWorkstation” zone of the firewall:

$ sudo firewall-cmd --zone FedoraWorkstation --change-interface vboxnet0

Add the NFS services (nfs, rpc-bind, mountd) and the portmapper port to the FedoraWorkstation zone, permanently:

$ sudo firewall-cmd --zone FedoraWorkstation --permanent --add-service nfs
$ sudo firewall-cmd --zone FedoraWorkstation --permanent --add-service rpc-bind
$ sudo firewall-cmd --zone FedoraWorkstation --permanent --add-service mountd
$ sudo firewall-cmd --zone FedoraWorkstation --permanent --add-port 2049/udp
$ sudo firewall-cmd --reload

Now, on the vagrant box, you can see the shares, while the firewall is running:

$ showmount -e 192.168.100.1
Export list for 192.168.100.1:
/path/to/webapp-code 192.168.100.101

So you vagrant up again and if everything goes well, your share will work.

Oh wait, except if your shared folder is inside your $HOME, which might very well be. So either you move your shared folder out of $HOME, or you fiddle with SELinux.

SELinux

Disclaimer: I couldn’t complete a fully working NFS share from my home to vagrant/virtualbox. The best I got is a timeout, and not a direct deny.

Verify your SELinux policy state:

$ /usr/sbin/getenforce
Enforcing

If you need to, temporarily use SELinux in permissive mode:

$ /usr/sbin/setenforce 0

0 is permissive, 1 is enforcing.

Bootstrap an infrastructure with Terraform, Chef and Cloud-Init

Our goal here is to manage an infrastructure described by Terraform and provisionned by Chef (with automatic registration on the Chef Server).

We’re going to use Terraform modules too.

We’re using here Chef 12 and Terraform 0.4.2.

Requirements

We’ll need

  • the Digital Ocean token
  • the Digital Ocean SSH key location
  • the Chef Server organisation name
  • the Chef Server validation key

Variables

Initialise some variables in variables.tf:

variable "do_token" {}
variable "do_ssh_key_file" {}

It means that we’ll be able to access the said variables like:

// example
token = "${var.do_token}"

We’ll fill the variables in terraform.tfvars:

do_token = "abcdef"
do_ssh_key_file = "/Users/sjourdan/.ssh/digitalocean"

Create a module

Let’s create a “common” module:

$ mkdir -p modules/common

This module will contain common information for our infrastructure.

One common information is the Chef Server validation key. Let’s include it in modules/common/terraform.tf:

output "validation_key" {
    value = <<EOF
    -----BEGIN RSA PRIVATE KEY-----
    [...]
    -----END RSA PRIVATE KEY-----
EOF
}

Terraform and Digital Ocean

Our main infrastructure file is named infra-lab.tf.

Use the module

Let’s use our module:

module "common" {
    source = "/complete/path/to/terraform/modules/common"
}

Use Digital Ocean

Let’s declare we want the Digital Ocean provider, using the credentials declared above:

provider "digitalocean" {
    token = "${var.do_token}"
}

Declare and configure hosts

Now we’ll need more information, like:

  • the OS image name: “ubuntu-14-04-x64”
  • how many nodes we’ll launch: “2”
  • how are we naming them: “node-x”
  • what’s the SSH key ID: “131228”
  • whether we want private networking enabled: “yes”

So an initial infra-lab.tf file would look like it, and basically would launch 2 Ubuntus named node-1 and node-2 on the Amsterdam-3 Digital Ocean datacenter.

# Create the node(s)
resource "digitalocean_droplet" "node" {
    image = "ubuntu-14-04-x64"
    count = 2
    name = "node-${count.index+1}"
    region = "ams3"
    size = "512mb"
    private_networking = true
    ssh_keys = [ "131228" ]

    connection {
      user = "root"
      type = "ssh"
      key_file = "${var.do_ssh_key_file}"
    }
}

Now it’s not enough!

Cloud Init

With Cloud-Init, boot-time initialisation stuff is a breeze.

Example

Let’s describe what a Cloud Init Chef YAML file would look like, with an empty runlist.

#cloud-config

users:
  - name: johndoe
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
    groups: sudo
    shell: /bin/bash

chef:
  install_type: "omnibus"
  omnibus_url: "https://www.opscode.com/chef/install.sh"
  force_install: false
  server_url: "https://api.opscode.com/organizations/cloudinit"
  node_name: "cloudinit-x"
  run_list:
  validation_name: "cloudinit-validator"
  validation_key: |
    -----BEGIN RSA PRIVATE KEY-----
    ...
    -----END RSA PRIVATE KEY-----
output: {all: '| tee -a /var/log/cloud-init-output.log'}
runcmd:
  - while [ ! -e /usr/bin/chef-client ]; do sleep 5; done; chef-client
disable_ec2_metadata: true

This would basically create an user johndoe, make is sudoer, install Chef, and register the node on the Chef Server by executing chef-client when available.

Cloud Init, Digital Ocean and Terraform

It happens Digital Ocean supports Cloud Init through user_data at bootstrap. Let’s integrate the above YML file inside our Terraform code:

resource "digitalocean_droplet" "node" {
    [...]
    user_data = <<EOF
#cloud-config
users:
  - name: johndoe
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
    groups: sudo
    shell: /bin/bash
chef:
  install_type: "omnibus"
  omnibus_url: "https://www.opscode.com/chef/install.sh"
  force_install: false
  server_url: "https://api.opscode.com/organizations/cloudinit"
  node_name: "node-${count.index+1}"
  run_list:
  validation_key: |
${module.common.validation_key}
  validation_name: "cloudinit-validator"
output: {all: '| tee -a /var/log/cloud-init-output.log'}
runcmd:
  - while [ ! -e /usr/bin/chef-client ]; do sleep 2; done; chef-client
disable_ec2_metadata: true
EOF

All combined, the whole infra-lab.tf file would look like this:

# Configure the DigitalOcean Provider
# And use Cloud Init for Chef

module "common" {
    source = "/complete/path/to/terraform/modules/common"
}

provider "digitalocean" {
    token = "${var.do_token}"
}

# Create the node(s)
resource "digitalocean_droplet" "node" {
    image = "ubuntu-14-04-x64"
    count = 2
    name = "node-${count.index+1}"
    region = "ams3"
    size = "512mb"
    private_networking = true
    ssh_keys = [ "131228" ]
    user_data = <<EOF
#cloud-config
users:
  - name: johndoe
    sudo: ['ALL=(ALL) NOPASSWD:ALL']
    groups: sudo
    shell: /bin/bash
chef:
  install_type: "omnibus"
  omnibus_url: "https://www.opscode.com/chef/install.sh"
  force_install: false
  server_url: "https://api.opscode.com/organizations/cloudinit"
  node_name: "node-${count.index+1}"
  run_list:
  validation_key: |
${module.common.validation_key}
  validation_name: "cloudinit-validator"
output: {all: '| tee -a /var/log/cloud-init-output.log'}
runcmd:
  - while [ ! -e /usr/bin/chef-client ]; do sleep 2; done; chef-client
disable_ec2_metadata: true
EOF

    connection {
      user = "root"
      type = "ssh"
      key_file = "${var.do_ssh_key_file}"
      port = 22
    }

}

Execute Terraform

First tell Terraform to get the module:

$ terraform get
Get: file:///full/path/to/terraform/modules/common

Then apply:

$ terraform apply

And the nodes will be there, registered on Chef within a few minutes (depending on the speed of the nodes, network, chef infrastructure…):

$ knife node list
node-1
node-2

Create & Manage a Docker Swarm cluster on Digital Ocean with Docker-Machine

A Docker Swarm cluster on Digital Ocean in 5 minutes.

The goal here is to have a working Docker Swarm scheduling cluster on Digital Ocean, so we’ll be able to launch containers with CPU or memory constraints on a number of isolated docker hosts from the cluster.

Note: this will be using docker-machine v0.1.0 with Digital Ocean

Requirements

An account on Digital Ocean.

Then export you DO token:

$ export DO_TOKEN="abcdefghijklmnopqrstuvwxyz"

Launch a first machine in the cluster

We’ll name this machine swarm-1.

$ docker-machine create 
  -d digitalocean 
  --digitalocean-access-token ${DO_TOKEN} 
  --digitalocean-region "lon1" 
  --digitalocean-size "512mb" 
  swarm-1
INFO[0000] Creating SSH key...
INFO[0002] Creating Digital Ocean droplet...
INFO[0004] Waiting for SSH...
INFO[0072] Configuring Machine...
INFO[0129] "swarm-1" has been created and is now the active machine.
INFO[0129] To point your Docker client at it, run this in your shell: $(docker-machine env swarm-1)

Generate a cluster token:

$ docker run swarm create
[...]
03b268b9b0be689f5ddb1b8ff7beb2c2

Export the token for later use:

$ export SWARM_TOKEN="03b268b9b0be689f5ddb1b8ff7beb2c2"

You can now safely delete this instance, we needed it only to get an ID:

$ docker-machine rm swarm-1

Create Swarm Master

Let’s create the swarm master (named imaginatively swarm-master):

$ docker-machine create 
  -d digitalocean 
  --digitalocean-access-token ${DO_TOKEN} 
  --digitalocean-region "lon1" 
  --digitalocean-size "512mb" 
  --swarm --swarm-master 
  --swarm-discovery token://${SWARM_TOKEN} 
  swarm-master
INFO[0000] Creating SSH key...
INFO[0001] Creating Digital Ocean droplet...
INFO[0003] Waiting for SSH...
INFO[0070] Configuring Machine...
INFO[0115] Configuring Swarm...
INFO[0143] "swarm-master" has been created and is now the active machine.
INFO[0143] To point your Docker client at it, run this in your shell: $(docker-machine env swarm-master)

Add nodes to the cluster

Now let’s add two nodes to the cluster using the token and swarm-discovery option:

$ docker-machine create 
  -d digitalocean 
  --digitalocean-access-token ${DO_TOKEN} 
  --digitalocean-region "lon1" 
  --digitalocean-size "512mb" 
  --swarm 
  --swarm-discovery token://${SWARM_TOKEN} 
  swarm-node-1

and

$ docker-machine create 
  -d digitalocean 
  --digitalocean-access-token ${DO_TOKEN} 
  --digitalocean-region "lon1" 
  --digitalocean-size "512mb" 
  --swarm 
  --swarm-discovery token://${SWARM_TOKEN} 
  swarm-node-2

Swarm Cluster check

Are the nodes all seen by docker-machine ?

$ docker-machine ls
NAME           ACTIVE   DRIVER         STATE     URL                       SWARM
dev                     virtualbox     Stopped
swarm-master            digitalocean   Running   tcp://46.101.42.71:2376   swarm-master (master)
swarm-node-1            digitalocean   Running   tcp://46.101.42.72:2376   swarm-master
swarm-node-2   *        digitalocean   Running   tcp://46.101.50.8:2376    swarm-master

Update your local environment to target the swarm-master:

$ $(docker-machine env --swarm swarm-master)

Ask for information to docker:

$ docker info
Containers: 4
Nodes: 3
 swarm-master: 46.101.42.71:2376
  └ Containers: 2
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 490 MiB
 swarm-node-1: 46.101.42.72:2376
  └ Containers: 1
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 490 MiB
 swarm-node-2: 46.101.50.8:2376
  └ Containers: 1
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 490 MiB

Good, we have 2 nodes and a master, each displaying available CPUs and memory.

Use constraints on the cluster

Let’s launch two nginx containers with a 400M memory constraint:

$ docker run --name web-1 -d -P -m 400M nginx

Is it running ?

$ docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED              STATUS              PORTS                                                     NAMES
5b4ac0011fd3        nginx:1             "nginx -g 'daemon of   About a minute ago   Up About a minute   46.101.42.71:49159-&gt;443/tcp, 46.101.42.71:49160-&gt;80/tcp   swarm-master/web-1

Looks like the scheduler choosed to run web-1 on the swarm master.

Docker info will show the reserved memory updated:

$ docker info
Containers: 6
Nodes: 3
 swarm-node-2: 46.101.50.8:2376
  └ Containers: 1
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 490 MiB
 swarm-master: 46.101.42.71:2376
  └ Containers: 3
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 400 MiB / 490 MiB
 swarm-node-1: 46.101.42.72:2376
  └ Containers: 2
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 490 MiB

Let’s add a new nginx web-2 container with similar constraints:

$ docker run --name web-2 -d -P -m 400M nginx
$ docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS                  PORTS                                                     NAMES
07fecf33a4e0        nginx:1             "nginx -g 'daemon of   3 seconds ago       Up Less than a second   46.101.50.8:49153-&gt;443/tcp, 46.101.50.8:49154-&gt;80/tcp     swarm-node-2/web-2
5b4ac0011fd3        nginx:1             "nginx -g 'daemon of   3 minutes ago       Up 2 minutes            46.101.42.71:49159-&gt;443/tcp, 46.101.42.71:49160-&gt;80/tcp   swarm-master/web-1

This time, the scheduler launched it on swarm-node-2, because there wasn’t enough guaranteed memory left on swarm-master.

Docker info will reflect this:

$ docker info
Containers: 6
Nodes: 3
 swarm-master: 46.101.42.71:2376
  └ Containers: 3
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 400 MiB / 490 MiB
 swarm-node-1: 46.101.42.72:2376
  └ Containers: 1
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 490 MiB
 swarm-node-2: 46.101.50.8:2376
  └ Containers: 2
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 400 MiB / 490 MiB

What is if request more resources than available ? Let’s request a container with 4Go of RAM:

$ docker run --name web-3 -d -P -m 4096M nginx
FATA[0000] Error response from daemon: no resources available to schedule container

OK, the scheduler knows.

Let’s launch web-3 with only 400M constraint, it will surely launch on swarm-node-1 as swarm-node-2 and swarm-master are already full memory-wise.

$ docker run --name web-3 -d -P -m 400M nginx

What does a docker ps say ?

$ docker ps
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS              PORTS                                                     NAMES
219433790d17        nginx:1             "nginx -g 'daemon of   29 seconds ago      Up 3 seconds        46.101.42.72:49153-&gt;443/tcp, 46.101.42.72:49154-&gt;80/tcp   swarm-node-1/web-3
07fecf33a4e0        nginx:1             "nginx -g 'daemon of   3 minutes ago       Up 2 minutes        46.101.50.8:49153-&gt;443/tcp, 46.101.50.8:49154-&gt;80/tcp     swarm-node-2/web-2
5b4ac0011fd3        nginx:1             "nginx -g 'daemon of   6 minutes ago       Up 5 minutes        46.101.42.71:49159-&gt;443/tcp, 46.101.42.71:49160-&gt;80/tcp   swarm-master/web-1

Get some info:

$ docker info
Containers: 7
Nodes: 3
 swarm-master: 46.101.42.71:2376
  └ Containers: 3
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 400 MiB / 490 MiB
 swarm-node-1: 46.101.42.72:2376
  └ Containers: 2
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 400 MiB / 490 MiB
 swarm-node-2: 46.101.50.8:2376
  └ Containers: 2
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 400 MiB / 490 MiB

Everything looks good.

Let’s remove web-2 and web-3 containers:

$ docker stop web-2 ; docker rm web-2
$ docker stop web-3 ; docker rm web-3

Let’s try to launch 2 nginx containers with a CPU contraint and a memory constraint:

$ docker run --name web-3 -d -P -c 1 -m 400M nginx
$ docker run --name web-4 -d -P -c 1 -m 400M nginx

This will be reflected on the docker info command, CPUs are reserved:

$ docker info
Containers: 7
Nodes: 3
 swarm-master: 46.101.42.71:2376
  └ Containers: 3
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 400 MiB / 490 MiB
 swarm-node-1: 46.101.42.72:2376
  └ Containers: 2
  └ Reserved CPUs: 1 / 1
  └ Reserved Memory: 400 MiB / 490 MiB
 swarm-node-2: 46.101.50.8:2376
  └ Containers: 2
  └ Reserved CPUs: 1 / 1
  └ Reserved Memory: 400 MiB / 490 MiB

Only swarm-master has one CPU left now.

If we try to launch one more container, there won’t be enough room:

$ docker run --name web-5 -d -P -c 1 -m 400M nginx
FATA[0000] Error response from daemon: no resources available to schedule container

But if web-5 don’t need the memory constraints, it can fit:

$ docker run --name web-5 -d -P -c 1 nginx

And most resources will be used:

$ docker info
Containers: 8
Nodes: 3
 swarm-master: 46.101.42.71:2376
  └ Containers: 4
  └ Reserved CPUs: 1 / 1
  └ Reserved Memory: 400 MiB / 490 MiB
 swarm-node-1: 46.101.42.72:2376
  └ Containers: 2
  └ Reserved CPUs: 1 / 1
  └ Reserved Memory: 400 MiB / 490 MiB
 swarm-node-2: 46.101.50.8:2376
  └ Containers: 2
  └ Reserved CPUs: 1 / 1
  └ Reserved Memory: 400 MiB / 490 MiB

You can now increase the cluster size as needed.

$ docker-machine create 
  -d digitalocean 
  --digitalocean-access-token ${DO_TOKEN} 
  --digitalocean-region "lon1" 
  --digitalocean-size "512mb" 
  --swarm 
  --swarm-discovery token://${SWARM_TOKEN} 
  swarm-node-3
  INFO[0000] Creating SSH key...
  INFO[0001] Creating Digital Ocean droplet...
  [...]

You’ll now see the new resources available on the cluster:

$ docker info
Containers: 9
Nodes: 4
 swarm-master: 46.101.42.71:2376
  └ Containers: 4
  └ Reserved CPUs: 1 / 1
  └ Reserved Memory: 400 MiB / 490 MiB
 swarm-node-1: 46.101.42.72:2376
  └ Containers: 2
  └ Reserved CPUs: 1 / 1
  └ Reserved Memory: 400 MiB / 490 MiB
 swarm-node-2: 46.101.50.8:2376
  └ Containers: 2
  └ Reserved CPUs: 1 / 1
  └ Reserved Memory: 400 MiB / 490 MiB
 swarm-node-3: 46.101.42.70:2376
  └ Containers: 1
  └ Reserved CPUs: 0 / 1
  └ Reserved Memory: 0 B / 490 MiB

Looks good! Happy clustering with Docker Swarm.

By the way: it works accross regions too! Means you can have various docker hosts inside the same Swarm from San-Francisco, New-York, Amsterdam, London and Frankfurt with no problem.

Notes on stuff digging