Migrate from Ansible¶
Most Ansible playbooks for a single server map onto sadame resource by
resource: packages, files, templates, services, users and guarded commands
work the same way, with Ruby instead of YAML and ERB instead of Jinja2.
This guide translates the ten modules that make up most playbooks, then
the inventory, using examples/migrate-from-ansible: an Ansible playbook
and its translation, which the test suite applies on Debian and Ubuntu.
What is the same, and what is not¶
| Ansible | sadame |
|---|---|
| playbook (YAML) | recipe (Ruby) |
hosts: web, --limit |
-g web, --limit |
vars:, group_vars, host_vars, -e |
vars.rb, group and host vars in hosts.rb, --var |
| Jinja2 templates | ERB templates with vars and locals |
notify: and handlers |
notifies: and subscribes: |
when: |
Ruby if |
ansible_facts: ansible_processor_vcpus, ansible_memtotal_mb, ansible_default_ipv4.address |
facts[:cpus], facts[:memory_mb], facts[:ipv4]; read when used, no gather_facts |
loop: |
Ruby each |
--check --diff |
sadame plan |
--syntax-check |
sadame validate, which also renders templates for each host |
tags:, --tags, --skip-tags |
tags: and include_recipe ..., tags:, --tags, --skip-tags |
forks |
--forks |
serial: |
--serial |
| no built-in drift command | sadame drift |
become: true |
sudo: true in hosts.rb, or --sudo |
| Python on every target | nothing on the targets; sadame copies itself |
| roles, collections, Galaxy | include_recipe and plain Ruby; no package manager for recipes |
Tasks run in order in both. A sadame resource reads the current state
before acting, as an idempotent Ansible module does; there is no
changed_when, because a resource changes exactly when its state
differs.
The ten common modules¶
Each block shows the Ansible task and its translation from the example.
package / apt / dnf¶
- name: Install packages
ansible.builtin.apt:
name: [nginx, openssl, cron, curl]
state: present
update_cache: true
# apt with update_cache: sadame runs apt-get update before the first
# install of a run.
%w[nginx openssl cron curl].each { |name| package name }
package picks apt, dnf, apk or pacman from the distribution, so one
recipe covers several families.
copy¶
- name: Login banner
ansible.builtin.copy:
dest: /etc/issue.net
content: "Authorized access only.\n"
mode: "0644"
- name: Backup script
ansible.builtin.copy:
src: files/backup.sh
dest: /usr/local/bin/backup
mode: "0755"
file "/etc/issue.net", content: "Authorized access only.\n", mode: "0644"
file "/usr/local/bin/backup", source: "backup.sh", mode: "0755"
template¶
- name: Site configuration
ansible.builtin.template:
src: templates/app.conf.j2
dest: /etc/nginx/conf.d/app.conf
mode: "0644"
notify: Reload nginx
# Handlers are notifications; delayed, like Ansible's, by default.
file "/etc/nginx/conf.d/app.conf",
template: "app.conf.erb",
mode: "0644",
notifies: [:reload, "service[nginx]"]
{{ server_name }} becomes <%= vars[:server_name] %>; {% for %} and
{% if %} become <% ... %> with Ruby. See
Recipe basics.
file¶
- name: Application directory
ansible.builtin.file:
path: /srv/app
state: directory
owner: www-data
mode: "0755"
- name: Old configuration is gone
ansible.builtin.file:
path: /etc/nginx/conf.d/old.conf
state: absent
directory "/srv/app", owner: "www-data", mode: "0755"
file "/etc/nginx/conf.d/old.conf", state: :absent
service / systemd¶
- name: nginx runs and starts at boot
ansible.builtin.service:
name: nginx
state: started
enabled: true
service "nginx", enable: true, state: :running
Handlers become the service's notification actions: the handler
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
is notifies: [:reload, "service[nginx]"] on the file that notified it.
user¶
- name: Deploy user
ansible.builtin.user:
name: deploy
shell: /bin/bash
groups: www-data
append: true
# groups: always appends, like append: true.
user "deploy", shell: "/bin/bash", groups: ["www-data"]
command / shell with creates¶
- name: Self-signed certificate
ansible.builtin.command:
cmd: >-
openssl req -x509 -newkey rsa:2048 -nodes -days 365 -subj /CN=app.example.com
-keyout /etc/ssl/private/app.key -out /etc/ssl/certs/app.crt
creates: /etc/ssl/private/app.key
exec "openssl req -x509 -newkey rsa:2048 -nodes -days 365 -subj /CN=app.example.com " \
"-keyout /etc/ssl/private/app.key -out /etc/ssl/certs/app.crt",
creates: "/etc/ssl/private/app.key"
creates: is the same; removes: has no equivalent, use only_if: "test
-e PATH". A command without a guard runs on every apply in both tools,
and sadame's drift always reports it.
lineinfile¶
- name: Default editor
ansible.builtin.lineinfile:
path: /etc/environment
regexp: "^EDITOR="
line: EDITOR=vim
line "EDITOR=vim", path: "/etc/environment", match: /^EDITOR=/
line is sadame's lineinfile: match plays the part of regexp, and
state: :absent removes the line. One difference: when several lines
match, line replaces all of them, where lineinfile replaces the last
one. Prefer managing the whole file with file when sadame can own it, so
the recipe states what the file contains; see line.
cron¶
- name: Nightly backup
ansible.builtin.cron:
name: backup
minute: "30"
hour: "2"
user: root
job: /usr/local/bin/backup
file "/etc/cron.d/backup", content: "30 2 * * * root /usr/local/bin/backup\n", mode: "0644"
Ansible's cron edits the user's crontab by default; the translation uses
a file in /etc/cron.d, which names the user on each line. See
examples/09-cron-and-logrotate.
get_url¶
- name: A page from another server
ansible.builtin.get_url:
url: https://www.example.com/
dest: /srv/app/example.html
mode: "0644"
exec "curl -fsSL -o /srv/app/example.html https://www.example.com/",
creates: "/srv/app/example.html"
The inventory¶
An INI inventory:
[web]
web1.example.com
web2.example.com workers=8
[db]
db1.example.com ansible_user=admin ansible_port=2222 ansible_become=true
[web:vars]
workers=4
[all:vars]
env=production
or its YAML form:
all:
vars:
env: production
children:
web:
vars:
workers: 4
hosts:
web1.example.com:
web2.example.com:
workers: 8
db:
hosts:
db1.example.com:
ansible_user: admin
ansible_port: 2222
ansible_become: true
becomes:
# ansible/inventory.ini and ansible/inventory.yml, translated. Ansible's
# all group is implicit; here it is declared, including the other groups.
group :web do
host "web1.example.com"
host "web2.example.com", vars: { workers: 8 }
vars workers: 4
end
group :db do
host "db1.example.com", user: "admin", port: 2222, sudo: true
end
group :all, include: [:web, :db] do
vars env: "production"
end
| Ansible | hosts.rb |
|---|---|
[web] with host lines |
group :web do host "..." end |
ansible_host |
address: |
ansible_user, ansible_port |
user:, port: |
ansible_become=true |
sudo: true |
ansible_ssh_common_args |
ssh_opts: |
[web:vars] |
vars inside the group |
[all:vars] |
a group :all that includes the others; Ansible's all is implicit, sadame's is not |
[app:children] |
group :app, include: [...] |
| dynamic inventory scripts and plugins | Ruby in hosts.rb; see Inventory |
Precedence is similar: a host's vars override its groups', and a group's
vars override the groups it includes. Ansible has many more layers
(role defaults, facts, set_fact); sadame has four, listed on the
Inventory page.
Gaps¶
Things Ansible does that sadame does not:
- sixteen facts (CPUs, memory, disk, addresses, virtualization) rather
than Ansible's hundreds: no package, service or mount facts, no
facts.d; - ten resource types (
package,file,line,directory,link,service,exec,user,group,git) where Ansible has thousands of modules: noapt_repository,mount,sysctl,firewalldand so on; usefile,lineand guardedexec(seeexamples/04-docker-hostfor a package repository); - no
max_fail_percentage:--serialstops after the first batch with a failure; - no Windows, no network devices, no agent or pull mode;
- no vault; read secrets from the environment in
vars.rb; - no roles from Galaxy: recipes are files you
include_recipe.
Common mistakes¶
- Translating
shelltasks one to one. Mostshelltasks in a playbook exist because a module was missing; check whether afileorpackageresource says the same thing as state. - Forgetting the guard. An
execwithoutcreates,not_iforonly_ifruns every time and makesdriftfail. - Expecting
allto exist. Declare it inhosts.rbif you use it.