Introduction

Not every application infrastructure requires a full blown GCP, AWS or Azure Kubernetes / Mesos apparatus. Docker Swarm (Mode) offers a comparable and simpler alternative to Kubernetes / Mesos. Swarm mode allows provisioning a Docker cluster starting with a single server. This tutorial demonstrates how you can leverage Terraform with Hetzner’s cost effective cloud platform to quickly deploy a Traefik (v2.3) reverse proxy / loadbalancer container to serve applications all on a single Docker Swarm host.

Network Layout Network Layout - Docker Swarm Host + Traefik + Services.

This deployment utilises three pre-built Terraform modules.

  • (docker-host) for provisioning the Docker Swarm Host & Cloud Volume on Hetzner
  • (traefik-v2) for provisioning a Traefik container on the Docker host.
  • (vault-dev) for provisioning a Vault container on the Docker host as an example app.

The docker-host module provisions a single Docker Swarm Host on Hetzner Cloud. A Cloud Volume is also provisioned, attached and mounted on the Docker Host for use as the Docker root directory where persisted data such as images and volumes are stored. The modules default variables provision a 2GB RAM / 1 vCPU cx11 server type @ € 2.49* monthly and a 10GB cloud volume @ € 0.40* monthly (see Hetzner’s website for current pricing)

For routing HTTP/TCP/UDP requests and certificate management via Let’s Encrypt the traefik-v2 module deploys a Traefik v2 reverse proxy container on the pre-provisioned Docker Host.

Finally the vault-dev module deploys a HashiCorp Vault container on the Docker Host as the example application.

You’re welcome to fork and tweak any of the above modules on GitHub to suit your needs.

Prerequisites

Step 1 - Create the Terraform Configuration

Clone the example repository https://github.com/colinwilson/example-terraform-modules/tree/docker-host

