Ansible-universe is an Ansible role build tool implementing the following features:
- Role syntax check
- Proper
README.md
generation - Role linter implementing best practices
- Packaging & publishing into private web (DAV) repositories
- Proper
tasks/main.yml
generation, with platforms runtime check
The following build targets are supported:
-
init
instantiate role template -
show
show role information -
dist
generate ansible distributable role files -
clean
delete all generated files -
check
include role in a dummy playbook and check syntax -
package
package role -
publish
publish role to a web repository
The following lifecycles are supported:
init
show
clean
-
publish
<package
<check
<dist
Ansible-universe uses the ansible-galaxy build manifest (meta/main.yml
) with extra attributes:
-
prefix
, variable prefix, defaults to rolename -
version
, defaults to 0.0.1 -
variables
, maps names to descriptions -
include_when
, mapstasks/
filenames to include conditions -
usage_complement
, optional, additional usage instructions -
maintenance_complement
, optional, additional maintenance instructions
Tutorial
For this tutorial, we're going to work on a simple use-case:
a provisioner for a nginx
service.
A role is a directory containing various assets, the first step is therefore to create that directory:
$ mkdir nginx
Let's initialize the role with Ansible-universe:
$ ansible-universe -C nginx init
The init
target creates a dummy (ansible-galaxy) role manifest: meta/main.yml
.
This manifest is also the build manifest for Ansible-universe.
This is actually the only required file for distributing a role.
Take some time to edit this file:
- set the author (you)
- set a description
- select supported platforms (Debian and Ubuntu in our case)
- etc.
You are then free to fill-in the other directories depending on your role. Remember only 8 sub-directories are specified, for further details, please check the Directory Layout section of the best practices, in the appendix.
As for this tutorial, we only need the tasks/
sub-directory.
This directory contains a single file for now, named nginx.yml
:
$ cat > tasks/nginx.yml <<EOF
---
- apt:
name: nginx
state: present
- service:
name: nginx
state: started
enabled: yes
-
EOF
Let's call Ansible-universe to generate and check everything:
$ ansible-universe -C nginx check
generating nginx/tasks/main.yml
generating nginx/README.md
playbook: playbook.yml
ERROR: expecting dict; got: None, error in /tmp/nginx/tasks/nginx.yml
** WARNING: syntax error
source: role 'nginx'
flag: syntax
** WARNING: missing 'name' attribute, please describe the target state
source: task 'nginx.yml[#2]'
flag: task_has_name
As indicated in the lifecycle
section, the check
target implies dist
, which is called first.
On dist
, two files are generated:
-
tasks/main.yml
, performing the platform check and including any other YAML file intasks/
. Conditions to inclusions can be specified via theinclude_when
attribute of the manifest. -
README.md
, gathering the role description, supported platforms and data on variables.
On check
, all checks are run, and in the above example, 2 warnings were raised.
The fact that ERROR is printed and then WARNING is a tad confusing.
What's happening is that the ansible-playbook
subprocess is terminating on an error and print it.
This is caught by Ansible-universe as a more generic warning stating that the syntax is incorrect.
A syntax error was detected, let's fix it by removing the last dash in tasks/nginx.yml
.
The other warning says that we didn't describe one of our tasks, add a name attribute to fix it.
Re-run Ansible-universe, you should get the following layout with no warning:
$ tree nginx/
nginx/
├── meta
│ └── main.yml
├── README.md
└── tasks
├── main.yml
└── nginx.yml
Your role is now ready to be distributed.
If you're using a VCS as repository, simply commit and push the files,
but remember to exclude (e.g. in .gitignore
) the build byproducts (*.hmap
, .build
.)
If you're using a web repository, proceed as follow (set a working repository URL beforehand):
$ ansible-universe -C nginx publish -r http://somewhere
generating nginx/.build/nginx-0.0.1.tgz
./README.md
./meta/main.yml
./tasks/main.yml
./tasks/nginx.yml
publishing nginx/.build/nginx-0.0.1.tgz to http://somewhere
Installation
$ pip install --user ansible-universe
To uninstall:
$ pip uninstall ansible-universe
Development Guide
The built-in linter can easily be extended with your own checks:
- in the universe directory, create a new module defining the
MANIFEST
global dict - in
__init__.py
, register that new module by its name in theMANIFESTS
dict
In your module, the MANIFEST
dictionary shall contains the following attributes:
- Required
type
: eitherrole
,variable
,subdir
ortask
. This specify the objects that will be passed to the predicate below. - Required
predicate
: a callback with two parameters (object, helpers) and returning a Boolean.helpers
is a dictionary containing various functions and objects:role
,marshall()
. - Required
message
: the message to display when the predicate is violated. - Optional
flag
: default to module name, symbol used with the-W
command line option.
Ansible Best Practices
PLAYBOOKS
Playbook Interface
Always assume your playbook users are not developers. Design your playbooks to be configurable through groups and variables. Users will use those in the inventory and varfiles. The inventory and varfiles are expected to be created/edited, but making your user modify a playbook is a design mistake.
Directory Layout
5 sub-directories are specified for a playbook:
group_vars/
host_vars/
library/
filter_plugins/
roles/
If you need additional assets, such as files or templates, then define a role to manage them.
Dependencies
Roles specific to your playbook have to be shipped with it.
3rd-parties dependencies have to be registered into the requirements.{txt,yml}
file
and resolved by ansible-galaxy
.
ROLES
If you need a piece of provisioning more than once, re-design it as a role. Roles are to Ansible what packages are to your platform or programming language.
Design
Role Interface
You can safely assume that role users are actually developers, as using them requires some more advanced Ansible skills. Design your roles to be configurable through variables.
Role Repository
Make your roles available either on a public VCS or on a public web repository. Prefer web repository to avoid access control issues. Indeed, contrary to other modern build stacks (e.g. Java, Python, Docker...), Ansible does not offer any kind of package repository; instead roles are simply “published” as code repositories (e.g. on github, etc.) This entails major issues:
- Unexpected Access Restrictions Code repositories are designed to enforce fine grained access control from the ground up; this means you have to explicitly be given access to a repository to use it. Package repositories have the opposite behavior: once published, a package is accessible to anyone (e.g. apt-get install, pip install...) except if configured otherwise (opt-in.) The intent is clear: on publication, the package is made available to everyone. Therefore, using a code repository as a package repository violates the UX principle of least surprise.
-
No Version Ordering & Expressions
A VCS uses an internal versioning system (e.g. a hash for git)
which is not related to the “human readable” version strings (e.g. “v1.0.0”.)
Version strings are generally tag names created manually by developers when necessary.
As version strings are unusable by git and by extension, by ansible-galaxy,
version expressions cannot be used when specifying a dependency.
With other build tools, it is common for instance to request an compatible version of a dependency
within a range and exclude a subset of releases known as broken, e.g.
>=2.1, <4a0, !=2.4rc, != 2.5rc
- Inefficient Binary Resources Storage A playbook may contain binary resources (e.g. images or pre-compiled bytecode.) Storing those resources into a code repository is a bad practice (REF?.)
Dependencies
3rd-parties dependencies have to be registered into the dependencies
attribute of the role manifest.
Isolation
Keep roles self-contained. Having shared variables between two roles is a design mistake. Instead make your roles configurable via variables and handle integration issues in the corresponding play.
Formal Requirements
The following requirements are all validated by default via the check
target:
$ cd myrole
$ ansible-universe check
You can switch on only the ones you're interested in with the -W<flag>,…
option.
-Wmanifest
Manifest, Make sure your role manifest contains the required information for publication:
- Version (extended attribute),
- Authors
- License
- Description
- Supported platforms
-Wreadme
Documentation, Given a playbook or a role, if groups or variables are not documented, they are non-existent as,
unfortunately, Ansible (as of version 1.9.2) has no native mechanism to probe them.
The documentation (generally the README.md
file) is therefore the only learning medium for the end-users.
Make sure your documentation it is up-to-date.
-Wnaming
Naming, Prefix (bis, rebis) all your playbook groups, playbook variables and role variables by a short and unique ID. Ideally the playbook name if it fits. Ansible only has a global namespace and having two identical variables will lead one to be overwritten by the other. This is also true for handler names.
-Wlayout
Directory Layout, Do not add any custom sub-directory to a role or playbook, this would lead to undefined behavior: at any point in a future version, other sub-directories might be needed by Ansible and if they are already used by your role for anything else, this will break.
As of Ansible version 1.9.2, 8 sub-directories are specified for a role:
defaults/
files/
handlers/
meta/
tasks/
templates/
vars/
library/
-Wtask_has_name
Name task, Make the intent of each task explicit by setting its name
attribute.
-Wtask_has_no_remote_user
Do not set a Remote User, It's tempting to always assume your playbooks/roles are run as root and to enforce it by setting remote_user
.
This is a bad idea as your users might want to use another user that has equivalent privileges (e.g. via sudo.)
The root account is often disabled on modern systems for security reasons.
If you need an explicit user for a given task, use sudo_user: <name>
and sudo: yes
.
-Wowner
On copy/template, Set a owner, If no owner is specified on a copy
or a template
task, the current user UID will be used.
But that user can change and so will the file owner depending on who is running the playbook.
This violates the idempotence rule.