KIF490:Einführung in Ansible (einfach wartbare, reproduzierbare Systemkonfigurationen)
Ansible Quickstart
What?
- Automation framework
- Open Source
- use cases
- software deployment
- configuration management
- infrastructure service orchestration
- security automation
Docs: https://docs.ansible.com/
How?
Define hosts (hosts file)
- in
/etc/ansible/hosts
or whatever is configured in/etc/ansible/ansible.cfg
- or by supplying it to an ansible command with
-i
- which hosts are we addressing? how to connect to them?
- in ini format or YAML
- hosts will later be identified by this name
- hosts can be grouped to apply actions to whatever hosts are in a group
- ini example:
mail.example.com
[webservers]
foo.example.com
bar.example.com
[webservers:vars]
port_for_thing=8080
[dbservers]
one.example.com
two.example.com
test_server ansible_host=three.example.com
- YAML example:
all:
hosts:
mail.example.com:
children:
webservers:
hosts:
foo.example.com:
bar.example.com:
vars:
port_for_thing: 8080
dbservers:
hosts:
one.example.com:
two.example.com:
test_server:
ansible_host: three.example.com
- default groups:
all
andungrouped
- also nice: ranges of hosts
webservers:
hosts:
www[01:50].example.com
- example host file from the kif minetest server:
all:
hosts:
kif_minetest:
- ...and the rest is specified in the
~/.ssh/config
(ssh jumps, key file etc.) - dynamic inventory possible
Writing playbooks
- in YAML
- specify tasks which will be run in sequence
- descriptive: tasks describe desired state, we don't worry about how it is achieved
- name for identification
- module with parameters (look at the docs)
- optional meta stuff
vars
: variables to pass in (e.g. if templating a file)with_items
etc.: items for each to run this taskwhen
: conditionals to skip the task if not mettags
to attach to run only or exclude specific subsets of tasks on runtime
- rule of thumb: don't be more clever than you need to, simplicity & readability first!
- run playbooks with
ansible-playbook -i my_hosts_file.yml example_playbook.yml
Example 1: Simple minetest server install on Ubuntu
---
- hosts: all
tasks:
- name: Install newest minetest server
apt:
pkg:
- minetest-server
state: latest
update_cache: true
cache_valid_time: 86400 # One day
become: true
This playbook has just one task it will run on any target specified in the inventory.
We use the apt
module to update the cache if older than a day and install the minetest-server
package. By specifying state: latest
, we instruct it to ensure the newest available version is installed (the default state: present
would already be satisfied if the package would be installed). Leaving out cache_valid_time
would cause the module to always update the cache (and take up time), keeping the default of update_cache: false
would base actions just on the existing cache every time
The whole task needs become: true
to meet the elevated user rights required by changing system critical aspects. become
can be thought of as an abstraction from sudo
in the case of linux, as Ansible can be used to manage a multitude of different systems, including Windows.[1]
Example 2: Repeating steps, evaluating variables
...
- name: Assign directories to minetest group and owner
file:
path: "{{ minetest_base_dir }}/{{ item }}"
state: directory
mode: u=rwX,g=rX,o=rX
owner: minetest
group: minetest
with_items:
- minetest
- minetest_game
- minetest_source
- minetest_mods
- world
- minetest_mapserver
become: true
...
We assume minetest_base_dir
is a variable either defined somewhere in our playbook or supplied by us from the command line (ansible-playbook -i my_hosts_file.yml
-e minetest_base_dir="/var/lib/minetest"
example_playbook.yml
) or from a file (ansible-playbook -i my_hosts_file.yml
-e @my_variables.yml
example_playbook.yml
).
We want to change the group and owner of a bunch of directories and their contents and assign a mode: files should be readable for everyone and only writable for the user owning them.[2]
The {{
curly braces }}
are Jinja2 syntax and here only denote variables, though we can do more complicated stuff. These should always be in quotation marks so the whole file is valid YAML syntax.
item
is only present because of our use of with_items
: We run the task for every item in that list, at every iteration the current item is assigned to the item
variable. Here we are just using strings, but we could use arbitrarily complex items, if our goal requires it. There are a bunch of aliases for with_items
, another one is loop
.
Example 3: Checking for file properties, acting based on a previous tasks output
- name: Get stats of a file
ansible.builtin.stat:
path: /etc/foo.conf
register: st
- name: Fail if the file does not belong to 'root'
ansible.builtin.fail:
msg: "Whoops! file ownership has changed"
when: st.stat.pw_name != 'root'
The stat
module never changes anything. It just retrieves status information about a file. But by registering the result of the task to a variable, we can use it for other parts of our playbook.
Be careful though, if you find yourself programming in a playbook, you're doing something wrong. Mostly you're either over-engineering/procrastinating on other problems or you really need a module that does that complicated stuff instead.
Example 4: Using commands when no modules for it are available
- name: Get newest Minetest
ansible.builtin.git:
repo: "{{ minetest_git_repo }}"
dest: "{{ minetest_base_dir }}/minetest_source"
force: true
version: "{{ minetest_version }}"
diff: false
become: true
register: minetest_repo
- name: Build Minetest
command:
cmd: "{{ item }}"
chdir: "{{ minetest_base_dir }}/minetest_source"
with_items:
- cmake . -DRUN_IN_PLACE=TRUE -DBUILD_SERVER=TRUE -DBUILD_CLIENT=FALSE -DPostgreSQL_TYPE_INCLUDE_DIR=/usr/include/postgresql/
- make -j4
become: true
register: minetest_build
when: minetest_repo.changed
There's not always a module for everything. Sometimes the usecase is too obscure, sometimes there is no meaningful way to ensure Ansible principles like idempotence.[3]
The above example is trying to build minetest for maximum control over the installed version. After resetting the git repository to the desired version, the binary is built. For this, we use the command
module – but it has no idea whether the desired state is already met or whether the command has to be run again. We manually tell it to skip this task if the repository did not change in the previous task.
There is a minor gotcha with this: Assume you abort the playbook while its running the build task because you forgot to do something else. If you run it again, the git update will be skipped, as it is already in its desired state. But now the build task will not be run again. If you have this in your playbooks, you need to be aware of this edge case.
This is another reason why logical links in playbooks should be kept to a minimum.
There is also the shell
module that feels very similar. But in contrast to command
, it runs the specified command in a shell. This opens up the use of shell features like piping, but comes at an increased security risk, especially if building the command from variables.
Using shell
is not recommended.
To make command
report success, failure or changed properties depending on other conditions than just the return code, use failed_when
, changed_when
and ignore_errors
.
Testing things
- syntax check with
yamllint
andansible-lint
- on real targets (e.g. different host files to not mess up production stuff)
- in containers or VMs
- Vagrant (see below: Further reading)
Keeping secrets
It is common practice to store important variables alongside playbooks and roles in even public repositories by encryping them with Ansible Vault.
These are production secrets, but never management access to the target machines. Jump hosts are one popular solution to grant access to many machines to various actors: Only this host stores the admins pubkeys, while the target machines only have the key from jump host.
Roles
Roles are re-usable playbooks that come with default variables, files and can be shared in collections using Ansible Galaxy. Roles are even easier to develop and test using Molecule to automate different testing stages (linting, provisioning, testing for idempotence, testing for side effects).
interesting stuff
- create container & deploy (example from docs.ansible.com)
- name: Create a jenkins container
community.general.docker_container:
docker_host: myserver.net:4243
name: my_jenkins
image: jenkins
- name: Add the container to inventory
ansible.builtin.add_host:
name: my_jenkins
ansible_connection: docker
ansible_docker_extra_args: "--tlsverify --tlscacert=/path/to/ca.pem --tlscert=/path/to/client-cert.pem --tlskey=/path/to/client-key.pem -H=tcp://myserver.net:4243"
ansible_user: jenkins
changed_when: false
- name: Create a directory for ssh keys
delegate_to: my_jenkins
ansible.builtin.file:
path: "/var/jenkins_home/.ssh/jupiter"
state: directory
Further reading
- Ansible docs: Group tasks to blocks: https://docs.ansible.com/ansible/latest/user_guide/playbooks_blocks.html
- Ansible docs: Guide on using Vagrant with Ansible: https://docs.ansible.com/ansible/latest/scenario_guides/guide_vagrant.html
- Vagrant can use various virtualization backends
- Images ("boxes") are available for specific backends; when running Vagrant with QEMU/KVM, you cannot use "ubuntu/bionic64", use "generic/ubuntu2004"
- https://ostechnix.com/how-to-use-vagrant-with-libvirt-kvm-provider/
- another Ansible beginners tutorial that looks easy to start with: https://learnxinyminutes.com/docs/ansible/
- an article on using Ansible to spawn and destroy Podman containers: https://fedoramagazine.org/using-ansible-to-configure-podman-containers/
- Ansible roles/playbooks to deploy the server in our current configuration or test locally: https://gitlab.fachschaften.org/minetest/minetest_scripts
- Ansible roles by TU Dortmund: https://gitlab.fachschaften.org/fsi-ansible
- ↑ Though it may require system specific modules
- ↑ We cannot just write
mode: 644
though, because directories need to be marked as executable to list their contents – they should have mode 755. Just as on the command line usingchmod
, thefile
module also accepts the syntax shown in the example. Using the bigX
instead if the small one means the executable bit is only set for directories or if the bit was already set before. - ↑ not changing stuff if the desired state is already met