Introduction

HashiCorp Nomad is a lightweight workload orchestrator that schedules containers, VMs, binaries, and batch jobs. Unlike Kubernetes, Nomad has a simple architecture (single binary) and integrates natively with Consul for service discovery and Vault for secrets. Ansible automates the full deployment: server cluster, client agents, job submissions, ACLs, and monitoring.

Deploy Nomad Server Cluster

---
- name: Deploy Nomad servers
  hosts: nomad_servers
  become: true
  vars:
    nomad_version: "1.8.0"
    nomad_datacenter: dc1
    nomad_region: global
    nomad_bootstrap_expect: 3
  tasks:
    - name: Install prerequisites
      ansible.builtin.package:
        name: [unzip, curl]
        state: present

    - name: Download Nomad
      ansible.builtin.get_url:
        url: "https://releases.hashicorp.com/nomad/{{ nomad_version }}/nomad_{{ nomad_version }}_linux_amd64.zip"
        dest: /tmp/nomad.zip

    - name: Extract Nomad
      ansible.builtin.unarchive:
        src: /tmp/nomad.zip
        dest: /usr/local/bin/
        remote_src: true
        mode: '0755'

    - name: Create Nomad user
      ansible.builtin.user:
        name: nomad
        system: true
        shell: /usr/sbin/nologin

    - name: Create directories
      ansible.builtin.file:
        path: "{{ item }}"
        state: directory
        owner: nomad
        mode: '0750'
      loop:
        - /etc/nomad.d
        - /opt/nomad/data

    - name: Deploy server config
      ansible.builtin.template:
        src: nomad-server.hcl.j2
        dest: /etc/nomad.d/nomad.hcl
        owner: nomad
        mode: '0640'
      notify: restart nomad

    - name: Create systemd service
      ansible.builtin.copy:
        dest: /etc/systemd/system/nomad.service
        content: |
          [Unit]
          Description=Nomad
          After=network-online.target consul.service
          [Service]
          User=nomad
          Group=nomad
          ExecStart=/usr/local/bin/nomad agent -config /etc/nomad.d
          ExecReload=/bin/kill -HUP $MAINPID
          KillMode=process
          KillSignal=SIGINT
          Restart=on-failure
          RestartSec=5
          LimitNOFILE=65536
          [Install]
          WantedBy=multi-user.target
        mode: '0644'
      notify:
        - daemon reload
        - restart nomad

    - name: Allow Nomad through firewall
      ansible.posix.firewalld:
        port: "{{ item }}/tcp"
        permanent: true
        state: enabled
        immediate: true
      loop: ["4646", "4647", "4648"]

    - name: Start Nomad
      ansible.builtin.service:
        name: nomad
        state: started
        enabled: true

  handlers:
    - name: daemon reload
      ansible.builtin.systemd:
        daemon_reload: true
    - name: restart nomad
      ansible.builtin.service:
        name: nomad
        state: restarted

Server Config

# templates/nomad-server.hcl.j2
datacenter = "{{ nomad_datacenter }}"
region     = "{{ nomad_region }}"
data_dir   = "/opt/nomad/data"
bind_addr  = "0.0.0.0"

advertise {
  http = "{{ ansible_default_ipv4.address }}"
  rpc  = "{{ ansible_default_ipv4.address }}"
  serf = "{{ ansible_default_ipv4.address }}"
}

server {
  enabled          = true
  bootstrap_expect = {{ nomad_bootstrap_expect }}
{% for server in groups['nomad_servers'] %}
{% if server != inventory_hostname %}
  server_join {
    retry_join = ["{{ hostvars[server].ansible_default_ipv4.address }}"]
  }
{% endif %}
{% endfor %}
}

telemetry {
  prometheus_metrics = true
  publish_allocation_metrics = true
  publish_node_metrics = true
}

{% if nomad_consul_enabled | default(true) %}
consul {
  address = "127.0.0.1:8500"
  auto_advertise = true
  server_auto_join = true
  client_auto_join = true
}
{% endif %}

Deploy Client Agents

