Using Ansible with an SSH bastion / jump host

Introduction

Ansible is a powerful tool when it comes to managing and automating your infrastructure. I've been using Ansible for more than a decade now and I love the way you can easily create playbooks and custom roles to speed up your daily work.

When it comes to running the Ansible playbooks to provision your VMs, your VMs usually need a public IP so that Ansible can connect via SSH and execute the desired commands. But what if your VMs do not have a public IP and are not accessible from the outside world?

There is a solution for this called jump host, which we have been using for decades with our terminals, and Ansible can also take advantage of this.

Option 1 - Inventory vars

The first way to achieve this is to use the inventory variables to pass your custom configuration. This approach is very helpful if you are using different projects and servers in a single Ansible configuration and want to pass the right configuration to the right endpoints that are logically separated from each other.

Let's assume we have an inventory file with three endpoints and a single jump host

[proxy]
bastion

[nodes]
private-vm-1
private-vm-2
private-vm-3

If we are on the same network as the VMs, we can simply run our playbooks and everything will work fine. However, if we are outside the network, we need to pass an SSH configuration to our nodes so that Ansible knows how to reach them.

Customize your inventory as follows and add a configuration only for the nodes children

[proxy]
bastion

[nodes]
private-vm-1
private-vm-2
private-vm-3

[nodes:vars]
ansible_ssh_common_args='-o ProxyCommand="ssh -p 2222 -W %h:%p -q username@bastion"'
In case your SSH daemon is listening in port 22, then there is no need to use a custom port argument.

Option 2 - Ansible configuration

We can achieve the same result with an Ansible configuration file that you can place in the same directory where you run the playbooks.

This approach should be used if your Ansible configuration is dedicated to a single logical project and is not used by others. This is also my preferred method as I usually split my infrastructure configuration into small pieces.

Add an additional ansible.cfg file in your project directory that will be used by Ansible when you run a playbook

[ssh_connection]
ssh_args = -o ProxyCommand="ssh -p 2222 -W %h:%p -q username@bastion"

Option 3 - SSH configuration

There is also a third option, which I personally do not prefer, but which can be very helpful depending on your setup. You can pass the same jump host configuration to the ~/.ssh/config file of your system. Then you don't have to worry about passing the right configuration to the right nodes, as the whole configuration is managed by your operating system.

Host bastion
   User username
   Hostname IP/HOST

Host private-vm-*
   ProxyJump bastion

This option is only useful if your nodes do not change frequently, as you must always pass this information to the system on which you are running the Ansible playbooks. Of course, this process can also be automated by Ansible, but that's another story.