Introduction
HashiCorp Consul provides service discovery, health checking, KV store, and service mesh (Consul Connect) for distributed infrastructure. Ansible automates the entire stack: server cluster bootstrap, client agent deployment on all hosts, service registration, DNS configuration, ACLs, and Connect sidecar proxies.
Deploy Consul Server Cluster
---
- name: Deploy Consul servers
hosts: consul_servers
become: true
vars:
consul_version: "1.19.0"
consul_datacenter: dc1
consul_encrypt_key: "{{ vault_consul_encrypt_key }}"
consul_data_dir: /opt/consul/data
tasks:
- name: Download Consul
ansible.builtin.get_url:
url: "https://releases.hashicorp.com/consul/{{ consul_version }}/consul_{{ consul_version }}_linux_amd64.zip"
dest: /tmp/consul.zip
- name: Install unzip
ansible.builtin.package:
name: unzip
state: present
- name: Extract Consul
ansible.builtin.unarchive:
src: /tmp/consul.zip
dest: /usr/local/bin/
remote_src: true
mode: '0755'
- name: Create Consul user
ansible.builtin.user:
name: consul
system: true
shell: /usr/sbin/nologin
- name: Create directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
owner: consul
mode: '0750'
loop:
- /etc/consul.d
- "{{ consul_data_dir }}"
- name: Deploy server config
ansible.builtin.template:
src: consul-server.hcl.j2
dest: /etc/consul.d/consul.hcl
owner: consul
mode: '0640'
notify: restart consul
- name: Create systemd service
ansible.builtin.copy:
dest: /etc/systemd/system/consul.service
content: |
[Unit]
Description=HashiCorp Consul
After=network-online.target
[Service]
User=consul
Group=consul
ExecStart=/usr/local/bin/consul agent -config-dir=/etc/consul.d
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
mode: '0644'
notify:
- daemon reload
- restart consul
- name: Allow Consul through firewall
ansible.posix.firewalld:
port: "{{ item }}/tcp"
permanent: true
state: enabled
immediate: true
loop: ["8300", "8301", "8302", "8500", "8600"]
- name: Start Consul
ansible.builtin.service:
name: consul
state: started
enabled: true
handlers:
- name: daemon reload
ansible.builtin.systemd:
daemon_reload: true
- name: restart consul
ansible.builtin.service:
name: consul
state: restarted
Server Config Template
# templates/consul-server.hcl.j2
datacenter = "{{ consul_datacenter }}"
data_dir = "{{ consul_data_dir }}"
node_name = "{{ inventory_hostname }}"
server = true
bootstrap_expect = {{ groups['consul_servers'] | length }}
bind_addr = "{{ ansible_default_ipv4.address }}"
client_addr = "0.0.0.0"
encrypt = "{{ consul_encrypt_key }}"
retry_join = [
{% for host in groups['consul_servers'] %}
{% if host != inventory_hostname %}
"{{ hostvars[host].ansible_default_ipv4.address }}",
{% endif %}
{% endfor %}
]
ui_config {
enabled = true
}
connect {
enabled = true
}
addresses {
dns = "0.0.0.0"
http = "0.0.0.0"
}
ports {
dns = 8600
http = 8500
}
performance {
raft_multiplier = 1
}
telemetry {
prometheus_retention_time = "24h"
disable_hostname = true
}
Deploy Client Agents
---
- name: Deploy Consul clients
hosts: all:!consul_servers
become: true
tasks:
- name: Install Consul binary
# ... same download steps ...
- name: Deploy client config
ansible.builtin.template:
src: consul-client.hcl.j2
dest: /etc/consul.d/consul.hcl
owner: consul
mode: '0640'
notify: restart consul
# templates/consul-client.hcl.j2
datacenter = "{{ consul_datacenter }}"
data_dir = "{{ consul_data_dir }}"
node_name = "{{ inventory_hostname }}"
server = false
bind_addr = "{{ ansible_default_ipv4.address }}"
encrypt = "{{ consul_encrypt_key }}"
retry_join = [
{% for host in groups['consul_servers'] %}
"{{ hostvars[host].ansible_default_ipv4.address }}",
{% endfor %}
]
connect {
enabled = true
}
Register Services
- name: Register web service
ansible.builtin.copy:
dest: /etc/consul.d/web-service.hcl
content: |
service {
name = "web"
port = 8080
tags = ["production", "v1"]
meta {
version = "1.0"
environment = "production"
}
check {
http = "http://localhost:8080/health"
interval = "10s"
timeout = "3s"
}
connect {
sidecar_service {
proxy {
upstreams {
destination_name = "api"
local_bind_port = 9091
}
upstreams {
destination_name = "database"
local_bind_port = 5432
}
}
}
}
}
owner: consul
mode: '0644'
notify: reload consul
KV Store
- name: Write key-value pairs
ansible.builtin.uri:
url: "http://localhost:8500/v1/kv/{{ item.key }}"
method: PUT
body: "{{ item.value }}"
loop:
- { key: "config/app/db_host", value: "db.example.com" }
- { key: "config/app/cache_ttl", value: "300" }
- { key: "config/app/log_level", value: "info" }
- name: Read key-value
ansible.builtin.uri:
url: "http://localhost:8500/v1/kv/config/app/db_host?raw"
return_content: true
register: kv_result
DNS Integration
- name: Configure systemd-resolved to use Consul DNS
ansible.builtin.copy:
dest: /etc/systemd/resolved.conf.d/consul.conf
content: |
[Resolve]
DNS=127.0.0.1:8600
Domains=~consul
mode: '0644'
notify: restart resolved
# Query services via DNS
# dig @127.0.0.1 -p 8600 web.service.consul SRV
ACL Bootstrap
- name: Bootstrap ACL system
ansible.builtin.uri:
url: "http://localhost:8500/v1/acl/bootstrap"
method: PUT
status_code: [200, 403]
register: acl_bootstrap
run_once: true
- name: Save bootstrap token
ansible.builtin.set_fact:
consul_master_token: "{{ acl_bootstrap.json.SecretID }}"
when: acl_bootstrap.status == 200
run_once: true
Health Check
- name: Check Consul cluster members
ansible.builtin.command: consul members
register: members
changed_when: false
- name: Check leader
ansible.builtin.uri:
url: "http://localhost:8500/v1/status/leader"
return_content: true
register: leader
- name: Check service health
ansible.builtin.uri:
url: "http://localhost:8500/v1/health/service/web?passing"
return_content: true
register: healthy_services
Troubleshooting
Agent Not Joining
- name: Test connectivity to servers
ansible.builtin.wait_for:
host: "{{ hostvars[item].ansible_default_ipv4.address }}"
port: 8301
timeout: 5
loop: "{{ groups['consul_servers'] }}"
Generate Encrypt Key
consul keygen
Related Articles
Conclusion
Consul provides service discovery, health checking, KV configuration, and service mesh from a single tool. Ansible deploys the server cluster and client agents, registers services with health checks, configures DNS integration so applications resolve service.consul names, and bootstraps ACLs. Combined with Nomad and Vault, it forms the complete HashiCorp infrastructure stack — all managed as code.