From 3e748f93038bde2f66e2071505cb7d7958096ab1 Mon Sep 17 00:00:00 2001 From: srw Date: Wed, 1 Oct 2025 03:47:56 +0000 Subject: [PATCH] initial commit --- LICENSE | 20 +++++ README.md | 6 ++ defaults/main.yml | 98 +++++++++++++++++++++++ handlers/main.yml | 28 +++++++ meta/main.yml | 40 ++++++++++ tasks/deploy.yml | 178 ++++++++++++++++++++++++++++++++++++++++++ tasks/environment.yml | 91 +++++++++++++++++++++ tasks/main.yml | 34 ++++++++ tasks/services.yml | 44 +++++++++++ 9 files changed, 539 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..68e3e33 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# ansible-role-redis-k8s + +Ansible role for deploying a statefulset for redis and sentinel + +- https://redis.io/ +- https://github.com/redis diff --git a/defaults/main.yml b/defaults/main.yml new file mode 100644 index 0000000..69cf0f5 --- /dev/null +++ b/defaults/main.yml @@ -0,0 +1,98 @@ +--- +redis_deployment_name: redis +redis_namespace: "{{ redis_deployment_name }}" + +redis_image: docker.io/redis +redis_image_tag: latest + +# KUBERNETES SETTINGS +redis_labels: + app.kubernetes.io/name: "{{ redis_deployment_name }}" + app.kubernetes.io/managed-by: ansible + +redis_selector: + app.kubernetes.io/name: "{{ redis_deployment_name }}" + +# requiredDuringSchedulingIgnoredDuringExecution,preferredDuringSchedulingIgnoredDuringExecution +redis_anti_affinity_type: requiredDuringSchedulingIgnoredDuringExecution + +# DoNotSchedule, ScheduleAnyway +redis_topology_constraint: DoNotSchedule + +redis_replicas: 3 +redis_min_available: 2 +redis_max_skew: 1 +redis_max_unavailable: 1 + +redis_strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: "{{ redis_max_unavailable }}" + +# security and resource limits +# redis_pod_security_context: +# runAsNonRoot: true +# fsGroup: 1000 +# seccompProfile: { type: RuntimeDefault } + +# redis_container_security_context: +# allowPrivilegeEscalation: false + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # seccompProfile: { type: RuntimeDefault } + +# REDIS SETTINGS +redis_port: 6379 +redis_parallel_syncs: 1 +redis_protected_mode: "no" +redis_append_only: "yes" +redis_max_memory: 256mb +redis_max_memory_policy: allkeys-lru +redis_storage_class: local-path +redis_storage_size: "1Gi" + +redis_container_resources: + requests: + cpu: "100m" + memory: "256Mi" + limits: + cpu: "1" + memory: "1Gi" + +redis_command_off: | + /bin/sh -lc ' + cp /config/redis.conf /conf/redis.conf + if hostname | grep -q -- "-0$"; then + sed -i "/^replicaof /d" /conf/redis.conf + fi + exec redis-server /conf/redis.conf + ' + +redis_command: > + /bin/sh -lc | + cp /config/redis.conf /conf/redis.conf + if hostname | grep -q -- '-0$'; then + sed -i '/^replicaof /d' /conf/redis.conf + fi + exec redis-server /conf/redis.conf + +# redis_server_env: +# OAUTH2_PROVIDER_APPLICATION_MODEL: "{{ redis_oauth_application }}" + +# SENTINEL SETTINGS +redis_sentinel_port: 26379 +redis_sentinel_quorum: 2 +redis_sentinel_down_after_ms: 10000 +redis_sentinel_failover_timeout: 10000 +redis_sentinel_master: "mymaster" + +redis_sentinel_container_resources: + requests: + cpu: "50m" + memory: "128Mi" + limits: + cpu: "500m" + memory: "512Mi" + +redis_sentinel_command: > + /bin/sh -lc exec redis-sentinel /config/sentinel.conf diff --git a/handlers/main.yml b/handlers/main.yml new file mode 100644 index 0000000..97e8550 --- /dev/null +++ b/handlers/main.yml @@ -0,0 +1,28 @@ +--- +- name: REDIS HANDLER check for existing deployment {{ redis_deployment_name }} + listen: redis-restart + kubernetes.core.k8s_info: + api_version: apps/v1 + kind: StatefulSet + name: "{{ redis_deployment_name }}" + namespace: "{{ redis_namespace }}" + register: redis_deploy_info + failed_when: false + changed_when: false + +- name: REDIS HANDLER restart deployment {{ redis_deployment_name }} + listen: redis-restart + kubernetes.core.k8s: + api_version: apps/v1 + kind: StatefulSet + name: "{{ redis_deployment_name }}" + namespace: "{{ redis_namespace }}" + state: patched + definition: + spec: + template: + metadata: + annotations: + kubectl.kubernetes.io/restartedAt: "{{ lookup('pipe', 'date -u +%Y-%m-%dT%H:%M:%SZ') }}" + when: + - redis_deploy_info.resources | length > 0 diff --git a/meta/main.yml b/meta/main.yml new file mode 100644 index 0000000..3ab0522 --- /dev/null +++ b/meta/main.yml @@ -0,0 +1,40 @@ +--- +dependencies: [] + +galaxy_info: + role_name: redis_k8s + author: srw + description: Ansible role for deploying redis and sentinel 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 + - redis diff --git a/tasks/deploy.yml b/tasks/deploy.yml new file mode 100644 index 0000000..e543e03 --- /dev/null +++ b/tasks/deploy.yml @@ -0,0 +1,178 @@ +--- +- name: REDIS DEPLOY + tags: + - redis + - redis-deploy + block: + + - name: REDIS DEPLOY PodDisruptionBudget + kubernetes.core.k8s: + state: "{{ redis_state | d('present') }}" + definition: + apiVersion: policy/v1 + kind: PodDisruptionBudget + metadata: + name: "{{ redis_deployment_name }}" + namespace: "{{ redis_namespace }}" + labels: "{{ redis_labels }}" + spec: + minAvailable: "{{ redis_min_available }}" + selector: + matchLabels: "{{ redis_selector }}" + + - name: REDIS DEPLOY Create Redis StatefulSet with Sentinel Sidecar + kubernetes.core.k8s: + state: "{{ redis_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: "{{ redis_deployment_name }}" + namespace: "{{ redis_namespace }}" + labels: "{{ redis_labels }}" + spec: + serviceName: redis + replicas: "{{ redis_replicas }}" + strategy: "{{ redis_strategy }}" + selector: + matchLabels: "{{ redis_selector }}" + template: + metadata: + labels: "{{ redis_labels }}" + annotations: + rollout.redis/token: "{{ redis_rollout_token | d(omit) }}" + spec: + topologySpreadConstraints: + - maxSkew: "{{ redis_max_skew }}" + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: "{{ redis_topology_constraint }}" + labelSelector: + matchLabels: "{{ redis_selector }}" + affinity: + podAntiAffinity: + "{{ redis_anti_affinity_type }}": + - labelSelector: + matchLabels: "{{ redis_selector }}" + topologyKey: kubernetes.io/hostname + securityContext: "{{ redis_pod_security_context | d(omit) }}" + initContainers: + - name: copy-sentinel-config + image: "{{ redis_image }}:{{ redis_image_tag }}" + command: + - /bin/sh + - -c + - | + cp /conf-ro/sentinel.conf /conf-rw/sentinel.conf + volumeMounts: + - name: conf-ro + mountPath: /conf-ro + readOnly: true + - name: conf-rw + mountPath: /conf-rw + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "100m" + containers: + # REDIS + - name: redis + image: "{{ redis_image }}:{{ redis_image_tag }}" + securityContext: "{{ redis_container_security_context | d(omit) }}" + resources: "{{ redis_container_resources | d(omit) }}" + args: + - redis-server + - /conf/redis.conf + ports: + - name: redis + containerPort: "{{ redis_port }}" + volumeMounts: + - name: conf-ro + mountPath: /conf/redis.conf + subPath: redis.conf + readOnly: true + - name: data + mountPath: /data + livenessProbe: + tcpSocket: + port: redis + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + exec: + command: + - redis-cli + - -p + - "{{ redis_port | string }}" + - ping + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + envFrom: + - secretRef: + name: "{{ redis_deployment_name }}-secrets" + + # SENTINEL + - name: redis-sentinel + image: "{{ redis_image }}:{{ redis_image_tag }}" + securityContext: "{{ redis_container_security_context | d(omit) }}" + resources: "{{ redis_sentinel_container_resources | d(omit) }}" + args: + - redis-sentinel + - /conf/sentinel.conf + ports: + - name: redis-sentinel + containerPort: "{{ redis_sentinel_port }}" + volumeMounts: + - name: conf-rw + mountPath: /conf + livenessProbe: + tcpSocket: + port: redis-sentinel + initialDelaySeconds: 30 + periodSeconds: 10 + readinessProbe: + exec: + command: + - redis-cli + - -p + - "{{ redis_sentinel_port | string }}" + - ping + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + envFrom: + - secretRef: + name: "{{ redis_deployment_name }}-secrets" + + volumes: + - name: conf-ro + configMap: + name: "{{ redis_deployment_name }}-configs" + items: + - key: redis.conf + path: redis.conf + - key: sentinel.conf + path: sentinel.conf + - name: conf-rw + emptyDir: {} + + volumeClaimTemplates: + - metadata: + name: data + labels: "{{ elasticsearch_labels }}" + spec: + accessModes: ["ReadWriteOnce"] + storageClassName: "{{ redis_storage_class }}" + resources: + requests: + storage: "{{ redis_storage_size }}" diff --git a/tasks/environment.yml b/tasks/environment.yml new file mode 100644 index 0000000..d42da91 --- /dev/null +++ b/tasks/environment.yml @@ -0,0 +1,91 @@ +--- +- name: REDIS ENVIRONMENT SETUP + tags: + - redis + - redis-env + block: + + - name: REDIS ENVIRONMENT create configmaps + kubernetes.core.k8s: + state: "{{ redis_state | d('present') }}" + server_side_apply: + field_manager: ansible + force_conflicts: true + definition: + apiVersion: v1 + kind: ConfigMap + metadata: + name: "{{ redis_deployment_name }}-configs" + namespace: "{{ redis_namespace }}" + labels: "{{ redis_labels }}" + data: + redis.conf: | + dir /data + bind 0.0.0.0 + port {{ redis_port }} + protected-mode {{ redis_protected_mode }} + appendonly {{ redis_append_only }} + maxmemory {{ redis_max_memory }} + maxmemory-policy {{ redis_max_memory_policy }} + + # --- Auth (server password) --- + requirepass ${REDIS_PASSWORD} + # Replicas authenticate to the master with the same password: + masterauth ${REDIS_PASSWORD} + + # Optional replication safety knobs (recommended if you want write safety): + # min-replicas-to-write 1 + # min-replicas-max-lag 5 + + # Optional announcements (usually not required inside K8s): + # replica-read-only yes + # replica-announce-ip ${POD_IP} + # replica-announce-port {{ redis_port }} + + sentinel.conf: | + port {{ redis_sentinel_port }} + protected-mode {{ redis_protected_mode }} + sentinel resolve-hostnames yes + sentinel monitor {{ redis_sentinel_master }} redis-0.redis.{{ redis_namespace }}.svc.cluster.local {{ redis_port }} 2 + sentinel down-after-milliseconds {{ redis_sentinel_master }} {{ redis_sentinel_down_after_ms }} + sentinel failover-timeout {{ redis_sentinel_master }} {{ redis_sentinel_failover_timeout }} + sentinel parallel-syncs {{ redis_sentinel_master }} {{ redis_parallel_syncs }} + + # Auth for Sentinel -> Redis (master/replicas) + sentinel auth-pass {{ redis_sentinel_master }} ${REDIS_PASSWORD} + + # If you want to protect the sentinel port itself, uncomment and make clients AUTH: + # requirepass ${REDIS_PASSWORD} + + - name: REDIS ENVIRONMENT base64 encode secrets + no_log: true + ansible.builtin.set_fact: + redis_secrets_b64: "{{ redis_secrets_b64 | default({}) | combine({item.key: (item.value | b64encode)}) }}" + loop: "{{ redis_secrets | dict2items }}" + + # - name: REDIS ENVIRONMENT verify secrets + # debug: + # msg: + # - "{{ redis_secrets }}" + # - "{{ redis_secrets_b64 }}" + + - name: REDIS ENVIRONMENT create secrets + no_log: true + kubernetes.core.k8s: + state: "{{ redis_state | d('present') }}" + server_side_apply: + field_manager: ansible + force_conflicts: true + definition: + apiVersion: v1 + kind: Secret + metadata: + name: "{{ redis_deployment_name }}-secrets" + namespace: "{{ redis_namespace }}" + labels: "{{ redis_labels }}" + type: Opaque + data: "{{ redis_secrets_b64 }}" + notify: + - redis-restart + when: + - redis_secrets is defined diff --git a/tasks/main.yml b/tasks/main.yml new file mode 100644 index 0000000..b836db5 --- /dev/null +++ b/tasks/main.yml @@ -0,0 +1,34 @@ +--- +- name: REDIS KUBERNETES DEPLOYMENT + delegate_to: localhost + connection: local + become: false + tags: never + block: + - name: REDIS create namespace {{ redis_namespace }} + tags: [redis, redis-env, redis-services, redis-deploy] + kubernetes.core.k8s: + state: present + definition: + apiVersion: v1 + kind: Namespace + metadata: + name: "{{ redis_namespace }}" + + - name: REDIS setup environment + ansible.builtin.include_tasks: environment.yml + tags: + - redis + - redis-env + + - name: REDIS create services + ansible.builtin.include_tasks: services.yml + tags: + - redis + - redis-services + + - name: REDIS create statefulset + ansible.builtin.include_tasks: deploy.yml + tags: + - redis + - redis-deploy diff --git a/tasks/services.yml b/tasks/services.yml new file mode 100644 index 0000000..1e73d91 --- /dev/null +++ b/tasks/services.yml @@ -0,0 +1,44 @@ +--- +- name: REDIS SERVICES + tags: + - redis + - redis-services + block: + + - name: REDIS SERVICES create headless service + kubernetes.core.k8s: + state: "{{ redis_state | d('present') }}" + definition: + apiVersion: v1 + kind: Service + metadata: + name: redis + namespace: "{{ redis_namespace }}" + labels: "{{ redis_labels }}" + spec: + clusterIP: None + publishNotReadyAddresses: true + selector: "{{ redis_selector }}" + ports: + - name: redis + port: "{{ redis_port }}" + targetPort: "{{ redis_port }}" + + - name: REDIS SERVICES create sentinel headless service + kubernetes.core.k8s: + state: "{{ redis_state | d('present') }}" + definition: + apiVersion: v1 + kind: Service + metadata: + name: redis-sentinel + namespace: "{{ redis_namespace }}" + labels: "{{ redis_labels }}" + spec: + clusterIP: None + publishNotReadyAddresses: true + selector: "{{ redis_selector }}" + ports: + - name: redis-sentinel + port: "{{ redis_sentinel_port }}" + targetPort: "{{ redis_sentinel_port }}"