Introduction
In this document we describe a possible naming convention, based on experience in larger environments. At first when you start creating playbooks, there seems to be no reason to have a naming convention. This is a pittfall a lot of companies fall into.
You use desciptive names and all plays are easily found in the repository.
But when the complexity grows, you might lose oversight and new colleagues might get lost in the woods of the playbook repositories. Documentation is also an issue where some organizations struggle with, where to document the code and where to doument for the usage (user doc). We will give you some pointers, but as every orginization is different in its requirements, we will not tell you this is the only way.
We expect you to have some experience with ansible, because we will not explain the inner workings of ansible here, but will explain how to keep your code manageble, even in larger setups. Not only repository naming is important, also variables, tags and templates. There are possibly many more, but we will get there when we get there.
In the next chapters we will dig in deeper on each subject.
Best practice for Ansible play and role handling
When you start using ansible you write a play to (for example) configure a server for use in the profuction environment for the enterprise. You could so this as a single play. This playbook can grow to be enormous in functionality and subsequently in number of lines. To prevent this, roles are created, this breaks up the the main play into manageble chunks of code, each with a dedicated function for the main play.
You could also use different files in the play for this, but this has the disadvantage that code is not reusable and might get copied into other playbooks, needing maintenance when the code changes (believe me, it will!).
Example 1
As an example, we have this playbook in a repository that will configure the server for production use. The play is not in one file, but broken into separate files, to deliver the functionality as a whole. We will not show you the contents of the whole play, that is not the target of this document.
configure_server (repository)
audit.yml
cups.yml
firewall.yml
grub.yml
ldap.yml
main.yml
packages.yml
satellite.yml
selinux.yaml
sudoers.yml
time_sync.yml
sshd.yml
users.yml
templates (directory)
sudo.j2
sshd.j2
grub.j2
audit.j2
README.md
SVD.md
As you see in the above example, the play consists of many files, a change to one file, is a change to the complete play and with many people working on th same play, this can become cumbersome.
As this is just a small example, you can imagine that changing and testing this becomes more problematic as the code grows. Reuse of code is not possible, without copying the file to a new repository and with every copy the locations to maintain a piece of code is added, and thus a risk for runnning old code that is not changed when the original code was changed.
Documenting this play is also a problem, the documentation is huge, if done in depth, and maintaining that will be error prone or things will get left out, making the documentation unuseable.
So how do we solve this and still keep the overview over all code repositories?
Breaking code up in roles
Now we will breakup the code into roles, this enables us to manage the individual parts seperately.
But here it comes....
If you just give the roles the name of the file in the original playbook, you will eventually get namespace issues and you can't distingish by name, what repository holds a role and what repository holds a playbook.
Definition
A play is a complete set of tasks that will do all configuration for an item (eg. a server in an environment). A role is a set of tasks that will do the complete configuration for a subitem of a play.
Using this best practice will help you to keep track of all code..
Best practice
We start with the playbook repositories, we will describe this here:
A playbook has a specific function (eg. configure_rhel_server or something like that).
So we start with this convention fo example:
pl_rhel_configure_server
The this is based on the following:
- pl: This is a play (obviously)
- rhel: The os/middleware name this play installs/configures
- last part: is a descriptive shortname for the functionality
The roles will then be named as followes:
rl_rhel_role_name
This is the based on the following: - rl: This is a role (and cannot be run as play, but as part of one or more) - rhel: The os / middleware in which this role has a role to play - last part: The descriptive name for the part this role is implementing
Important
When you have a play that incorporates a lot of roles, the play book itself should have no functionality in it, other than directing the roles, so the determins if a role is being run or not. It does no changes on the systems it runs against.
This keeps the household clean...
Why this naming convention
Then naming convention help to keep thing organized in larger environments and over time. When you haven't worked on your code for a long time, you probably won't remember all relations between plays and roles. Lets take a closer look at the git repositories when the naming convention is applied.
Example 2 (git project overview)
pl_rhel_config_server
pl_apache_web_server
rl_apache_certificate
rl_apache_content
rl_apache_packages
rl_rhel_audit_config
rl_rhel_cups_config
rl_rhel_firewall
rl_rhel_grub
rl_rhel_ldap
rl_rhel_packages
rl_rhel_satellite_registration
rl_rhel_selinux
rl_rhel_sudoers
rl_rhel_time_sync
rl_rhel_sshd_config
rl_rhel_local_users
In this example, there are only 2 play with a lot of roles, but because of the naming convention, you instantly know which roles belong to which plays.
You can see, when the number of plays increases, that this becomes important.
Maintenance with a larger team is also easier, you can work independently on multiple parts of the code (roles), without merge conflicts.
Documentation is also easier, as each role is documented in its own repository, making it easier to read and maintain. As a result, documentation is more acurate.
Git
As you will be storing these versioned plays and roles into git, create groups in git in which teams with the same responsibilities work together, this will help you to organiza the work even better.
File naming
This is the part of the naming convention that most developers and operators need the least amount of help. This is almost natural to them. Therefore, we will just give some pointers. As all ansible code is broken into roles ansible galaxy dictates most of the files to be named main.yml in the directory structure it dictates. But we still have to add templates, files and maybe playbooks to get all the code functionality in there.
But there a a few files that escape the fixed structure, like "templates", "task_files" and "files".
Templates
As you already know, templates must have a fixed extension, so we won't bother you with that. In a play, there can be a lot of templates used for the configuration of an application. In the execution output of a play, you will not se the template beeing used, but you will see the task name of the task applying the template. Keep those as close as possible to
eachother, when a play fails, you can identify the template that failed, from the task name that failed. This can help to shorten a time consuming search.
Keep template names as close as you can to the file they create:
- httpd.conf.j2 - creates - httpd.conf
task_files
Many times, you must loop over a number of tasks in an single loop, the way for this is to include a tasks file, name this file to describe the loop this is included in.
Name the task that includes the file to that task at hand:
- name: Include loop_for_multi_tasks.yml
ansible.builtin.include_tasks:
file: loop_for_multi_tasks.yml
loop: "{{ some_var }}
This way, when thing fail, you know where to look.
Files
Most files thet will be copied are supplied to us developers and must not be changed. That is the case for when they land on the system, not in how they reside in our code. The most common thing with those files is, remembering where they came from, or who supplied them. Sometimes it is best to have that name added to the filename in our repositories and copy it to its original name on the system.
Best practice
Be sure the whole team is using the same naming schema, so there is never a doubt to a name of any kind.
Variable naming
This is maybe the most important part of the naming convention. Variable naming is important so that there will be no namespace conflicts with unexpected results as a side effect. When variable naming is not done correctly, the contents of a variable might not always be what you expect it to be, when it is overwritten by another task.
Think of the inventory variables in ansible as layers of a cake, but when the name is reused, they will overwrite eachother!
Variables that you define at runtime must be unique, or you might overwrite a value that is needed elsewhere and will give unexpected results.
you want distinct variables so it looks nice and they won't interfere with eachother.
This will make maintenance much easier as you know you won't have to look for a variable with the same name anywhere else. It will make code reuse
easier. Implementing this needs to get some more thought, but it will save time in the end.
How do we create unique variables
As we breakup the code into pieces, we lose some oversight over variable names. Next a few rules to ensure variable names will be unique:
Naming convention
All variable names should conform to the following naming convention.
Role variables
Variables orgiginating and used in a role, should start with the role name prefix, if you use ansible-lint on a role, you will get an error is you don't.
So for the role:
role_do_something:
- name: set role_vars
ansible.builtin.set_fact:
role_do_something_varname1: "This is the first unique var"
This way the variable name is always unique, as long the role_name is unique in a play.
When you create roles for almost every action you do in a play, a play will just be a collection af roles to run.
other variables
If you need additional variables, refer below, where every variable gets a name with the structure mentioned, so we can see what is is used for and where it comes from.
<start>_<middle>_<name>
Where <start> should refer to the provenance of the variable like:
- inv for inventory
- ply for play
Where <middle> should refer to the product for which the variable is appropriate.
Where <name> should be a descriptive name for the variable.
Inventory variables
The very top layer is the inventory, here the variables are difined on the global
level, these variables are availlable for each play on a host, depending on the
grouping of these variables in your inventory.
The inventory variables should be well documented and never be overridden in a play,
if this should be needed anywhere, you should check your inventory for errors.
Temporary variables
Tempory variables that are created and used inside a play or a role, such as registered
values and loop variables should also be unique, within the role or play.
Registered values
A registered value should alway start with an underscore and must be unique for the play.
Because this is a variable that will not be used outside the playbook, there is no further requirement.
Loop variables
Loop variables should never be "item", but start them with loop_ followed by a descriptive name.
This ensures the uniqueness for the loop_var and will reduce namespace conflicts.