diff --git a/docs/mongodb_guide.md b/docs/mongodb_guide.md index a6a7d3cf..13f74f9e 100644 --- a/docs/mongodb_guide.md +++ b/docs/mongodb_guide.md @@ -95,6 +95,7 @@ The following table contains the most commonly overridden variables. | `mongodb_tls_enabled` | Boolean | Flag to enable MongoDB TLS. | `false` | | `mongodb_user_admin_password` | String | The MongoDB admin user password. | `admin` | | `mongodb_user_itential_password` | String | The MongoDB itential user password. | `itential` | +| `mongodb_preferred_primary` | String | Node hostname that will be considered primary, empty means the first hostname in the inventory | `` | > :warning: It is assumed that these default passwords will be changed to meet more rigorous security standards. These are intended to be defaults strictly used just for ease of the @@ -301,3 +302,9 @@ state that the installation tag produced you can run this command: ```bash ansible-playbook itential.deployer.mongodb -i --tags initialize_mongo_config ``` + +This tag is used to dynamically adjust the **MongoDB replica set member priorities** and influence which node becomes the **primary**: + +```bash +ansible-playbook itential.deployer.mongodb -i --tags reconfigure_priority +``` diff --git a/plugins/modules/mongodb_config_state.py b/plugins/modules/mongodb_config_state.py index b9de3636..9c2425e6 100644 --- a/plugins/modules/mongodb_config_state.py +++ b/plugins/modules/mongodb_config_state.py @@ -117,7 +117,7 @@ def run_module(): uri = build_connection_string(module.params) - client = MongoClient(uri) + client = MongoClient(uri, directConnection=True) database = client.get_database("admin") hello = database.command("hello") diff --git a/roles/mongodb/defaults/main/mongodb.yml b/roles/mongodb/defaults/main/mongodb.yml index 44785c8c..acfe0dbb 100644 --- a/roles/mongodb/defaults/main/mongodb.yml +++ b/roles/mongodb/defaults/main/mongodb.yml @@ -71,3 +71,11 @@ mongodb_user_itential_password: itential # The name of the mongo replica set mongodb_replset_name: "{{ mongodb_replset_name_default }}" + +# Preferred primary member for MongoDB replica sets. +# Leave empty to use the first host in the mongodb inventory group. +mongodb_preferred_primary: "" + +# Replica set member priorities. +mongodb_primary_priority: 10 +mongodb_secondary_priority: 5 diff --git a/roles/mongodb/tasks/configure-mongodb-replicaset.yml b/roles/mongodb/tasks/configure-mongodb-replicaset.yml index 14f7698f..3f1b9987 100644 --- a/roles/mongodb/tasks/configure-mongodb-replicaset.yml +++ b/roles/mongodb/tasks/configure-mongodb-replicaset.yml @@ -25,10 +25,12 @@ register: mongodb_state vars: ansible_python_interpreter: "{{ mongodb_python_venv }}/bin/python3" + tags: reconfigure_priority - name: Print MongoDB configuration state ansible.builtin.debug: msg: "{{ mongodb_state }}" + tags: reconfigure_priority # Execute the template to apply changes to the mongo.conf for replication - name: Create MongoDB config file (replicaset) @@ -56,28 +58,116 @@ - name: Set empty array of mongo servers ansible.builtin.set_fact: mongodb_servers: [] - when: not mongodb_state.replication_enabled + when: + - inventory_hostname in groups.mongodb + - groups.mongodb.index(inventory_hostname) == 0 + tags: reconfigure_priority + +- name: Set preferred primary host + ansible.builtin.set_fact: + mongodb_preferred_primary_host: "{{ groups.mongodb[0] }}" + mongodb_preferred_primary_resolved: "{{ mongodb_preferred_primary | default('', true) | length == 0 }}" + when: + - inventory_hostname in groups.mongodb + - groups.mongodb.index(inventory_hostname) == 0 + tags: reconfigure_priority + +- name: Normalize preferred primary override + ansible.builtin.set_fact: + mongodb_preferred_primary_input: >- + {{ mongodb_preferred_primary | regex_replace(':' + (mongodb_port | string) + '$', '') }} + when: + - inventory_hostname in groups.mongodb + - groups.mongodb.index(inventory_hostname) == 0 + - mongodb_preferred_primary | default('', true) | length > 0 + tags: reconfigure_priority + +- name: Resolve preferred primary override by inventory hostname + ansible.builtin.set_fact: + mongodb_preferred_primary_host: "{{ mongodb_preferred_primary_input }}" + mongodb_preferred_primary_resolved: true + when: + - inventory_hostname in groups.mongodb + - groups.mongodb.index(inventory_hostname) == 0 + - mongodb_preferred_primary | default('', true) | length > 0 + - mongodb_preferred_primary_input in groups.mongodb + tags: reconfigure_priority + +- name: Resolve preferred primary override by ansible_host or short hostname + ansible.builtin.set_fact: + mongodb_preferred_primary_host: "{{ item }}" + mongodb_preferred_primary_resolved: true + loop: "{{ groups.mongodb }}" + when: + - inventory_hostname in groups.mongodb + - groups.mongodb.index(inventory_hostname) == 0 + - mongodb_preferred_primary | default('', true) | length > 0 + - mongodb_preferred_primary_input not in groups.mongodb + - mongodb_preferred_primary_input in [ + item, + (item.split('.')[0]), + (hostvars[item].ansible_host | default('')) + ] + tags: reconfigure_priority + +- name: Validate preferred primary override + ansible.builtin.assert: + that: + - mongodb_preferred_primary_resolved | bool + fail_msg: >- + mongodb_preferred_primary must match a host in groups.mongodb. + Received {{ mongodb_preferred_primary }}. + Valid inventory hosts: {{ groups.mongodb | join(', ') }} + when: + - inventory_hostname in groups.mongodb + - groups.mongodb.index(inventory_hostname) == 0 + - mongodb_preferred_primary | default('', true) | length > 0 + tags: reconfigure_priority # This task should always run, arbiter or not - name: Create the replicaset members list (no arbiter) ansible.builtin.set_fact: - mongodb_servers: "{{ mongodb_servers + [item + ':' + mongodb_port | string] }}" - with_items: "{{ groups.mongodb }}" + mongodb_servers: >- + {{ mongodb_servers + [{'host': item + ':' + (mongodb_port | string), + 'priority': ((item == mongodb_preferred_primary_host) | + ternary(mongodb_primary_priority, mongodb_secondary_priority))}] + }} + loop: "{{ groups.mongodb }}" when: - - not mongodb_state.replication_enabled - inventory_hostname in groups.mongodb - groups.mongodb.index(inventory_hostname) == 0 + tags: reconfigure_priority # This task will only run when there is an arbiter defined in the hosts file - name: Add the arbiter to the list of servers when there is one ansible.builtin.set_fact: - mongodb_servers: "{{ mongodb_servers + [item + ':' + mongodb_port | string] }}" - with_items: "{{ groups.mongodb_arbiter }}" + mongodb_servers: >- + {{ mongodb_servers + [{'host': item + ':' + (mongodb_port | string), 'priority': 0}] }} + loop: "{{ groups.mongodb_arbiter }}" when: - - not mongodb_state.replication_enabled - inventory_hostname in groups.mongodb - groups.mongodb.index(inventory_hostname) == 0 - groups.mongodb_arbiter is defined + tags: reconfigure_priority + +- name: Debug replication state + ansible.builtin.debug: + msg: "replication_enabled={{ mongodb_state.replication_enabled }} primary={{ mongodb_state.primary }} members={{ mongodb_state.members }}" + when: + - inventory_hostname in groups.mongodb + - groups.mongodb.index(inventory_hostname) == 0 + tags: reconfigure_priority + +- name: Wait for all MongoDB members to be reachable before creating the replicaset + ansible.builtin.wait_for: + host: "{{ item }}" + port: "{{ mongodb_port }}" + timeout: 60 + loop: "{{ groups.mongodb + (groups.mongodb_arbiter | default([])) }}" + when: + - not mongodb_state.replication_enabled + - inventory_hostname in groups.mongodb + - groups.mongodb.index(inventory_hostname) == 0 - name: Create the replicaset community.mongodb.mongodb_replicaset: @@ -119,6 +209,94 @@ vars: ansible_python_interpreter: "{{ mongodb_python_venv }}/bin/python3" +- name: Refresh MongoDB configuration state after replicaset changes + itential.deployer.mongodb_config_state: + login_database: admin + login_host: "{{ inventory_hostname }}" + login_port: "{{ mongodb_port }}" + register: mongodb_state_after_replicaset + when: + - inventory_hostname in groups.mongodb + - groups.mongodb.index(inventory_hostname) == 0 + vars: + ansible_python_interpreter: "{{ mongodb_python_venv }}/bin/python3" + tags: reconfigure_priority + +- name: Reconfigure the replicaset priorities + community.mongodb.mongodb_replicaset: + arbiter_at_index: "{{ (groups.mongodb_arbiter | default([]) | length > 0) | ternary(mongodb_servers | length - 1, omit) }}" + auth_mechanism: "SCRAM-SHA-256" + login_user: "{{ (mongodb_state.auth_enabled | bool) | ternary(mongodb_user_admin, omit) }}" + login_password: "{{ (mongodb_state.auth_enabled | bool) | ternary(mongodb_user_admin_password, omit) }}" + login_port: "{{ mongodb_port }}" + login_database: admin + login_host: "{{ mongodb_state_after_replicaset.primary | regex_replace(':.*$', '') }}" + members: "{{ mongodb_servers }}" + replica_set: "{{ mongodb_replset_name }}" + reconfigure: true + validate: true + register: reconfig_result + when: + - inventory_hostname in groups.mongodb + - groups.mongodb.index(inventory_hostname) == 0 + - mongodb_state_after_replicaset.primary is defined + - mongodb_state_after_replicaset.primary | length > 0 + vars: + ansible_python_interpreter: "{{ mongodb_python_venv }}/bin/python3" + tags: reconfigure_priority + +- name: Print reconfigure result + ansible.builtin.debug: + msg: "{{ reconfig_result }}" + when: + - reconfig_result is defined + - reconfig_result is not skipped + tags: reconfigure_priority + +- name: Step down non-preferred primary after replicaset changes + community.mongodb.mongodb_shell: + mongo_cmd: auto + login_user: "{{ mongodb_state.auth_enabled | ternary(mongodb_user_admin, omit) }}" + login_password: "{{ mongodb_state.auth_enabled | ternary(mongodb_user_admin_password, omit) }}" + login_port: "{{ mongodb_port }}" + login_database: admin + login_host: "{{ mongodb_state_after_replicaset.primary | regex_replace(':.*$', '') }}" + eval: "db.adminCommand({replSetStepDown: 60, force: true})" + register: stepdown_result + failed_when: false + when: + - inventory_hostname in groups.mongodb + - groups.mongodb.index(inventory_hostname) == 0 + - reconfig_result is changed + - mongodb_state_after_replicaset.primary is defined + - (mongodb_state_after_replicaset.primary | regex_replace(':.*$', '')) != mongodb_preferred_primary_host + vars: + ansible_python_interpreter: "{{ mongodb_python_venv }}/bin/python3" + tags: reconfigure_priority + +- name: Ensure replicaset is stable after primary stepdown + community.mongodb.mongodb_status: + login_user: "{{ mongodb_state.auth_enabled | ternary(mongodb_user_admin, omit) }}" + login_password: "{{ mongodb_state.auth_enabled | ternary(mongodb_user_admin_password, omit) }}" + login_port: "{{ mongodb_port }}" + login_database: admin + login_host: "{{ inventory_hostname }}" + replica_set: "{{ mongodb_replset_name }}" + poll: "{{ mongodb_status_poll }}" + interval: "{{ mongodb_status_interval }}" + validate: minimal + register: rs_after_stepdown + failed_when: + - "'Unable to determine if auth is enabled' not in rs_after_stepdown.msg" + - "'replicaset is in a converged state' not in rs_after_stepdown.msg" + when: + - inventory_hostname in groups.mongodb + - stepdown_result is defined + - stepdown_result is not skipped + vars: + ansible_python_interpreter: "{{ mongodb_python_venv }}/bin/python3" + tags: reconfigure_priority + # Starting in MongoDB 5.0, the implicit default write concern is w: majority. # However, special considerations are made for deployments containing arbiters: # The voting majority of a replica set is 1 plus half the number of voting diff --git a/roles/mongodb/tasks/configure-mongodb.yml b/roles/mongodb/tasks/configure-mongodb.yml index 24b9abae..db4d7ebc 100644 --- a/roles/mongodb/tasks/configure-mongodb.yml +++ b/roles/mongodb/tasks/configure-mongodb.yml @@ -8,7 +8,10 @@ - name: Configure MongoDB replica set ansible.builtin.include_tasks: file: configure-mongodb-replicaset.yml + apply: + tags: reconfigure_priority when: mongodb_replication_enabled | bool + tags: reconfigure_priority # Configure auth - name: Configure MongoDB Auth