initial commit

This commit is contained in:
srw 2025-10-01 03:48:28 +00:00
commit 6ddd5c4d32
9 changed files with 530 additions and 0 deletions

20
LICENSE Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2024 Shane Wadleigh
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# ansible-role-elasticsearch-k8s
Ansible role for deploying a statefulset for elasticsearch cluster
- https://www.elastic.co/

116
defaults/main.yml Normal file
View File

@ -0,0 +1,116 @@
---
elasticsearch_deployment_name: elasticsearch
elasticsearch_namespace: "{{ elasticsearch_deployment_name }}"
# elasticsearch_image: docker.elastic.co/elasticsearch/elasticsearch-wolfi
elasticsearch_image: docker.elastic.co/elasticsearch/elasticsearch
elasticsearch_image_tag: latest
# KUBERNETES SETTINGS
elasticsearch_replicas: 3
elasticsearch_min_available: 2
elasticsearch_max_skew: 1
elasticsearch_max_unavailable: 1
# only update pods N-1..2; leave 0..1 untouched
# elasticsearch_rollout_partition: 2
# requiredDuringSchedulingIgnoredDuringExecution,preferredDuringSchedulingIgnoredDuringExecution
elasticsearch_anti_affinity_type: preferredDuringSchedulingIgnoredDuringExecution
# DoNotSchedule, ScheduleAnyway
elasticsearch_topology_constraint: ScheduleAnyway
elasticsearch_labels:
app.kubernetes.io/name: "{{ elasticsearch_deployment_name }}"
app.kubernetes.io/managed-by: ansible
elasticsearch_selector:
app.kubernetes.io/name: "{{ elasticsearch_deployment_name }}"
elasticsearch_strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: "{{ elasticsearch_max_unavailable }}"
elasticsearch_container_resources:
requests:
cpu: "500m"
memory: "2Gi"
limits:
cpu: "2"
memory: "4Gi"
elasticsearch_health_check:
- curl
- -u
- "elastic:${ELASTIC_PASSWORD}"
- --insecure
- https://localhost:9200/_cluster/health
# security and resource limits
# elasticsearch_pod_security_context:
# runAsNonRoot: true
# fsGroup: 1000
# seccompProfile: { type: RuntimeDefault }
# elasticsearch_container_security_context:
# allowPrivilegeEscalation: false
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# seccompProfile: { type: RuntimeDefault }
# ELASTICSEARCH SETTINGS
elasticsearch_http_port: 9200
elasticsearch_transport_port: 9300
elasticsearch_cluster_name: elasticsearch-cluster
elasticsearch_java_opts: "-Xms1g -Xmx1g"
elasticsearch_security: "true"
elasticsearch_storage_class: local-path
elasticsearch_storage_size: "10Gi"
elasticsearch_cluster_nodes:
- "elasticsearch-0"
- "elasticsearch-1"
- "elasticsearch-2"
elasticsearch_seed_hosts:
- "elasticsearch-0.elasticsearch-transport.{{ elasticsearch_namespace }}.svc.cluster.local"
- "elasticsearch-1.elasticsearch-transport.{{ elasticsearch_namespace }}.svc.cluster.local"
- "elasticsearch-2.elasticsearch-transport.{{ elasticsearch_namespace }}.svc.cluster.local"
elasticsearch_cert_names:
- "elasticsearch-http.{{ elasticsearch_namespace }}.svc.cluster.local"
- "elasticsearch-transport.{{ elasticsearch_namespace }}.svc.cluster.local"
- "*.elasticsearch-http.{{ elasticsearch_namespace }}.svc.cluster.local"
- "*.elasticsearch-transport.{{ elasticsearch_namespace }}.svc.cluster.local"
elasticsearch_node_roles:
- data
- master
# /usr/share/elasticsearch/config/certs/elastic-stack-ca.p12
elasticsearch_configs:
ES_JAVA_OPTS: "{{ elasticsearch_java_opts }}"
network.host: "0.0.0.0"
cluster.name: "{{ elasticsearch_cluster_name }}"
cluster.initial_master_nodes: "{{ elasticsearch_cluster_nodes | join(',') }}"
discovery.seed_hosts: "{{ elasticsearch_seed_hosts | join(',') }}"
node.roles: "{{ elasticsearch_node_roles | join(',') }}"
node.store.allow_mmap: "false"
xpack.license.self_generated.type: "basic"
xpack.security.enabled: "{{ elasticsearch_security | ternary('true', 'false') }}"
xpack.security.enrollment.enabled: "false"
xpack.security.http.ssl.enabled: "true"
xpack.security.http.ssl.keystore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
xpack.security.http.ssl.keystore.password: "{{ elastic_cert_password | d(omit) }}"
xpack.security.transport.ssl.enabled: "true"
xpack.security.transport.ssl.verification_mode: certificate
xpack.security.transport.ssl.client_authentication: required
xpack.security.transport.ssl.keystore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
xpack.security.transport.ssl.truststore.path: /usr/share/elasticsearch/config/certs/elastic-certificates.p12
xpack.security.transport.ssl.keystore.password: "{{ elastic_cert_password | d(omit) }}"
xpack.security.transport.ssl.truststore.password: "{{ elastic_cert_password | d(omit) }}"

