Introduction
Part One covered the use of the docker-host Terraform module to provision a Docker host (in Swarm mode) on Hetzner Cloud.
We can now again make use of Terraform modules to deploy a Traefik container on this (or any) host to serve as a load balancer/reverse proxy for other containered applications running on the same host.
In this tutorial we’ll setup Traefik to route incoming requests for a Vault backend service.
Network Layout - Traefik routing HTTP requests based on host name.
Two modules are used in a Terraform configuration to achieve this.
- (traefik-v2) for provisioning a Traefik container on the Docker host.
- (vault-dev) for provisioning a Vault container on the Docker host.
The docker-traefik-v2 module deploys a pre-configured Traefik (2.3.x) reverse proxy container. The same configuration deploys the HashiCorp Vault application using the docker-vault-dev module and connects it to the traefik
overlay network so Traefik can route incoming requests to it.
Prerequisites
- A Docker host (in Swarm mode)
- Terraform ≥ v0.13
- Knowledge of Docker & Terraform
Step 1 - Create the Terraform Configuration
Clone the example repository https://github.com/colinwilson/example-terraform-modules/tree/docker-traefik-and-vault
git clone -b docker-traefik-and-vault https://github.com/colinwilson/example-terraform-modules
example-terraform-modules/
|-- .gitignore
|-- README.md
|-- main.tf
|-- outputs.tf
|-- terraform.tfvars
`-- variables.tf
Step 2 - Set Values for the Required Input Variables
Using the example below, update the input variables in the terraform.tfvars
file to match your own.
# terraform.tfvars (example)
docker_host = "ssh://my-docker-host:22"
traefik_hostname = "traefik.example.com"
traefik_password = "my_secret_password"
vault_hostname = "vault.example.com"
acme_email = "myname@example.com"
The
docker_host
variable should be set to the connection string (required by the Docker provider).Set a password for accessing the Traefik UI,
traefik_password
. (The Traefik module hashes your password using Bcrypt)Set the hostnames via which both Traefik and Vault will be publicly accessible e.g.
traefik.example.com
,vault.example.com
(set DNS records for both hostnames pointing to the IP address of your Docker host). This allows Traefik to automatically configure an SSL certificate for both services via Let’s Encrypt using
httpChallenge
.Note: The DNS records for both hostnames should be configured BEFORE deploying your configuration (running
terraform apply
)Finally set the
acme_email
variable to an email address you wish to register with Let’s Encrypt for SSL certificate requests:
Step 3 - Deploy Traefik & Vault on 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 for both modules.
EXAMPLE OUTPUT - (click to expand)
Initializing modules...
Downloading github.com/colinwilson/terraform-docker-traefik-v2 for docker-traefik...
- docker-traefik in .terraform\modules\docker-traefik
Downloading github.com/colinwilson/terraform-docker-vault-dev for docker-vault...
- docker-vault in .terraform\modules\docker-vault
Initializing the backend...
Initializing provider plugins...
- Finding latest version of kreuzwerker/docker...
- Finding latest version of hashicorp/template...
- Finding latest version of hashicorp/local...
- Using hashicorp/local v2.0.0 from the shared cache directory
- Using kreuzwerker/docker v2.8.0 from the shared cache directory
- Using hashicorp/template v2.2.0 from the shared cache directory
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.
Terraform has been successfully initialized!
# ...
Now run terraform apply
to create the Traefik and Vault containers.
terraform apply
EXAMPLE OUTPUT - (click to expand)
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
<= read (data resources)
Terraform will perform the following actions:
# module.docker-traefik.docker_config.traefik-yaml will be created
+ resource "docker_config" "traefik-yaml" {
+ data = (sensitive value)
+ id = (known after apply)
+ name = (known after apply)
}
# module.docker-traefik.docker_network.network will be created
+ resource "docker_network" "network" {
+ attachable = true
+ check_duplicate = true
+ driver = "overlay"
+ id = (known after apply)
+ internal = (known after apply)
+ ipam_driver = "default"
+ name = "traefik"
+ options = (known after apply)
+ scope = (known after apply)
+ ipam_config {
+ aux_address = (known after apply)
+ gateway = (known after apply)
+ ip_range = (known after apply)
+ subnet = (known after apply)
}
}
# module.docker-traefik.docker_service.traefik will be created
+ resource "docker_service" "traefik" {
+ id = (known after apply)
+ name = "traefik"
+ endpoint_spec {
+ mode = (known after apply)
+ ports {
+ protocol = "tcp"
+ publish_mode = "host"
+ published_port = 80
+ target_port = 80
}
+ ports {
+ protocol = "tcp"
+ publish_mode = "host"
+ published_port = 443
+ target_port = 443
}
}
+ labels {
+ label = (known after apply)
+ value = (known after apply)
}
+ mode {
+ global = (known after apply)
+ replicated {
+ replicas = (known after apply)
}
}
+ task_spec {
+ force_update = (known after apply)
+ networks = [
+ "traefik",
]
+ restart_policy = (known after apply)
+ runtime = (known after apply)
+ container_spec {
+ env = {
+ "CF_API_EMAIL" = ""
+ "CF_API_KEY" = ""
+ "CF_DNS_API_TOKEN" = ""
+ "CF_ZONE_API_TOKEN" = ""
}
+ image = "traefik:2.3.5"
+ isolation = "default"
+ stop_grace_period = (known after apply)
+ configs {
+ config_id = (known after apply)
+ config_name = (known after apply)
+ file_gid = "0"
+ file_mode = 292
+ file_name = "/traefik.yaml"
+ file_uid = "0"
}
+ dns_config {
+ nameservers = (known after apply)
+ options = (known after apply)
+ search = (known after apply)
}
+ healthcheck {
+ interval = (known after apply)
+ retries = (known after apply)
+ start_period = (known after apply)
+ test = (known after apply)
+ timeout = (known after apply)
}
+ labels {
+ label = "traefik.enable"
+ value = "true"
}
+ labels {
+ label = "traefik.http.middlewares.https_redirect.redirectscheme.permanent"
+ value = "true"
}
+ labels {
+ label = "traefik.http.middlewares.https_redirect.redirectscheme.scheme"
+ value = "https"
}
+ labels {
+ label = "traefik.http.middlewares.traefik-basic-auth.basicauth.removeheader"
+ value = "true"
}
+ labels {
+ label = "traefik.http.middlewares.traefik-basic-auth.basicauth.users"
+ value = (known after apply)
}
+ labels {
+ label = "traefik.http.routers.http_catchall.entrypoints"
+ value = "http"
}
+ labels {
+ label = "traefik.http.routers.http_catchall.middlewares"
+ value = "https_redirect"
}
+ labels {
+ label = "traefik.http.routers.http_catchall.rule"
+ value = "HostRegexp(`{any:.+}`)"
}
+ labels {
+ label = "traefik.http.routers.metrics.entrypoints"
+ value = "http"
}
+ labels {
+ label = "traefik.http.routers.metrics.middlewares"
+ value = "traefik-basic-auth"
}
+ labels {
+ label = "traefik.http.routers.metrics.rule"
+ value = "Host(`traefik`) && PathPrefix(`/metrics`)"
}
+ labels {
+ label = "traefik.http.routers.metrics.service"
+ value = "prometheus@internal"
}
+ labels {
+ label = "traefik.http.routers.traefik.entrypoints"
+ value = "https"
}
+ labels {
+ label = "traefik.http.routers.traefik.middlewares"
+ value = "traefik-basic-auth"
}
+ labels {
+ label = "traefik.http.routers.traefik.rule"
+ value = "Host(`traefik-beta.example.com`)"
}
+ labels {
+ label = "traefik.http.routers.traefik.service"
+ value = "api@internal"
}
+ labels {
+ label = "traefik.http.routers.traefik.tls.certresolver"
+ value = "letsEncryptStaging"
}
+ mounts {
+ read_only = false
+ source = "traefik_acme"
+ target = "/etc/traefik/acme"
+ type = "volume"
}
+ mounts {
+ read_only = true
+ source = "/var/run/docker.sock"
+ target = "/var/run/docker.sock"
+ type = "bind"
}
}
+ placement {
+ constraints = (known after apply)
+ prefs = (known after apply)
+ platforms {
+ architecture = (known after apply)
+ os = (known after apply)
}
}
+ resources {
+ limits {
+ memory_bytes = (known after apply)
+ nano_cpus = (known after apply)
+ generic_resources {
+ discrete_resources_spec = (known after apply)
+ named_resources_spec = (known after apply)
}
}
+ reservation {
+ memory_bytes = (known after apply)
+ nano_cpus = (known after apply)
+ generic_resources {
+ discrete_resources_spec = (known after apply)
+ named_resources_spec = (known after apply)
}
}
}
}
}
# module.docker-traefik.docker_volume.traefik_acme will be created
+ resource "docker_volume" "traefik_acme" {
+ driver = (known after apply)
+ id = (known after apply)
+ mountpoint = (known after apply)
+ name = "traefik_acme"
}
# module.docker-vault.data.local_file.vault_hcl will be read during apply
# (config refers to values not yet known)
<= data "local_file" "vault_hcl" {
+ content = (known after apply)
+ content_base64 = (known after apply)
+ filename = ".terraform/modules/docker-vault/vault-config.hcl"
+ id = (known after apply)
}
# module.docker-vault.data.template_file.vault_hcl will be read during apply
# (config refers to values not yet known)
<= data "template_file" "vault_hcl" {
+ id = (known after apply)
+ rendered = (known after apply)
+ template = <<-EOT
storage "file" {
path = "/vault/file"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = true
}
ui = true
disable_mlock = true
EOT
}
# module.docker-vault.docker_config.vault_hcl will be created
+ resource "docker_config" "vault_hcl" {
+ data = (sensitive value)
+ id = (known after apply)
+ name = (known after apply)
}
# module.docker-vault.docker_service.vault-dev will be created
+ resource "docker_service" "vault-dev" {
+ id = (known after apply)
+ name = "vault-dev"
+ endpoint_spec {
+ mode = (known after apply)
+ ports {
+ name = (known after apply)
+ protocol = (known after apply)
+ publish_mode = (known after apply)
+ published_port = (known after apply)
+ target_port = (known after apply)
}
}
+ labels {
+ label = (known after apply)
+ value = (known after apply)
}
+ mode {
+ global = (known after apply)
+ replicated {
+ replicas = (known after apply)
}
}
+ task_spec {
+ force_update = (known after apply)
+ networks = [
+ "traefik",
]
+ restart_policy = (known after apply)
+ runtime = (known after apply)
+ container_spec {
+ args = [
+ "server",
]
+ env = {
+ "SKIP_SETCAP" = "true"
+ "VAULT_ADDR" = "http://127.0.0.1:8200"
+ "VAULT_API_ADDR" = "http://127.0.0.1:8200"
}
+ image = "vault:1.6.0"
+ isolation = "default"
+ stop_grace_period = (known after apply)
+ configs {
+ config_id = (known after apply)
+ config_name = (known after apply)
+ file_gid = "0"
+ file_mode = 292
+ file_name = "/vault/config/vault-config.hcl"
+ file_uid = "0"
}
+ dns_config {
+ nameservers = (known after apply)
+ options = (known after apply)
+ search = (known after apply)
}
+ healthcheck {
+ interval = (known after apply)
+ retries = (known after apply)
+ start_period = (known after apply)
+ test = (known after apply)
+ timeout = (known after apply)
}
+ labels {
+ label = "traefik.enable"
+ value = "true"
}
+ labels {
+ label = "traefik.http.routers.vault-dev.entrypoints"
+ value = "https"
}
+ labels {
+ label = "traefik.http.routers.vault-dev.rule"
+ value = "Host(`vault-beta.example.com`)"
}
+ labels {
+ label = "traefik.http.routers.vault-dev.tls.certresolver"
+ value = "letsEncryptStaging"
}
+ labels {
+ label = "traefik.http.services.vault-dev.loadbalancer.server.port"
+ value = "8200"
}
+ mounts {
+ read_only = false
+ source = "vault_data"
+ target = "/vault/file"
+ type = "volume"
}
+ mounts {
+ read_only = false
+ source = "vault_logs"
+ target = "/vault/logs"
+ type = "volume"
}
+ mounts {
+ read_only = false
+ source = "vault_polices"
+ target = "/vault/policies"
+ type = "volume"
}
}
+ placement {
+ constraints = (known after apply)
+ prefs = (known after apply)
+ platforms {
+ architecture = (known after apply)
+ os = (known after apply)
}
}
+ resources {
+ limits {
+ memory_bytes = (known after apply)
+ nano_cpus = (known after apply)
+ generic_resources {
+ discrete_resources_spec = (known after apply)
+ named_resources_spec = (known after apply)
}
}
+ reservation {
+ memory_bytes = (known after apply)
+ nano_cpus = (known after apply)
+ generic_resources {
+ discrete_resources_spec = (known after apply)
+ named_resources_spec = (known after apply)
}
}
}
}
}
# module.docker-vault.docker_volume.vault_data will be created
+ resource "docker_volume" "vault_data" {
+ driver = (known after apply)
+ id = (known after apply)
+ mountpoint = (known after apply)
+ name = "vault_data"
}
# module.docker-vault.docker_volume.vault_logs will be created
+ resource "docker_volume" "vault_logs" {
+ driver = (known after apply)
+ id = (known after apply)
+ mountpoint = (known after apply)
+ name = "vault_logs"
}
# module.docker-vault.docker_volume.vault_policies will be created
+ resource "docker_volume" "vault_policies" {
+ driver = (known after apply)
+ id = (known after apply)
+ mountpoint = (known after apply)
+ name = "vault_polices"
}
Plan: 9 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ acme_mountpoint = (known after apply)
+ traefik_network_name = "traefik"
+ traefik_service_config_name = (known after apply)
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 Traefik and Vault containers and you should see a message confirming the creation of resources (line 1 below). The output
values generated by both modules should also be returned.
Apply complete! Resources: 9 added, 0 changed, 0 destroyed.
Outputs:
acme_mountpoint = "/mnt/docker_data_volume/volumes/traefik_acme/_data"
traefik_network_name = "traefik"
traefik_service_config_name = "traefik-yaml-2020-12-18T06.56.56Z"
vault_service_config_name = "vault_hcl-2020-12-18T06.57.07Z"
Step 4 - Confirm Traefik and Vault have been successfully deployed
After a few minutes Traefik should be proxying both its own UI and that of the Vault container. You should be able to visit the hostnames configured earlier to access each service.
Note: Both modules by default configure a staging SSL certificate, so expect to see browser warnings. Setting a
live_cert
variable in either module totrue
will configure a live cert for the corresponding service.
Visiting vault.example.com should present you with the default setup page to begin initialising the Vault install:
When visiting traefik.example.com, before being granted access to the Traefik dashboard your browser will prompt you to authenticate using the password configured in the terraform.tfvars
file earlier:
Note: The Traefik module has the RedirectScheme middleware configured by default so all non-secure HTTP requests are automatically redirected to HTTPS
Step 5. Clean up
You can destroy both containers by running terraform destroy
. After you respond to the prompt with yes
, Terraform will destroy all the containers and Docker volumes created prior.
terraform destroy
EXAMPLE OUTPUT - (click to expand)
module.docker-vault.docker_service.vault-dev: Destroying... [id=ukpukp31n4mz55xoiqgy2k91l]
module.docker-vault.docker_service.vault-dev: Destruction complete after 2s
module.docker-vault.docker_volume.vault_data: Destroying... [id=vault_data]
module.docker-vault.docker_config.vault_hcl: Destroying... [id=p0hbxkitvpwf0t3dqvwyh6e0c]
module.docker-vault.docker_volume.vault_logs: Destroying... [id=vault_logs]
module.docker-vault.docker_volume.vault_policies: Destroying... [id=vault_polices]
module.docker-vault.docker_config.vault_hcl: Destruction complete after 0s
module.docker-vault.docker_volume.vault_data: Destruction complete after 3s
module.docker-vault.docker_volume.vault_logs: Destruction complete after 7s
module.docker-vault.docker_volume.vault_policies: Destruction complete after 7s
module.docker-traefik.docker_network.network: Destroying... [id=uufct6ond0qdzlnyzmna88lqe]
module.docker-traefik.docker_service.traefik: Destroying... [id=vs40s524qdkwj1bnenmqogeop]
module.docker-traefik.docker_service.traefik: Destruction complete after 2s
module.docker-traefik.docker_volume.traefik_acme: Destroying... [id=traefik_acme]
module.docker-traefik.docker_config.traefik-yaml: Destroying... [id=bbm9rnfaiqttfzdeur0lhtzgo]
module.docker-traefik.docker_config.traefik-yaml: Destruction complete after 1s
module.docker-traefik.docker_network.network: Destruction complete after 3s
module.docker-traefik.docker_volume.traefik_acme: Destruction complete after 3s
Destroy complete! Resources: 9 destroyed.
Conclusion
That’s it! You’ve now successfully deployed a Traefik load balancer/proxy container for routing incoming requests to backend services/containers. In this case to a Vault application service backend. The pre-built Terraform modules demonstrate how easy it is to automate the speedy deployment of infrastructure.