Ansible and AWS – Part 1

DevOps ToolChain, WikiPedia, CC BY-SA 4.0

It’s been some time since I’ve posted to this blog, which is a shame, because I do indeed enjoy writing as well as sharing what I’ve learned with the hope it helps someone else. The truth is, however, that writing quality articles takes a lot of time. I also suffer from that thought that surely someone else out there has written on a given topic and done a much better job that I could do. And the reality is, someone probably has, but that shouldn’t stop me from doing something I enjoy.

So with that, here’s the first of what I hope to be several more articles on how to harness the power of Ansible with AWS. I firmly believe in learning by doing and I also believe that at first, you should take few shortcuts. What I mean by that is this: are you the type of person that’s saddened by the fact that some schools allow calculators in grade school? Or, even better, not bothering teaching someone how to use a library? And by that I mean how to get off of your ass, go to the library, navigate the stacks, and find a wealth of information you just might not find on Google? To be fair, I’m not really the Get off my lawn! type, but it does irk me somewhat when I see people automating things they don’t have a fundamental understanding of in the first place.

With that, my approach will be to start off with doing some things manually. Then I’ll illustrate how to take the manual steps and automate them. Sometimes it isn’t worth automating something; usually it’s something you do infrequently enough and the energy required to automate it outweighs the benefit. I usually use the DevOps equivalent of the three times rule. If I’ve done something by hand three times in a row, it’s a good candidate for automation, but not necessarily before then.

Ansible

Ansible is just so wonderful. If you’ve never heard of it, it falls into the general class of automation tools that allow you to codify a set of instructions for doing or building something. I tried to make that as abstract as possible, because the something could be:

  • installing a web server application
  • updating a configuration file
  • adding a new user to a set of servers
  • applying a new software patch
  • building a virtual machine

and so on. It’s almost as if these sets of instructions are like playbooks or recipes or cookbooks. So much so many of the popular tools call their files variations of these phrases. Ansible uses tasks, roles, and playbooks.

Installing Ansible

To use a tool you’ll have to install it. I develop on a Mac and can tell you that brew install ansible works great (if you don’t have Brew installed on your Mac you should be ashamed of yourself). As of this writing I’m using Ansible version 2.5.2, as that is what brew installed.

Using Ansible

Ansible is easy to use, though at first it may not seem like it. I know you want to cut to the chase and just figure out how to automate that thing and you want it now. Why do I have to do all this stuff just to do this thing I think should be easy! I can hear you say it because I say it all the time. That initial learning curve is always a pain in the ass.

Here are some simple hints to get started as quickly as possible. First, create a folder called ansible-helloworld and use it as home base. Then, you’re going to create two files: ansible.cfg and ansible_hosts.

ansible.cfg:

[defaults]
inventory=ansible_hosts
host_key_checking=False

[ssh_connection]
pipelining=True

Now, for ansible_hosts, this is where you put the names of your servers that you’re going to be applying your tasks to. We don’t have any hosts yet, so we need to create one. You can, of course, apply Ansible tasks to your own Mac, but we won’t do that. You could also install VirtualBox and create a virtual machine to play with, but we’ll skip that too and go straight to AWS.

Amazon Web Services

Like Ansible, AWS is awesome. I have on my bookshelf a copy of The Cloud at Your Service, where I was first introduced to AWS. This is awesome, I thought. Rather than hosting a webserver from this old Sony Vaio (whatever happened to that thing anyway?), now I can create a VM in the cloud. Forgive those that are middle-aged and still look back at where computing was, and where it is today, and think wow.

Since AWS was my first love, I’ve tended to stick with it, and to be honest, for good reason. The product has become cheaper over the years even while adding more services such as S3, Route 53, VPCs (there was a time VPCs didn’t exist), and now things like RDS and much more. Yes, there are other cloud computing platforms out there (Azure, Google, Linode, Digital Ocean, Scaleway, and many more), and given the need I could go and probably master them all, but for now I’m quite content with AWS. If you find yourself using another platform, Ansible will still work, you just may need to tweak some of the techniques and steps presented here.