28
handlers/main.yml Normal file
View File

@ -0,0 +1,28 @@
---
- name: ELASTICSEARCH HANDLER check for existing deployment {{ elasticsearch_deployment_name }}
listen: elasticsearch-restart
kubernetes.core.k8s_info:
api_version: apps/v1
kind: StatefulSet
name: "{{ elasticsearch_deployment_name }}"
namespace: "{{ elasticsearch_namespace }}"
register: elasticsearch_deploy_info
failed_when: false
changed_when: false
- name: ELASTICSEARCH HANDLER restart deployment {{ elasticsearch_deployment_name }}
listen: elasticsearch-restart
kubernetes.core.k8s:
api_version: apps/v1
kind: StatefulSet
name: "{{ elasticsearch_deployment_name }}"
namespace: "{{ elasticsearch_namespace }}"
state: patched
definition:
spec:
template:
metadata:
annotations:
kubectl.kubernetes.io/restartedAt: "{{ lookup('pipe', 'date -u +%Y-%m-%dT%H:%M:%SZ') }}"
when:
- elasticsearch_deploy_info.resources | length > 0

40
meta/main.yml Normal file
View File

@ -0,0 +1,40 @@
---
dependencies: []
galaxy_info:
role_name: elasticsearch_k8s
author: srw
description: Ansible role for deploying elasticsearch cluster to kubernetes
company: "NMD, LLC"
license: "license (BSD, MIT)"
min_ansible_version: "2.10"
platforms:
- name: Fedora
versions:
- all
- name: Debian
versions:
- buster
- bullseye
- bookworm
- trixie
- name: Ubuntu
versions:
- bionic
- focal
- jammy
- name: Alpine
version:
- all
- name: ArchLinux
versions:
- all
galaxy_tags:
- server
- system
- containers
- kubernetes
- k8s
- k3s
- rke2
- elasticsearch

130
tasks/deploy.yml Normal file
View File

