diff --git a/ansible/prod/ansible.cfg b/ansible/prod/ansible.cfg new file mode 100644 index 0000000..f1329c6 --- /dev/null +++ b/ansible/prod/ansible.cfg @@ -0,0 +1,13 @@ +[defaults] +inventory = inventory/generated/prod.yml +remote_user = root +host_key_checking = False +retry_files_enabled = False +interpreter_python = auto_silent +roles_path = ../roles + +[privilege_escalation] +become = True +become_method = sudo +become_user = root +become_ask_pass = False diff --git a/ansible/prod/group_vars/all.yml b/ansible/prod/group_vars/all.yml new file mode 100644 index 0000000..0dc7fa1 --- /dev/null +++ b/ansible/prod/group_vars/all.yml @@ -0,0 +1,4 @@ +# Global variables for prod +storagebox_account: "u469968" +admin_allowed_cidrs: "127.0.0.1/8" +timezone: "Europe/Istanbul" diff --git a/ansible/prod/group_vars/prod.yml b/ansible/prod/group_vars/prod.yml new file mode 100644 index 0000000..394988f --- /dev/null +++ b/ansible/prod/group_vars/prod.yml @@ -0,0 +1,6 @@ +# Prod environment specific variables +storagebox_user: "{{ storagebox_account }}-sub2" # Prod sub-account suffix +storagebox_url: "https://{{ storagebox_user }}.your-storagebox.de/" +storagebox_mount_point: "/mnt/storagebox" +swarm_manager_ip: "10.20.10.11" +# storagebox_password: "{{ vault_storagebox_password }}" diff --git a/ansible/prod/prod-bootstrap.yml b/ansible/prod/prod-bootstrap.yml new file mode 100644 index 0000000..86b8e3d --- /dev/null +++ b/ansible/prod/prod-bootstrap.yml @@ -0,0 +1,31 @@ +--- +- name: Prod Environment Bootstrap (Common Roles) + hosts: all + become: yes + roles: + - role: base + tags: [base] + - role: hardening + tags: [hardening] + - role: docker + tags: [docker] + - role: node_dirs + tags: [node_dirs] + - role: storagebox + tags: [storagebox] + +- name: Swarm Infrastructure Setup (Prod HA) + hosts: iklim-app-* + become: yes + serial: 1 + roles: + - role: swarm + tags: [swarm] + +# Prod'da DB node'ları da worker olarak swarm'a katılır +- name: DB Nodes Swarm Join + hosts: iklim-db-* + become: yes + roles: + - role: swarm + tags: [swarm] diff --git a/ansible/requirements.yml b/ansible/requirements.yml new file mode 100644 index 0000000..f037361 --- /dev/null +++ b/ansible/requirements.yml @@ -0,0 +1,5 @@ +--- +collections: + - name: ansible.posix + - name: community.general + - name: community.docker diff --git a/ansible/roles/base/tasks/main.yml b/ansible/roles/base/tasks/main.yml new file mode 100644 index 0000000..16b7048 --- /dev/null +++ b/ansible/roles/base/tasks/main.yml @@ -0,0 +1,46 @@ +--- +- name: Update all packages + ansible.builtin.dnf: + name: "*" + state: latest + update_cache: yes + +- name: Install EPEL release + ansible.builtin.dnf: + name: epel-release + state: present + +- name: Install base packages + ansible.builtin.dnf: + name: + - curl + - wget + - git + - jq + - tar + - unzip + - bash-completion + - gettext + - tree + - ca-certificates + - fail2ban + - chrony + - python3 + - python3-pip + - htop + - btop + state: present + +- name: Set timezone + community.general.timezone: + name: "{{ timezone }}" + +- name: Ensure chrony is running + ansible.builtin.service: + name: chronyd + state: started + enabled: yes + +- name: Set hostname + ansible.builtin.hostname: + name: "{{ inventory_hostname }}" diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml new file mode 100644 index 0000000..0162ba5 --- /dev/null +++ b/ansible/roles/docker/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Restart Docker + ansible.builtin.service: + name: docker + state: restarted diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000..60b01f2 --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,47 @@ +--- +- name: Add Docker repository + ansible.builtin.get_url: + url: https://download.docker.com/linux/rhel/docker-ce.repo + dest: /etc/yum.repos.d/docker-ce.repo + mode: '0644' + +- name: Install Docker packages + ansible.builtin.dnf: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin + state: present + +- name: Ensure /etc/docker directory exists + ansible.builtin.file: + path: /etc/docker + state: directory + mode: '0755' + +- name: Configure Docker daemon (Log Rotation) + ansible.builtin.template: + src: daemon.json.j2 + dest: /etc/docker/daemon.json + mode: '0644' + notify: Restart Docker + +- name: Ensure Docker is started and enabled + ansible.builtin.service: + name: docker + state: started + enabled: yes + +- name: Allow Docker traffic in firewalld + ansible.posix.firewalld: + port: "{{ item }}" + permanent: yes + immediate: yes + state: enabled + loop: + - 2377/tcp + - 7946/tcp + - 7946/udp + - 4789/udp diff --git a/ansible/roles/docker/templates/daemon.json.j2 b/ansible/roles/docker/templates/daemon.json.j2 new file mode 100644 index 0000000..6e49d82 --- /dev/null +++ b/ansible/roles/docker/templates/daemon.json.j2 @@ -0,0 +1,7 @@ +{ + "log-driver": "json-file", + "log-opts": { + "max-size": "50m", + "max-file": "5" + } +} diff --git a/ansible/roles/hardening/handlers/main.yml b/ansible/roles/hardening/handlers/main.yml new file mode 100644 index 0000000..1a56055 --- /dev/null +++ b/ansible/roles/hardening/handlers/main.yml @@ -0,0 +1,15 @@ +--- +- name: Restart sshd + ansible.builtin.service: + name: sshd + state: restarted + +- name: Restart fail2ban + ansible.builtin.service: + name: fail2ban + state: restarted + +- name: Restart journald + ansible.builtin.service: + name: systemd-journald + state: restarted diff --git a/ansible/roles/hardening/tasks/main.yml b/ansible/roles/hardening/tasks/main.yml new file mode 100644 index 0000000..8db3fcf --- /dev/null +++ b/ansible/roles/hardening/tasks/main.yml @@ -0,0 +1,70 @@ +--- +- name: Disable SELinux + ansible.posix.selinux: + state: disabled + register: selinux_status + +- name: Reboot if SELinux changed + ansible.builtin.reboot: + when: selinux_status.changed + +- name: Configure SSH Hardening + ansible.builtin.lineinfile: + path: /etc/ssh/sshd_config + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + state: present + loop: + - { regexp: "^PasswordAuthentication", line: "PasswordAuthentication no" } + - { regexp: "^PermitRootLogin", line: "PermitRootLogin prohibit-password" } + - { regexp: "^PermitEmptyPasswords", line: "PermitEmptyPasswords no" } + - { regexp: "^MaxAuthTries", line: "MaxAuthTries 3" } + notify: Restart sshd + +- name: Install dnf-automatic + ansible.builtin.dnf: + name: dnf-automatic + state: present + +- name: Enable dnf-automatic timer + ansible.builtin.systemd: + name: dnf-automatic.timer + state: started + enabled: yes + +- name: Configure fail2ban jail + ansible.builtin.template: + src: jail.local.j2 + dest: /etc/fail2ban/jail.local + notify: Restart fail2ban + +- name: Ensure firewalld is running + ansible.builtin.service: + name: firewalld + state: started + enabled: yes + +- name: Configure firewalld default zone + ansible.builtin.shell: firewall-cmd --set-default-zone=drop + when: ansible_facts.services['firewalld.service'].state == 'running' + changed_when: false + +- name: Configure journald log limits + ansible.builtin.lineinfile: + path: /etc/systemd/journald.conf + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + state: present + loop: + - { regexp: "^#?MaxRetentionSec=", line: "MaxRetentionSec=7day" } + - { regexp: "^#?SystemMaxUse=", line: "SystemMaxUse=500M" } + notify: Restart journald + +- name: Allow SSH in firewalld from admin CIDRs + ansible.posix.firewalld: + service: ssh + source: "{{ item }}" + state: enabled + permanent: yes + immediate: yes + loop: "{{ admin_allowed_cidrs.split(' ') }}" diff --git a/ansible/roles/hardening/templates/jail.local.j2 b/ansible/roles/hardening/templates/jail.local.j2 new file mode 100644 index 0000000..c0c1853 --- /dev/null +++ b/ansible/roles/hardening/templates/jail.local.j2 @@ -0,0 +1,10 @@ +[DEFAULT] +ignoreip = 127.0.0.1/8 {{ admin_allowed_cidrs }} +bantime = 21600 +findtime = 300 +maxretry = 5 +banaction = iptables-multiport +backend = systemd + +[sshd] +enabled = true diff --git a/ansible/roles/node_dirs/tasks/main.yml b/ansible/roles/node_dirs/tasks/main.yml new file mode 100644 index 0000000..0571af5 --- /dev/null +++ b/ansible/roles/node_dirs/tasks/main.yml @@ -0,0 +1,29 @@ +--- +- name: Create base directory + ansible.builtin.file: + path: /opt/iklimco + state: directory + mode: '0755' + +- name: Create app specific directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - /opt/iklimco/ssl + - /opt/iklimco/init + - /opt/iklimco/init/postgresql + - /opt/iklimco/init/mongodb + - /opt/iklimco/stacks + when: inventory_hostname in groups['app'] + +- name: Create db specific directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - /opt/iklimco/db + - /opt/iklimco/backup + when: inventory_hostname in groups['db'] diff --git a/ansible/roles/storagebox/tasks/main.yml b/ansible/roles/storagebox/tasks/main.yml new file mode 100644 index 0000000..3e98099 --- /dev/null +++ b/ansible/roles/storagebox/tasks/main.yml @@ -0,0 +1,40 @@ +--- +- name: Install davfs2 + ansible.builtin.dnf: + name: davfs2 + state: present + +- name: Configure davfs2 secrets + ansible.builtin.lineinfile: + path: /etc/davfs2/secrets + line: "{{ storagebox_url }} {{ storagebox_user }} {{ storagebox_password }}" + create: yes + mode: "0600" + owner: root + group: root + +- name: Create mount point + ansible.builtin.file: + path: "{{ storagebox_mount_point }}" + state: directory + mode: "0755" + +- name: Add fstab entry for StorageBox + ansible.builtin.lineinfile: + path: /etc/fstab + line: "{{ storagebox_url }} {{ storagebox_mount_point }} davfs _netdev,auto,user,rw,uid=root,gid=root 0 0" + state: present + +- name: Mount StorageBox + ansible.builtin.mount: + path: "{{ storagebox_mount_point }}" + src: "{{ storagebox_url }}" + fstype: davfs + opts: "_netdev,auto,user,rw,uid=root,gid=root" + state: mounted + +- name: Write mount marker + ansible.builtin.copy: + content: "mounted by ansible" + dest: "{{ storagebox_mount_point }}/.mounted_marker" + mode: '0644' diff --git a/ansible/roles/storagebox_ssh_key/tasks/main.yml b/ansible/roles/storagebox_ssh_key/tasks/main.yml new file mode 100644 index 0000000..95e8cfa --- /dev/null +++ b/ansible/roles/storagebox_ssh_key/tasks/main.yml @@ -0,0 +1,8 @@ +--- +- name: Generate SSH key for StorageBox + ansible.builtin.user: + name: root + generate_ssh_key: yes + ssh_key_type: ed25519 + ssh_key_file: .ssh/id_ed25519_storagebox + ssh_key_comment: "{{ inventory_hostname }}-storagebox" diff --git a/ansible/roles/swarm/tasks/main.yml b/ansible/roles/swarm/tasks/main.yml new file mode 100644 index 0000000..3698f0c --- /dev/null +++ b/ansible/roles/swarm/tasks/main.yml @@ -0,0 +1,74 @@ +--- +- name: Check if Swarm is initialized + ansible.builtin.shell: docker info --format '{{.Swarm.LocalNodeState}}' + register: swarm_status + changed_when: false + +# 1. İlk Manager'ın (Leader) başlatılması +- name: Initialize Docker Swarm (Leader) + ansible.builtin.shell: > + docker swarm init + --advertise-addr {{ private_ip }} + when: + - inventory_hostname == groups['app'][0] + - swarm_status.stdout != 'active' + register: swarm_init_result + +# 2. Join Token'ların alınması (Sadece Leader üzerinden) +- name: Get Swarm Manager Join Token + ansible.builtin.shell: docker swarm join-token manager -q + register: manager_token + delegate_to: "{{ groups['app'][0] }}" + when: inventory_hostname == groups['app'][0] + changed_when: false + +- name: Get Swarm Worker Join Token + ansible.builtin.shell: docker swarm join-token worker -q + register: worker_token + delegate_to: "{{ groups['app'][0] }}" + when: inventory_hostname == groups['app'][0] + changed_when: false + +# 3. Diğer App sunucularının Manager olarak katılması (Prod HA için) +- name: Join Swarm as Manager + ansible.builtin.shell: > + docker swarm join + --token {{ hostvars[groups['app'][0]]['manager_token']['stdout'] }} + {{ swarm_manager_ip }}:2377 + when: + - inventory_hostname in groups['app'] + - inventory_hostname != groups['app'][0] + - swarm_status.stdout != 'active' + +# 4. DB sunucularının Worker olarak katılması +- name: Join Swarm as Worker + ansible.builtin.shell: > + docker swarm join + --token {{ hostvars[groups['app'][0]]['worker_token']['stdout'] }} + {{ swarm_manager_ip }}:2377 + when: + - inventory_hostname in groups['db'] + - swarm_status.stdout != 'active' + +# 5. Overlay Network oluşturulması (Sadece bir kez Leader üzerinden) +- name: Create iklimco-net overlay network + community.docker.docker_network: + name: iklimco-net + driver: overlay + attachable: yes + state: present + delegate_to: "{{ groups['app'][0] }}" + run_once: true + +# 6. Node Etiketleri (Labels) +- name: Label App nodes (service) + ansible.builtin.shell: docker node update --label-add type=service {{ inventory_hostname }} + delegate_to: "{{ groups['app'][0] }}" + when: inventory_hostname in groups['app'] + changed_when: false + +- name: Label DB nodes (db) + ansible.builtin.shell: docker node update --label-add role=db {{ inventory_hostname }} + delegate_to: "{{ groups['app'][0] }}" + when: inventory_hostname in groups['db'] + changed_when: false diff --git a/ansible/test/ansible.cfg b/ansible/test/ansible.cfg new file mode 100644 index 0000000..b25172b --- /dev/null +++ b/ansible/test/ansible.cfg @@ -0,0 +1,14 @@ +[defaults] +inventory = inventory/generated/test.yml +remote_user = root +host_key_checking = False +retry_files_enabled = False +interpreter_python = auto_silent +# Üst dizindeki ortak rolleri kullan +roles_path = ../roles + +[privilege_escalation] +become = True +become_method = sudo +become_user = root +become_ask_pass = False diff --git a/ansible/test/group_vars/all.yml b/ansible/test/group_vars/all.yml new file mode 100644 index 0000000..6c230bd --- /dev/null +++ b/ansible/test/group_vars/all.yml @@ -0,0 +1,4 @@ +# Global variables for all environments +storagebox_account: "u469968" +admin_allowed_cidrs: "127.0.0.1/8" # Overridden in inventory or vault +timezone: "Europe/Istanbul" diff --git a/ansible/test/group_vars/test-vault.yml b/ansible/test/group_vars/test-vault.yml new file mode 100644 index 0000000..2427aa5 --- /dev/null +++ b/ansible/test/group_vars/test-vault.yml @@ -0,0 +1,22 @@ +$ANSIBLE_VAULT;1.1;AES256 +39313733646339343230326361633435636632393938663537396530393131363335326664346334 +3533366238616262366665373638373030393536383962390a626532373431336632366264356261 +31336533663537303964613862336530363335616334313839363333383863323462376135636134 +3963356335393733650a393439336365343132373038393362653136353462646636396430376561 +66633161633434326265376631353734323661643830386437303631386438336536646538326465 +35653864633631656461313235316637383063656164353536336634373663353466346161623731 +38393365313439623261363732393333376266336663303565373866643135396437356339643136 +34303735336365353930353065343234373032363063356133393436383636313038643934663435 +61366635396363613537396563613235303665363230656366353739656364376636356433333766 +34323464343438356262363337303937646561366366386233353338333434633333373464373234 +31323763303366363239353537343966316439656134663033653965613635393562363663323962 +34646238386232616464343162386164626638306439346138336263386537653536336130616638 +39306366663164373235373863366237313933373633613464353364643630386666336134616364 +64363633306465323831623831323139373931393938623233636536636664353839643866393138 +37313261623737346433653535393835356635353662386632373964613832333434303739396164 +61653438326261346464316230656262393466643939636335363662383466616363333265303536 +61663337393038356165316261323035383361666266333665346363623166333434383166653936 +34396636373638656633643135316566663736363931393633393365343161636239306535623935 +38623165393963383131616261383539643234343064306366663434333166353131333431343532 +36363362303131373165646666343938663964323063643363303131336462386431396431323162 +34643539326266333236656130616134616663373966613464663136386239303861 diff --git a/ansible/test/group_vars/test.yml b/ansible/test/group_vars/test.yml new file mode 100644 index 0000000..02d5689 --- /dev/null +++ b/ansible/test/group_vars/test.yml @@ -0,0 +1,7 @@ +# Test environment specific variables +storagebox_user: "{{ storagebox_account }}-sub4" +storagebox_url: "https://{{ storagebox_user }}.your-storagebox.de/" +storagebox_mount_point: "/mnt/storagebox" +swarm_manager_ip: "10.10.10.11" +admin_allowed_cidrs: "78.187.87.109/32 95.70.151.248/32" +# storagebox_password: "{{ vault_storagebox_password }}" # In test-vault.yml diff --git a/ansible/inventory/generated/test.yml b/ansible/test/inventory/generated/test.yml similarity index 96% rename from ansible/inventory/generated/test.yml rename to ansible/test/inventory/generated/test.yml index 624ae3c..94d1897 100644 --- a/ansible/inventory/generated/test.yml +++ b/ansible/test/inventory/generated/test.yml @@ -1,14 +1,14 @@ "all": "children": + "app": + "hosts": + "iklim-app-01": + "ansible_host": "167.235.194.61" + "ansible_user": "root" + "private_ip": "10.10.10.11" "db": "hosts": "iklim-db-01": - "ansible_host": "167.235.194.61" - "ansible_user": "root" - "private_ip": "10.10.20.11" - "swarm": - "hosts": - "iklim-app-01": "ansible_host": "167.235.205.93" "ansible_user": "root" - "private_ip": "10.10.10.11" + "private_ip": "10.10.20.11" diff --git a/ansible/test/test-bootstrap.yml b/ansible/test/test-bootstrap.yml new file mode 100644 index 0000000..131048e --- /dev/null +++ b/ansible/test/test-bootstrap.yml @@ -0,0 +1,25 @@ +--- +- name: Test Environment Bootstrap (Common Roles) + hosts: all + become: yes + roles: + - role: base + tags: [base] + - role: hardening + tags: [hardening] + - role: docker + tags: [docker] + - role: node_dirs + tags: [node_dirs] + - role: storagebox + tags: [storagebox] + - role: storagebox_ssh_key + tags: [storagebox_ssh_key] + +- name: Swarm Infrastructure Setup + hosts: all + become: yes + serial: 1 # Manager'in önce bitmesi ve token'i worker'a vermesi için + roles: + - role: swarm + tags: [swarm]