Simplify Ansible Inventory for Dynamic GCP VM Management

Introduction

Organizing and maintaining your virtual machines can become a real mess, especially when you're working on larger projects. As a dev-op, you have to use your time smart and be as effective as possible. That's why Ansible was born and has become so popular.

Ansible is a very powerful tool when it comes to automation and it helps you a lot in organizing your infrastructure. Especially if you use Terraform to start or shut down machines on demand or use an autoscaling configuration.

So you never know how many computers are currently running and what IP addresses they have in your network. So when it comes to maintaining them, you need a solution that does all the hard work for you.

Prerequisites

Let's say we need to manage a simple web application with the following setup using custom VMs in Google Cloud:

- MySQL        > 1
- Readis       > 1
- App Admin    > 1
- App Frontend > 1-5
This is how our application setup could look like

The application instances in particular are configured using auto-scaling and can therefore range in number from one to five. As a Dev-Ops, we want to create an Ansible configuration that organizes and executes automated tasks. This way, our infrastructure can be maintained at any time and the required tasks can be executed on the VMs.

For this example, let's just assume that the project is up and running in GCP and we already have a GCP service account with sufficient permissions to access the resources. We have also created and downloaded a service account key that we will use to authenticate Ansible to GCP.

We have also prepared a set of Ansible playbooks to fulfill all our tasks. Now we just need to create our inventory configuration to distribute the right Ansible tasks to the right VMs.

Authentication for your GCP project

Before we can execute at least one Ansible task on our VMs, we have to connect to GCP and authenticate ourselves.

In our working directory, in which we will execute all Ansible commands, we will create a new directory, which we will call inventory. In it, we need to create a new file inventory.gcp.yaml and add the following content

plugin: gcp_compute
scopes:
  - https://www.googleapis.com/auth/compute
projects:
  - ${GCP_PROJECT}
auth_kind: serviceaccount
service_account_file: ${PATH_TO_KEY}
./inventory/inventory.gcp.yaml

This is basically the minimum configuration required to connect to GCP with Ansible. In most cases, your Ansible installation already has the gcp_compute plugin installed. Otherwise you can find more information here.

Simply replace GCP_PROJECT with the ID of your project and change PATH_TO_KEY with the path to the key file of your service account.

Execute the following command to see if the configuration works

ansible-inventory -i inventory --list

If everything went well, you will see a lot of output about all VMs with their details running in your GCP project, and at the very end you can see the VMs with their IPs.

"ungrouped": {
    "hosts": [
        ...
    ]
}

Define Ansible Groups using Network Tags

Now that we can connect to our GCP project, we need to organize our VMs into groups. Ansible uses a hierarchical way of organizing resources. This means that we always have an implicit root group called all and below that we start to organize our VMs into subgroups and their subgroups.

In our example, we could split our VMs into the following groups and subgroups:

- db
- redis
- app
|- admin
|- fronten
This is how our VMs could be organized into groups

This is exactly the hierarchy we need to have in our Ansible inventory. Let's now add more content to our inventory.gcp.yaml file.

...
groups:
  db: "'db' in tags['items']" 
  redis: "'redis' in tags['items']"
  app: "'app' in tags['items']"
  app_admin: "'app-admin' in tags['items']"
  app_frontend: "'app-frontend' in tags['items']"
./inventory/inventory.gcp.yaml

For this configuration to work, we need to add all tags to our VMs as so-called Network Tags, which can be added when creating or editing VMs. Note that each of our application instances must have the two tags app and app_admin or app_frontend. This ensures that all our application instances are added to the app group, but only the admin VM is added to the app_admin group, and so on.

We can now check our inventory setup by executing the following command again

ansible-inventory -i inventory --list

This time we get almost the same output and our newly defined groups are displayed.

...
"all": {
  "children": [
    "app",
    "db",
    "redis",
  ]
},
"app": {
  "children": [
    "app_admin",
    "app_frontend"
  ],
  "hosts": [
    ...
  ]
},
"app_admin": {
  "hosts": [
    ...
  ]
}
...

Now we are ready to go and can use our playbooks to address the desired groups.

Define Ansible Groups using Labels

In some cases, the use of network tags is not desirable, as they actively assign firewall rules to the VMs and can therefore lead to unexpected results. In this case, there is another way to organize your VMs into groups using labels.

Labels are like environment variables and can have a key and a value. This is pretty handy when it comes to structuring and organizing your infrastructure. I personally don't use them often, but I will show you how to use them.

Now let's add some labels to our VMs. We will define a service label containing the name of the service and for the application instances we will also add an additional component label with the value admin or frontend.

- db
  > service: db
- redis
  > service: redis
- app-admin
  > service: app
  > component: admin
- app-frontend
  > service: app
  > component: frontend
This is how we could organize our labels in our VMs

Now we need to add a few more definitions to our inventory.gcp.yaml file so that Ansible knows how to use the labels.

...
keyed_groups:
  - prefix: gcp
    key: labels
./inventory/inventory.gcp.yaml

The keyed_groups configuration can be used to generate a variety of Ansible groups that use the key and value concatenation of a dictionary, in our case it is our label dictionary. We can now check out our inventory by running the following command again

ansible-inventory -i inventory --list

In this case, we still have all the outputs as before, but in between we find new groups that are automatically generated by Ansible.

...
"gcp_service_db": {
  "hosts": [
    ...
  ]
},
"gcp_service_redis": {
  "hosts": [
    ...
  ]
},
"gcp_service_app": {
  "hosts": [
    ...
  ]
},
"gcp_component_admin": {
  "hosts": [
    ...
  ]
},
"gcp_component_frontend": {
  "hosts": [
    ...
  ]
},
...

Now Ansible has combined all the key and value pairs and created a variety of groups with the prefix we defined. Now we can use these groups and add them to our Ansible playbooks so that the right tasks are executed on the right machines.

Separate your VMs using Filters

In a real-world example, your VMs will most likely be separated by an environment such as development or production. In this case, your dev and prod VMs can all have the same labels and tags, so running your Ansible playbooks will affect all of them.

Let's say we only want to manage our production VMs. In this case, we need to add an additional label environment to all our production VMs and update our inventory file by adding filters so Ansible knows which VMs we want to target specifically.

Let us now add further configurations to our inventory.gcp.yaml file

filters:
  - labels.environment = prod
./inventory/inventory.gcp.yaml

If we now check our inventory, only VMs that have the environment: prod label are listed in the output.

Conclusion

As we have just seen, there are many ways to organize your infrastructure with Ansible. By using them all, we can make our playbooks very specific and ensure that each individual task is only executed on the VMs to which it belongs.