@ -0,0 +1,130 @@
---
- name: ELASTICSEARCH DEPLOY
tags:
- elasticsearch
- elasticsearch-deploy
block:
- name: ELASTICSEARCH DEPLOY create PodDisruptionBudget
kubernetes.core.k8s:
state: "{{ elasticsearch_state | d('present') }}"
definition:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: "{{ elasticsearch_deployment_name }}"
namespace: "{{ elasticsearch_namespace }}"
labels: "{{ elasticsearch_labels }}"
spec:
minAvailable: "{{ elasticsearch_min_available }}"
selector:
matchLabels: "{{ elasticsearch_selector }}"
- name: ELASTICSEARCH DEPLOY create StatefulSet
kubernetes.core.k8s:
state: "{{ elasticsearch_state | d('present') }}"
server_side_apply:
field_manager: ansible
force_conflicts: true
wait: true
wait_timeout: 600
definition:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: "{{ elasticsearch_deployment_name }}"
namespace: "{{ elasticsearch_namespace }}"
labels: "{{ elasticsearch_labels }}"
spec:
serviceName: elasticsearch-transport
replicas: "{{ elasticsearch_replicas }}"
strategy: "{{ elasticsearch_strategy }}"
updateStrategy:
type: RollingUpdate
podManagementPolicy: Parallel
selector:
matchLabels: "{{ elasticsearch_selector }}"
template:
metadata:
labels: "{{ elasticsearch_labels }}"
annotations:
rollout.elasticsearch/token: "{{ elasticsearch_rollout_token | d(omit) }}"
spec:
topologySpreadConstraints:
- maxSkew: "{{ elasticsearch_max_skew }}"
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: "{{ elasticsearch_topology_constraint }}"
labelSelector:
matchLabels: "{{ elasticsearch_selector }}"
affinity:
podAntiAffinity:
"{{ elasticsearch_anti_affinity_type }}":
- labelSelector:
matchLabels: "{{ elasticsearch_selector }}"
topologyKey: kubernetes.io/hostname
securityContext: "{{ elasticsearch_pod_security_context | d(omit) }}"
initContainers:
- name: increase-fd-ulimits
image: busybox:1.36
command: ["sh","-c","ulimit -n 65536 || true"]
containers:
- name: elasticsearch
image: "{{ elasticsearch_image }}:{{ elasticsearch_image_tag }}"
securityContext: "{{ elasticsearch_container_security_context | d(omit) }}"
resources: "{{ elasticsearch_container_resources | d(omit) }}"
env:
- name: node.name
valueFrom:
fieldRef:
fieldPath: metadata.name
ports:
- name: transport
containerPort: "{{ elasticsearch_transport_port }}"
- name: http
containerPort: "{{ elasticsearch_http_port }}"
volumeMounts:
- name: data
mountPath: /usr/share/elasticsearch/data
- name: logs
mountPath: /usr/share/elasticsearch/logs
- name: certs
mountPath: /usr/share/elasticsearch/config/certs
readOnly: true
livenessProbe:
exec:
command: "{{ elasticsearch_health_check }}"
initialDelaySeconds: 60
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 6
readinessProbe:
exec:
command: "{{ elasticsearch_health_check }}"
initialDelaySeconds: 20
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 12
envFrom:
- configMapRef:
name: "{{ elasticsearch_deployment_name }}-configs"
- secretRef:
name: "{{ elasticsearch_deployment_name }}-secrets"
volumes:
- name: logs
emptyDir: {}
- name: certs
secret:
secretName: "{{ elasticsearch_certs_secret_name | d('elasticsearch-certs') }}"
volumeClaimTemplates:
- metadata:
name: data
labels: "{{ elasticsearch_labels }}"
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: "{{ elasticsearch_storage_class }}"
resources:
requests:
storage: "{{ elasticsearch_storage_size }}"

105
tasks/environment.yml Normal file
View File

@ -0,0 +1,105 @@
---
- name: ELASTICSEARCH ENVIRONMENT SETUP
tags:
- elasticsearch
- elasticsearch-env
block:
- name: ELASTICSEARCH ENVIRONMENT create configmaps
kubernetes.core.k8s:
state: "{{ elasticsearch_state | d('present') }}"
server_side_apply:
field_manager: ansible
force_conflicts: true
definition:
apiVersion: v1
kind: ConfigMap
metadata:
name: "{{ elasticsearch_deployment_name }}-configs"
namespace: "{{ elasticsearch_namespace }}"
labels: "{{ elasticsearch_labels }}"
data: "{{ elasticsearch_configs }}"
notify:
- elasticsearch-restart
when:
- elasticsearch_configs is defined
- name: ELASTICSEARCH ENVIRONMENT base64 encode secrets
no_log: true
ansible.builtin.set_fact:
elasticsearch_secrets_b64: "{{ elasticsearch_secrets_b64 | default({}) | combine({item.key: (item.value | b64encode)}) }}"
loop: "{{ elasticsearch_secrets | dict2items }}"
# - name: elasticsearch ENVIRONMENT verify secrets
# debug:
# msg:
# - "{{ elasticsearch_secrets }}"
# - "{{ elasticsearch_secrets_b64 }}"
- name: ELASTICSEARCH ENVIRONMENT create secrets
no_log: true
kubernetes.core.k8s:
state: "{{ elasticsearch_state | d('present') }}"
server_side_apply:
field_manager: ansible
force_conflicts: true
definition:
apiVersion: v1
kind: Secret
metadata:
name: "{{ elasticsearch_deployment_name }}-secrets"
namespace: "{{ elasticsearch_namespace }}"
labels: "{{ elasticsearch_labels }}"
type: Opaque
data: "{{ elasticsearch_secrets_b64 }}"
notify:
- elasticsearch-restart
when:
- elasticsearch_secrets is defined
- name: ELASTICSEARCH CERTGEN (OpenSSL local)
when: elasticsearch_cert_secret.resources | length == 0
tags:
- elasticsearch
- elasticsearch-certs
block:
- name: ELASTICSEARCH CERTGEN create temporary directory for cert generation
ansible.builtin.tempfile:
state: directory
register: temp_dir
- name: ELASTICSEARCH CERTGEN create CA and certificates via local podman
ansible.builtin.shell: >
podman run --rm --user $(id -u) --userns keep-id -v {{ temp_dir.path }}:/certs "{{ elasticsearch_image }}:{{ elasticsearch_image_tag }}" /bin/sh -c '
elasticsearch-certutil ca --silent --out /certs/elastic-stack-ca.p12 --pass "{{ elastic_cert_password | d(omit) }}" &&
elasticsearch-certutil cert --silent --ca /certs/elastic-stack-ca.p12 --ca-pass "{{ elastic_cert_password | d(omit) }}" --out /certs/elastic-certificates.p12 --pass "{{ elastic_cert_password | d(omit) }}" {% for dns_name in elasticsearch_cert_names %}--dns "{{ dns_name }}" {% endfor %}&&
echo "Cert generation complete"'
- name: ELASTICSEARCH CERTGEN read generated cert files
ansible.builtin.slurp:
src: "{{ item }}"
register: cert_files
loop:
- "{{ temp_dir.path }}/elastic-stack-ca.p12"
- "{{ temp_dir.path }}/elastic-certificates.p12"
- name: ELASTICSEARCH CERTGEN create cert secret
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: Secret
metadata:
name: "{{ elasticsearch_certs_secret_name | d('elasticsearch-certs') }}"
namespace: "{{ elasticsearch_namespace }}"
labels: "{{ elasticsearch_labels }}"
type: Opaque
data:
elastic-stack-ca.p12: "{{ cert_files.results[0].content }}"
elastic-certificates.p12: "{{ cert_files.results[1].content }}"
- name: ELASTICSEARCH CERTGEN clean up temporary directory
ansible.builtin.file:
path: "{{ temp_dir.path }}"
state: absent

