Migrate an Ansible playbook
In this tutorial you’ll convert a short Ansible playbook into a circuit. The converter translates the tasks it can, and leaves the rest as commented YAML marked TODO(convert). You’ll read each TODO, finish it by hand, and validate the circuit until it’s ready to apply.
Time: 15 minutes.
Prerequisites
Section titled “Prerequisites”- Conn3x v0.6.3 installed. You don’t need Ansible.
- To apply the result: a Rocky Linux, RHEL, AlmaLinux or Fedora server with firewalld running, and a user with passwordless
sudo, as in Configure your first real server. You can convert and validate without one.
The playbook
Section titled “The playbook”Create a directory with two files in it. site.yml installs nginx, gives it a status page on port 8080, installs two tools, and opens the port in the firewall:
- hosts: web become: true vars: status_port: 8080 tasks: - name: Install nginx ansible.builtin.dnf: name: nginx state: present
- name: Write the status page config ansible.builtin.copy: dest: /etc/nginx/conf.d/status.conf content: | server { listen {{ status_port }}; location /status { return 200 "up\n"; } } mode: "0644" notify: Reload nginx
- name: Start nginx ansible.builtin.service: name: nginx state: started enabled: true
- name: Install tools ansible.builtin.package: name: "{{ item }}" loop: - git - jq
- name: Open the status port ansible.posix.firewalld: port: "{{ status_port }}/tcp" permanent: true state: enabled when: ansible_os_family == "RedHat"
- name: Show the port ansible.builtin.debug: msg: "Status page on port {{ status_port }}"
handlers: - name: Reload nginx ansible.builtin.service: name: nginx state: reloadedhosts.ini is the inventory. Use your server’s address and user:
[web]web1 ansible_host=203.0.113.10 ansible_user=adminConvert it
Section titled “Convert it”conn3x convert ansible -i hosts.ini -out web.circuit.hcl site.ymlConverted 4 tasks. 2 things need you; each is marked TODO(convert): line 35, Open the status port: uses when, which the converter can't translate yet line 42, Show the port: uses the debug module, which has no Conn3x equivalent yetThe command exits with code 3 while anything is marked TODO, and 0 once everything converts. The line numbers point into site.yml.
Read the result
Section titled “Read the result”Open web.circuit.hcl:
# Converted from site.yml by conn3x convert ansible.# Search for TODO(convert) to find what still needs a person.
variable "ssh_private_key_file" { type = string description = "Private key Conn3x logs in with."}
circuit "web" { hosts = { web1 = "203.0.113.10" }
ssh { user = "admin" private_key_file = var.ssh_private_key_file become = true }
step "install_nginx" { package { name = "nginx" } }
step "write_the_status_page_config" { file { path = "/etc/nginx/conf.d/status.conf" content = "server {\n listen 8080;\n location /status { return 200 \"up\\n\"; }\n}\n" mode = "0644" } notify = [handler.reload_nginx] }
step "start_nginx" { service { name = "nginx" state = "started" enabled = true } }
step "install_tools" { for_each = ["git", "jq"] package { name = item.value } }
# TODO(convert): Task "Open the status port" (site.yml line 35) uses when, which the converter can't translate yet. # - name: Open the status port # ansible.posix.firewalld: # port: "{{ status_port }}/tcp" # permanent: true # state: enabled # when: ansible_os_family == "RedHat"
# TODO(convert): Task "Show the port" (site.yml line 42) uses the debug module, which has no Conn3x equivalent yet. # - name: Show the port # ansible.builtin.debug: # msg: "Status page on port {{ status_port }}"
handler "reload_nginx" { service { name = "nginx" action = "reload" } }}Compare it with the playbook:
- The play became the circuit
web.hosts,userandbecomecame from the inventory and the play. - Each task became a step named after it, in the same order.
dnfbecame thepackagemodule, andcopybecamefile. {{ status_port }}was filled in from the play’svars, so the content says8080.notify: Reload nginxbecamenotify = [handler.reload_nginx], and the handler reloads instead of restarting, asstate: reloadeddid.- The loop over a plain list became a step with
for_each, one package per item. - The key path became a variable,
ssh_private_key_file, because an inventory doesn’t usually say which key to use.
The commented-out tasks don’t stop the file from validating. Check that now:
conn3x initconn3x validateSuccess! The configuration is valid.Finish the TODOs
Section titled “Finish the TODOs”Each TODO says why the converter stopped. Decide what the task should mean in Conn3x, then replace the comment.
Open the status port. The when runs the task only on Red Hat family hosts. A Conn3x step runs on every host in the circuit, so the condition becomes a choice of which hosts the circuit covers. Every host in this inventory runs Rocky Linux, so the condition always holds, and the task becomes a plain firewalld step. permanent: true and state: enabled need no counterpart: the firewalld module opens the port now and after reboots. Replace the first TODO with:
step "open_the_status_port" { firewalld { port = "8080/tcp" } }If you had Debian hosts in the same group, you’d move them to a second circuit that uses ufw instead.
Show the port. A debug task prints a message during a run. Circuits configure hosts, and printing isn’t a change to a host, so delete the second TODO. If you want the port in the output, add an output value.
The end of the circuit now reads:
step "install_tools" { for_each = ["git", "jq"] package { name = item.value } }
step "open_the_status_port" { firewalld { port = "8080/tcp" } }
handler "reload_nginx" { service { name = "nginx" action = "reload" } }}Validate again, and search for anything left:
conn3x validategrep -n 'TODO(convert)' web.circuit.hclSuccess! The configuration is valid.2:# Search for TODO(convert) to find what still needs a person.Only the header comment is left. Delete those two lines if you like.
If you have a server for it, pass your key path and apply:
conn3x apply -var ssh_private_key_file=~/.ssh/id_ed25519Type yes. The output here is shortened: Conn3x also prints each resource’s ID, which starts with the server’s address.
conn3x_package.web__install_nginx["web1"]: Creating...conn3x_package.web__install_nginx["web1"]: Creation complete after 2sconn3x_file.web__write_the_status_page_config["web1"]: Creating...conn3x_file.web__write_the_status_page_config["web1"]: Creation complete after 0sconn3x_service.web__start_nginx["web1"]: Creating...conn3x_service.web__start_nginx["web1"]: Creation complete after 0sconn3x_package.web__install_tools["web1/jq"]: Creating...conn3x_package.web__install_tools["web1/git"]: Creating...conn3x_package.web__install_tools["web1/git"]: Creation complete after 4sconn3x_package.web__install_tools["web1/jq"]: Creation complete after 5sconn3x_firewalld.web__open_the_status_port["web1"]: Creating...conn3x_firewalld.web__open_the_status_port["web1"]: Creation complete after 1sconn3x_service_action.web__reload_nginx["web1"]: Creating...conn3x_service_action.web__reload_nginx["web1"]: Creation complete after 0s
Apply complete! Resources: 7 added, 0 changed, 0 destroyed.The install_tools step has one instance per host and item, web1/git and web1/jq. A second conn3x plan reports no changes.
Clean up
Section titled “Clean up”conn3x destroy -var ssh_private_key_file=~/.ssh/id_ed25519Type yes. Destroy ends with Destroy complete! Resources: 7 destroyed. and removes the steps from state. nginx, its configuration and the packages stay on the server. The firewalld step defaults to on_remove = "delete", so destroy closes port 8080 again.
Next steps
Section titled “Next steps”- Migrate from Ansible lists every module the converter handles and what it leaves for you.
- The conn3x convert ansible reference covers its options and exit codes.
- Host facts read a host’s OS at plan time, for a
whenthat has to stay a condition.