Getting started with Ansible
I have a server, her name is Pinky
Pinky does a lot of things but pinky has one problem: Pinky is totally hand-made. Everything in it has been installed by hand, configured by hand, and maintained by hand. This is ok.
I mean, it's ok, until it's not ok. It has backups and everything, but when a chance presents to, for example, move to a new server, because I just got a nice new computer ... I would need to do everything by hand again.
So, let's fix this using technology. I have known about ansible for a long time, I have used things like ansible. I have used packer, and salt, and puppet, and (related) docker, and kubernetes, and terraform, and cloudformation, and chef, and ... you get the idea.
But I have never used ansible!
So, here's my plan:
- I will start doing ansible playbooks for pinky.
- Since ansible is idempotent, I can run the playbooks on pinky and nothing should change.
- I can also run them on the new server, and everything should be set up.
- At some point the new server will be sufficiently pinky-like and I can switch.
So, what is ansible?
In non-technical terms: Ansible is a tool to change things on machines. Ansible can:
- Setup a user
- Copy a file
- Install a package
- Configure a thing
- Enable a service
- Run a command
And so on.
Additionally:
- It will only do things that need to be done.
- It will do things in the requested order.
- It will do things in multiple machines.
First: inventory
The first thing I need to do is to tell ansible where to run things. This is done using an inventory file. The inventory file is a list of machines, and groups of machines, that ansible can run things on.
Mine is very simple, a file called hosts in the same directory as the playbook:
[servers]
pinky ansible_user=ralsina
rocky ansible_user=rock
[servers:vars]
ansible_connection=ssh
This defines two machines, called pinky
(current server) and rocky
(new server).
Since rocky
is still in pretty much brand new shape it has only the default user
it came with, called rock
. I have logged into it and done some things ansible needs:
- Enabled ssh
- Made it so my personal machine where ansible runs can log in without a password
- Installed python
- Made rock a
sudoer
so it can run commands as root usingsudo
So, I tell ansible I can log in as ralsina
in pinky
and as rock
in rocky
,
in both cases using ssh.
First playbook
I want to be able to log into these machines using my user ralsina
and my ssh key. So, I will create a playbook that does that. Additionally, I want my shell fish
and my prompt starship
to be installed and enabled.
A playbook is just a YAML file that lists tasks to be done. We start with some generic stuff like "what machines to run this on" and "how do I become root?"
# Setup my user with some QoL packages and settings
- name: Basic Setup
hosts: servers
become_method: ansible.builtin.sudo
tasks:
And then guess what? Tasks. Each task is a thing to do. Here's the first one:
- name: Install some packages
become: true
ansible.builtin.package:
name:
- git
- vim
- htop
- fish
- rsync
- restic
- vim
state: present
There "ansible.builtin.package" is a module that installs packages. Ansible has tons of modules, and they are all documented in the ansible documentation.
Each task can take parameters, which depend on what the module does. In this case, as you can see there's a list of packages to install, and the state
means I want them to be there.
BUT while rocky
is a Debian, pinky
is arch (btw), so there is at least
one package I need to install only in rocky. That's the next task:
- name: Install Debian-specific packages
become: true
when: ansible_os_family == 'Debian'
ansible.builtin.apt:
name:
- ncurses-term
state: present
Same thing, except:
- It uses a debian-specific package thing, called
ansible.builtin.apt
- It has a
when
clause that only runs the task if the OS family is Debian.
What next? Well, more tasks! Here they are, you can understand what each one does by looking up the docs for each ansible module.
- name: Add the user ralsina
become: true
ansible.builtin.user:
name: ralsina
create_home: true
password_lock: true
shell: /usr/bin/fish
- name: Authorize ssh
become: true
ansible.posix.authorized_key:
user: ralsina
state: present
key: "{{ lookup('file', '/home/ralsina/.ssh/id_rsa.pub') }}"
- name: Make ralsina a sudoer
become: true
community.general.sudoers:
name: ralsina
user: ralsina
state: present
commands: ALL
nopassword: true
- name: Create fish config directory
ansible.builtin.file:
path: /home/ralsina/.config/fish/conf.d
recurse: true
state: directory
mode: '0755'
- name: Get starship installer
ansible.builtin.get_url:
url: https://starship.rs/install.sh
dest: /tmp/starship.sh
mode: '0755'
- name: Install starship
become: true
ansible.builtin.command:
cmd: sh /tmp/starship.sh -y
creates: /usr/local/bin/starship
- name: Enable starship
ansible.builtin.copy:
dest: /home/ralsina/.config/fish/conf.d/starship.fish
mode: '0644'
content: |
starship init fish | source
And that's it! I can run this playbook using ansible-playbook -i hosts setup_user.yml
and it will do all those things on both pinky
and rocky
, if needed:
> ansible-playbook -i hosts setup_user.yml
PLAY [Basic Setup] ******************************
TASK [Gathering Facts] **************************
ok: [rocky]
ok: [pinky]
TASK [Install some packages] ********************
ok: [rocky]
ok: [pinky]
TASK [Install Debian-specific packages] *********
skipping: [pinky]
ok: [rocky]
TASK [Add the user ralsina] *********************
ok: [rocky]
ok: [pinky]
TASK [Authorize ssh] ****************************
ok: [rocky]
ok: [pinky]
TASK [Make ralsina a sudoer] ********************
ok: [rocky]
ok: [pinky]
TASK [Create fish config directory] *************
changed: [rocky]
changed: [pinky]
TASK [Get starship installer] *******************
ok: [rocky]
ok: [pinky]
TASK [Install starship] *************************
ok: [rocky]
ok: [pinky]
TASK [Enable starship] **************************
changed: [rocky]
changed: [pinky]
PLAY RECAP **************************************
pinky : ok=9 changed=2 unreachable=0 failed=0 skipped=1
rescued=0 ignored=0
rocky : ok=10 changed=2 unreachable=0 failed=0 skipped=0
rescued=0 ignored=0
If you look carefully you can see rocky ran one more task, and pinky skipped one (the debian-specific package installation), and that only two things got actually executed on each machine.
I could run this a dozen times from now on, and it would not do anything.
Did it work?
Sure, I can ssh into rocky
and everything is nice:
> ssh rocky
Linux rock-5c 5.10.110-37-rockchip #27a257394 SMP Thu May 23 02:38:59 UTC 2024 aarch64
The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.
Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Wed Jun 26 15:32:33 2024 from 100.73.196.129
Welcome to fish, the friendly interactive shell
Type `help` for instructions on how to use fish
ralsina in 🌐 rock-5c in ~
There is a starship prompt, and I can use fish. And I can sudo. Nice!
I can now change the inventory so rocky
also uses the ralsina
user and delete the rock
user.
Next steps
There is a lot more to ansible, specifically roles but this is already enough to get useful things done, and hopefully it will be useful to you too.