Finally, this isn’t an AWS tutorial by any means, though we will get into some handy Ansible modules that take some of the drudgery out of creating instances and databases (in which case if you’re on another platform you will definitely need to find some other module that does the equivalent). So if you don’t know how to create an EC2 instance and set some basic security groups, you might start here.

One last note before we get back to Ansible, and that’s I’ll be using Ubuntu Server 16.04, also known as Xenial Xerus. You may prefer to work with CentOS, Red Hat Enterprise, or plain Debian. While Ansible is generally distribution agnostic, some things may not work exactly the same.

Back to Ansible

Alright, I hope you created an EC2 instance in AWS. I created one in the Ohio region (us-east-2), and as promised used Ubuntu 16.04 LTS (ami-916f59f4). For basic tutorials go ahead and use the t2.micro instance, it’s a lot cheaper. You should also have ensured that you have access to this instance, that is, your public SSH key was installed on this new instance and your security groups permit you to access it (protecting your EC2 infrastructure with security groups is another great topic that I hope to get to one day).

My instance was given a public IP of 18.188.72.168 and my key was installed, so ssh ubuntu@18.188.72.168 logged me right in:

ssh ubuntu@18.188.72.168
Welcome to Ubuntu 16.04.4 LTS (GNU/Linux 4.4.0-1052-aws x86_64)

Hot damn.

One of the unfortunate things about the default AMI for Ubuntu 16.04 is that it doesn’t include python on it. Now granted, Python has only been around since 1991, but still, you’d think it was worth including as a default package in an Ubuntu instance. This is important because Ansible requires python to be on the target instance (we’re talking specifically about Linux instances here). So, until we show you how to create your own AMI, we need to install Python on this VM. Easy enough:

ubuntu@ip-172-31-22-141:~$ sudo apt-get install python
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
  libpython-stdlib libpython2.7-minimal libpython2.7-stdlib python-minimal
  python2.7 python2.7-minimal
Suggested packages:
  python-doc python-tk python2.7-doc binutils binfmt-support
The following NEW packages will be installed:
  libpython-stdlib libpython2.7-minimal libpython2.7-stdlib python
  python-minimal python2.7 python2.7-minimal
0 upgraded, 7 newly installed, 0 to remove and 0 not upgraded.
Need to get 3,877 kB of archives.
After this operation, 16.6 MB of additional disk space will be used.
Do you want to continue? [Y/n]

Of course we entered Y.

Now for the fun part, some Ansible!

Your ansible_hosts File

In your ansible_hosts file just put this:

[all]
18.188.72.168

Now, that’s the public IP address of my instance, and you aren’t going to use that, you’ll use your instance’s IP. The point is your ansible_hosts file needs either an IP or FQDN of the machine(s) that you’re going to be working with. It’s that simple really.

Your First Playbook

You’re ready for your first playbook. Call it whatever your like, we’ll just use playbook.yml for now. And here we go:

playbook.yml:

That’s it! See you in Part 2!

Ansible Modules

Just kidding. We’ll add a bit more to our first playbook, but first, let’s take a look at the syntax of the file. The extension .yml is on purpose as Ansible roles, tasks, variable configurations, playbooks, etc. are all written using YAML, a “human friendly data serialization standard for all programming languages.” YAML’s syntax is indentation-driven (like Python), so you need to make sure everything is properly indented or you’ll get incomprehensible errors. For a quick check you can install something like yamllint (availabe on the Mac with brew install yamllint).

For a brief moment we’ll ignore the hosts and remote_user tags and focus instead on the tasks section. We have one task listed here: hostname. Ansible provides great reference documentation to look up all of the things you can do with this module. This is a simple one as it only takes one parameter, name.

Now, don’t confuse the name parameter of the hostname module with that of the name parameter of the task. This used to trip me up. It’s easier to see task entries when there are more than one, say let’s do that. I’m going to be very specific on how this is written (and then tell you not to do it):

Notice how the apt module (which also has great reference documentation) is the second list entry under tasks; we know this because of the dash. But then there is some indentation (two spaces) and three key-value pairs (name, state, and update_cache). Then the indentation is backed out and we have name. The second name here (which has the value Install htop) is associated with the task at hand (in our case, apt).

