This post is part of an ongoing series on Ansible, and you can find all the other posts in the series here.
So far in this Ansible series, we've worked with Cisco and Arista devices. In this part, we're adding a third vendor, Juniper. We'll build a small lab with two Juniper routers, and use Ansible to configure their interfaces, IP addresses, and OSPF from scratch.
Juniper works quite differently from Cisco and Arista in a few areas, and that's really what this part is about. Instead of connecting over SSH and sending CLI commands, Ansible talks to Juniper devices using NETCONF. Every change goes into a candidate config and only takes effect once it's committed. And thanks to the way Junos compares configs, Ansible can show us exactly what it's going to change, in the same format as show | compare on the CLI.
This part is part of a series on Ansible for network engineers, so I won't go into every detail of things we've already covered, such as inventory, host_vars, loops, and check mode. If any of those are new to you, I recommend going through the earlier parts first. The OSPF example in this part also follows the same approach as the one we used for Cisco, so if you've read that part, a lot of this will look familiar, and you can focus on the differences.
Juniper Ansible collection for Junos - https://galaxy.ansible.com/ui/repo/published/juniper/device/docs/
Lab Topology
The lab for this part is very simple. We have two Juniper vJunos-router devices, j1 and j2, connected to each other with a single link on ge-0/0/0. I created the lab using Containerlab, with the following topology file.

---
name: junos-ospf
mgmt:
network: netlab_mgmt
ipv4-subnet: 192.168.204.0/24
topology:
kinds:
juniper_vjunosrouter:
image: juniper-vjunos-router-24.2R1-S2.5
nodes:
j1:
kind: juniper_vjunosrouter
mgmt-ipv4: 192.168.204.101
j2:
kind: juniper_vjunosrouter
mgmt-ipv4: 192.168.204.102
links:
- endpoints: ["j1:ge-0/0/0", "j2:ge-0/0/0"]
Make sure you change the image to match the name of your own vJunos image. vJunos-router runs as a virtual machine inside the container, so it needs a Linux host with KVM support, and a fair amount of memory for each router. It also takes several minutes to boot, even after Containerlab says the lab is up, so give it a little time before trying to log in.
Containerlab gives each router a basic config to get started, with a hostname, a management IP, and a user called admin. Here's the starting config on j1, which I've trimmed to the important parts.
admin@j1> show configuration
system {
host-name j1;
login {
user admin {
class super-user;
...
}
}
services {
netconf {
ssh;
}
ssh {
root-login allow;
}
}
management-instance;
}
interfaces {
fxp0 {
unit 0 {
family inet {
address 10.0.0.15/24;
}
}
}
}
The most important part here is services netconf ssh, which enables NETCONF on the router. Ansible needs this to connect to Juniper devices, and Containerlab has already enabled it for us. If you're working with your own devices, you can enable it with the following command in configuration mode.
set system services netconf ssh
Apart from the management interface, fxp0, there's no other interface config at all. ge-0/0/0 has no IP address, and there's no OSPF config, so Ansible will configure everything else.
Setting Up the Project
I've created a new project directory called ansible_junos for this part. Here's what it looks like once we're done.
ansible_junos/
├── ansible.cfg
├── inventory.yml
├── group_vars/
│ └── junos.yml
├── host_vars/
│ ├── j1.yml
│ └── j2.yml
└── ospf.yml
Installing the Packages
Just like the OSPF part, I'm using uv for this project. On top of Ansible and paramiko, Juniper needs two extra Python packages. If you are not familiar with uv check out my other post here.
uv init --python 3.12
uv add ansible paramiko ncclient xmltodict
source .venv/bin/activate
- ncclient - A Python library for NETCONF, which Ansible uses to talk to the Juniper routers.
- xmltodict - NETCONF uses XML, and the Juniper modules use this library to convert the XML into data Ansible can work with. If it's missing, the modules fail with an error saying they can't import it.
If you're using pip, you can install the same packages with pip install ansible paramiko ncclient xmltodict.
The Juniper Collection
For Juniper devices, we'll use the juniper.device collection, which is maintained by Juniper. It comes with the ansible package, so there's nothing extra to install, and you can confirm it's there with ansible-galaxy collection list juniper.device.
You'll probably come across another collection called junipernetworks.junos in older examples and blog posts. It's been deprecated, and its modules have moved into juniper.device, so the module names are the same, they just start with juniper.device instead. For example, junipernetworks.junos.junos_facts is now juniper.device.junos_facts. If you use the old names, you'll see a deprecation warning pointing you to the new collection.
Inventory and Group Variables
The ansible.cfg file is the same as in the previous parts.
[defaults]
inventory = inventory.yml
host_key_checking = False
The inventory has a single group called junos, with our two routers.
---
all:
children:
junos:
hosts:
j1:
ansible_host: 192.168.204.101
j2:
ansible_host: 192.168.204.102
The connection settings go in group_vars/junos.yml.
---
ansible_network_os: juniper.device.junos
ansible_connection: ansible.netcommon.netconf
ansible_user: admin
ansible_password: admin@123
Make sure you update the password to match your own lab. The interesting line here is ansible_connection. For Cisco and Arista, we've always used network_cli, which connects over SSH and sends CLI commands, just like we would manually. For Juniper, we're using netconf instead.
NETCONF is a protocol designed specifically for managing network devices. Rather than typing commands and reading the output as text, Ansible sends and receives structured data in XML. It still runs over SSH behind the scenes, on port 830.
Testing the Connection
Before writing any playbooks, let's make sure Ansible can connect to the routers, by gathering facts from j1 with an ad-hoc command.
