To demonstrate how to use Consul/Nomad to host containerized services, we’re going to deploy a simple Flask web-service with a Redis database, and add a route in Traefik so that it is accessible from the public internet.
$ cd gcp-hashi-cluster/docs/tutorials/flask-redis-counter/count-service/
$ ./build_image.sh
Step 6/8 : ENV PYTHONPATH /app
---> Using cache
---> bbe1559dc882
Step 7/8 : ENV PORT 8080
---> Using cache
---> a5714a7f9d43
Step 8/8 : CMD ["python", "/app/main.py"]
---> Using cache
---> 40fa80bc1e8c
Successfully built 40fa80bc1e8c
Successfully tagged eu.gcr.io/hashi-cluster-cbj98124-7cb8/count-webserver:v0.1
This tag is of a local Docker image, it has not yet been pushed to a GCP container registry.
$ cd gcp-hashi-cluster/operations/ # operations scripts must be run from here
$ ./nomad/push-docker-image.sh eu.gcr.io/hashi-cluster-cbj98124-7cb8/count-webserver:v0.1
Success! Your image will be available to Nomad as: 'nomad/count-webserver:v0.1'
This pushes your image to a private GCP container registry and then launches an Ansible playbook to pull it on every Nomad client node.
Important
Due to an unresolved configuration issue, Nomad won’t be able to pull images from a private GCP container registry. As a workaround the script above pulls images outside of Nomad and gives them a local tag in the form: nomad/your-service:v0.1. You will need to use this tag in your Nomad jobs.
Authenticate with your dashboard username and password (set in build/conf/project-defaults.json)
Click Run Job in the top right.
Copy/paste the contents of docs/tutorials/flask-redis-counter/count-webserver.nomad in here, ensure the Docker image tag is correct.
Click Plan and the Run
Notice the sidecar_service { } stanza configures an upstream connection to a service redis-db and makes it available at a local_bind_port: 16379. This can be set to any valid port and doesn’t have to match the port of the target service. The container within count-service-task can fetch this value from an environment variable NOMAD_UPSTREAM_PORT_redis-db.
We’ll use a public image for Redis so there is no need to build or push it, simply submit this job to Nomad: docs/tutorials/flask-redis-counter/count-webserver.nomad
Nomad will have registered two Consul services count-webserver and redis-db. To allow them communicate via the Consul Connect service mesh we must create an explicit intention. The initialization script already added a Traefik -> * intention (which you may prefer to replace), we will now add a count-webserver -> redis-db intention:
Authenticate using your dashboard username and password, same as with the Nomad UI.
You will also need to login using an ACL token. Click login on the top right, and paste your Consul UI token (read/write) from step 4.5.2.
Navigate to Intentions and click Create
Select count-webserver as the Source Service and redis-db as the Destination Service, choose Allow and click Save
Routes in the Traefik instance are configured in /etc/traefik/dynamic-conf.toml. This gets rendered dynamically by consul-template.
Tip
The implementation underlying how service routes are updated on Traefik servers is little obtuse (see: services/traefik/systemd/watch-traefik-routes-updated.service), you may prefer to disable this and simply maintain dynamic-conf.toml manually as a static file.
Create a json file with the following content:
{
"dashboards_ip_allowlist": ["0.0.0.0/0"],
"routes": [
{
"traefik_service_name": "count-webserver",
"consul_service_name": "count-webserver",
"routing_rule": "PathPrefix(`/counter`)",
"connect_enabled": true
}
]
}
This defines a Traefik service and router rule that routes incoming HTTP requests to our count-webserver service via a Consul Connect sidecar proxy.
Tip
The PathPrefix should be a valid prefix in your service’s HTTP API. Traefik can also add/remove path prefixes before forwarding requests to services (see: StripPrefix, HeadersRegexp).
Next run the following script to publish your service routes:
$ cd gcp-hashi-cluster/operations/traefik/
$ ./overwrite-service-routes.sh <my-routes.json>
This uploads the json file and re-renders configurations for Traefik and its local sidecar proxy.
Go to https://traefik.your-domain.com/ and authenticate with your dashboard username/password. You should see that a new route and service has been created. Note that a service in Traefik isn’t equivalent to a Consul service but we’re linking them together here with a route.
Next visit https://your-domain.com/counter/hello and it should say “Hello” back!
Finally test the counter, go to: https://your-domain.com/counter/increment . You should see the number 1 and this should increment on every refresh. If this fails it means count-webserver cannot connect to redis-db.
Tip
If any of these steps fail, submit an issue on github with your error, or schedule a call with me for assistance.
Suppose our counter service needs to authenticate with a 3rd party API, we don’t want to place keys directly in the application code. Vault integrates with Nomad to deliver sensitive secrets to applications.
The write-only Vault token generated earlier allows writing secrets to this a path secret/nomad/. Vault-server-1 has this set as its VAULT_TOKEN environment variable, for convenience.
Create a json file with a path (prefixed by secret/nomad/) and some fields/values to store.
{
"secret/nomad/counter/social-auth-facebook": {
"app_key": "16696501350101",
"app_secret": "731ebc29cne367cv6213c1"
}
}
Run the write-secrets.sh script, this connects to vault-server-1 and runs vault kv put.
$ cd operations/
$ ./vault/write-secrets.sh my-secrets.json
Uncomment the vault and template sections in count-webserver.nomad and resubmit the job. Due to a quirk in Vault’s KV V2 API the path prefix should be secret/data/nomad/ instead of secret/nomad/ Nomad and Vault have been configured with a policy nomad-client-base for reading secrets this path.
/* ... */
task "count-service-task" {
driver = "docker"
config {
image = "nomad/count-webserver:v0.1"
}
vault {
policies = ["nomad-client-base"]
change_mode = "noop"
}
template {
data = <<EOH
{{ with secret "secret/data/nomad/counter/social-auth-facebook" }}
FACEBOOK_KEY="{{ .Data.data.app_key }}"
FACEBOOK_SECRET="{{ .Data.data.app_secret }}"
{{ end }}
EOH
destination = "secrets/file.env"
env = true
}
}
Your secrets will be available to the service containers as environment variables: FACEBOOK_KEY and FACEBOOK_SECRET.