5. Deploy your first service

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.

5.1. Build the Docker image

$ 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.

5.2. Push the Docker image

$ 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.

5.3. Submit the webserver Nomad Job

  • Go to https://nomad.your-domain.com/ui

  • 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

_images/nomad_job_submit.png

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.

5.4. Submit the Redis Nomad Job

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

5.5. Add a Consul intention

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:

  • Go to https://consul.your-domain.com/ui

  • 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

5.6. Create a routing rule in Traefik

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
        }
    ]
}

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.

5.7. Verify your services are working

  • 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.

5.8. Storing secrets in Vault

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.