git clone -b docker-host https://github.com/colinwilson/example-terraform-modules
example-terraform-modules/
|-- .gitignore
|-- README.md
|-- main.tf
|-- outputs.tf
|-- terraform.tfvars
`-- variables.tf
Alternatively, manually create the above file structure by copying the following code snippets. (click to expand)

Copy and paste the following code to your main.tf file:

# main.tf
terraform {
  required_providers {
    hcloud = {
      source = "hetznercloud/hcloud"
    }
  }
}

provider "hcloud" {
  token = var.hcloud_token
}

module "hcloud-docker-host" {
  source = "github.com/colinwilson/terraform-hcloud-docker-host"

  ssh_public_key      = var.ssh_public_key
  //server              = var.server
  //ssh_public_key_name = var.ssh_public_key_name
  //volume_size         = var.volume_size
  //volume_filesystem   = var.volume_filesystem

}

server, ssh_public_key_name, volume_size and volume_filesystem are all optional input variables.

Copy and paste the following code to your outputs.tf file:

# outputs.tf
output "server_ip" {
  description = "Docker Host IP address"
  value       = module.hcloud-docker-host.ipv4_address
}

output "volume_size" {
  description = "Size of provisioned Cloud Volume"
  value       = module.hcloud-docker-host.volume_size
}

output "volume_mount_point" {
  description = "Mountpoint of provisioned Cloud Volume"
  value       = module.hcloud-docker-host.volume_mount_point
}

Copy and paste the following code to your variables.tf file:

# variables.tf
variable "hcloud_token" {
  description = "Hetzner Cloud API Token"
  type        = string
}

variable "ssh_public_key" {
  description = "SSH Public Key"
  type        = string
}

//variable "ssh_public_key_name" {}
//variable "server" {}
//variable "volume_size" {}
//variable "volume_filesystem" {}

Step 2 - Set Values for the Required Input Variables

Following the example below add both the Hetzner Cloud project API Token generated earlier and your SSH Public Key as input variables to your terraform.tfvars file:

# terraform.tfvars (example)
hcloud_token = "l113WfwCFwZCbVcxVsQHHuMAJINQV6K8hhyVjzOMymotxb2z1oBh6ANwheFvV2lF"

ssh_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJNcwP5mhs5/F2T9GFHmg4z6E6sbOG+Ynx2iPERKeOGm"

Step 3 - Provision the Docker Host

Switch to your example-terraform-modules directory and initialize your configuration by running terraform init.

terraform init

Terraform will proceed to download the required provider plugins.

Initializing modules...
- hcloud-docker-host in ..\terraform-hcloud-docker-host

Initializing the backend...

Initializing provider plugins...
- Finding latest version of hetznercloud/hcloud...
- Installing hetznercloud/hcloud v1.23.0...
- Installed hetznercloud/hcloud v1.23.0 (signed by a HashiCorp partner, key ID 5219EACB3A77198B)

# ...

Now run terraform apply to create your Docker (Swarm) Host.

terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # module.hcloud-docker-host.hcloud_server.server will be created
  + resource "hcloud_server" "server" {
      + backup_window = (known after apply)
      + backups       = false
      + datacenter    = (known after apply)
      + id            = (known after apply)
      + image         = "ubuntu-20.04"
      + ipv4_address  = (known after apply)
      + ipv6_address  = (known after apply)
      + ipv6_network  = (known after apply)
      + keep_disk     = false
      + location      = "nbg1"
      + name          = "docker-host"
      + server_type   = "cx11"
      + ssh_keys      = [
          + "default",
        ]
      + status        = (known after apply)
      + user_data     = "BfHwIc7NggsC/JJGTzbYp+r+Au0="
    }

  # module.hcloud-docker-host.hcloud_ssh_key.default will be created
  + resource "hcloud_ssh_key" "default" {
      + fingerprint = (known after apply)
      + id          = (known after apply)
      + name        = "default"
      + public_key  = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJNcwP5mhs5/F2T9GFHmg4z6E6sbOG+Ynx2iPERKeOGm"
    }

  # module.hcloud-docker-host.hcloud_volume.master will be created
  + resource "hcloud_volume" "master" {
      + automount    = true
      + format       = "ext4"
      + id           = (known after apply)
      + linux_device = (known after apply)
      + location     = (known after apply)
      + name         = "docker_data_volume"
      + server_id    = (known after apply)
      + size         = 10
    }

Plan: 3 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

Respond to the prompt with yes to apply the changes and continue.

Terraform will now create the Docker Host and you should see the Docker Host IP address in the configuration output along with some additional volume configuration info.

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Outputs:

server_ip = 116.207.49.143
volume_mount_point = /dev/disk/by-id/scsi-0HC_Volume_8179379
volume_size = 10

Step 4 - Check Docker’s Persistent Data is Stored on the Cloud Volume

To ensure Docker data such as volumes and images are indeed stored on the mounted Hetzner Cloud Volume, login via SSH, create a test volume and inspect it.

Once logged in check the provisioned cloud volume (/dev/sdb) exists and is mounted to the (/mnt/docker_data_volume) directory.

root@docker-host:~# df -h

Filesystem      Size  Used Avail Use% Mounted on
udev            953M     0  953M   0% /dev
tmpfs           194M  728K  194M   1% /run
/dev/sda1        19G  2.4G   16G  14% /
tmpfs           970M     0  970M   0% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs           970M     0  970M   0% /sys/fs/cgroup
/dev/sda15       61M  2.7M   58M   5% /boot/efi
/dev/sdb        9.8G   37M  9.3G   1% /mnt/docker_data_volume
tmpfs           194M     0  194M   0% /run/user/0

Create a test Docker volume.

root@docker-host:~# docker volume create test_volume
test_volume

Now inspect the volume using the following command.

root@docker-host:~# docker volume inspect test_volume

The Mountpoint key value should indicate that test_volume is located inside the directory where the cloud volume is mounted.


[
    {
        "CreatedAt": "2020-11-25T02:59:28+01:00",
        "Driver": "local",
        "Labels": {},
        "Mountpoint": "/mnt/docker_data_volume/volumes/test_volume/_data",
        "Name": "test_volume",
        "Options": {},
        "Scope": "local"
    }
]

Step 5. Clean up

You can destroy the host by running terraform destroy. After you respond to the prompt with yes, Terraform will destroy the Docker host you created.

terraform destroy
module.hcloud-docker-host.hcloud_volume.master: Refreshing state... [id=8257019]
module.hcloud-docker-host.hcloud_ssh_key.default: Refreshing state... [id=2447474]
module.hcloud-docker-host.hcloud_server.server: Refreshing state... [id=8808780]
module.hcloud-docker-host.hcloud_volume_attachment.main: Refreshing state... [id=8257019]
module.hcloud-docker-host.hcloud_volume_attachment.main: Destroying... [id=8257019]
module.hcloud-docker-host.hcloud_ssh_key.default: Destroying... [id=2447474]

# ...

Destroy complete! Resources: 4 destroyed.

Note: Failing to destroy the infrastructure you created during this tutorial could result in charges from Hetzner.

Conclusion

That’s it! You’ve now provisioned a Docker Swarm Host on the Hetzner Cloud Platform. This demonstrates how speedy provisioning of even simple infrastructure can be automated using Terraform.

Part Two will cover deploying the Traefik reverse proxy and a Vault container on your Docker host again using pre-built Terraform modules.

References

* Prices exclude VAT