KIF490:Einführung in Ansible (einfach wartbare, reproduzierbare Systemkonfigurationen)

Aus KIF
Die druckbare Version wird nicht mehr unterstützt und kann Darstellungsfehler aufweisen. Bitte aktualisiere deine Browser-Lesezeichen und verwende stattdessen die Standard-Druckfunktion des Browsers.

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 and ungrouped
  • 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 task
    • when: conditionals to skip the task if not met
    • tags 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 and ansible-lint
  • on real targets (e.g. different host files to not mess up production stuff)
  • in containers or VMs


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

- 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


  1. Though it may require system specific modules
  2. 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 using chmod, the file module also accepts the syntax shown in the example. Using the big X instead if the small one means the executable bit is only set for directories or if the bit was already set before.
  3. not changing stuff if the desired state is already met