If you’re confused, don’t worry. YAML (and any serialization syntax) can be baffling and bewildering. But if you follow some basic conventions and keep playbooks simple (which will involve breaking things down over time into roles and other tasks), it’ll be easy to read.

Now, what you aren’t supposed to do is put the name parameter for the task at the bottom. Clean that up!

Much better. Now, let’s run it!

ansible-playbook playbook.yml

PLAY [all] *********************************************************************

TASK [Gathering Facts] *********************************************************
ok: [18.188.72.168]

TASK [Set our hostname] ********************************************************
changed: [18.188.72.168]

TASK [Install htop] ************************************************************
changed: [18.188.72.168]

PLAY RECAP *********************************************************************
18.188.72.168              : ok=3    changed=2    unreachable=0    failed=0

Idempotency

In general, an operation is idempotent if it produces the same result even after being executed multiple times. This is an important property to strive for in Ansible playbooks, though my experience is that it isn’t always achievable. It may appear on the surface that our above playbook is idempotent though it isn’t. For argument’s sake let’s look at what we would desire from an idempotent playbook:

  • if the hostname isn’t set to ansible-helloworld, set it to ansible-helloworld
  • if htop isn’t installed, install it, otherwise do nothing

On the first pass of the playbook we see changed=2, indicating that two tasks changed something on the server. Indeed, we set the hostname and then installed htop. Let’s run it again:

PLAY RECAP *********************************************************************
18.188.72.168              : ok=3    changed=0    unreachable=0    failed=0

Now, nothing has changed. Run it again!

PLAY RECAP *********************************************************************
18.188.72.168              : ok=3    changed=0    unreachable=0    failed=0

Success! An idempotent playbook.

Until update_cache: true triggers an upgrade of the htop application from the underlying repository. Strictly speaking this one line prevents the playbook from being idempotent, and if it were critical that the server didn’t receive any apt-get updates, removing update_cache can help but would likely be insufficient.

Final Thoughts

We didn’t cover the remote_user, become, etc. directives in our playbook, but that’s okay. If you remove them the playbook won’t work at all. Let’s add one last interesting twist to the playbook, and that’s installing more than htop to the server. I happen to like Z Shell (and even more, Oh My Zsh) installed on my servers. So let’s install it.

We could create a separate apt task (separate from the one installing htop) to install zsh, but that seems a bit silly. Let’s use the with_items capability like this:

Confusing syntax alert! Believe it or not, it took me some time before this syntax felt natural, because it’s sort of like writing for loops in Apache Ant. It’s clunky, and namely because you’re taking a markup-type language and trying to create logical constructs in it. It just feels weird at first.

The first thing that’s weird is the "{{ item }}" syntax. What’s with the quotes? There weren’t quotes before. Why those braces? Two braces? Not one brace? What is item? Who sets that? Is it magic?

Try removing the quotes. Go ahead. You’ll be sorry you did when you’re greeted with We could be wrong, but this one looks like it might be an issue with missing quotes. Always quote template expression brackets when they start a value. Assholes. So the quotes need to stay there.

The braces mark off template interpolation. Whatever the variable item is set to will be used. The with_items parameter supplies, in turn, each of the items, substituting the items (no pun intended) in the list into the item variable. So in this way you can see how the playbook will run now:

# ansible-playbook playbook.yml

PLAY [all] *********************************************************************

TASK [Gathering Facts] *********************************************************
ok: [18.188.72.168]

TASK [Set our hostname] ********************************************************
ok: [18.188.72.168]

TASK [Install htop] ************************************************************
changed: [18.188.72.168] => (item=[u'htop', u'zsh'])

PLAY RECAP *********************************************************************
18.188.72.168              : ok=3    changed=1    unreachable=0    failed=0

That’s it for Part 1 of this series! If you’ve never worked with Ansible before, I hope this was helpful in getting you started; yes, there are other tutorials out there on Ansible but I wanted to lay the foundation for a series of articles that will walk you through how to go from a simple playbook that installs a couple of packages to roles that can install, configure, and back up MySQL databases and much, much more.

If you have any suggestions for things you’d like to see in an article, let me know by posting a comment or on Twitter @iachievedit.

Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *