diff --git a/plays/redis.yml b/plays/redis.yml new file mode 100644 index 0000000..a04f192 --- /dev/null +++ b/plays/redis.yml @@ -0,0 +1,12 @@ +--- +- name: Install & configure redis + hosts: all + vars: + custom_firewall_ports: + - 6379 + install_redis_exporter: true + description: 'cache node for retailor redis cache' + roles: + - role: roles/firewall + - role: roles/redis + diff --git a/roles/redis/defaults/main.yml b/roles/redis/defaults/main.yml new file mode 100644 index 0000000..42ab044 --- /dev/null +++ b/roles/redis/defaults/main.yml @@ -0,0 +1,41 @@ +--- +# --- Role / topology --- +redis_role: "{{ lookup('env', 'REDIS_ROLE') | default('primary', true) }}" # primary | replica +redis_primary_host: "{{ lookup('env', 'REDIS_PRIMARY_HOST') | default('', true) }}" + +# --- Network --- +redis_bind_addresses: "127.0.0.1 10.0.0.0/8" +redis_bind_addresses: "0.0.0.0" +redis_port: 6379 +redis_trusted_cidr: "{{ lookup('env', 'REDIS_ALLOWED_CIDR') | default('10.0.1.0/24', true) }}" + +# --- Auth (override via vault; do NOT commit plaintext) --- +redis_password: "{{ lookup('env', 'REDIS_PASSWORD') | default('', false) }}" + +# --- Memory --- +redis_maxmemory: "{{ lookup('env', 'REDIS_TOTAL_RAM_GB') | default('2', true) | int }}gb" +redis_maxmemory_policy: "allkeys-lru" # use "noeviction" if Redis is a source of truth + +# --- Persistence --- +redis_appendonly: "yes" +redis_appendfsync: "everysec" # everysec | always | no + +# --- Dangerous command handling --- +redis_disable_flushall: true +redis_disable_flushdb: true +redis_disable_debug: true +redis_disable_keys: true +redis_rename_config: true +redis_rename_shutdown: true + +# --- Package --- +redis_package_state: present + +# --- Limits --- +redis_nofile_limit: 65536 + +# TODO(kevinmidboe): just reference the certs acme.sh generates +redis_tls_enabled: "{{ lookup('env', 'REDIS_TLS_ENABLED') | default('', false) }}" +redis_tls_cert_path: "" +redis_tls_key_path: "" +redis_tls_ca_path: "" diff --git a/roles/redis/tasks/main.yml b/roles/redis/tasks/main.yml new file mode 100644 index 0000000..a56aa60 --- /dev/null +++ b/roles/redis/tasks/main.yml @@ -0,0 +1,155 @@ +--- +- name: Update apt cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 + when: ansible_os_family == "Debian" + +- name: Install redis-server and firewall tooling + ansible.builtin.apt: + name: + - redis-server + - ufw + state: "{{ redis_package_state }}" + +- name: Ensure redis data/log/run directories exist with correct ownership + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: redis + group: redis + mode: "0750" + loop: + - /var/lib/redis + - /var/log/redis + - /run/redis + +# --- Kernel tuning --- +- name: Apply sysctl tuning for Redis + ansible.builtin.copy: + dest: /etc/sysctl.d/99-redis.conf + content: | + vm.overcommit_memory = 1 + vm.swappiness = 1 + net.core.somaxconn = 511 + mode: "0644" + notify: Reload sysctl + +- name: Disable Transparent Huge Pages via systemd unit + ansible.builtin.copy: + dest: /etc/systemd/system/disable-thp.service + content: | + [Unit] + Description=Disable Transparent Huge Pages (THP) + Before=redis-server.service + + [Service] + Type=oneshot + ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' + ExecStart=/bin/sh -c 'echo never > /sys/kernel/mm/transparent_hugepage/defrag' + RemainAfterExit=yes + + [Install] + WantedBy=multi-user.target + mode: "0644" + notify: + - Reload systemd + - Enable and start disable-thp + +- name: Raise nofile limits for redis user + ansible.builtin.copy: + dest: /etc/security/limits.d/redis.conf + content: | + redis soft nofile {{ redis_nofile_limit }} + redis hard nofile {{ redis_nofile_limit }} + mode: "0644" + +- name: Add systemd override for redis-server (limits + restart policy) + ansible.builtin.file: + path: /etc/systemd/system/redis-server.service.d + state: directory + mode: "0755" + +- name: Deploy systemd override.conf + ansible.builtin.copy: + dest: /etc/systemd/system/redis-server.service.d/override.conf + content: | + [Service] + LimitNOFILE={{ redis_nofile_limit }} + Restart=always + RestartSec=5 + mode: "0644" + notify: + - Reload systemd + - Restart redis + +# --- Main config --- +- name: Deploy redis.conf from template + ansible.builtin.template: + src: redis.conf.j2 + dest: /etc/redis/redis.conf + owner: redis + group: redis + mode: "0640" + notify: Restart redis + +- name: Deploy logrotate config + ansible.builtin.copy: + dest: /etc/logrotate.d/redis-server + content: | + /var/log/redis/redis-server.log { + weekly + rotate 8 + compress + delaycompress + missingok + notifempty + create 640 redis redis + } + mode: "0644" + +# --- Firewall --- +- name: Allow SSH through ufw + community.general.ufw: + rule: allow + name: OpenSSH + +- name: Allow Redis port from trusted CIDR only + community.general.ufw: + rule: allow + port: "{{ redis_port | string }}" + proto: tcp + src: "{{ redis_trusted_cidr }}" + +- name: Set default ufw policies + community.general.ufw: + state: enabled + policy: deny + direction: incoming + +- name: Allow outgoing traffic + community.general.ufw: + policy: allow + direction: outgoing + +# --- Service --- +- name: Ensure redis-server is enabled and started + ansible.builtin.systemd: + name: redis-server + enabled: true + state: started + daemon_reload: true + +- name: Verify Redis responds to PING + ansible.builtin.command: > + redis-cli -a {{ redis_password }} --no-auth-warning + -h 127.0.0.1 -p {{ redis_port }} ping + register: redis_ping_result + changed_when: false + no_log: true + +- name: Assert Redis is healthy + ansible.builtin.assert: + that: + - "'PONG' in redis_ping_result.stdout" + fail_msg: "Redis did not respond with PONG after configuration." diff --git a/roles/redis/templates/redis.conf.j2 b/roles/redis/templates/redis.conf.j2 new file mode 100644 index 0000000..e69de29