Random stuffJekyll2018-11-10T11:16:43+00:00https://leucos.github.io/Michel Blanchttps://leucos.github.io/mb@mbnet.frhttps://leucos.github.io/ansible-layout2015-07-02T00:00:00+00:002015-07-02T00:00:00+00:00Michel Blanchttps://leucos.github.iomb@mbnet.fr
<p><em>(revised 20181110 per @theenglishway suggestions)</em></p>
<p>I have been writing playbooks for quite a while now. Along the way, I
went through various stages, and used different ways to layout Ansible
files. I guess that after going down this trial and error path, I
finally came up with something I will stick to.</p>
<p>I am not saying that this is the be-all and end-all of Ansible files
layout but may be it will fast forward you to a saner file layout, and
you’ll be able to move on from there. This post will probably help you
if you are new to Ansible, trying to figure out what to put and where.
I hope it will prove usefull if you have some Ansible experience too.</p>
<h2 id="some-terminology">Some terminology</h2>
<p>In this post, I will mostly talk about 3 things: roles, inventories and
playbooks. Other items do exist (plays, tasks, …) but those 3 elements
shape the big picture of the layout.</p>
<h3 id="roles">Roles</h3>
<p>A role is a collection of tasks and templates (among other things, but
those are the most common) focused on one very specific goal. For
instance, you can have a role that installs nginx, another that deploys
ssh keys for admins, etc…</p>
<p>Nginx role will install and configure nginx. Nothing else. It won’t
create DNS entries, trim logs, add a ftp server or anything. It just
installs nginx. Period.</p>
<h3 id="inventories">Inventories</h3>
<p>An inventory is a list of hosts, eventually assembled into groups, on
which you will run ansible playbooks. Ansible automatically puts all defined
hosts in the aptly named group <code class="highlighter-rouge">all</code>.</p>
<p>For instance, you could have hosts <code class="highlighter-rouge">www1</code> and <code class="highlighter-rouge">www2</code>, assembled in group
<code class="highlighter-rouge">webservers</code>, and later reference the group or individual hosts,
depending on your needs.</p>
<p>Inventories can also come with variables applied to hosts or groups
(including <code class="highlighter-rouge">all</code>).</p>
<p>Inventories can be dynamic. If the inventory file is executable, Ansible
will run it and use its output as the inventory (note that, in this
case, the format is not the same as static inventory).</p>
<p>You can of course have multiple inventories, segregated from each other.
We will take advantage from this later on.</p>
<h3 id="playbooks">Playbooks</h3>
<p>The last piece of the puzzle is the playbook. The playbook is the pivot
between and inventory and roles. This is where you basically tell
Ansible: <em>please install roles foo, bar and baz on machines alice, bob
and charlie</em>.</p>
<h2 id="role-layout">Role layout</h2>
<p>Role layout is pretty well documented at Ansible website. A role contains
several directories. All directories are optional besides <code class="highlighter-rouge">tasks</code>. For each
directory, the entry point is <code class="highlighter-rouge">main.yml</code>. Thus, the only compulsory file in a
role is <code class="highlighter-rouge">tasks/main.yml</code>.</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ansible-foobar/
├── defaults
│ └── main.yml
├── files
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── tasks
│ ├── check_vars.yml
│ ├── foobar.yml
│ └── main.yml
└── templates
└── foobar.conf.j2
</code></pre></div></div>
<p>Let’s cover briefly the layout an see the function of each file and
directory.</p>
<h3 id="defaultsmainyml"><code class="highlighter-rouge">defaults/main.yml</code></h3>
<p>This directory contains defaults for variables used in roles. I
encourage you to define every variable used in your role, for several
reasons:</p>
<ul>
<li>this file will be a nice and always up to date reference list of
settings configuration in your roles</li>
<li>having configured variables will prevent your role failing in an
uncontrolled way (more on this later).</li>
</ul>
<p>If some of these variables are used in templates to generate config
files, I highly encourage you to use your target OS defaults. The principle of
least surprise should apply here.</p>
<p>Best practices assumes that you are using <em>pseudo-namespacing</em> for your
role’s variables (e.g. for role <code class="highlighter-rouge">foobar</code>, all variables should begin
with <code class="highlighter-rouge">foobar_</code>) to avoid collisions with other roles.</p>
<h3 id="files"><code class="highlighter-rouge">files/</code></h3>
<p>This directory holds files that do not require Jinja interpolation, and can be copied as-is on the remote nodes.</p>
<h3 id="handlersmainyml"><code class="highlighter-rouge">handlers/main.yml</code></h3>
<p>This is where you define handlers that get notified by tasks. Handlers
are just standard tasks. You can use <code class="highlighter-rouge">include</code> in this file if you want
to separate handlers (for different OSes versions for instances), but
try to keep the file number as low as possible so you don’t end up
hunting down stuff everywhere.</p>
<p>If your handler restarts any service, you have to make sure that the
service config file is valid before attempting to restart it. Some
daemons allow this (e.g. nginx, haproxy, apache). If your service does
not, provide some fallback mechanism. You don’t want your playbook to
screw up your running system because you typoed a configuration
variable. See the <code class="highlighter-rouge">validate</code> option in the
<a href="http://docs.ansible.com/template_module.html">template module</a>.</p>
<p>Note that handlers are just standard tasks.</p>
<h3 id="metamainyml"><code class="highlighter-rouge">meta/main.yml</code></h3>
<p>This metadata has (AFAIK) only two variables:</p>
<ul>
<li><code class="highlighter-rouge">galaxy_info</code>: meta information for galaxy about your role. You just
don’t need this if you don intend to push your role to Galaxy. For
details on the format, see TODO: find ref</li>
<li><code class="highlighter-rouge">dependencies</code>: what roles this role depends on.</li>
</ul>
<p>The latter is of utmost importance, and setting it right deserves a blog
post on it’s own. Until then, the rule of thumb to remember is to <strong>only
include compulsory role dependencies for the target host</strong>.</p>
<p>This means that adding <code class="highlighter-rouge">nginx</code> dependency in a <code class="highlighter-rouge">php-fpm</code> role sounds
perfectly reasonable<sup id="fnref:1"><a href="#fn:1" class="footnote">1</a></sup>. However, adding a <code class="highlighter-rouge">mysql</code> dependency to your
web application role is not, because <code class="highlighter-rouge">mysql</code> can be deployed on another
server.</p>
<p><strong>A note 3 years later</strong>: I do not use dependencies anymore. I had issues
regarding role’s defaults variables behavior. Also, the playbook is the
main focus area when building infrastructure code. Having explicit
dependencies in the playbook is the way to go. No weird or hard to track
magic.</p>
<h3 id="tasksmainyml"><code class="highlighter-rouge">tasks/main.yml</code></h3>
<p>This file is the tasks entry point. However, it should be mostly empty.
Why ? Because you want to use Ansible tags. Tags are a great way to
limit task execution for an Ansible run, where only tagged tasks are
run.</p>
<p>For instance, in a playbook that deploys your application, you could
choose to run only tasks regarding nginx.</p>
<p>The problem is that tagging every task in <code class="highlighter-rouge">main.yml</code> would be
cumbersome, error prone, and clutter the code unnecessarily.</p>
<p>The best way to tag all your tasks is to include your real task file
from <code class="highlighter-rouge">tasks/main.yml</code> and tag the whole file:</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="pi">-</span> <span class="na">import_tasks</span><span class="pi">:</span> <span class="s">foobar.yml</span>
<span class="na">tags</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">foobar</span></code></pre></figure>
<p>Here, I name the real task file <code class="highlighter-rouge">foobar.yml</code> with the same name as the role
(quite handy with <code class="highlighter-rouge">find</code> or <code class="highlighter-rouge">locate</code>; no need to guess which <code class="highlighter-rouge">main.yml</code> you are
looking for) and apply the tag <code class="highlighter-rouge">foobar</code> to all tasks in the role.</p>
<p>You can repeat this if you have a big list of tasks and want to split
them in several files. You could, for instance, separate configuration
and installation matters, and add another specific tag for each of them:</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="pi">-</span> <span class="na">import_tasks</span><span class="pi">:</span> <span class="s">foobar-install.yml</span>
<span class="na">tags</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">foobar</span>
<span class="pi">-</span> <span class="s">foobar:install</span>
<span class="pi">-</span> <span class="na">import_tasks</span><span class="pi">:</span> <span class="s">foobar-config.yml</span>
<span class="na">tags</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">foobar</span>
<span class="pi">-</span> <span class="s">foobar:config</span></code></pre></figure>
<p>Here I added two tags to the installation part (<code class="highlighter-rouge">foobar</code> and
<code class="highlighter-rouge">foobar:install</code>), and two for the configuration part (<code class="highlighter-rouge">foobar</code> and
<code class="highlighter-rouge">foobar:config</code>).</p>
<p>Note that the <code class="highlighter-rouge">:</code> between, for instance, <code class="highlighter-rouge">foobar</code> and <code class="highlighter-rouge">config</code> has no
meaning. Ansible treats tags as dumb strings. It is just a personnal
convention (Redis like) for refining tags.</p>
<p>With this setup, you could run only the configuration part of your role
by issuing:</p>
<p><code class="highlighter-rouge">ansible-playbook playbook.yml -t foobar:config</code></p>
<p>The <code class="highlighter-rouge">-t</code> and <code class="highlighter-rouge">-l</code> combination is a very powerful weapon to target
a specific host with a precise change (think of this as pointing to a
matrix cell targetting host (i.e. row) and tag (i.e. column)).</p>
<h4 id="a-word-of-caution">A word of caution</h4>
<p>Do not overdo tags: most of the time, this is YAGNI (You Ain’t Gonna
Need It). Create a tag if you’re gonna need it. It can be hard to
mentally predict what will happen if you do too much. Beware of the
<code class="highlighter-rouge">never</code> tag, that will skip tasks <strong>unless</strong> you explicitely use another
tag.</p>
<p>For instance:</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="pi">-</span> <span class="na">import_tasks</span><span class="pi">:</span> <span class="s">foobar-uninstall.yml</span>
<span class="s">tags</span>
<span class="s">- never</span>
<span class="s">- foobar</span></code></pre></figure>
<p>will execute tasks in <code class="highlighter-rouge">foobar-uninstall.yml</code> if tag <code class="highlighter-rouge">foobar</code> is
specified at the command line.</p>
<h3 id="taskscheck_varsyml"><code class="highlighter-rouge">tasks/check_vars.yml</code></h3>
<p>I use this file to ensure that required variables are defined.</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="c1">#</span>
<span class="c1"># Checking that required variables are set</span>
<span class="c1">#</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checking that required variables are set</span>
<span class="na">fail</span><span class="pi">:</span> <span class="s">msg="{{ item }} is not defined"</span>
<span class="na">when</span><span class="pi">:</span> <span class="s">item not in vars</span>
<span class="na">loop</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">foobar_database</span>
<span class="pi">-</span> <span class="s">foobar_deploy_user</span></code></pre></figure>
<p>Then, include this file in <code class="highlighter-rouge">tasks/main.yml</code>:</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="pi">-</span> <span class="na">import_tasks</span><span class="pi">:</span> <span class="s">check_vars.yml</span>
<span class="na">tags</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">foobar</span>
<span class="pi">-</span> <span class="s">foobar:check</span>
<span class="pi">-</span> <span class="s">check</span>
<span class="pi">-</span> <span class="na">import_tasks</span><span class="pi">:</span> <span class="s">foobar.yml</span>
<span class="na">tags</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">foobar</span></code></pre></figure>
<h3 id="templates"><code class="highlighter-rouge">templates/*</code></h3>
<p>This is the place where templates (i.e. files with interpolated
variables goes). While this is not necessary, I often reference them
using a relative path like so:</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Template foo</span>
<span class="na">template</span><span class="pi">:</span>
<span class="na">src</span><span class="pi">:</span> <span class="s">../templates/foo.conf.j2</span>
<span class="na">dest</span><span class="pi">:</span> <span class="s">/some/place/in/the/node/filesystem/foo.conf</span></code></pre></figure>
<p>The goal of using relative path is to be able to hit <code class="highlighter-rouge">gf</code> in Vim and
open the file directly. You can get rid of that and just use <code class="highlighter-rouge">src:
foo.conf.j2</code>. It is just a readability/convenience tradeoff.</p>
<p>The file name I use is the <strong>intended filename at the destination</strong>,
appended with <code class="highlighter-rouge">.j2</code> so it is clear that it is a Jinja2 template, and
easier to search (<code class="highlighter-rouge">find</code> or <code class="highlighter-rouge">locate</code>).</p>
<p>Some folks like to replicate the destination hierarchy (e.g. <code class="highlighter-rouge">src: etc/sysconfig
/network-scripts/ifcfg-ethx.cfg.j2</code>). This is a matter of taste, but personaly I
don’t see the point of having those deep hierarchies in the role if the naming is correct.</p>
<h3 id="varsmainyml"><code class="highlighter-rouge">vars/main.yml</code></h3>
<p>It is sometimes difficult to grok the difference between
<code class="highlighter-rouge">vars/main.yml</code> and <code class="highlighter-rouge">defaults/main.yml</code>. After all, they both contain
variable assignements.</p>
<p>I do not always use a <code class="highlighter-rouge">vars/main.yml</code>, but when I do, I put “constants
like” variables in it. These are variables that are not intended to be
overriden.</p>
<p>For instance the github repository for a particular piece of code (e.g.
your web application) will certainly go there. However, the version you
want to deploy won’t.</p>
<p>All in all, it is just a mechanism to take those values out of tasks files
readability and role life cycle.</p>
<h2 id="inventories-and-playbook-layout">Inventories and playbook layout</h2>
<p>A playbook glues together roles and inventories. Thus playbooks depend on roles
and inventories. But while you have mechanisms to list roles requirements in a
playbook, you don’t have any for inventories.</p>
<p>Since the playbook can not live without the targeted inventories I include my
inventories in my playbooks.</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>playbook-foobar/
├── ansible.cfg
├── requirements.txt
├── roles/
│ └── requirements.yml
├── inventories
│ ├── development
│ | ├── group_vars
│ | │ └── all
│ | └── hosts
│ ├── integration
│ | ├── group_vars
│ | │ └── all
│ | └── hosts
│ └── production
│ ├── group_vars
│ │ └── all
│ └── hosts
├── site.yml
└── playbooks
├── 10_database.yml
└── 20_stuff.yml
</code></pre></div></div>
<h3 id="ansiblecfg-roles-and-rolesrequirementsyml"><code class="highlighter-rouge">ansible.cfg</code>, <code class="highlighter-rouge">roles/</code> and <code class="highlighter-rouge">roles/requirements.yml</code></h3>
<p>This file controles ansible behaviour. You can have one in <code class="highlighter-rouge">/etc/ansible</code> or as
a personal dotfile (<code class="highlighter-rouge">~/.ansible.cfg</code>). Adding an <code class="highlighter-rouge">ansible.cfg</code> file in the
playbook root will ensure that the required settings for the playbook to run are
really there. The precedence order for Ansible config files is<sup id="fnref:2"><a href="#fn:2" class="footnote">2</a></sup>:</p>
<ol>
<li><code class="highlighter-rouge">ANSIBLE_CONFIG</code> (an environment variable pointing to a file)</li>
<li><code class="highlighter-rouge">ansible.cfg</code> (in the current directory)</li>
<li><code class="highlighter-rouge">.ansible.cfg</code> (in the home directory)</li>
<li><code class="highlighter-rouge">/etc/ansible/ansible.cfg</code></li>
</ol>
<p>Ansible will use the first config file found.</p>
<p>In this config file, I always set at least two options:</p>
<figure class="highlight"><pre><code class="language-ini" data-lang="ini"><span class="py">hostfile</span> <span class="p">=</span> <span class="s">./inventories/dev</span>
<span class="py">roles_path</span> <span class="p">=</span> <span class="s">./roles:/some/path/to/roles/repos</span></code></pre></figure>
<p>The first one (<code class="highlighter-rouge">hostfile</code>) sets which inventory Ansible will use. More
explanations will come below.</p>
<p>The second one set the path where Ansible will look for roles. I typically set
two directories here (separated by <code class="highlighter-rouge">:</code>, like shell’s <code class="highlighter-rouge">PATH</code>):</p>
<ul>
<li>the first directory will be used by ansible galaxy to install imported
roles. I set it to <code class="highlighter-rouge">./roles</code> but the name doesn’t matter. Don’t forget
to add the directory content (except <code class="highlighter-rouge">requirements.yml</code>) in your
playbook’s <code class="highlighter-rouge">.gitignore</code> like so:</li>
</ul>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>!/roles
/roles/*
!/roles/requirements.yml
</code></pre></div></div>
<ul>
<li>sometimes I add a second directory that points to my roles
developmenent directory path</li>
</ul>
<p>The advantages for this setup are two fold: first, you have a dedicated path,
ignored by your SCM, where you will download roles. The roles will be searched
there first. Secondly, if a role is not found, it will be searched in your role
development directory. This let you hack on your roles while writing a playbook.
You don’t need to go through a <em>commit/push/install</em> cycle when you are coding
your roles for this playbook.</p>
<p>Roles dependencies for your playbook are listed in <code class="highlighter-rouge">requirements.yml</code>
and can be installed with <code class="highlighter-rouge">ansible-galaxy install -r requirements.yml</code>:</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="c1"># Role on galaxy</span>
<span class="pi">-</span> <span class="s">you.rolename</span>
<span class="c1"># Public role on github</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">role-public</span>
<span class="na">src</span><span class="pi">:</span> <span class="s">https://github.com/erasme/role-public.git</span>
<span class="c1"># Private role on github</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">role-private</span>
<span class="na">src</span><span class="pi">:</span> <span class="s">git+ssh://git@github.com/you/role-private.git</span></code></pre></figure>
<h3 id="inventories-1"><code class="highlighter-rouge">inventories/</code></h3>
<p>This directory holds all inventories you want to apply your playbook too. The
most common pattern is to use per-environment inventories: one for
<code class="highlighter-rouge">development</code>, one for <code class="highlighter-rouge">integration</code>, another for <code class="highlighter-rouge">production</code>, etc…</p>
<p>Of course, the <code class="highlighter-rouge">hostfile</code> variable in <code class="highlighter-rouge">ansible.cfg</code> should point to
<code class="highlighter-rouge">development</code> to avoid accidentaly messing with production. Executing the
playbook on non-development inventories will force you tu use the <code class="highlighter-rouge">-i</code>, which is
a good safety measure.</p>
<p>While you can define variables in groups (in <code class="highlighter-rouge">group_vars</code>) and hosts
(<code class="highlighter-rouge">host_vars</code>), you should stuff as much variables as possible in
<code class="highlighter-rouge">group_vars/all</code>. The rationale is that it is much easier to find a
variable when a single file is involved. Variables scattered in a dozen
of files are <em>not</em> manageable.</p>
<p>And when you’ll want to create an additional inventory (e.g. create <code class="highlighter-rouge">production</code>
from <code class="highlighter-rouge">development</code>), it will be much easier to change a single file and set the
variables to proper values than to do the same in several files.</p>
<p>Note that <code class="highlighter-rouge">group_vars/all</code> can be a directory containing several files. I
usually split variables in a clear text file (<code class="highlighter-rouge">group_vars/all/all</code>) and a
ciphered one (<code class="highlighter-rouge">group_vars/all_secret</code>) using the transparent vaulting techniques
described in
<a href="/articles/transparent-vault-revisited/">this post</a>.</p>
<p><strong>Note 3 years later</strong>: now ansible allow you to vault a single variable in
an inventory. Use it !</p>
<p>Here is a handy bash alias that crypts text selected with a mouse:</p>
<p><code class="highlighter-rouge">alias vault_clip_crypt='echo Passing $(xclip -rmlastnl -o) to ansible-vault && echo -n "$(xclip -rmlastnl -o)" | ansible-vault encrypt_string'</code></p>
<h3 id="siteyml-and-playbooks"><code class="highlighter-rouge">site.yml</code> and <code class="highlighter-rouge">playbooks/</code></h3>
<p>This directory contains the playbooks themselves. I always create a
“master” playbook called <code class="highlighter-rouge">site.yml</code> in the playbook root directory,
which includes all other playbooks located in <code class="highlighter-rouge">playbooks/</code>.</p>
<p>I prefix playbooks with a number (Basic style) so I get a sense of the
order playbook will be executed just by looking at <code class="highlighter-rouge">playbooks/</code> content.</p>
<p>For instance:</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="c1">#!/usr/bin/env ansible-playbook</span>
<span class="pi">-</span> <span class="na">import_playbook</span><span class="pi">:</span> <span class="s">playbooks/10_database.yml</span>
<span class="pi">-</span> <span class="na">import_playbook</span><span class="pi">:</span> <span class="s">playbooks/20_stuff.yml</span></code></pre></figure>
<p>The rationale is to be able to use <code class="highlighter-rouge">ansible-pull</code> easily if needed (<code class="highlighter-rouge">ansible-
pull</code>, by default, tries to execute a playbook called<code class="highlighter-rouge">site.yml</code>). The other
point is to split playbook in related parts.</p>
<p>For instance, you could have a playbook the takes care of setting up the
database, another that will set the OS level stuff (e.g. ssh keys, firewalling,
…), another one that takes care of deploying your web application, etc… When
needed, You can use all the playbooks at once with <code class="highlighter-rouge">site.yml</code>, or just focus on
a specific problem running the appropriate playbook (no need to run the ssh-key
setup if you’re just deploying the latest version of your web application).</p>
<p>The shebang line at the top of the file (<code class="highlighter-rouge">#!/usr/bin/env ansible-playbook</code>) will
make the playbook directly executable (adjust <code class="highlighter-rouge">ansible-playbook</code> path and <code class="highlighter-rouge">chmod
+x</code> the playbook file). You can still pass additional
<code class="highlighter-rouge">ansible-playbook</code> parameters if required.</p>
<h3 id="requirementstxt"><code class="highlighter-rouge">requirements.txt</code></h3>
<p>This file contains the result of a <code class="highlighter-rouge">pip freeze</code>. I now only use <code class="highlighter-rouge">pip</code>
under <code class="highlighter-rouge">virtualenv</code> to install ansible and required modules. It makes it
really easy to switch ansible (and even python) version between
projects.</p>
<p>So when someone needs to work on this project, the workflow is simple:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone http://github.com/some/playbook-repos
<span class="nb">cd </span>playbook-repos
mkvirtualenv playbook-repos <span class="nt">--no-site-packages</span>
pip install <span class="nt">-r</span> requirements.txt
ansible-galaxy install <span class="nt">-r</span> roles/requirements.yml
</code></pre></div></div>
<p>and you’re good to go.</p>
<h2 id="layout-antipatterns">Layout Antipatterns</h2>
<p>When I started using Ansible, I cumulated several antipatterns at the
same time: trying to emcompass all my infrastructure in a single
inventory containing per-host fine grained variables, used in a single
playbook, without using any role.</p>
<p>While this sounds feasible, it is doomed to failure unless you manage a
very small infrastructure. Let’s zoom in briefly on each mistake.</p>
<h3 id="trying-to-encompass-all-your-infrastructure-in-one-playbook">Trying to encompass all your infrastructure in one playbook</h3>
<p>Is is tempting to aim for a one-liner that will magically deploy all
your infrastructure in one shot. This gives you some bragging rights at
your next meetup, and feels like the ultimate sysadmin masterpiece.</p>
<p>However, it has many drawbacks:</p>
<ul>
<li>
<p>it will be slow: do you really want to run a playbook over dozens of
more tasks or roles, just to change an entry in <code class="highlighter-rouge">/etc/hosts</code> ? Yes,
there are workaround for this, but it will require some command line
magic, a lot of thinking.</p>
</li>
<li>
<p>it mixes bananas and apples: you should strive for separation of
concerns in your playbooks if you want be able to read them (and, as a
consequence, maintain them).</p>
</li>
</ul>
<p>As a consequence, your infrastructure code will be unnecessary hard to
test and maintain.</p>
<h3 id="per-host-fine-grained-variables">Per-host fine grained variables</h3>
<p>This is a corolary of the previous antipattern: when you try to
encompass your whole infrastructure, you start to think, inheritance,
variables overriding and refining.</p>
<p>And while doing this, you add considerable complexity to your
inventories. It is very hard to track down variables definitions when
you overrides them in <code class="highlighter-rouge">group_vars/some_group</code>, <code class="highlighter-rouge">group_vars/all</code>,
<code class="highlighter-rouge">hosts_vars/machine</code>, role defaults, …</p>
<p>Now this can get even worse when you use the <code class="highlighter-rouge">hash_behavior: merge</code>
Ansible configuration setting: it introduces more confusion, and makes
your Ansible work potentially unshareable with people using
<code class="highlighter-rouge">hash_behaviour: replace</code>. Since I am
<a href="https://github.com/ansible/ansible/commit/e28e538c6ed7520ecef305c776eb6036aff42d06">guilty</a>
on this one, it is time to make some apologies. Sorry folks. Michael
DeHaan did not like it, and he was right.</p>
<h3 id="single-playbook">Single playbook</h3>
<p>A single playbook relates to the first Sin again, but also applies to
more focused playbooks where you only deploy one thing. Splitting your
playbooks between various logically related roles will fasten your
deployments. Again, why running ssh key distribution, storage cluster
deployment, web stack, middlewares and application when you just change
the color of a button in your web app ?</p>
<p>Split your playbook in related parts that reflects your stack
architecture. They will be faster and easier to use.</p>
<h3 id="no-roles-tasks-only">No roles (tasks only)</h3>
<p>Well, this is obvious. Even if you don’t want to share, make roles and
strive for code reuse. Reused code will save you time of course, but it
is also battle tested since it is used more frequently.</p>
<p>Tasks-only playbook can be used for a quick hit and run, solving a
transient problem that doesn’t offer any code reuse opportunities.</p>
<p>I also try to avoid tasks along roles in playbooks: this hurts the
abstraction level you manage to build using roles. When thinking in
terms of roles, you don’t need to think about the nitty gritty details
of the roles when reading your playbooks. If your roles are thouroughly
tested, you can read your infrastructure in seconds. Add tasks to the
mix, and you loose this superpower.</p>
<div class="footnotes">
<ol>
<li id="fn:1">
<p>Yes, you could separate your application server (e.g. php-fpm) and put it on a different machine than your webserver, it ll depends on your local context. <a href="#fnref:1" class="reversefootnote">↩</a></p>
</li>
<li id="fn:2">
<p>http://docs.ansible.com/intro_configuration.html <a href="#fnref:2" class="reversefootnote">↩</a></p>
</li>
</ol>
</div>
<p><a href="https://leucos.github.io/ansible-files-layout">Laying out roles, inventories and playbooks</a> was originally published by Michel Blanc at <a href="https://leucos.github.io">Random stuff</a> on July 02, 2015.</p>
https://leucos.github.io/decoupling-your-ansible-roles2015-06-27T00:00:00+00:002015-06-27T00:00:00+00:00Michel Blanchttps://leucos.github.iomb@mbnet.fr
<p>Having tightly coupled role is the best way to have a hard time
maintaining roles and playbooks, and live in fear of changing anything
in them.</p>
<p>Here is a journey into role decoupling.</p>
<h2 id="the-problem">The problem</h2>
<p>Let say we have a role (<em>my_app</em>) that depend on <em>php-fpm</em> role. In the php-fpm
role, we want to display errors in HTML output depending on the application
running environment (e.g. always display unless we’re running in production
environment).</p>
<p>The application running environment is available in <code class="highlighter-rouge">myapp_environment</code>.</p>
<h2 id="first-idea">First idea</h2>
<p>The first idea that comes to mind is to change the php.ini according to
<code class="highlighter-rouge">myapp_environment</code>, like so:</p>
<figure class="highlight"><pre><code class="language-jinja" data-lang="jinja"><span class="cp">{%</span> <span class="k">if</span> <span class="nv">myapp_environment</span> <span class="o">==</span> <span class="s2">"production"</span> <span class="cp">%}</span>
display_errors = Off
<span class="cp">{%</span> <span class="k">else</span> <span class="cp">%}</span>
display_errors = On
<span class="cp">{%</span> <span class="k">endif</span> <span class="cp">%}</span></code></pre></figure>
<p>The problem with this approach if that php-fpm role now needs
<code class="highlighter-rouge">myapp_environment</code> to be defined, which is quite absurd.</p>
<p>So instead, you could rename the variable <code class="highlighter-rouge">environment</code>, and use this in
both roles (<code class="highlighter-rouge">myapp</code> and <code class="highlighter-rouge">php-fpm</code>). This is better, but not much. The
problem with this approach is that a plain <code class="highlighter-rouge">environment</code> variable is not
linked (by it’s name) to any role, and this can lead to great confusion
is it is used and set in many roles or in different places in the
inventory.</p>
<h2 id="another-try">Another try</h2>
<p>So the best way is to have two variables, <code class="highlighter-rouge">php_fpm_environment</code> and <code class="highlighter-rouge">myapp_environment</code> which makes is meaningful. But now how can I sync
them together ?</p>
<p>One ways is to match them in your inventory, like so :</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="c1"># Somewhere in inventory</span>
<span class="na">myapp_environment</span><span class="pi">:</span> <span class="s2">"</span><span class="s">production"</span>
<span class="na">php_fpm_environment</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">myapp_environment</span><span class="nv"> </span><span class="s">}}"</span></code></pre></figure>
<p>However, this has some drawbacks. For instance, we are still talking about
<code class="highlighter-rouge">php_fpm_environment</code> and, while not a big deal, it has no php-fpm
meaning per se and it is not obvious what this variable does.</p>
<p>Also, in the <code class="highlighter-rouge">php.ini</code> template, we will still have to test against the string
“production” to set <code class="highlighter-rouge">display_errors</code>. Testing against a string set somewhere
else is quite dangerous. What is the production name for the app is “live”
instead ? Our php-fpm role is broken now.</p>
<h2 id="some-progress">Some progress</h2>
<p>We could go a better way: let’s call the variable <code class="highlighter-rouge">php_fpm_display_error</code> (more
meaningful) and make it a boolean. We now can do this :</p>
<figure class="highlight"><pre><code class="language-jinja" data-lang="jinja"><span class="cp">{%</span> <span class="k">if</span> <span class="nv">php_fpm_display_errors</span> <span class="cp">%}</span>
display_errors = On
<span class="cp">{%</span> <span class="k">else</span> <span class="cp">%}</span>
display_errors = Off
<span class="cp">{%</span> <span class="k">endif</span> <span class="cp">%}</span></code></pre></figure>
<p>and somewhere in inventory:</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="na">myapp_environment</span><span class="pi">:</span> <span class="s2">"</span><span class="s">production"</span>
<span class="na">php_fpm_display_errors</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">myapp_environment</span><span class="nv"> </span><span class="s">==</span><span class="nv"> </span><span class="s">'production'</span><span class="nv"> </span><span class="s">}}"</span></code></pre></figure>
<h2 id="streamlining-our-solution">Streamlining our solution</h2>
<p>Well, this is better now. But it is not perfect. The inventory is more verbose
than required and handles something that it shouldn’t have to take care
of. It is also quite easy to forget to add it to the inventory and end up
with errors showing in production.</p>
<p>By moving this logic away from the inventory, and directly in the role
dependencies, this configuration setting becomes completely transparent, and we get rid of redundancy. We just have to add the following lines in <code class="highlighter-rouge">myapp/meta/main.yml</code>:</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="na">dependencies</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">role</span><span class="pi">:</span> <span class="s">role-php-fpm</span>
<span class="na">php_fpm_display_errors</span><span class="pi">:</span> <span class="s2">"</span><span class="s">{{</span><span class="nv"> </span><span class="s">myapp_environment</span><span class="nv"> </span><span class="s">==</span><span class="nv"> </span><span class="s">'production'</span><span class="nv"> </span><span class="s">}}"</span></code></pre></figure>
<p>Now, php-fpm role is completely decoupled from myapp role, and the production
setting is completely transparent to the role user. Setting <code class="highlighter-rouge">myapp_environment</code>
is enough to have the depending role set variables accordingly. You don’t even
have to be aware of the <code class="highlighter-rouge">myapp</code> role dependency. If you swap, let say
nginx/php-fpm for apache/php, you just have to change the role dependency and
have no impact on your inventory. If you want to name your production
environment “live”, you can do so by changing <code class="highlighter-rouge">meta/main.yml</code> and not
touching anything else.</p>
<p>Keeping role decoupled is the best way to have manageable and reusable
roles. Try to make them self sufficient, and avoid cross variables or
even worse, group names in roles !</p>
<p><a href="https://leucos.github.io/decoupling-your-ansible-roles">Decoupling your Ansible roles</a> was originally published by Michel Blanc at <a href="https://leucos.github.io">Random stuff</a> on June 27, 2015.</p>
https://leucos.github.io/articles/transparent-vault-revisited2015-05-25T00:00:00-00:002015-05-26T00:00:00+00:00Michel Blanchttps://leucos.github.iomb@mbnet.fr
<h2 id="doing-it-the-wrong-way">Doing it the wrong way</h2>
<p><a href="/articles/ansible-transparent-vault/">Last attempt</a>
to make ansible vault encryption/decryption transparent wasn’t quite
right. Decrypting files after commit wasn’t a good idea as
<a href="https://github.com/ralovely">Raphael Campardou</a> noticed.</p>
<p>In search for a better idea, I eventually realized that hooks where not
the right place to do it: yes, you can guard from commiting files that
should be encrypted, but hacking around hooks to build a crypt/decrypt
pipeline is doomed to failure.</p>
<h2 id="doing-it-better">Doing it better</h2>
<p>While looking for alternate ways, I remembered I hacked around with
git filters back in the days to see clear-text diffs for OpenOffice
files.</p>
<p>Git let’s you apply <code class="highlighter-rouge">smudge</code>, <code class="highlighter-rouge">clean</code> and <code class="highlighter-rouge">textconv</code> filters to files
which are applied this way:</p>
<ul>
<li>filter/smudge: after checkout, reads blob from STDIN and outputs the
workfile from STDOUT</li>
<li>filter/clean: converts the worktree file to blob upon check in</li>
<li>diff/textconv: applied before diffing files</li>
</ul>
<p>So, for our needs, <em>smudge</em> and <em>textconv</em> are good places to decrypt,
while <em>clean</em> is the place to encrypt.</p>
<h2 id="implementation">Implementation</h2>
<p>The implementation requires to write the 3 filters (<em>smudge</em>, <em>clean</em>,
<em>textconv</em>) and configure your git repos to use the filters.</p>
<p>Those filters should be executable.</p>
<p>As we did in last post, we will use a <code class="highlighter-rouge">.vault_password</code> file in the
project root directory containing the vault key (don’t forget to add it
to your <code class="highlighter-rouge">.gitignore</code> file !). The filters fail if the file is not
present.</p>
<h3 id="smudge">Smudge</h3>
<p>The problem that came up to write the smudge & clean filters is that the
blob content is fed on STDIN, and <code class="highlighter-rouge">ansible-vault</code> can only
encrypt/decrypt files <em>in-place</em>.</p>
<p>So we have to write the blob in a temporary file. While this is not
really a problem for the smudge filter, it is for the clean filter since
the temporary file contains the clear-text version of the file. The temp
file is created with restricted permissions, but you’ve been warned.</p>
<p>Smudge’s filter job is simple:</p>
<ul>
<li>write STDIN content to temp file</li>
<li>decrypt the temp file and swallow the output in a variable (using
<code class="highlighter-rouge">ansible-vault view</code> after setting the PAGER to <code class="highlighter-rouge">cat</code>)</li>
<li>if the file was a vault encrypted file, display the variable, else,
bail out.</li>
</ul>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c">#!/bin/sh</span>
<span class="k">if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-r</span> <span class="s1">'.vault_password'</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span><span class="nb">exit </span>1
<span class="k">fi
</span><span class="nv">tmp</span><span class="o">=</span><span class="sb">`</span>mktemp<span class="sb">`</span>
<span class="nb">cat</span> <span class="o">></span> <span class="nv">$tmp</span>
<span class="nb">export </span><span class="nv">PAGER</span><span class="o">=</span><span class="s1">'cat'</span>
<span class="nv">CONTENT</span><span class="o">=</span><span class="sb">`</span>ansible-vault view <span class="s2">"</span><span class="nv">$tmp</span><span class="s2">"</span> <span class="nt">--vault-password-file</span><span class="o">=</span>.vault_password 2> /dev/null<span class="sb">`</span>
<span class="k">if </span><span class="nb">echo</span> <span class="nv">$CONTENT</span> | <span class="nb">grep</span> <span class="s1">'ERROR: data is not encrypted'</span> <span class="o">></span> /dev/null<span class="p">;</span> <span class="k">then
</span><span class="nb">echo</span> <span class="s2">"Looks like one file was commited clear text"</span>
<span class="nb">echo</span> <span class="s2">"Please fix this before continuing !"</span>
<span class="nb">exit </span>1
<span class="k">else
</span><span class="nb">echo</span> <span class="nv">$CONTENT</span>
<span class="k">fi
</span>rm <span class="nv">$tmp</span></code></pre></figure>
<p>As you guessed, <code class="highlighter-rouge">ansible-vault</code> does not output errors on STDERR but on
STDOUT.</p>
<h3 id="clean">Clean</h3>
<p>The clean filter works almost the same way:</p>
<ul>
<li>write STDIN to a temp file</li>
<li>encrypt the temp file in place</li>
<li>write the temp file to STDOUT</li>
</ul>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c">#!/bin/sh</span>
<span class="k">if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-r</span> <span class="s1">'.vault_password'</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span><span class="nb">exit </span>1
<span class="k">fi
</span><span class="nv">tmp</span><span class="o">=</span><span class="sb">`</span>mktemp<span class="sb">`</span>
<span class="nb">cat</span> <span class="o">></span> <span class="nv">$tmp</span>
ansible-vault encrypt <span class="nv">$tmp</span> <span class="nt">--vault-password-file</span><span class="o">=</span>.vault_password <span class="o">></span> /dev/null 2>&1
<span class="nb">cat</span> <span class="s2">"</span><span class="nv">$tmp</span><span class="s2">"</span>
rm <span class="nv">$tmp</span></code></pre></figure>
<p>This one was quite easy. We could also use modelines, by encrypting only if
“vault: true” is present in the 4 first lines. This way, we could apply
the filters to all the files. However I ditched the idea for performance
reasons (see below).</p>
<h3 id="diff-filter">Diff filter</h3>
<p>The filter works like the smudge filter except that it uses the file
name passed as a parameter.</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c">#!/bin/sh</span>
<span class="k">if</span> <span class="o">[</span> <span class="o">!</span> <span class="nt">-r</span> <span class="s1">'.vault_password'</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span><span class="nb">exit </span>1
<span class="k">fi
</span><span class="nb">export </span><span class="nv">PAGER</span><span class="o">=</span><span class="s1">'cat'</span>
<span class="nv">CONTENT</span><span class="o">=</span><span class="sb">`</span>ansible-vault view <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="nt">--vault-password-file</span><span class="o">=</span>.vault_password 2> /dev/null<span class="sb">`</span>
<span class="k">if </span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$CONTENT</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="s1">'ERROR: data is not encrypted'</span> <span class="o">></span> /dev/null<span class="p">;</span> <span class="k">then
</span><span class="nb">cat</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
<span class="k">else
</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$CONTENT</span><span class="s2">"</span>
<span class="k">fi</span></code></pre></figure>
<h3 id="git-configuration">Git configuration</h3>
<h4 id="attributes">Attributes</h4>
<p>Now that the various filters are out and chmoded +x, we need to set-up
out git repos to use them.</p>
<p>For this, we need to tell git on which files we want to apply the
filters, using a <code class="highlighter-rouge">.gitattributes</code> file in our project top directory.</p>
<p>The following <code class="highlighter-rouge">.gitattributes</code> file</p>
<figure class="highlight"><pre><code class="language-text" data-lang="text">*_vault* filter=vault diff=vault</code></pre></figure>
<p>will run filters on repository blobs/files that match <code class="highlighter-rouge">*_vault*</code>.</p>
<p>I initially intended to run the filters on all files, using modelines.
However, performance was really bad, so I finally ended up removing a
full wildcard (<code class="highlighter-rouge">*</code>) and restrict filter selection to specific files.
You can repeat the lines ad nauseam if you want to catch multiple
fileglobs.</p>
<h4 id="gitconfig">Gitconfig</h4>
<p>I put my filters in <code class="highlighter-rouge">~/.bin/</code>, but the location doesn’t matter. You can
event add them to the project and commit them, so everyone has them.</p>
<p>The following section needs to be added to the project’s <code class="highlighter-rouge">.git/config</code>
file:</p>
<figure class="highlight"><pre><code class="language-text" data-lang="text">[filter "vault"]
smudge = ~/.bin/smudge_vault
clean = ~/.bin/clean_vault
[diff "vault"]
textconv = ~/.bin/diff_vault
</code></pre></figure>
<h3 id="test">Test</h3>
<p>Adding a file that matches a glob in <code class="highlighter-rouge">.gitattributes</code> should now trigger
transparent encryption.</p>
<p>Here is a sample transcript.</p>
<script type="text/javascript" src="https://asciinema.org/a/7oaviuh8v2pi39zeojxrn8434.js" id="asciicast-7oaviuh8v2pi39zeojxrn8434" async=""></script>
<h3 id="big-fat-warning">Big fat warning</h3>
<p>The <code class="highlighter-rouge">git cat-file</code> part is not here for decoration. At least the first
time, ensure that encryption works.</p>
<h3 id="the-filters">The filters</h3>
<script src="https://gist.github.com/leucos/1bfcfc7252e8c262956e.js"> </script>
<p><a href="https://leucos.github.io/articles/transparent-vault-revisited/">Transparent encryption with ansible vault revisited</a> was originally published by Michel Blanc at <a href="https://leucos.github.io">Random stuff</a> on May 26, 2015.</p>
https://leucos.github.io/articles/ansible-transparent-vault2015-05-25T00:00:00-00:002015-05-25T00:00:00+00:00Michel Blanchttps://leucos.github.iomb@mbnet.fr
<h1 id="big-fat-warning">Big Fat Warning</h1>
<p><strong>THIS FILE IS LEFT HERE FOR REFERENCE</strong></p>
<p>However, the method described here is WRONG. Check out
<a href="/articles/transparent-vault-revisited/">next post</a> instead !</p>
<h2 id="pain-points">Pain points</h2>
<p><code class="highlighter-rouge">ansible-vault</code> is handy. You can crypt your stuff before commiting it so your
private stuff (AWS/DigitalOcean/… keys, passwords, …) don’t end up
world-readable on GitHub.</p>
<p>However, it is too easy to decrypt your stuff, forget about it, and commit it
without encrypting it back. It is also quite tedious to ansible-vault
encrypt/decrypt all day long.</p>
<h2 id="solution">Solution</h2>
<p><a href="https://github.com/ralovely">Raphael Campardou</a> proposed a <a href="https://gist.github.com/ralovely/9367737">nice
solution</a> to prevent commiting
ansible vault files.</p>
<p>In his solution, you have to name your files <code class="highlighter-rouge">*_vault.yml</code> so they get busted
by a pre-commit hook if they are not currently encrypted.</p>
<p>This is nice: by naming your files appropriately, you can not commit them unless
they are ansible-vault crypted beforehand.</p>
<p>I extended his idea so it can apply to any file in an Ansible repository, with
very little configuration, and added a post-commit hook so files gets
transparently decrypted after being commited.</p>
<h2 id="transparent-encryptiondecryption">Transparent encryption/decryption</h2>
<p>The goal is simple: automagically encrypt the proper files before commit,
commit them, then decrypt them afterwards so we can hack again without
any manual intervention. All this with minimal configuration.</p>
<h3 id="marking-file-for-encryption">Marking file for encryption</h3>
<p>The center trick is to find a way to mark a file for encryption. Modelines
(a.k.a. emacs local variable lines) to the rescue.</p>
<p>To tell git hooks that a file requires encryption, we’ll add this line to
the top of the file (or on line 2 if the file already has a shebang
line) :</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code># -*- vault: true; -*-
</code></pre></div></div>
<p>Any file having <code class="highlighter-rouge">vault: true</code> in a modeline is set to <strong>require encryption before
commit</strong>.</p>
<p>The icing on the cake is that you can use this modeline to set the filetype
too[1], and help your editor to find out the proper file content, which is
quite handy with some files not ending in <code class="highlighter-rouge">yml</code>:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># -*- mode: yaml; vault: true; -*-</span>
</code></pre></div></div>
<p>This is supported out of the box by vim and Emacs. If you use SublimeText,
you can use the <a href="https://github.com/kvs/STEmacsModelines">STEmacsModelines</a>
package.</p>
<h3 id="using-the-hooks">Using the hooks</h3>
<p>The pre-commit hook will encrypt files marked with <code class="highlighter-rouge">vault: true</code>. If a
<code class="highlighter-rouge">.vault_password_hooks</code> file is present in the project root directory, it will be
used as the password.</p>
<p>If this file doesn’t exist, you’ll be promted for an encryption password and
this password will be saved in <code class="highlighter-rouge">.vault_password_hooks</code>, in your project’s root.</p>
<p>If <code class="highlighter-rouge">.vault_password_hooks</code> is listed in <code class="highlighter-rouge">.gitignore</code>, this file will persist and you
won’t be asked for a password anymore for encryption as well as for decryption.
Otherwise, <code class="highlighter-rouge">.vault_password_hooks</code> will be erased after encryption to avoid commiting
the file.</p>
<p>After commiting the files, the post-commit hook will use the same password to
decrypt the previously encrypted files.</p>
<p><strong>TL;DR:</strong> add <code class="highlighter-rouge">.vault_password_hooks</code> to your <code class="highlighter-rouge">.gitignore</code>, add <code class="highlighter-rouge"># -*-
vault: true; -*-</code> to files that requires encryption and you’re set.</p>
<p>You end up with a workflow where your files are transparently encrypted before
commit and decrypted after.</p>
<h2 id="hooks">Hooks</h2>
<p>Put the hooks in <code class="highlighter-rouge">.git/hooks/</code> and don’t forget to <code class="highlighter-rouge">chmod +x
{pre,post}-commit</code> them.</p>
<script src="https://gist.github.com/leucos/405b406b9d6bde0c3d39.js"> </script>
<p><a href="https://leucos.github.io/articles/ansible-transparent-vault/">Transparent encryption/decryption with ansible vault</a> was originally published by Michel Blanc at <a href="https://leucos.github.io">Random stuff</a> on May 25, 2015.</p>
https://leucos.github.io/articles/ansible-contract-inventory2015-05-11T00:00:00-00:002015-05-03T00:00:00+00:00Michel Blanchttps://leucos.github.iomb@mbnet.fr
<h2 id="the-problem">The problem</h2>
<p>You’ve been there too. Spinning up droplets on DigitalOcean with Ansible
and using a dynamic inventory script is quite a pain.</p>
<p>Most approaches use the <code class="highlighter-rouge">digital_ocean</code> ansible module in
playbooks to spin up droplets, along with the <code class="highlighter-rouge">digital_ocean.py</code> dynamic
inventory script, using this kind of workflow:</p>
<ul>
<li>define your droplets in a YAML file (eventually with size, region,
etc…)</li>
<li>create a playbook that will loop over droplet list (<code class="highlighter-rouge">with_items</code> or
equivalent) and spin up the droplet</li>
<li>dynamically add started droplets to inventory</li>
</ul>
<p>This approach has many drawbacks, and, to be honest, is not really usable.</p>
<h3 id="slooooooow">Slooooooow</h3>
<p>First, it is damn slow. Droplet creation is serialized. Since
<code class="highlighter-rouge">digital_ocean</code> waits for the droplet to come up, and since DO itself
advertizes ‘Start your droplet in 55 seconds !’, you can do the math.
Starting a single droplet is quite long, so spinning up your multi-tier,
fault-tolerant, distributed architecture will take ages.</p>
<p>You probably can use <code class="highlighter-rouge">async</code> + <code class="highlighter-rouge">poll</code> to spin up the droplets. I didn’t
try and don’t know where this would lead. But you’d still face the other
issues.</p>
<h3 id="naming">Naming</h3>
<p>You droplets won’t have real names. They will be known by their IPs.
Sure, if you use the <code class="highlighter-rouge">name</code> parameter during creation, you might be able
to use it, but at best, this will be a group name.</p>
<p>You could also use <code class="highlighter-rouge">add_host</code> in your bootstrapping script, but this is
a run time hack, so forget about setting variables in <code class="highlighter-rouge">host_vars</code>.</p>
<p>Since droplets are mostly nameless, grouping them is hard. Sure, you can
do it at run time with <code class="highlighter-rouge">add_host</code> too, but you won’t leverage
<code class="highlighter-rouge">group_vars</code> usage.</p>
<p>Anyway, all those run-time naming hacks will force you to loop over all
your droplets definitions, hit DO API to make sure they’re alive, then
loop over API responses to add hosts and groups EVERY time you execute a
playbook.</p>
<h3 id="localhost-is-forced-in">localhost is forced in</h3>
<p>Spinning up instances on DO will require to run the <code class="highlighter-rouge">digital_ocean</code>
module as a <code class="highlighter-rouge">local_action</code> or using <code class="highlighter-rouge">delegate_to: localhost</code>. This means
that you are bound to declare localhost in your inventory. This is a
real pain, since it makes the <code class="highlighter-rouge">all</code> group mostly unusable, unless you
change all your playbook hosts definitions from <code class="highlighter-rouge">hosts: all</code> to <code class="highlighter-rouge">hosts:
all:!localhost</code>. Pretty bad for readability.</p>
<p>Let’s stop here, there are already enough reasons to find an alternate
way. There are probably other cons, and certainly pros too for the
dynamic approach, but I fell that this way of doing it is barely usable
for serious, repeatable stuff.</p>
<h2 id="alternate-aproach">Alternate aproach</h2>
<p>In the end, we would like to work as we do with on-prem hardware: have a
static inventory.</p>
<p>The idea is to create this static inventory first, and then use a
bootstrapping script that will use this inventory as a contract to apply
on DigitalOcean.</p>
<p>The script will list all hosts in your inventory (using <code class="highlighter-rouge">ansible
--list-hosts</code>), and parallelize droplet creation on digital ocean.</p>
<p>When all droplets are created, it will create a complementary inventory
file in your inventory directory containing hosts with their respective
IPs.</p>
<p>At this point, you have a perfectly static inventory, and can run your
ansible playbook normally, without hitting external APIs (serialized),
without naming problems, … Things are just <em>normal</em>, fast and
reliable, without edge cases introduced by dynamic inventories.</p>
<p>Using this approach on a 8 droplets setup, the time to set-up instances
went from 9’33” down to 1’56”. And the time to destroy instances went
from 0’55” down to 0’3” (see demo below). Of course, more droplets,
more gain.</p>
<p>And these are just create/destroy gains. You also benefit from static
inventory for all your lifecycle playbook runs, since you never hit DO API and
don’t have to build inventory at run time, which is always slower despite the
inventory cache.</p>
<h3 id="example">Example</h3>
<p>Assuming you have an inventory directory in <code class="highlighter-rouge">inventories/devel/</code>,
containing a <code class="highlighter-rouge">hosts</code> file, you can spin up your droplets like this:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">do_boot.sh inventories/devel/</code></pre></figure>
<p>When you’re finished with your infrastructure, call the same command with
the <code class="highlighter-rouge">deleted</code> parameter:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">do_boot.sh inventories/devel/ deleted</code></pre></figure>
<p>That’s all.</p>
<p>The script has defaults regarding droplet size, region, image and ssh
key. You can change the defaults in the script to something that suits you, and
override these defaults per droplet in your inventory:</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="pi">[</span><span class="nv">www</span><span class="pi">]</span>
<span class="s">www1</span>
<span class="s">www2</span>
<span class="s">www3 do_region=2</span>
<span class="pi">[</span><span class="nv">database</span><span class="pi">]</span>
<span class="s">db1 do_size=62 do_image=12345</span>
<span class="pi">[</span><span class="nv">redis</span><span class="pi">]</span>
<span class="s">redis1</span>
<span class="pi">[</span><span class="nv">elastic</span><span class="pi">]</span>
<span class="s">elastic1 do_size=60</span></code></pre></figure>
<p>###Spinning up and down 8 droplets in 2’15”</p>
<script type="text/javascript" src="https://asciinema.org/a/19479.js" id="asciicast-19479" async=""></script>
<h2 id="script">Script</h2>
<p>You can grab the script in this <a href="https://gist.github.com/leucos/6f8d93de3493431acd29">gist</a>.</p>
<p><strong>UPDATE:</strong> if you run Ansible v2.0+, use <a href="https://gist.github.com/leucos/2c361f7d4767f8aea6dd">this script
instead</a>. It will
use the new digital Ocean API (v2.0 too). You just need to set
<code class="highlighter-rouge">DO_API_TOKEN</code>.</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c">#!/bin/bash</span>
<span class="c">#</span>
<span class="c"># Change defaults below</span>
<span class="c"># ---------------------</span>
<span class="c">#</span>
<span class="c"># Digital Ocean default values</span>
<span class="c"># You can override them using do_something in your inventory file</span>
<span class="c"># Example:</span>
<span class="c"># </span>
<span class="c"># [www]</span>
<span class="c"># www1 do_size=62 do_image=12345</span>
<span class="c"># ...</span>
<span class="c">#</span>
<span class="c"># If you don't override in your inventory, the defaults below will apply</span>
<span class="nv">DEFAULT_SIZE</span><span class="o">=</span>66 <span class="c"># 512mb (override with do_size)</span>
<span class="nv">DEFAULT_REGION</span><span class="o">=</span>5 <span class="c"># ams2 (override with do_region)</span>
<span class="nv">DEFAULT_IMAGE</span><span class="o">=</span>9801950 <span class="c"># Ubuntu 14.04 x64 (override with do_image)</span>
<span class="nv">DEFAULT_KEY</span><span class="o">=</span>785648 <span class="c"># SSH key, change this ! (override with do_key)</span>
<span class="c"># localhost entry for temporary inventory</span>
<span class="c"># This is a temp inventory generated to start the DO droplets</span>
<span class="c"># You might want to change ansible_python_interpreter</span>
<span class="nv">LOCALHOST_ENTRY</span><span class="o">=</span><span class="s2">"localhost ansible_python_interpreter=/usr/bin/python2"</span>
<span class="c"># Set state to present by default</span>
<span class="nv">STATE</span><span class="o">=</span><span class="k">${</span><span class="nv">2</span><span class="k">:-</span><span class="s2">"present"</span><span class="k">}</span>
<span class="c"># digital_ocean module command to use</span>
<span class="c"># name, size, region, image and key will be filled automatically</span>
<span class="nv">COMMAND</span><span class="o">=</span><span class="s2">"state=</span><span class="nv">$STATE</span><span class="s2"> command=droplet private_networking=yes unique_name=yes"</span>
<span class="c"># ---------------------</span>
<span class="k">function </span>bail_out <span class="o">{</span>
<span class="nb">echo</span> <span class="nv">$1</span>
<span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"Usage: </span><span class="nv">$0</span><span class="s2"> <inventory_directory> [present|deleted]</span><span class="se">\n</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="se">\t</span><span class="s2">inventory_directory: the directory containing the inventory goal (compulsory)"</span>
<span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="se">\t</span><span class="s2">present: the droplet will be created if it doesn't exist (default)"</span>
<span class="nb">echo</span> <span class="nt">-e</span> <span class="s2">"</span><span class="se">\t</span><span class="s2">deleted: the droplet will be destroyed if it exists"</span>
<span class="nb">exit </span>1
<span class="o">}</span>
<span class="c"># Check that inventory is a directory</span>
<span class="c"># We need this since we generate a complementary inventory with IP addresses for hosts</span>
<span class="nv">INVENTORY</span><span class="o">=</span><span class="nv">$1</span>
<span class="o">[[</span> <span class="o">!</span> <span class="nt">-d</span> <span class="s2">"</span><span class="nv">$INVENTORY</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&&</span> bail_out <span class="s2">"Inventory does not exist, is not a
directory, or is not set"</span>
<span class="o">[[</span> <span class="o">!</span> <span class="nt">-e</span> <span class="nv">$DO_CLIENT_ID</span> <span class="o">]]</span> <span class="o">||</span> bail_out <span class="s2">"DO_CLIENT_ID not set"</span>
<span class="o">[[</span> <span class="o">!</span> <span class="nt">-e</span> <span class="nv">$DO_API_KEY</span> <span class="o">]]</span> <span class="o">||</span> bail_out <span class="s2">"DO_API_KEY not set"</span>
<span class="c"># Get a list of hosts from inventory dir</span>
<span class="nv">HOSTS</span><span class="o">=</span><span class="k">$(</span>ansible <span class="nt">-i</span> <span class="nv">$1</span> <span class="nt">--list-hosts</span> all | awk <span class="s1">'{ print $1 }'</span> | tr <span class="s1">'\n'</span> <span class="s1">' '</span><span class="k">)</span>
<span class="c"># Clean up previously generated inventory</span>
rm <span class="nv">$INVENTORY</span>/generated
<span class="c"># Creating temporary inventory with only localhost in it</span>
<span class="nv">TEMP_INVENTORY</span><span class="o">=</span><span class="k">$(</span>mktemp<span class="k">)</span>
<span class="nb">echo </span>Creating temporary inventory <span class="k">in</span> <span class="nv">$TEMP_INVENTORY</span>
<span class="nb">echo</span> <span class="nv">$LOCALHOST</span> <span class="o">></span> <span class="nv">$TEMP_INVENTORY</span>
<span class="c"># Create droplets in //</span>
<span class="k">for </span>i <span class="k">in</span> <span class="nv">$HOSTS</span><span class="p">;</span> <span class="k">do
</span><span class="nv">SIZE</span><span class="o">=</span><span class="k">$(</span><span class="nb">grep</span> <span class="nv">$i</span> <span class="nv">$1</span>/hosts | <span class="nb">grep </span>do_size | sed <span class="nt">-e</span> <span class="s1">'s/.*do_size=\(\d*\)/\1/'</span><span class="k">)</span>
<span class="nv">REGION</span><span class="o">=</span><span class="k">$(</span><span class="nb">grep</span> <span class="nv">$i</span> <span class="nv">$1</span>/hosts | <span class="nb">grep </span>do_region | sed <span class="nt">-e</span> <span class="s1">'s/.*do_region=\(\d*\)/\1/'</span><span class="k">)</span>
<span class="nv">IMAGE</span><span class="o">=</span><span class="k">$(</span><span class="nb">grep</span> <span class="nv">$i</span> <span class="nv">$1</span>/hosts | <span class="nb">grep </span>do_image | sed <span class="nt">-e</span> <span class="s1">'s/.*do_image=\(\d*\)/\1/'</span><span class="k">)</span>
<span class="nv">KEY</span><span class="o">=</span><span class="k">$(</span><span class="nb">grep</span> <span class="nv">$i</span> <span class="nv">$1</span>/hosts | <span class="nb">grep </span>do_key | sed <span class="nt">-e</span> <span class="s1">'s/.*do_key=\(\d*\)/\1/'</span><span class="k">)</span>
<span class="nv">SIZE</span><span class="o">=</span><span class="k">${</span><span class="nv">SIZE</span><span class="k">:-</span><span class="nv">$DEFAULT_SIZE</span><span class="k">}</span>
<span class="nv">REGION</span><span class="o">=</span><span class="k">${</span><span class="nv">REGION</span><span class="k">:-</span><span class="nv">$DEFAULT_REGION</span><span class="k">}</span>
<span class="nv">IMAGE</span><span class="o">=</span><span class="k">${</span><span class="nv">IMAGE</span><span class="k">:-</span><span class="nv">$DEFAULT_IMAGE</span><span class="k">}</span>
<span class="nv">KEY</span><span class="o">=</span><span class="k">${</span><span class="nv">KEY</span><span class="k">:-</span><span class="nv">$DEFAULT_KEY</span><span class="k">}</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="k">${</span><span class="nv">STATE</span><span class="k">}</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"present"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span><span class="nb">echo</span> <span class="s2">"Creating </span><span class="nv">$i</span><span class="s2"> of size </span><span class="nv">$SIZE</span><span class="s2"> using image </span><span class="nv">$IMAGE</span><span class="s2"> in region </span><span class="nv">$REGION</span><span class="s2"> with key </span><span class="nv">$KEY</span><span class="s2">"</span>
<span class="k">else
</span><span class="nb">echo</span> <span class="s2">"Deleting </span><span class="nv">$i</span><span class="s2">"</span>
<span class="k">fi</span>
<span class="c"># echo " => $COMMAND name=$i size_id=$SIZE image_id=$IMAGE region_id=$REGION ssh_key_ids=$KEY"</span>
ansible localhost <span class="nt">-c</span> <span class="nb">local</span> <span class="nt">-i</span> <span class="nv">$TEMP_INVENTORY</span> <span class="nt">-m</span> digital_ocean <span class="se">\</span>
<span class="nt">-a</span> <span class="s2">"</span><span class="nv">$COMMAND</span><span class="s2"> name=</span><span class="nv">$i</span><span class="s2"> size_id=</span><span class="nv">$SIZE</span><span class="s2"> image_id=</span><span class="nv">$IMAGE</span><span class="s2"> region_id=</span><span class="nv">$REGION</span><span class="s2"> ssh_key_ids=</span><span class="nv">$KEY</span><span class="s2">"</span> &
<span class="k">done
</span><span class="nb">wait</span>
<span class="c"># Now do it again to fill up complementary inventory</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="k">${</span><span class="nv">STATE</span><span class="k">}</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"present"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
for </span>i <span class="k">in</span> <span class="nv">$HOSTS</span><span class="p">;</span> <span class="k">do
</span><span class="nb">echo </span>Checking droplet <span class="nv">$i</span>
<span class="nv">IP</span><span class="o">=</span><span class="k">$(</span>ansible localhost <span class="nt">-c</span> <span class="nb">local</span> <span class="nt">-i</span> <span class="nv">$TEMP_INVENTORY</span> <span class="nt">-m</span> digital_ocean <span class="nt">-a</span> <span class="s2">"state=present command=droplet unique_name=yes name=</span><span class="nv">$i</span><span class="s2">"</span> | <span class="nb">grep</span> <span class="s2">"</span><span class="se">\"</span><span class="s2">ip_address"</span> | awk <span class="s1">'{ print $2 }'</span> | cut <span class="nt">-f2</span> <span class="nt">-d</span><span class="s1">'"'</span><span class="k">)</span>
<span class="nb">echo</span> <span class="s2">"</span><span class="nv">$i</span><span class="s2"> ansible_ssh_host=</span><span class="nv">$IP</span><span class="s2">"</span> <span class="o">>></span> <span class="nv">$INVENTORY</span>/generated
<span class="k">done
fi
</span><span class="nb">echo</span> <span class="s2">"All done !"</span></code></pre></figure>
<p><a href="https://leucos.github.io/articles/ansible-contract-inventory/">Making dynamic inventory usable with Ansible and Digital Ocean</a> was originally published by Michel Blanc at <a href="https://leucos.github.io">Random stuff</a> on May 03, 2015.</p>
https://leucos.github.io/articles/testing-ansible-roles-part-22015-03-15T00:00:00+00:002015-03-15T00:00:00+00:00Michel Blanchttps://leucos.github.iomb@mbnet.fr
<p>Now that we have created our basic role in <a href="/articles/testing-ansible-roles-part-1/">part 1</a>, we need to set-up a Vagrant machine and some tooling to run our tests.</p>
<h2 id="creating-the-vagrant-machine">Creating the Vagrant machine</h2>
<h3 id="vagrantfile">Vagrantfile</h3>
<p>To spin up a Vagrant machine, we need to create a <code class="highlighter-rouge">Vagrantfile</code>. We’ll create it in our role top directory:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="no">Vagrant</span><span class="p">.</span><span class="nf">configure</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">config</span><span class="o">|</span>
<span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">box</span> <span class="o">=</span> <span class="s2">"ubuntu/trusty64"</span>
<span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">define</span> <span class="s2">"nginx"</span> <span class="k">do</span> <span class="o">|</span><span class="n">nginx</span><span class="o">|</span>
<span class="k">end</span>
<span class="n">config</span><span class="p">.</span><span class="nf">vm</span><span class="p">.</span><span class="nf">provision</span> <span class="s2">"shell"</span><span class="p">,</span>
<span class="ss">:path</span> <span class="o">=></span> <span class="s2">"vagrant_specs.sh"</span><span class="p">,</span>
<span class="ss">:upload_path</span> <span class="o">=></span> <span class="s2">"/home/vagrant/specs"</span><span class="p">,</span>
<span class="c1"># change role name below</span>
<span class="ss">:args</span> <span class="o">=></span> <span class="s2">"--install ansible-nginx"</span>
<span class="k">end</span></code></pre></figure>
<p>You can change <code class="highlighter-rouge">config.vm.box</code> to another Vagrant box that better suits your
needs, but keep in mind RoleSpec is very Debian/Ubuntu inclined. We’ll provision
this machine with a shell script (not with Ansible, so we don’t end up in an
inception style situation).</p>
<h3 id="provisionning-script">Provisionning script</h3>
<p>The provisionning script, <code class="highlighter-rouge">vagrant_specs.sh</code> serves two purposes:</p>
<ol>
<li>
<p>it takes care of installing RoleSpec and setting up the test directory when
called with <code class="highlighter-rouge">--install</code>. This happens only at vagrant provisionning time (e.g.
<code class="highlighter-rouge">vagrant up</code> of <code class="highlighter-rouge">vagrant provision</code>)</p>
</li>
<li>
<p>it can be called to run the test suite; to make invocation easier, it will copy itself to <code class="highlighter-rouge">/usr/local/bin/specs</code></p>
</li>
</ol>
<p>Create the <code class="highlighter-rouge">vagrant_specs.sh</code> with the following content:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c">#!/bin/bash</span>
<span class="c">#</span>
<span class="c"># Vagrant provisionning script</span>
<span class="c">#</span>
<span class="c"># Usage for provisionning VM & running (in Vagrant file):</span>
<span class="c"># </span>
<span class="c"># script.sh --install <role></span>
<span class="c">#</span>
<span class="c"># e.g. : </span>
<span class="c"># script.sh --install ansible-nginx</span>
<span class="c"># </span>
<span class="c"># Usage for running only (from host):</span>
<span class="c">#</span>
<span class="c"># vagrant ssh -c specs</span>
<span class="c">#</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"x</span><span class="nv">$1</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"x--install"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span>mv ~vagrant/specs /usr/local/bin/specs
chmod 755 /usr/local/bin/specs
<span class="nb">sudo </span>apt-get install <span class="nt">-qqy</span> git
su vagrant <span class="nt">-c</span> <span class="s1">'git clone --depth 1 https://github.com/nickjj/rolespec'</span>
<span class="nb">cd</span> ~vagrant/rolespec <span class="o">&&</span> make install
su vagrant <span class="nt">-c</span> <span class="s1">'rolespec -i ~/testdir'</span>
su vagrant <span class="nt">-c</span> <span class="s2">"ln -s /vagrant/ ~/testdir/roles/</span><span class="nv">$2</span><span class="s2">"</span>
su vagrant <span class="nt">-c</span> <span class="s2">"ln -s /vagrant/tests/</span><span class="nv">$2</span><span class="s2">/ ~/testdir/tests/"</span>
<span class="nb">exit
</span><span class="k">fi
</span><span class="nb">cd</span> ~vagrant/testdir <span class="o">&&</span> rolespec <span class="nt">-r</span> <span class="k">$(</span><span class="nb">ls </span>roles<span class="k">)</span> <span class="s2">"</span><span class="nv">$*</span><span class="s2">"</span></code></pre></figure>
<p>and make it executable (<code class="highlighter-rouge">chmod +x vagrant_specs.sh</code>).</p>
<h3 id="running-the-vagrat-box">Running the Vagrat box</h3>
<p>Now, let’s check this ! It might take a while if you don’t already have the
vagrant image on your box:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ vagrant up
Bringing machine 'nginx' up with 'virtualbox' provider...
==> nginx: Importing base box 'ubuntu/trusty64'...
==> nginx: Matching MAC address for NAT networking...
==> nginx: Checking if box 'ubuntu/trusty64' is up to date...
==> nginx: Setting the name of the VM: ansible-nginx_nginx_1426331325901_88232
...
==> nginx: Cloning into 'rolespec'...
==> nginx: Installing RoleSpec scripts in /usr/local/bin ...
==> nginx: Installing RoleSpec libs in /usr/local/lib/rolespec ...
==> nginx: Initialized new RoleSpec directory in /home/vagrant/testdir
$
</code></pre></div></div>
<h2 id="creating-tests">Creating tests</h2>
<p>We’re almost done. Only two files left to create. First, we RoleSpec needs an inventory. Nothing fancy here, we just need to create an inventory file with a single host, <code class="highlighter-rouge">placeholder_fqdn</code>, RoleSpec will take care of the rest:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span><span class="nb">echo</span> <span class="s2">"placeholder_fqdn"</span> <span class="o">></span> tests/ansible-nginx/inventory/hosts</code></pre></figure>
<h3 id="writing-the-test-file">Writing the test file</h3>
<p>And finally, we need a test file, where we can check if our playbook works. We can check the syntax, the idempotency, the resulting templates, etc…</p>
<p>This test file is simply a bash script, in which we include some RoleSpec files to get access to its DSL.</p>
<p>Let’s start with a simple one, and create <code class="highlighter-rouge">tests/ansible-nginx/test</code> with the following content:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c">#!/bin/bash</span>
<span class="c"># -*- bash -*-</span>
<span class="c"># This gives you access to the custom DSL</span>
<span class="nb">.</span> <span class="s2">"</span><span class="k">${</span><span class="nv">ROLESPEC_LIB</span><span class="k">}</span><span class="s2">/main"</span>
<span class="c"># Install a specific version of Ansible</span>
install_ansible <span class="s2">"v1.8.3"</span>
<span class="c"># Check syntax first, and then that the playbook runs</span>
assert_playbook_runs
<span class="c"># Check that the playbook is idempotent</span>
assert_playbook_idempotent</code></pre></figure>
<p>Don’t forget to make the test file executable (<code class="highlighter-rouge">chmod +x tests/ansible-nginx/test</code>).</p>
<h3 id="runing-tests">Runing tests</h3>
<p>Our simple tests are setup. To run them, we need to execute
<code class="highlighter-rouge">/usr/local/bin/specs</code> in the Vagrant host.</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">vagrant ssh <span class="nt">-c</span> <span class="s1">'specs'</span></code></pre></figure>
<p>RoleSpecs will then download Ansible (version 1.8.3 since this is what we
asked for), install it, and run our test case.</p>
<script type="text/javascript" src="https://asciinema.org/a/17711.js" id="asciicast-17711" async=""></script>
<p>As you can see in the recording, RoleSpec:</p>
<ul>
<li>installs Ansible (<code class="highlighter-rouge">ROLESPEC: [Install Ansible - v1.8.3]</code>)</li>
<li>executes the playbook with <code class="highlighter-rouge">assert_playbook_runs</code> (<code class="highlighter-rouge">TEST: [Run playbook syntax check]</code> and <code class="highlighter-rouge">TEST: [Run playbook]</code>)</li>
<li>check that the playbook is idempotent with <code class="highlighter-rouge">assert_playbook_idempotent</code> (<code class="highlighter-rouge">TEST: [Re-run playbook]</code>)</li>
</ul>
<p>Pretty neat !</p>
<h3 id="runing-tests-faster">Runing tests faster</h3>
<p>There is one downside though: it takes almost 3 minutes to run. However, you can
speed up subsequent runs as long as you don’t have to change the Ansible
version: since Ansible is already installed, there is no need to install it
again every time. Using the <code class="highlighter-rouge">-p</code> option will run in <em>playbook mode</em>, which means
it will only run <code class="highlighter-rouge">assert_playbook_runs</code> test.</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">vagrant ssh <span class="nt">-c</span> <span class="s1">'specs'</span></code></pre></figure>
<script type="text/javascript" src="https://asciinema.org/a/17712.js" id="asciicast-17712" async=""></script>
<p>25 seconds only, we cut the runtime by six, not bad.</p>
<h2 id="local-continuous-integration">Local continuous integration</h2>
<p>Now that we have reasonable playbook test run time, we can add local continuous integration to our setup.
We will use <a href="https://github.com/guard/guard">Guard</a> for this.</p>
<p>Assuming you have a ruby environment setup, just install <code class="highlighter-rouge">guard</code> and <code class="highlighter-rouge">guard-shell</code> gems.</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">gem install guard guard-shell <span class="nt">--no-ri</span> <span class="nt">--no-rdoc</span></code></pre></figure>
<p>Then create a <code class="highlighter-rouge">Guardfile</code> in the roles top directory, with the following content:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># -- -*- mode: ruby; -*-</span>
<span class="n">guard</span> <span class="ss">:shell</span> <span class="k">do</span>
<span class="n">watch</span><span class="p">(</span><span class="sr">%r{^(?!tests).*/.*</span><span class="se">\.</span><span class="sr">yml$}</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">m</span><span class="o">|</span>
<span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">m</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s2"> changed - running tests"</span>
<span class="nb">system</span><span class="p">(</span><span class="s1">'vagrant ssh -c "specs -p"'</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>This file will ask <code class="highlighter-rouge">guard</code> to execute <code class="highlighter-rouge">vagrant ssh -c "specs -p"</code> everytime it
detects a change in a file ending with <code class="highlighter-rouge">.yml</code> in the project’s subdirectories.
Note that we excluded the <code class="highlighter-rouge">tests</code> directory since it contains somewhere a
<code class="highlighter-rouge">test.yml</code> playbook file generated by RoleSpec at run time. If we don’t exclude
it from the guard watch, the test will loop forever.</p>
<p>Now run <code class="highlighter-rouge">guard</code>, change a file (.e.g. <code class="highlighter-rouge">touch tasks/main.yml</code>), and see what happens.</p>
<p>In the next part, we will add some more tests, and see what we can do with <a href="rolespec">RoleSpec</a>.</p>
<p><a href="https://leucos.github.io/articles/testing-ansible-roles-part-2/">Testing Ansible roles, part 2</a> was originally published by Michel Blanc at <a href="https://leucos.github.io">Random stuff</a> on March 15, 2015.</p>
https://leucos.github.io/articles/testing-ansible-roles-part-12015-03-14T00:00:00+00:002015-03-14T00:00:00+00:00Michel Blanchttps://leucos.github.iomb@mbnet.fr
<p><a href="rolespec">RoleSpec</a> does a great job helping out testing your roles. It is
maintained and used primarily to test the <a href="debops">DebOps</a> role suite by the
fine folks hanging out in <a href="irc://irc.freenode.net/debops">#debops</a> IRC channel.
RoleSpec handles all the boiler plate to run tests (installing the right version
of Ansible, adjusting paths, taking care of the inventory, wrapping your role in
a playbook, …) and privides a simple DSL to write tests.</p>
<p>However, in its current state, RoleSpec is mostly intended to run a test suite
on travis. And this test suite is separated from your role.</p>
<p>I personally prefer to have my role tests along the Ansible role, in a <code class="highlighter-rouge">tests</code>
directory.</p>
<p>We see below how we can achieve this with RoleSpec, and will leverage Vagrant
for this. We’ll also use Guard to continuously test our role while writing it.</p>
<h2 id="a-simple-role">A simple role</h2>
<p>Let’s start by creating a simple nginx role:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash">mkdir <span class="nt">-p</span> ansible-nginx/<span class="o">{</span>defaults,handlers,tasks,templates,tests/ansible-nginx/inventory<span class="o">}</span></code></pre></figure>
<p>The <code class="highlighter-rouge">tests</code> directory will be used for our tests later.</p>
<p>If you already have a role want to convert it, create the <code class="highlighter-rouge">tests/ansible-
nginx/</code> directory and skip straight to
<a href="/articles/testing-ansible-roles-part-2/">part 2</a>.</p>
<h3 id="defaults">Defaults</h3>
<p>In <code class="highlighter-rouge">default/main.yml</code>, we’ll declare a few default values for our role. We won’t do much,
in our role, just install nginx and set a few variables, so let’s keep this
simple:</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="na">nginx_root</span><span class="pi">:</span> <span class="s">/var/lib/nginx/</span>
<span class="na">nginx_worker_connections</span><span class="pi">:</span> <span class="s">1024</span>
<span class="na">nginx_ie8_support</span><span class="pi">:</span> <span class="s">yes</span>
<span class="na">nginx_port</span><span class="pi">:</span> <span class="s">80</span></code></pre></figure>
<h3 id="handlers">Handlers</h3>
<p>For the handlers part, <code class="highlighter-rouge">handlers/main.yml</code> will contain a basic restart handler, followed by a port check for good measure:</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Restart nginx</span>
<span class="na">action</span><span class="pi">:</span> <span class="s">service name=nginx state=restarted</span>
<span class="na">notify</span><span class="pi">:</span> <span class="s">Check nginx</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Check nginx</span>
<span class="na">wait_for</span><span class="pi">:</span> <span class="s">port={{ nginx_port }} delay=5 timeout=10</span></code></pre></figure>
<h3 id="tasks">Tasks</h3>
<p>Now the task part. I always put my tasks in a separate file, and include this
file from <code class="highlighter-rouge">main.yml</code>. This trick will allow you to set a tag for the whole
included file, like so:</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="pi">-</span> <span class="na">include</span><span class="pi">:</span> <span class="s">nginx.yml tags=nginx</span></code></pre></figure>
<p>And then, in <code class="highlighter-rouge">nginx.yml</code>, put the real tasks:</p>
<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Adds nginx ppa</span>
<span class="na">apt_repository</span><span class="pi">:</span>
<span class="s">repo=ppa:nginx/stable</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Adds PPA key</span>
<span class="na">apt_key</span><span class="pi">:</span>
<span class="s">url=http://keyserver.ubuntu.com:11371/pks/lookup?op=get&search=0x00A6F0A3C300EE8C</span>
<span class="s">state=present</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Installs nginx</span>
<span class="na">apt</span><span class="pi">:</span>
<span class="s">pkg=nginx-full</span>
<span class="s">state=latest</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Writes nginx.conf</span>
<span class="na">template</span><span class="pi">:</span>
<span class="s">src="../templates/nginx.conf.j2"</span>
<span class="s">dest=/etc/nginx/nginx.conf</span>
<span class="s">validate='nginx -tc %s'</span>
<span class="na">notify</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">Restart nginx</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Replaces nginx default server</span>
<span class="na">template</span><span class="pi">:</span>
<span class="s">src="../templates/default.j2"</span>
<span class="s">dest=/etc/nginx/sites-available/default</span>
<span class="na">notify</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">Restart nginx</span></code></pre></figure>
<h3 id="templates">Templates</h3>
<p>We just need to add 2 templates, and our role will be ready. The first one is the main <code class="highlighter-rouge">nginx.conf.j2</code> file:</p>
<figure class="highlight"><pre><code class="language-jinja" data-lang="jinja">user www-data;
worker_processes <span class="cp">{{</span> <span class="nv">ansible_processor_count</span> <span class="cp">}}</span>;
pid /var/run/nginx.pid;
events {
worker_connections <span class="cp">{{</span> <span class="nv">nginx_worker_connections</span> <span class="cp">}}</span>;
# multi_accept on;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
tcp_nodelay on;
# SSL stuff
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
<span class="cp">{%</span> <span class="k">if</span> <span class="nv">nginx_ie8_support</span> <span class="cp">%}</span>
ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
<span class="cp">{%</span> <span class="k">else</span> <span class="cp">%}</span>
ssl_ciphers "EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH+aRSA+RC4:EECDH:EDH+aRSA:!RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXPORT:!PSK:!SRP:!DSS";
<span class="cp">{%</span> <span class="k">endif</span> <span class="cp">%}</span>
ssl_session_cache shared:SSL:32m;
ssl_buffer_size 8k;
ssl_session_timeout 10m;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
##
# Gzip Settings
##
gzip on;
gzip_disable "msie6";
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_types application/javascript
application/json
application/x-javascript
application/xml
application/xml+rss
image/svg+xml
text/css
text/plain
text/xml
text/javascript;
##
# If HTTPS, then set a variable so it can be passed along.
##
map $scheme $server_https {
default off;
https on;
}
##
# Virtual Host Configs
##
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}</code></pre></figure>
<p>The file is a bit long, but it just contains basic settings. Note that we’re
aligning the number of worker processes to the number of processors reported by
Ansible for the host.</p>
<p>We are also switching cipher suites depending on whether we want to support IE8
or not.</p>
<p>Then, we just add a default virtualhost on our server:</p>
<figure class="highlight"><pre><code class="language-nginx" data-lang="nginx"><span class="k">server</span> <span class="p">{</span>
<span class="kn">listen</span> <span class="p">{</span><span class="err">{</span> <span class="kn">nginx_port</span> <span class="err">}}</span><span class="p">;</span>
<span class="kn">root</span> <span class="p">{</span><span class="err">{</span> <span class="kn">nginx_root</span> <span class="err">}}</span><span class="p">;</span>
<span class="kn">index</span> <span class="s">index.html</span> <span class="s">index.htm</span><span class="p">;</span>
<span class="c1"># Make site accessible from http://localhost/
</span> <span class="kn">server_name</span> <span class="s">_</span><span class="p">;</span>
<span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
<span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri</span><span class="n">/</span> <span class="n">/index.php?q=</span><span class="nv">$uri</span><span class="s">&</span><span class="nv">$args</span><span class="p">;</span>
<span class="p">}</span>
<span class="kn">error_page</span> <span class="mi">404</span> <span class="n">/404.html</span><span class="p">;</span>
<span class="c1"># redirect server error pages to the static page /50x.html
</span> <span class="c1">#
</span> <span class="kn">error_page</span> <span class="mi">500</span> <span class="mi">502</span> <span class="mi">503</span> <span class="mi">504</span> <span class="n">/50x.html</span><span class="p">;</span>
<span class="kn">location</span> <span class="p">=</span> <span class="n">/50x.html</span> <span class="p">{</span>
<span class="kn">root</span> <span class="n">/usr/share/nginx/html/</span><span class="p">;</span>
<span class="p">}</span>
<span class="c1"># deny access to .htaccess files, if Apache's document root
</span> <span class="c1"># concurs with nginx's one
</span> <span class="c1">#
</span> <span class="kn">location</span> <span class="p">~</span> <span class="sr">/\.ht</span> <span class="p">{</span>
<span class="kn">deny</span> <span class="s">all</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span></code></pre></figure>
<p>Our role is now ready. We can now setup the tooling for our tests as explained in <a href="/articles/testing-ansible-roles-part-2/">part 2</a></p>
<p><a href="https://leucos.github.io/articles/testing-ansible-roles-part-1/">Testing Ansible roles, part 1</a> was originally published by Michel Blanc at <a href="https://leucos.github.io">Random stuff</a> on March 14, 2015.</p>
https://leucos.github.io/articles/expiring-redis-cache-from-ruby2015-03-01T00:00:00-00:002015-03-01T00:00:00+00:00Michel Blanchttps://leucos.github.iomb@mbnet.fr
<h2 id="redis-as-padrino-cache">REDIS as Padrino cache</h2>
<p>Using REDIS as and application cache is very handy. You can easily use
it in, say, <a href="http://www.padrinorb.com/">Padrino</a> like this:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="k">module</span> <span class="nn">MyApp</span>
<span class="k">class</span> <span class="nc">App</span> <span class="o"><</span> <span class="no">Padrino</span><span class="o">::</span><span class="no">Application</span>
<span class="n">enable</span> <span class="ss">:caching</span>
<span class="n">set</span> <span class="ss">:cache</span><span class="p">,</span> <span class="no">Padrino</span><span class="o">::</span><span class="no">Cache</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span> <span class="ss">:Redis</span><span class="p">,</span>
<span class="ss">:host</span> <span class="o">=></span> <span class="no">ENV</span><span class="p">[</span><span class="s1">'REDIS_SERVER'</span><span class="p">],</span>
<span class="ss">:port</span> <span class="o">=></span> <span class="no">ENV</span><span class="p">[</span><span class="s1">'REDIS_PORT'</span><span class="p">],</span>
<span class="ss">:db</span> <span class="o">=></span> <span class="mi">0</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="no">MyApp</span><span class="o">::</span><span class="no">App</span><span class="p">.</span><span class="nf">controllers</span> <span class="ss">:test</span> <span class="k">do</span>
<span class="n">get</span> <span class="ss">:index</span><span class="p">,</span> <span class="ss">:cache</span> <span class="o">=></span> <span class="kp">true</span> <span class="k">do</span>
<span class="n">cache_key</span> <span class="p">{</span> <span class="n">current_account</span><span class="p">.</span><span class="nf">email</span> <span class="o">+</span> <span class="s2">":test:index"</span> <span class="p">}</span>
<span class="n">expires</span> <span class="mi">3600</span>
<span class="vi">@title</span> <span class="o">=</span> <span class="s2">"Some page with expensively computed values"</span>
<span class="n">render</span> <span class="s1">'test/index'</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>Note that we have a specific cache entry for each user
(<code class="highlighter-rouge">current_account.email</code>). For instance, a user with email <code class="highlighter-rouge">foo@bar.com</code>
will have this entry cached at <code class="highlighter-rouge">foo@bar.com:test:index</code></p>
<h2 id="cache-invalidation">Cache invalidation</h2>
<p>Now, sometimes you need to expire the cache forcibly. For instance,
let’s say you know you’ve changed something in the database and that you
don’t want stale data to be served, you can invalidate the cache
manually. Or may be you want to invalidate a complete user cache at
login time.</p>
<p>However, this is not easy in our case, since we want to remove all
entries matching <code class="highlighter-rouge">*:test:index</code> (or <code class="highlighter-rouge">foo@bar.com:*</code> if we want to
completely wipe out the user cache).</p>
<p>The first idea that comes to mind is to use the Redis <code class="highlighter-rouge">KEYS</code> command that can
accept globs to match key names, like <code class="highlighter-rouge">KEYS foo@bar.com:*</code>.</p>
<p>But in the documentation<sup id="fnref:1"><a href="#fn:1" class="footnote">1</a></sup>, you’ll find a big fat warning about KEYS:</p>
<blockquote>
<p>Warning: consider KEYS as a command that should only be used in production
environments with extreme care. It may ruin performance when it is executed
against large databases. This command is intended for debugging and special
operations, such as changing your keyspace layout. Don’t use KEYS in your
regular application code. If you’re looking for a way to find keys in a
subset of your keyspace, consider using SCAN or sets.</p>
</blockquote>
<p>Scary as it sounds.</p>
<h2 id="cursors-to-the-rescue">Cursors to the rescue</h2>
<p>REDIS comes with a nice, but not very known feature since v2.8: SCAN. The SCAN
command is a cursor based iterator. You give him a key pattern, and every time
you call it, it will return the next set of matching keys, and an index for the
next call.</p>
<p>Here is a piece of code that can invalidate key wildcards from padrino :</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="no">MyApp</span><span class="o">::</span><span class="no">App</span><span class="p">.</span><span class="nf">controllers</span> <span class="ss">:test</span> <span class="k">do</span>
<span class="n">define_method</span> <span class="ss">:invalidate_cache_like</span> <span class="k">do</span> <span class="o">|</span><span class="n">wildcard</span><span class="o">|</span>
<span class="n">r</span> <span class="o">=</span> <span class="no">Redis</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:host</span> <span class="o">=></span> <span class="no">ENV</span><span class="p">[</span><span class="s1">'REDIS_SERVER'</span><span class="p">],</span> <span class="ss">:port</span> <span class="o">=></span> <span class="no">ENV</span><span class="p">[</span><span class="s1">'REDIS_PORT'</span><span class="p">])</span>
<span class="n">cursor</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="k">while</span> <span class="n">cursor</span> <span class="o">!=</span> <span class="s2">"0"</span> <span class="k">do</span>
<span class="n">cursor</span><span class="p">,</span> <span class="n">keys</span> <span class="o">=</span> <span class="n">r</span><span class="p">.</span><span class="nf">scan</span><span class="p">(</span><span class="n">cursor</span><span class="o">||</span><span class="n">e</span><span class="p">,</span> <span class="p">{</span> <span class="ss">match: </span><span class="n">wildcard</span><span class="p">})</span>
<span class="n">keys</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">k</span><span class="o">|</span>
<span class="n">r</span><span class="p">.</span><span class="nf">del</span><span class="p">(</span><span class="n">k</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>You can not easily invalidate a cache wildcard calling
<code class="highlighter-rouge">invalidate_cache_like</code>.</p>
<p>For instance, at user login, you could call :</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="sb">`invalidate_cache_like "</span><span class="si">#{</span><span class="n">current_account</span><span class="p">.</span><span class="nf">email</span><span class="si">}</span><span class="sb">:test:index"</span></code></pre></figure>
<p>and the user cache is now cleared.</p>
<h2 id="benchmarking">Benchmarking</h2>
<p>Let’s play with <code class="highlighter-rouge">Benchmark</code> a bit to compare <code class="highlighter-rouge">SCAN</code> and <code class="highlighter-rouge">KEYS</code> performance on a
moderately sized database. While we’re at it, we’ll also check these commands using <code class="highlighter-rouge">redis</code> and <code class="highlighter-rouge">hiredis</code> drivers, to see if it makes any difference.</p>
<p>I used the following piece of code for that:</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1">#!/bin/env ruby</span>
<span class="nb">require</span> <span class="s1">'hiredis'</span>
<span class="nb">require</span> <span class="s1">'em-synchrony'</span>
<span class="nb">require</span> <span class="s1">'redis'</span>
<span class="nb">require</span> <span class="s1">'benchmark'</span>
<span class="k">def</span> <span class="nf">build_cache</span><span class="p">(</span><span class="n">redis</span><span class="p">)</span>
<span class="p">(</span><span class="s1">'aaaa'</span><span class="o">..</span><span class="s1">'zzzz'</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">s</span><span class="o">|</span>
<span class="n">redis</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="n">s</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">invalidate_cache_cursor</span><span class="p">(</span><span class="n">redis</span><span class="p">,</span> <span class="n">wildcard</span><span class="p">)</span>
<span class="n">cursor</span> <span class="o">=</span> <span class="kp">nil</span>
<span class="k">while</span> <span class="n">cursor</span> <span class="o">!=</span> <span class="s2">"0"</span> <span class="k">do</span>
<span class="n">cursor</span><span class="p">,</span> <span class="n">keys</span> <span class="o">=</span> <span class="n">redis</span><span class="p">.</span><span class="nf">scan</span><span class="p">(</span><span class="n">cursor</span><span class="o">||</span><span class="mi">0</span><span class="p">,</span> <span class="p">{</span> <span class="ss">match: </span><span class="n">wildcard</span><span class="p">})</span>
<span class="n">keys</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">k</span><span class="o">|</span>
<span class="n">redis</span><span class="p">.</span><span class="nf">del</span><span class="p">(</span><span class="n">k</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">invalidate_cache_keys</span><span class="p">(</span><span class="n">redis</span><span class="p">,</span> <span class="n">wildcard</span><span class="p">)</span>
<span class="n">redis</span><span class="p">.</span><span class="nf">keys</span><span class="p">(</span><span class="n">wildcard</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">k</span><span class="o">|</span>
<span class="n">redis</span><span class="p">.</span><span class="nf">del</span><span class="p">(</span><span class="n">k</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">hiredis</span> <span class="o">=</span> <span class="no">Redis</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:driver</span> <span class="o">=></span> <span class="ss">:hiredis</span><span class="p">)</span>
<span class="n">redis</span> <span class="o">=</span> <span class="no">Redis</span><span class="p">.</span><span class="nf">new</span><span class="p">()</span>
<span class="no">Benchmark</span><span class="p">.</span><span class="nf">bm</span><span class="p">(</span><span class="mi">22</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">x</span><span class="o">|</span>
<span class="p">[</span><span class="ss">:ruby</span><span class="p">,</span> <span class="ss">:hiredis</span><span class="p">].</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">d</span><span class="o">|</span>
<span class="n">r</span> <span class="o">=</span> <span class="no">Redis</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">:driver</span> <span class="o">=></span> <span class="n">d</span><span class="p">)</span>
<span class="n">build_cache</span><span class="p">(</span><span class="n">r</span><span class="p">)</span>
<span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s2">"looping (</span><span class="si">#{</span><span class="n">d</span><span class="si">}</span><span class="s2">):"</span><span class="p">)</span> <span class="p">{</span>
<span class="p">(</span><span class="s1">'aaa'</span><span class="o">..</span><span class="s1">'aaz'</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">l</span><span class="o">|</span>
<span class="n">invalidate_cache_keys</span><span class="p">(</span><span class="n">r</span><span class="p">,</span> <span class="s2">"</span><span class="si">#{</span><span class="n">l</span><span class="si">}</span><span class="s2">*"</span><span class="p">)</span>
<span class="k">end</span>
<span class="p">}</span>
<span class="n">build_cache</span><span class="p">(</span><span class="n">r</span><span class="p">)</span>
<span class="n">x</span><span class="p">.</span><span class="nf">report</span><span class="p">(</span><span class="s2">"scanning (</span><span class="si">#{</span><span class="n">d</span><span class="si">}</span><span class="s2">):"</span><span class="p">)</span> <span class="p">{</span>
<span class="p">(</span><span class="s1">'aaa'</span><span class="o">..</span><span class="s1">'aaz'</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">l</span><span class="o">|</span>
<span class="n">invalidate_cache_cursor</span><span class="p">(</span><span class="n">r</span><span class="p">,</span> <span class="s2">"</span><span class="si">#{</span><span class="n">l</span><span class="si">}</span><span class="s2">*"</span><span class="p">)</span>
<span class="k">end</span>
<span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span></code></pre></figure>
<p>After a few minutes running, I got those surprising results:</p>
<div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./redis-expire-wildcard.rb
user system total real
looping (ruby): 0.040000 0.010000 0.050000 ( 1.059056)
scanning (ruby): 49.000000 11.490000 60.490000 ( 61.113561)
looping (hiredis): 0.020000 0.010000 0.030000 ( 1.073681)
scanning (hiredis): 19.680000 12.880000 32.560000 ( 44.972220)
</code></pre></div></div>
<p>First, there is no much improvements using <code class="highlighter-rouge">hiredis</code> over <code class="highlighter-rouge">redis</code> when looping
in our case. This sounds legit, since we loop only 26 times here and the
<code class="highlighter-rouge">hiredis</code> performance benefit doesn’t rise with so few commands (<code class="highlighter-rouge">hiredis</code> does
a much more better job if you change the tested range so more commands are
issued).</p>
<p>Second, using <code class="highlighter-rouge">SCAN</code> here is <em>much</em> slower than using <code class="highlighter-rouge">KEYS</code> !</p>
<p>So why use <code class="highlighter-rouge">SCAN</code> instead of <code class="highlighter-rouge">KEYS</code> ? The problem with <code class="highlighter-rouge">KEYS</code> is that it will block your server while retrieving all the keys. The cursor based approach will return small chunks of keys and won’t block the server for the time of a whole key scan.</p>
<p>However, handling cursor based expiration can be tricky in a web application.
Since it takes so much longer (but is friendlier to Redis), you might have to
handle it in a separate task from your application process (in Sidekiq for instance).</p>
<p>It all depends on your app. You can start using simply <code class="highlighter-rouge">KEYS</code>, but will have to
keep in mind that cursors will be needed if usage or concurrent trafic rises and
monitor your Redis statistics for this.</p>
<div class="footnotes">
<ol>
<li id="fn:1">
<p><a href="http://www.padrinorb.com/">http://www.padrinorb.com/</a> <a href="#fnref:1" class="reversefootnote">↩</a></p>
</li>
</ol>
</div>
<p><a href="https://leucos.github.io/articles/expiring-redis-cache-from-ruby/">Invalidating REDIS cache from Ruby</a> was originally published by Michel Blanc at <a href="https://leucos.github.io">Random stuff</a> on March 01, 2015.</p>
https://leucos.github.io/articles/restoring-arch-bootloader2015-02-14T00:00:00+00:002015-02-14T00:00:00+00:00Michel Blanchttps://leucos.github.iomb@mbnet.fr
<ul>
<li>
<p>Grab latest Arch, create a bootable key (do this before you’re doomed)<sup id="fnref:1"><a href="#fn:1" class="footnote">1</a></sup>.</p>
</li>
<li>
<p>Press F2 at boot, change the boot order to start on the key in <strong>UEFI</strong> mode</p>
</li>
<li>
<p>Boot on Arch, then</p>
</li>
</ul>
<script src="https://gist.github.com/leucos/3f63c07d8326309d7fb1.js"> </script>
<ul>
<li>Cross fingers…</li>
</ul>
<p>In case you need to reinstall all the things<sup id="fnref:2"><a href="#fn:2" class="footnote">2</a></sup>:</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nb">sudo </span>pacman <span class="nt">-Sy</span> <span class="sb">`</span>yaourt <span class="nt">-Q</span> | <span class="nb">grep</span> <span class="nt">-v</span> <span class="s1">'^aur'</span>| <span class="nb">grep</span> <span class="nt">-v</span> <span class="s1">'^local'</span> | cut <span class="nt">-f2</span>
<span class="nt">-d</span><span class="s1">'/'</span> | awk <span class="s1">'{ print $1 }'</span><span class="sb">`</span></code></pre></figure>
<div class="footnotes">
<ol>
<li id="fn:1">
<p>Multisystem is pretty handy for this. You can put several OSes on the key, and choose what to boot. <a href="http://liveusb.info/dotclear/">http://liveusb.info/dotclear/</a> <a href="#fnref:1" class="reversefootnote">↩</a></p>
</li>
<li id="fn:2">
<p>if you don’t use <code class="highlighter-rouge">yaourt</code>, remove the grep part <a href="#fnref:2" class="reversefootnote">↩</a></p>
</li>
</ol>
</div>
<p><a href="https://leucos.github.io/articles/restoring-arch-bootloader/">Restoring Arch bootloader for the future self</a> was originally published by Michel Blanc at <a href="https://leucos.github.io">Random stuff</a> on February 14, 2015.</p>