Hi Maciej
Thanks a lot for this detailed explanation. I already had a glimpse at
the Gitlab-CI pipeline yesterday and I can only say that this is super
awsome. I haven't looked into Jane yet, but your description and the
results you achieved with it sound great. Thanks a lot once more for
this great effort you're investing in DebOps and with that into a
reproducible and scalable Debian-based server infrastructure setup.
I hope you have relaxing holidays and I'm looking forward to functional
testing of the Ansible roles in 2018 :-)
Cheers,
ganto
On Fri, 2017-12-15 at 00:29 +0100, Maciej Delmanowski wrote:
Hello everyone,
The tl;dr version
-----------------
Today I pushed the code of the new test suite for the DebOps monorepo
that
i've been working on for the last three weeks or so. The test suite
uses
GitLab CI, Vagrant, KVM and LXC to test DebOps roles and playbooks in
different configurations. You can check out the results here:
https://gitlab.com/debops/debops/pipelines/15127360
The DebOps repository on GitLab is automatically mirrored from the
one on
GitHub, to perform the role tests using the new GitLab CI pipeline.
Along
with it, you can check out the DebOps tests performed on Travis-CI:
https://travis-ci.org/debops/debops
The code on Travis is used to lint the code in the repository (YAML,
Python
and soon Bash source code, reStructuredText documentation based on
Sphinx), as
well as test if the DebOps Docker image is correctly built before the
new
changes are pushed to Docker.
How DebOps was tested in the past
---------------------------------
Previous iteration of DebOps, which used separate repositories for
each
Ansible role and the set of playbooks, relied on Travis-CI to perform
the code
tests. The test suite was created by me and Nick Janetakis around
2015 and
used a central git repository to store the test suite itself;
Rolespec,
written by Nick, was an application that performed the testing. The
role
repositories themselves didn't have any test code in them -
centralization
allowed for a common test environment for all roles and made the role
commits
a bit cleaner since any issues in the test suite could be resolved in
its own
repository.
Unfortunately with the merge of the project repositories to a single
monorepo
this system had to be abandoned - Travis-CI has a short time limit
for a test,
which could be occasionally exceeded by the larger roles like
'debops.gitlab',
etc. One option to test the monorepo was usage of the Travis build
matrix,
however a limit of 200 separate builds in this system meant that with
currently ~120 roles it will quickly be not enough, not to mention
that using
build matrix in this way means abusing free resources provided by
Travis,
which I really wanted to avoid.
Therefore I decided to rely on Travis-CI only for the code linting
and
ensuring that the monorepo was "sane" enough to work. The testing of
the
individual roles and their combinations in playbooks had to be done
separately.
Current state of the DebOps tests on Travis-CI
----------------------------------------------
The main project repository on GitHub is hooked up to the Travis-CI
public
infrastructure to perform automatic tests of any new pull requests.
Because if
this, it's a good place to validate the incoming code to quickly find
simple
errors. At the moment the Travis-CI performs the following tests:
- PEP8 compliance of the Python scripts is tested using
'pycodestyle'. At the
moment the result of this test is ignored by DebOps due to massive
amount of
errors currently present in the various Python scripts in the
repository.
When everything is cleaned up, this test will be enabled and the
PEP8
compliance will be enforced.
- test of the 'debops-tools' scripts using nose2 package and existing
set of
tests written by Hartmut Goebel, back when they were in the
separate
repository.
- Build of the DebOps documentation located in the 'docs/'
subdirectory, based
on reStructuredText, Sphinx and ReadTheDocs. This test should
ensure that
the documentation available on
https://docs.debops.org/ will be
rendered
correctly. Currently the documentation included in the monorepo is
not
published there yet, the role documentation needs to be moved in
place.
- Ansible playbook syntax check performed on the 'site.yml' playbook
which
includes almost all other Ansible code. This test should catch
simple issues
with the Ansible code before more involved testing is performed. At
the
moment there's massive amount of warnings due to Ansible not
finding
inventory groups defined in the playbooks; the Ansible developers
already
know about this issue and hopefully this will be fixed in the
future.
- YAML syntax of the entire project is tested using 'yamllint' with
some
relaxed configuration that should account for the style of the YAML
used in
DebOps. At the moment the result of this test is ignored due to
massive
amount of errors in YAML files all over the code base. When they
are fixed,
this test will be enforced.
- The syntax of the Dockerfile is checked by the 'hadolint' script to
ensure
that the changes to it will result in an usable container image.
- The 'debops/debops' Docker image is built locally on Travis-CI to
check if
new code affects the build (official DebOps Docker images are
automatically
rebuilt when new commits are accepted in the monorepo). This also
serves as
the sanity test; the 'debops.core' role is executed inside of the
created
Docker container to check if Ansible stack works correctly.
- At the end, the test suite checks if the DebOps repository contains
any
changes or untracked files not accounted by the '.gitignore' file
to ensure
that the repository stays in a pristine state.
The above test suite is accessible locally using the Makefile
targets, as long
as the required applications are available on the host. You can run
the 'make
help' command in the root of the DebOps monorepo to see available
Makefile
targets; executing the 'make test' command will perform all of the
above
tests. The test suite will check if Docker is available locally and
if not,
the Docker tests will be skipped; other tests will result in an error
if the
required applications are not available.
One thing I definitely plan to add to the above test suite is using
'shellcheck' to test all of the Bash scripts present in the DebOps
monorepo.
If you have any other useful suggestions for what to add to the test
suite at
this stage, let me know.
GitLab CI to the rescue
-----------------------
What remained to implement was testing of the DebOps roles and
playbooks
themselves in a real environment, as similar to the actual production
environment as possible - that means separate "hosts", real DNS
domain,
networking, usage of Debian instead of Ubuntu as a test platform.
Due to the scale of the project, writing custom software to manage
all of the
tests was out of the question. Thankfully, GitLab provides a ready to
use
Continuous Integration in the form of Gitlab CI (integrated in the
main
application) and separate GitLab Runners which perform the actual
operations.
Choosing GitLab for this task was an easy choice. DebOps includes
'debops.gitlab' and 'debops.gitlab_runner' roles for a long time,
which allows
an easy installation of a local development environment. This is
important
because the tests used by DebOps are performed using the 'shell'
executor,
access to which should be tightly controlled (see below for longer
explanation). Easy deployment of GitLab Runners allows for simple
scaling if
necessary, which is a plus with a large number of tests.
Due to the recent changes in the GitLab licensing model, the Debian
Project
plans to replace its existing community collaboration platform,
Alioth, with
a GitLab instance. Choosing the same development platform should
ensure, that
inclusion of DebOps in the new collaboration platform should be easy
to
perform, if and when that happens.
Unfortunately, the GitLab feature set available at the time
restricted how the
new DebOps test suite could be designed. GitLab supports several
different
"executors" which can be used to perform operations on the Runner
hosts. Some
of them (Parallels, VirtualBox, SSH and Kubernetes) were unsuitable
right from
the get go, because I didn't use the mentioned virtualization
solutions, or
remote execution via SSH was unnecessary.
That left the Docker and shell executors. Docker executor is very
handy
- installing Docker on the GitLab Runner and providing the Runner
UNIX account
to the Docker daemon is enough to have a complete testing solution
with good
isolation for test jobs. Unfortunately, Docker itself is not
sufficient to
test all of the DebOps roles, some of which operate on low-level host
functionality like network interfaces, bootloader, LVM, etc.
Additionally,
Docker containers are designed to run only one application at a time,
where
DebOps roles usually manage multiple services at once to provide
desired
functionality, for example a database engine, web server and the
application
itself. Using Docker for testing DebOps roles would severly limit
available
options - DebOps is focused on management of the real hosts,
therefore
a suitable alternative was required, either LXC containers or virtual
machines.
This left the 'shell' executor, which provides full access to the
UNIX
environment on the GitLab Runner host. The runner itself is executed
using an
unprivileged user account (at least that's how the DebOps role sets
it up),
however managing LXC containers or virtual machines requires elevated
privileges. This is risky and with wrong deployment can lead to a
security
breach, where anonymous users can put malicious code in the git
repository and
create a pull request, which gets automatically executed on GitLab
Runners.
Due to that security vulnerability, direct access to the GitLab tests
is not
possible - only code reviewed by DebOps maintainers and accepted in
the GitHub
repository gets pulled by GitLab and executed in the CI pipeline. The
equivalent feature on GitLab, merge requests, are disabled in the
'debops/debops' repository. This shouldn't be a huge issue, since the
whole
test environment can be self-hosted and interested users can deploy
it on
their own infrastructure if they so wish.
The 'shell' executor works directly on the GitLab Runner host, and
running
DebOps playbooks directly on it would quickly make that host
unusable.
Configuring GitLab Runners in each LXC container or a virtual machine
was
unsuitable because that would require adding them over and over as
runners in
GitLab CI. Because of that, an additional layer of abstraction was
required.
Vagrant brings in isolation
---------------------------
Vagrant provides this form of abstraction, which can be easily used
from the
command line, and therefore automated. Fortunately, Debian Stretch
release
includes packages for 'vagrant-lxc' and 'vagrant-libvirt', which make
the
whole solution almost entirely native to Debian. The Vagrant libvirt
plugin
can manage KVM hosts through libvirt, so a suitable virtual machine
platform
becomes available; the Vagrant lxc plugin can manage LXC containers,
which are
faster than virtual machines and can happily test the bulk of the
DebOps roles
which don't require full virtualization.
To make support for these plugins available out of the box in DebOps,
I updated the 'debops.gitlab_runner' role to detect the presence of
LXC or
libvirtd support and configure corresponding Vagrant plugins on the
host. This
fact is reflected in the tags defined automatically for each GitLab
Runner,
and using these tags, specific DebOps role tests can be redirected to
a suitable platform. The whole project can be tested using
libvirt/KVM,
support for LXC containers is optional and makes the execution of the
entire
test suite faster.
A few pain points were left to resolve at this point. DebOps is
maintained as
a monorepo, with multiple roles that can work and be tested
independently.
However, due to how GitLab CI is designed, a small change in one file
requires
all of the CI jobs to be executed, every time. The current number of
tests
(120+) and slow test platform I'm using results in 3-4 hour test
times for the
entire project, which was definitely unacceptable.
One way to solve this was caching the container and virtual machine
images
- installation of required packages like Ansible, pycodestyle, etc.
could be
performed once and reused for multiple test jobs afterwards. At
first,
I planned to create a separate CI stage that created the container/VM
image
which could be reused later, but this became problematic because it
prevented
scaling horizontally from just one GitLab Runner to multiple Runners
- each CI
job is directed towards one Runner, with this arrangement there was
no way to
use multiple GitLab Runners at a time.
Configuration for each job was also an issue. Ideally, each DebOps
role,
playbook, sets of multiple playbooks, etc. would have their own entry
in the
one single '.gitlab-ci.yml' file (multiple separate files are not
supported).
Due to number of jobs this had to be somewhat standardized so that
management
of the tests and adding new ones would be easy and sane enough to do.
Sheer number of tests called for a tool that knew how to execute
Ansible
playbooks, multiple times, and check for results, for example
verifying
idempotency. Having a way to augment each test independently as
needed due to
the role requirements, and a way to include notification about each
the state
of each job would be a bonus.
There are a few test frameworks available that could fit some of the
bill,
like Rolespec, Test Kitchen, Molecule, but due to unique DebOps
requirements
I felt that none of them was suitable enough. So, I wrote my own.
Introducing Jane - Just ANother Executor
----------------------------------------
Jane is a fictional character in the Orson Scott Card series of books
about
Ender, which also include ansibles. She is a distributed, omnipresent
entity
evolved from a computer program AI, that manages Ender's financial
assets,
among other things.
The 'jane' script, located at 'lib/tests/jane' in the DebOps
monorepo, is
a Bash 4 script that serves multiple roles within the DebOps test
suite.
GitLab CI is the primary "executor" of the tests jobs, hence jane is
just
another executor working on behalf of the GitLab Runner shell
executor, that
works in each separate job in parallel.
Jane is used on each stage of the CI pipeline - in GitLab Runner
itself to
execute different steps of the job, during Vagrant host provisioning
to inform
about different stages of the provisioner script execution, and
inside of the
VM/container to execute Ansible playbooks and other tests. At each
step of the
CI job, Jane takes care to transfer interesting environment variables
over
from GitLab Runner, through Vagrant provisioning to the resulting
VM/container
so that each job has consistent environment.
Jane can detect if a specific Vagrant box with cached VM/container
image is
available; if there isn't one, a random Jane instance on a given
GitLab Runner
host starts to create one automatically. The rest of Jane instances
pause and
wait for the box image to be finished, before using it themselves.
This
happens independently on each GitLab Runner host, therefore scaling
out to
multiple Runners is painless.
Very early in the job execution, Jane checks for changed files in
specific
paths in the repository or special strings in the git log of all
commits in
a particular pull request, including changes from the previous set of
merged
commits. If nothing interesting is found, a given Jane instance can
end the
particular CI job early, which results in shortening the time for the
complete
pipeline run from 3-4 hours to about 20-40 minutes, depending on the
scope of
changes. The tests can also be enforced using special environment
variable,
either for a specific CI job, an entire CI stage or globally for all
jobs.
Jane is very talkative and colorfully describes each step of a CI
job, in
excruciating detail. This can be very useful in debugging of a failed
job or
checking why a particular CI job was even executed.
With Jane, each CI job has easy to use, templated and standardized
configuration defined in the 'gitlab-ci.yml' file using environment
variables.
The configuration can be used to augment Ansible inventory (Jane is
used
during playbook execution as a dynamic Ansible inventory source) with
custom
inventory groups or variables, as necessary. Multiple playbooks can
be
executed in sequence therefore testing roles that depend on other
service
roles like databases is certainly possible. In the future, Jane will
learn how
to manage multiple Vagrant hosts at once, so that multi-host tests
will become
available.
Lots of things still to do
--------------------------
It's getting late and this mail is long enough already, therefore
I'll try to
finish up quickly. There are still a few things I want to do before
the new
test suite is finished. I think that right now the most important is
documentation - the new test suite feels almost like a separate
project in and
of itself beside the main one (DebOps), and it could probably be
extracted
from the DebOps monorepo and published as standalone, for other
similar
projects to use. Extensive documentation should help with that. I'm
heading
out to holidays and will be mostly offline until the New Year, but in
some
free time I'll try and document everything properly, there are many
interesting things this test suite is capable of.
At the moment the roles and playbooks are only validated by Ansible
itself via
an idempotency test, but there's no further integration testing done.
I plan
to implement this using testinfra, because it's written in Python and
similar language to Ansible should make maintenance of the
integration tests
easier, hopefully. Unfortunately testinfra is not currently packaged
in
Debian, therefore it's installed from PyPI; I hope that in the future
Debian
release it will be available natively.
That would be it for now. I wish all of you happy holidays, and see
you back
on IRC in 2018. :-)
Cheers,
Maciej
_______________________________________________
debops-users mailing list
debops-users(a)lists.debops.org
https://lists.debops.org/mailman/listinfo/debops-users