From 6ddd5c4d32dba8dbd96b7b669718956afc4ac98b Mon Sep 17 00:00:00 2001 From: srw Date: Wed, 1 Oct 2025 03:48:28 +0000 Subject: [PATCH] initial commit --- LICENSE | 20 +++++++ README.md | 5 ++ defaults/main.yml | 116 +++++++++++++++++++++++++++++++++++++ handlers/main.yml | 28 +++++++++ meta/main.yml | 40 +++++++++++++ tasks/deploy.yml | 130 ++++++++++++++++++++++++++++++++++++++++++ tasks/environment.yml | 105 ++++++++++++++++++++++++++++++++++ tasks/main.yml | 43 ++++++++++++++ tasks/services.yml | 43 ++++++++++++++ 9 files changed, 530 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 defaults/main.yml create mode 100644 handlers/main.yml create mode 100644 meta/main.yml create mode 100644 tasks/deploy.yml create mode 100644 tasks/environment.yml create mode 100644 tasks/main.yml create mode 100644 tasks/services.yml diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5addd2c --- /dev/null +++ b/LICENSE @@ -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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..aa0138f --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# ansible-role-elasticsearch-k8s + +Ansible role for deploying a statefulset for elasticsearch cluster + +- https://www.elastic.co/ diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..20493bb --- /dev/null +++ b/defaults/main.yml @@ -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) }}" diff --git a/handlers/main.yml b/handlers/main.yml new file mode 100644 index 0000000..44dc74d --- /dev/null +++ b/handlers/main.yml @@ -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 diff --git a/meta/main.yml b/meta/main.yml new file mode 100644 index 0000000..d876451 --- /dev/null +++ b/meta/main.yml @@ -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 diff --git a/tasks/deploy.yml b/tasks/deploy.yml new file mode 100644 index 0000000..1c89803 --- /dev/null +++ b/tasks/deploy.yml @@ -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 }}" diff --git a/tasks/environment.yml b/tasks/environment.yml new file mode 100644 index 0000000..0e62e34 --- /dev/null +++ b/tasks/environment.yml @@ -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 diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..6c231a6 --- /dev/null +++ b/tasks/main.yml @@ -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 diff --git a/tasks/services.yml b/tasks/services.yml new file mode 100644 index 0000000..8ae50ec --- /dev/null +++ b/tasks/services.yml @@ -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 }}"