---
- name: Deploy Nomad clients
  hosts: nomad_clients
  become: true
  tasks:
    - name: Install Nomad binary
      # ... same download steps as server ...

    - name: Deploy client config
      ansible.builtin.template:
        src: nomad-client.hcl.j2
        dest: /etc/nomad.d/nomad.hcl
        owner: nomad
        mode: '0640'
      notify: restart nomad

    - name: Install Docker (for Docker driver)
      ansible.builtin.package:
        name: docker.io
        state: present

    - name: Add nomad user to docker group
      ansible.builtin.user:
        name: nomad
        groups: docker
        append: true
# templates/nomad-client.hcl.j2
datacenter = "{{ nomad_datacenter }}"
data_dir   = "/opt/nomad/data"
bind_addr  = "0.0.0.0"

client {
  enabled = true
{% for server in groups['nomad_servers'] %}
  servers = ["{{ hostvars[server].ansible_default_ipv4.address }}:4647"]
{% endfor %}

  meta {
    "node_type" = "{{ nomad_node_type | default('general') }}"
  }

  host_volume "data" {
    path      = "/opt/nomad/volumes/data"
    read_only = false
  }
}

plugin "docker" {
  config {
    allow_privileged = false
    volumes {
      enabled = true
    }
  }
}

Submit Jobs

- name: Submit Nomad job
  ansible.builtin.uri:
    url: "http://{{ groups['nomad_servers'][0] }}:4646/v1/jobs"
    method: POST
    body_format: json
    body:
      Job:
        ID: webapp
        Type: service
        Datacenters: ["{{ nomad_datacenter }}"]
        TaskGroups:
          - Name: web
            Count: 3
            Tasks:
              - Name: nginx
                Driver: docker
                Config:
                  image: "nginx:latest"
                  ports: ["http"]
                Resources:
                  CPU: 500
                  MemoryMB: 256
            Networks:
              - Port:
                  - Label: http
                    To: 80
            Services:
              - Name: webapp
                PortLabel: http
                Provider: consul
                Checks:
                  - Type: http
                    Path: /
                    Interval: 10000000000
                    Timeout: 2000000000
    status_code: 200
  delegate_to: localhost

Job from HCL File

- name: Deploy job from file
  ansible.builtin.command: nomad job run /path/to/job.nomad
  environment:
    NOMAD_ADDR: "http://{{ groups['nomad_servers'][0] }}:4646"
  changed_when: true

ACL Bootstrap

- name: Bootstrap ACL system
  ansible.builtin.command: nomad acl bootstrap
  register: acl_bootstrap
  environment:
    NOMAD_ADDR: "http://localhost:4646"
  run_once: true
  changed_when: "'Secret' in acl_bootstrap.stdout"
  failed_when: false

Health Check

- name: Check Nomad leader
  ansible.builtin.uri:
    url: "http://localhost:4646/v1/status/leader"
    return_content: true
  register: leader

- name: List cluster members
  ansible.builtin.command: nomad server members
  register: members
  changed_when: false

- name: Check node status
  ansible.builtin.command: nomad node status
  register: nodes
  changed_when: false

- name: Check job status
  ansible.builtin.command: nomad job status
  register: jobs
  changed_when: false

Troubleshooting

Allocation Failures

- name: Check allocation status
  ansible.builtin.command: "nomad alloc status {{ alloc_id }}"
  register: alloc_status
  changed_when: false
  environment:
    NOMAD_ADDR: "http://localhost:4646"

Client Not Joining

- name: Check client connectivity to servers
  ansible.builtin.wait_for:
    host: "{{ hostvars[item].ansible_default_ipv4.address }}"
    port: 4647
    timeout: 5
  loop: "{{ groups['nomad_servers'] }}"

Conclusion

Nomad is the simpler alternative to Kubernetes — a single binary that schedules containers, VMs, and raw binaries. Ansible deploys the server cluster and client agents, configures Consul integration for service discovery, and submits jobs via API or CLI. Use Nomad when you need orchestration without Kubernetes complexity: smaller teams, mixed workloads (containers + legacy), and multi-datacenter federation.