# Conventions ## Ansible managed, or not? When a server has just a few modifications made via Ansible, it's often enough to put a block marker (if just a section of a file is managed by Ansible) or a `{{ ansible_managed }}` tag at the top of the files (if this file is fully managed by Ansible). When a server is fully managed by Ansible, it's even better to use the `ansible-managed` role. It can be found in the **ansible-roles** repository. For the moment it only changes the "Message Of The Day" file (in `/etc/motd`). Don't forget to customize the `project_repository` variable ; it is used in this file. ## Roles We can use the `ansible-galaxy init` command to bootstrap a new role : ``` $ ansible-galaxy init foo - foo was created successfully $ tree foo foo ├── defaults │   └── main.yml ├── files ├── handlers │   └── main.yml ├── meta │   └── main.yml ├── README.md ├── tasks │   └── main.yml ├── templates ├── tests │   ├── inventory │   └── test.yml └── vars └── main.yml ``` All `main.yml` file will be picked up by Ansible automatically, with respect to their own responsibility. The main directory is `tasks`. It will contains tasks, either all in the `main.yml` file, or grouped in files that can be included in the main file. `defaults/main.yml` is the place to put the list of all variables for the role with a default value. `vars` will hold files with variables definitions. Those differ from the defaults because of a much higher precedence (see below). `files` is the directory where we'll put files to copy on hosts. They will be copied "as-is". When a role has multiple logical groups of tasks, it's best to create a sub-directory for each group that needs files. The name of files in these directories doesn't have to be the same as the destination name. Example : ``` copy: src: apt/jessie_backports_preferences dest: /etc/apt/apt.conf.d/backports ``` `templates` is the twin of `files`, but differs in that it contains files that can be pre-processed by the Jinja2 templating language. It can contain variables that will be extrapolated before copying the file to its destination. `handlers` is the place to put special tasks that can be triggered by the `notify` argument of modules. For example an `nginx -s reload` command. `meta/main.yml` contains … well … "meta" information. There we can define role dependencies, but also some "galaxy" information like the desired Ansible version, supported OS and distributions, a description, author/ownership, license… `tests` and `.travis.yml` are here to help testing with a test matrix, a test inventory and a test playbook. We can delete parts we don't need. ### How much goes into a role We create roles (instead of a plain tasks files) when it makes sense as a whole, and it is more that a series of tasks. It often has variables, files/templates, handlers… ## Syntax ### Pure YAML It's possible to use a compact (Ansible specific) syntax, ``` - name: Add evomaintenance trap for '{{ user.name }}' lineinfile: state=present dest='/home/{{ user.name }}/.profile' insertafter=EOF line='trap "sudo /usr/share/scripts/evomaintenance.sh" 0' when: evomaintenance_script.stat.exists ``` but we prefer the pure-YAML syntax ``` - name: Add evomaintenance trap for '{{ user.name }}' lineinfile: state: present dest: '/home/{{ user.name }}/.profile' insertafter: EOF line: 'trap "sudo /usr/share/scripts/evomaintenance.sh" 0' when: evomaintenance_script.stat.exists ``` Here are some reasons : * when lines get long, it's easier to read ; * it's a pure YAML syntax, so there is no Ansible-specific preprocessing * … which means that IDE can show the proper syntax highlighting ; * each argument stands on its own. ## Variables ### defaults When a role is using variables, they must be defined (for example in the `defaults/main.yml`) with a default value (possibly Null). That way, there will never be a "foo is undefined" situation. If a variable can't have a default value, it must be marked as [mandatory](http://docs.ansible.com/ansible/playbooks_filters.html#forcing-variables-to-be-defined). Example : ``` - name: Setting default timezone lineinfile: line: "{{ evolinux_system_timezone | mandatory }}" ``` ### progressive specificity In many roles, we use a *progressive specificity* pattern for some variables. The most common is for "alert_email" ; we want to have a default email address where all alerts or messages will be sent, but it can be customized globally, and also customized per task/role. For the *evolinux-base* role we have those defaults : ``` general_alert_email: "root@localhost" reboot_alert_email: Null log2mail_alert_email: Null raid_alert_email: Null ``` In the *log2mail* template, we set the email address like this : ``` mailto = {{ log2mail_alert_email or general_alert_email | mandatory }} ``` If nothing is customized, the mail will be sent to root@localhost, if general_alert_email is changed, it will be used, but if log2mail_alert_email is set to a non-null value, it will have precedence. ## precedence There are multiple places where we can define variables and there is a specific precedence order for the resolution. Here is [the (ascending) order](http://docs.ansible.com/ansible/playbooks_variables.html#variable-precedence-where-should-i-put-a-variable) : * role defaults * inventory vars * inventory group_vars * inventory host_vars * playbook group_vars * playbook host_vars * host facts * play vars * play vars_prompt * play vars_files * registered vars * set_facts * role and include vars * block vars (only for tasks in block) * task vars (only for the task) * extra vars (always win precedence) ## Configuration patterns ### lineinfile vs. blockinfile vs. copy/template When possible, we prefer using the [lineinfile](http://docs.ansible.com/ansible/lineinfile_module.html) module to make very specific changes. If a `regexp` argument is specified, every line that matches the pattern will be updated. It's a good way to comment/uncomment variable, or add a piece inside a line. When it's not possible (multi-line changes, for example), we can use the [blockinfile](http://docs.ansible.com/ansible/blockinfile_module.html) module. It manages blocks of text with begin/end markers. The marker can be customized, mostly to use the proper comment syntax, but also to prevent collisions within a file. If none of the previous can be used, we can use [copy](http://docs.ansible.com/ansible/copy_module.html) or [template](http://docs.ansible.com/ansible/template_module.html) modules to copy an entire file. ### defaults and custom files We try not to alter configuration files managed by packages. It makes upgrading easier, so when a piece of software has a "foo.d" configuration directory, we add custom files there. We usually put a `z-evolinux-defaults` with our core configuration. This file can be changed later via Ansible and must not be edited by hand. Example : ``` copy: src: evolinux-defaults.cnf dest: /etc/mysql/conf.d/z-evolinux-defaults.cnf force: yes ``` We also create a blank `zzz-evolinux-custom` file, with commented examples, to allow custom configuration that will never be reverted by Ansible. Example : ``` copy: src: evolinux-custom.cnf dest: /etc/mysql/conf.d/zzz-evolinux-custom.cnf force: no ``` The source file or template shouldn't to be prefixed for ordering (eg. `z-` or `zzz-`). It's the task's responsibility to choose how destination files must be ordered. ### check mode Ansible can run playbooks in "check mode" with the `--check` argument of `ansible-playbook`. It won't do any changing action but will report what would be changed or not. When an action depends on a variable registered by a previous action (that performs no change, like a grep, stat…) we should add a `check_mode: yes` (`always_run: yes` in Ansible <= 2.1). Example : ``` - name: Check if Munin plugins exists stat: path: /etc/munin/plugins/ register: munin_plugins_dir check_mode: no - name: Get Munin plugin copy: src: munin/drbd-plugin dest: /etc/munin/plugins/drbd mode: "0755" when: munin_plugins_dir.stat.exists notify: restart munin-node ``` ### packages installation When making a role or a task the necessary packages must be installed explicitly. For example for the "mysql" role we obviously need the MySQL packages, but we also need the "apg" package to generate new passwords. This package is installed by "evolinux-base" but the "mysql" role can be executed on a fresh server. ### merge arrays Some roles need to have an array of values in a variable. For example, any roles use a list of trusted IP addresses (firewall, http auth, ssh whitelist…). It this array needs to include some values from a late file inclusion (from var_files, cli argument…) it becomes impossible to merge with another variable. The workaround is to have 2 different default variables (eg. `evolix_trusted_ips` and `additional_trusted_ips`), witha default value of `[]` and merge them into the final variable. One of the variables (typically `evolix_xxx`) can be "hardcoded" in a vault and the final array remains extensible. Example from the minifirewall role (with a final default value) : ``` evolix_trusted_ips: [] additional_trusted_ips: [] # Let's merge evolix_trusted_ips with additional_trusted_ips # and default to ['0.0.0.0/0'] if the result is still empty minifirewall_trusted_ips: "{{ evolix_trusted_ips | union(additional_trusted_ips) | unique | default(['0.0.0.0/0'], true) }}" ``` ## Caveats ### Unix permissions must be written as String values Many modules have a `mode` attribute to specify the permissions on the files or directories. We can use the symbolic notation (`u+rwx` or `u=rw,g=r,o=r`) or the octal notation (`0755`). It is clearly documented that when using the octal notation a leading 0 must be present, but it is not clearly documented that we **MUST** use a String format, and not a Numeric format (or whatever Python types). Examples : * `mode: 755` → Bad! * `mode: 1777` → Bad! * `mode: "0755"` → Good * `mode: "1777"` → Good This is most probably due to the way Python deals with numeric values and octal vs. decimal based integers. The String type guarantees that the proper value is used.