When automating server provisioning with Ansible, one common hurdle occurs when playbooks need to modify the default SSH port (22) as part of the setup process. This creates a chicken-and-egg problem: you need to connect to change the port, but after changing the port, subsequent connections fail unless you update your inventory.
Ansible provides several ways to handle this scenario through its inventory system and connection plugins. The key is understanding how to structure your inventory and leverage Ansible's retry mechanisms.
The most straightforward method is to define alternative ports directly in your inventory file:
[web_servers]
server1 ansible_host=192.168.1.100 ansible_port=22
server1 ansible_host=192.168.1.100 ansible_port=2222
However, this creates duplicate entries. A better approach uses host variables:
[web_servers]
server1 ansible_host=192.168.1.100
[web_servers:vars]
ansible_port=2222
fallback_port=22
Create a custom connection plugin or use a pre-task to test connectivity:
- name: Check SSH connectivity on default port
hosts: all
gather_facts: no
tasks:
- name: Test initial connection
wait_for:
port: "{{ ansible_port | default(22) }}"
host: "{{ ansible_host }}"
timeout: 5
ignore_errors: yes
register: initial_connection
- name: Set fallback port if needed
set_fact:
ansible_port: "{{ fallback_port | default(omit) }}"
when: initial_connection is failed
Configure your ansible.cfg to handle connection failures more gracefully:
[defaults]
retry_files_enabled = True
retry_files_save_path = ~/.ansible/retry-files
connection_retries = 3
For more complex environments, consider writing a dynamic inventory script that:
1. Attempts connection on primary port
2. Falls back to secondary port if needed
3. Returns the proper connection parameters
Here's a Python example:
#!/usr/bin/env python
import json
import socket
def test_port(host, port, timeout=3):
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
s.connect((host, port))
s.close()
return True
except:
return False
def main():
inventory = {
"_meta": {
"hostvars": {}
},
"all": {
"hosts": ["server1"],
"vars": {}
}
}
host = "192.168.1.100"
primary_port = 2222
fallback_port = 22
if test_port(host, primary_port):
inventory["_meta"]["hostvars"]["server1"] = {
"ansible_host": host,
"ansible_port": primary_port
}
else:
inventory["_meta"]["hostvars"]["server1"] = {
"ansible_host": host,
"ansible_port": fallback_port
}
print(json.dumps(inventory))
if __name__ == "__main__":
main()
- Always document your port change procedures in playbooks
- Use tags to separate port-changing tasks from other configuration
- Implement proper error handling for connection failures
- Consider using SSH certificates instead of port changes for security
When automating server setup with Ansible, changing the default SSH port (22) creates a chicken-and-egg problem. The initial playbook needs port 22 access to change the port, but subsequent runs require the new port. This becomes particularly problematic when:
- Deploying security-hardened server images
- Running playbooks against existing infrastructure
- Managing servers with custom SSH configurations
Ansible provides two primary approaches to handle port changes:
# Method 1: Inventory variable fallback
[webservers]
server1 ansible_host=192.168.1.100 ansible_port=2222
# Method 2: Group variables with precedence
[webservers:vars]
ansible_port=2222
ansible_ssh_retries=3
For robust automation, consider these implementation patterns:
# multi-port inventory approach
[initial_setup]
new_server ansible_host=10.0.0.5
[production:children]
initial_setup
[production:vars]
ansible_port=45678
ansible_connection=ssh
ansible_ssh_common_args='-o ConnectTimeout=5 -o ConnectionAttempts=2'
The key parameters for connection resilience:
ansible_ssh_retries
: Number of retry attemptsansible_timeout
: Connection timeout in secondsansible_ssh_common_args
: Additional SSH parameters
For complex environments, implement port scanning within the playbook:
- name: Detect SSH port
hosts: all
gather_facts: no
tasks:
- name: Try common SSH ports
vars:
port_list: [22, 2222, 45678, 54321]
block:
- name: Attempt connection
ansible.builtin.setup:
delegate_to: "{{ inventory_hostname }}"
vars:
ansible_port: "{{ item }}"
when: ssh_port is not defined
ignore_errors: yes
register: result
loop: "{{ port_list }}"
- name: Set working port
ansible.builtin.set_fact:
ssh_port: "{{ item }}"
when: not result.failed
loop: "{{ port_list }}"
loop_control:
loop_var: item
- Use separate inventories for initial setup vs maintenance
- Implement connection testing in CI/CD pipelines
- Document port configurations in group_vars
- Consider using SSH jump hosts for secure environments
Configure proper error handling in ansible.cfg:
[defaults]
retry_files_enabled = False
callback_whitelist = profile_tasks,timer
And in playbooks:
- name: Secure server setup
block:
- include_tasks: secure_ssh.yml
rescue:
- debug:
msg: "Connection failed, attempting fallback ports"
- include_tasks: port_fallback.yml