43
tasks/main.yml Normal file
View File

@ -0,0 +1,43 @@
---
- name: ELASTICSEARCH KUBERNETES DEPLOYMENT
delegate_to: localhost
connection: local
become: false
tags: never
block:
- name: ELASTICSEARCH create namespace {{ elasticsearch_namespace }}
tags: [elasticsearch, elasticsearch-env, elasticsearch-certs, elasticsearch-services, elasticsearch-deploy]
kubernetes.core.k8s:
state: present
definition:
apiVersion: v1
kind: Namespace
metadata:
name: "{{ elasticsearch_namespace }}"
- name: ELASTICSEARCH check if certs exist
tags: [elasticsearch, elasticsearch-env, elasticsearch-certs, elasticsearch-services, elasticsearch-deploy]
kubernetes.core.k8s_info:
api_version: v1
kind: Secret
name: "{{ elasticsearch_certs_secret_name | d('elasticsearch-certs') }}"
namespace: "{{ elasticsearch_namespace }}"
register: elasticsearch_cert_secret
- name: ELASTICSEARCH setup environment
ansible.builtin.include_tasks: environment.yml
tags:
- elasticsearch
- elasticsearch-env
- name: ELASTICSEARCH create services
ansible.builtin.include_tasks: services.yml
tags:
- elasticsearch
- elasticsearch-services
- name: ELASTICSEARCH create statefulset
ansible.builtin.include_tasks: deploy.yml
tags:
- elasticsearch
- elasticsearch-deploy

43
tasks/services.yml Normal file
View File

@ -0,0 +1,43 @@
---
- name: ELASTICSEARCH SERVICES
tags:
- elasticsearch
- elasticsearch-services
block:
- name: ELASTICSEARCH SERVICES create headless transport service
kubernetes.core.k8s:
state: "{{ elasticsearch_state | d('present') }}"
definition:
apiVersion: v1
kind: Service
metadata:
name: elasticsearch-transport
namespace: "{{ elasticsearch_namespace }}"
labels: "{{ elasticsearch_labels }}"
spec:
clusterIP: None
publishNotReadyAddresses: true
selector: "{{ elasticsearch_selector }}"
ports:
- name: transport
port: "{{ elasticsearch_transport_port }}"
targetPort: "{{ elasticsearch_transport_port }}"
- name: ELASTICSEARCH SERVICES create http service
kubernetes.core.k8s:
state: "{{ elasticsearch_state | d('present') }}"
definition:
apiVersion: v1
kind: Service
metadata:
name: elasticsearch-http
namespace: "{{ elasticsearch_namespace }}"
labels: "{{ elasticsearch_labels }}"
spec:
type: ClusterIP
selector: "{{ elasticsearch_selector }}"
ports:
- name: http
port: "{{ elasticsearch_http_port }}"
targetPort: "{{ elasticsearch_http_port }}"