Introduction
Elasticsearch is the backbone of the ELK stack and one of the most widely deployed search and analytics engines. A production cluster requires careful node configuration, memory tuning, security setup, and lifecycle management — all of which Ansible automates reliably across dozens of nodes.
Inventory Structure
# inventory/elasticsearch/hosts
[elasticsearch:children]
es_master
es_data
es_coordinating
[es_master]
es-master-1 ansible_host=10.0.1.10
es-master-2 ansible_host=10.0.1.11
es-master-3 ansible_host=10.0.1.12
[es_data]
es-data-1 ansible_host=10.0.2.10
es-data-2 ansible_host=10.0.2.11
es-data-3 ansible_host=10.0.2.12
es-data-4 ansible_host=10.0.2.13
[es_coordinating]
es-coord-1 ansible_host=10.0.3.10
es-coord-2 ansible_host=10.0.3.11
Install Elasticsearch
---
- name: Deploy Elasticsearch Cluster
hosts: elasticsearch
become: true
vars:
es_version: "8.17.0"
es_cluster_name: "production"
es_heap_size: "{{ (ansible_memtotal_mb * 0.5) | int }}m"
es_data_path: /var/lib/elasticsearch
es_log_path: /var/log/elasticsearch
tasks:
- name: Import Elasticsearch GPG key
ansible.builtin.rpm_key:
key: https://artifacts.elastic.co/GPG-KEY-elasticsearch
state: present
when: ansible_os_family == "RedHat"
- name: Add Elasticsearch repository
ansible.builtin.yum_repository:
name: elasticsearch
description: Elasticsearch 8.x
baseurl: https://artifacts.elastic.co/packages/8.x/yum
gpgcheck: true
gpgkey: https://artifacts.elastic.co/GPG-KEY-elasticsearch
when: ansible_os_family == "RedHat"
- name: Install Elasticsearch
ansible.builtin.package:
name: "elasticsearch-{{ es_version }}"
state: present
- name: Configure JVM heap size
ansible.builtin.copy:
dest: /etc/elasticsearch/jvm.options.d/heap.options
content: |
-Xms{{ es_heap_size }}
-Xmx{{ es_heap_size }}
mode: "0644"
notify: restart elasticsearch
- name: Set system limits
ansible.builtin.copy:
dest: /etc/security/limits.d/elasticsearch.conf
content: |
elasticsearch soft nofile 65536
elasticsearch hard nofile 65536
elasticsearch soft memlock unlimited
elasticsearch hard memlock unlimited
mode: "0644"
- name: Configure sysctl for Elasticsearch
ansible.posix.sysctl:
name: vm.max_map_count
value: "262144"
sysctl_set: true
state: present
reload: true
- name: Deploy Elasticsearch configuration
ansible.builtin.template:
src: elasticsearch.yml.j2
dest: /etc/elasticsearch/elasticsearch.yml
owner: elasticsearch
group: elasticsearch
mode: "0660"
notify: restart elasticsearch
- name: Enable and start Elasticsearch
ansible.builtin.systemd:
name: elasticsearch
enabled: true
state: started
handlers:
- name: restart elasticsearch
ansible.builtin.systemd:
name: elasticsearch
state: restarted
Configuration Template
# templates/elasticsearch.yml.j2
cluster.name: {{ es_cluster_name }}
node.name: {{ inventory_hostname }}
path.data: {{ es_data_path }}
path.logs: {{ es_log_path }}
network.host: {{ ansible_default_ipv4.address }}
http.port: 9200
transport.port: 9300
# Node roles
{% if inventory_hostname in groups['es_master'] %}
node.roles: [master]
{% elif inventory_hostname in groups['es_data'] %}
node.roles: [data, data_content, data_hot, ingest]
{% elif inventory_hostname in groups['es_coordinating'] %}
node.roles: []
{% endif %}
# Discovery
discovery.seed_hosts:
{% for host in groups['es_master'] %}
- {{ hostvars[host]['ansible_host'] }}:9300
{% endfor %}
cluster.initial_master_nodes:
{% for host in groups['es_master'] %}
- {{ host }}
{% endfor %}
# Security
xpack.security.enabled: true
xpack.security.transport.ssl.enabled: true
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.transport.ssl.keystore.path: /etc/elasticsearch/certs/transport.p12
xpack.security.transport.ssl.truststore.path: /etc/elasticsearch/certs/transport.p12
xpack.security.http.ssl.enabled: true
xpack.security.http.ssl.keystore.path: /etc/elasticsearch/certs/http.p12
Index Lifecycle Management
- name: Configure ILM policy
ansible.builtin.uri:
url: "https://{{ es_coordinating_host }}:9200/_ilm/policy/logs-policy"
method: PUT
user: elastic
password: "{{ vault_es_password }}"
force_basic_auth: true
validate_certs: false
body_format: json
body:
policy:
phases:
hot:
min_age: 0ms
actions:
rollover:
max_primary_shard_size: 50gb
max_age: 1d
set_priority:
priority: 100
warm:
min_age: 3d
actions:
shrink:
number_of_shards: 1
forcemerge:
max_num_segments: 1
set_priority:
priority: 50
cold:
min_age: 30d
actions:
searchable_snapshot:
snapshot_repository: s3_repository
delete:
min_age: 90d
actions:
delete: {}
Snapshot and Backup
- name: Configure S3 snapshot repository
ansible.builtin.uri:
url: "https://{{ es_coordinating_host }}:9200/_snapshot/s3_backups"
method: PUT
user: elastic
password: "{{ vault_es_password }}"
force_basic_auth: true
validate_certs: false
body_format: json
body:
type: s3
settings:
bucket: "{{ es_backup_bucket }}"
region: "{{ aws_region }}"
base_path: "elasticsearch/{{ es_cluster_name }}"
- name: Create daily snapshot
ansible.builtin.uri:
url: "https://{{ es_coordinating_host }}:9200/_slm/policy/daily-snapshot"
method: PUT
user: elastic
password: "{{ vault_es_password }}"
force_basic_auth: true
validate_certs: false
body_format: json
body:
schedule: "0 30 2 * * ?"
name: "<daily-snap-{now/d}>"
repository: s3_backups
config:
indices: ["*"]
ignore_unavailable: true
retention:
expire_after: 30d
min_count: 7
max_count: 30
Health Check Playbook
- name: Check Elasticsearch cluster health
hosts: localhost
connection: local
tasks:
- name: Get cluster health
ansible.builtin.uri:
url: "https://{{ es_coordinating_host }}:9200/_cluster/health"
user: elastic
password: "{{ vault_es_password }}"
force_basic_auth: true
validate_certs: false
register: health
- name: Assert cluster is healthy
ansible.builtin.assert:
that:
- health.json.status in ['green', 'yellow']
- health.json.number_of_nodes >= 7
- health.json.unassigned_shards == 0
fail_msg: "Cluster unhealthy: {{ health.json.status }}, {{ health.json.unassigned_shards }} unassigned shards"
- name: Display cluster info
ansible.builtin.debug:
msg: |
Cluster: {{ health.json.cluster_name }}
Status: {{ health.json.status }}
Nodes: {{ health.json.number_of_nodes }}
Shards: {{ health.json.active_shards }} active, {{ health.json.relocating_shards }} relocating
Troubleshooting
# Cluster health
curl -k -u elastic:password https://localhost:9200/_cluster/health?pretty
# Node info
curl -k -u elastic:password https://localhost:9200/_cat/nodes?v
# Shard allocation
curl -k -u elastic:password https://localhost:9200/_cat/shards?v&s=state
# Index stats
curl -k -u elastic:password https://localhost:9200/_cat/indices?v&s=docs.count:desc
Related Articles
- Ansible Fluent Bit Log Forwarding
- Ansible Rsyslog Centralized Logging
- Ansible Prometheus Grafana Integration
- Ansible Redis Cache Cluster
- Ansible Vault Encrypt Decrypt
Conclusion
A production Elasticsearch cluster requires coordinated configuration of master, data, and coordinating nodes with proper JVM tuning, security, and lifecycle management. Ansible makes this repeatable — from initial deployment through rolling upgrades, snapshot management, and health monitoring.