Android & Ansible

This post shows how to setup a fully functioning Android development environment (including Android Studio) on a Linux system by running a single command with the help of Ansible.

Why? Link to heading

Just before the holidays my desktop tower decided to crash and refused to boot back up again.

Instead of trying to figure out what had gone wrong, or be annoyed that this had happened in the first place, it was actually just what I needed.

See I had been using a bare dotfile git repository for a few years to capture my system configuration, and while functioning well enough it didn’t capture my whole system setup, requiring manual work when it came to setting up a new system. So earlier this year I started creating a shell script that would, when finished, take care of that manual part for me. This would involve things like file creation, permissions, user and group management, software installation and configuration, firewalls etc.

However when talking to a retired sysadmin friend of mine about this, he suggested trying out Ansible instead. It was after all created for this kind of thing in the first place. So when the system failed I saw it as an opportunity to get started on this project.

Ansible Link to heading

Ansible is an IT automation platform tool for quickly configuring systems based on predefined recipes that it calls ‘playbooks’. This is commonly known as ‘infrastructure as code’. It was created by Red Hat and is developed in the open on Github.

The playbooks can be combined into collections, and are them selves a collection of tasks, each of which are contained inside YAML files. These tasks can pretty much do anything you might imagine doing on the command line, and Ansible comes loaded with utilities as well as a big community ecosystem of premade playbooks, roles and tasks.

I won’t go much further into descriping all of its capabilities, as there is simply too much to mention, but will rather suggest you go and read through the official documentation to learn more.

Even though Ansible was initially developed for Linux systems, it can now also be used on Windows and I believe also on MacOS. This post will however only focus on Linux, and specifically Ubuntu, as that is my daily driver.

The setup Link to heading

As of writing this I have been working on my Ansible playbook for around three weeks, and it is on its third iteration. The first one was no good due to me learning the ropes and making all of the mistakes (as tends to happen in those situations), and the second iteration was not modular enough for my liking.

In the current (and hopefully last) iteration I decided that each piece of software, library or tool of a significant enough size would get pulled out and into it’s own role repository, so as to make it modular and reusable in isolation. These repositories are then in turn combined into a larger playbook

While I will probably make the whole setup public at some point, the rest of this post will only revolve around how to setup the Android development enviroment via Ansible.

I feel like it needs to be said at this point that none of the steps this article will go through are really necessary for the required environment needed for Android development. The Android tools team have done a wonderful job of bundling everything that is required into one easily installed package. That is Android Studio.

Studio already comes bundled with Java, Gradle and the whole toolchain needed to bootstrap a system (via the cmdlinetools package) and if you are just getting started with Android development I would highly suggest going with the standard installation method of fetching the package here.

However if, like me, you want to have more fine control over the configuration of the system, and in a more reproducable manner, then please continue on reading.

Note: Please continue reading also if you are just starting out with Android development, as I believe there are things in this article worth learning for you as well.

The bootstrap Link to heading

Ansible can be used in various different ways. It can either be operated in a push or a pull model, run locally or remotely either as a dedicated Ansible server or just on another normal workstation. In my case I decided to use my already functioning laptop (from now on called the server, but not to be confused as being a dedicated Ansible server) for both creating the playbook as well as to push it to my freshly reflashed tower (from now on called the client).

This setup however requires some initial configuration on the client which Ansible is not able to do for us automatically. In essence this is an ‘chicken and an egg’ problem.

As mentioned previously playbooks can be run locally on the systems that they configure, so if you are not interested in this remote pushing setup you can skip on to the next section.

To solve this problem I created a simple script that installs Ansible and other dependencies as well as creating the user that will be used by Ansible on the server when SSH-ing into the client to make the configurations:

I like my programs silent, but my scripts verbose.

By running this script on the new client system, it creates a specific user which the Ansible instance on the server side uses to SSH into the client system and make the changes later dictated in the playbook.

If everything worked we should now be able to SSH into the client from the server by running the following command (given that the associated private key part of the public key used in the script is also present on the server):

ssh <user>@<ip> -p <port>

We also need to make sure Ansible is installed on the server before moving on to the next section.

sudo apt install ansible

Playbook Link to heading

Ansible is somewhat particular about directory and file names, with each having a specific purpose. I recommend giving the official documentation a look over to learn more about what they are.

For the purpose of this article I’ve created a demo repository that will go through each step of the process of using the roles that have been created for setting up the Android development environment. The repository can be found here and each branch showcases each step on the way along with instructions on what should be filled in to make it work.

Each step will also be described in the subsequent sections below.

Step 1. Initialization Link to heading

The first step branch of the repository contains the bare minimum skeleton for the playbook as can be seen here.

The README indicates what fields need to be filled in and with what relevant information that was used in both the bootstrap script earlier and topographical information related to the network you are working with.

Note: If you are running the playbook locally, I believe you can set the host information to ’localhost’.

After filling in the required information, the playbook can be verified by running:

ansible-playbook playbook.yml -vv

This should cause the client to update and clean its apt package manager, nothing more.

The playbook repository at this point.

Step 2. Java installation Link to heading

The second step is to actually start configuring the playbook to use the external reusable repositories, beginning with the Java installation role that I have created and which can be found here.

To use the Java role repository in the playbook we first need to create a file called requirements.yml and add the role repository in it, like this:

- name: hth-java
  scm: git
  version: "0.0.1"

This requirement file can now be used to load the roles defined in it into a local role storage on the server, making the roles available for use by Ansible.

To do that we need to run the following command:

ansible-galaxy install -r requirements.yml

If everything went well we should now be able to reference the newly downloaded role within the playbook and start configuring it for use on the client.

To include the role in the playbook we only need to add the following section inside the playbook.yml file, between the two task clusters that already exist there:

- hosts: workstation
  become: true
    - hth-java

This Java role is opinionated about how to setup the system, just like all the roles I have created. However I’ve tried to use variables for most of the configuration, meaning that most of the opinionated stuff can be easily changed via the host_vars files.

The repository’s README lists what configuration options are available and what constrains some of them have, so I won’t go into all of them here. This module uses the system’s apt package manager to fetch and install the requested Java versions.

The configurations for the role go into the /host_vars/client file:

java_global: true
java_vendor: openjdk
java_type: jdk
java_version: 19
java_architecture: amd64

This will install the Java Development Kit version 19 from OpenJDK and set it as the default Java version globally on the system. The architecture variable is there in case other values are needed in the future.

Finally the playbook can be run like before to apply the changes to the client:

ansible-playbook playbook.yml -vv

As the next sections will all each include new roles as dependencies there is also a new script in the branch called, which will take care of both processing the requirement file as well as executing the playbook.

The playbook repository at this point.

Step 3. Gradle Link to heading

While it is not necessarily needed to be installed specifically (as it comes bundled with Android Studio), I still decided to create a role for installing Gradle. It can come in handy to have it installed separately from Android Studio, and this role could also come in handy on a dedicated build server. The role repository can be found here.

Just like before the repository is added to the requirements.yml:

- name: hth-gradle
  scm: git
  version: "0.0.1"

And the role is added to the task group inside the playbook.yml:

- hosts: workstation
  tags: base
  become: true
    - hth-java
    - hth-gradle

The role’s repository README explains the options available. This role downloads the release artifacts directly from Gradle and processes them. To verify the artifacts it requires that their associated checksum be also supplied. Those can be found here.

The following configuration is now added to the host_vars/client file:

gradle_global: true
gradle_version: "7.6"
gradle_version_checksum: 8cc27038d5dbd815759851ba53e70cf62e481b87494cc97cfd97982ada5ba634

By executing the script the client system should now have Gradle installed and accessible on the global path.

The playbook repository at this point.

Step 4. Android SDK Link to heading

Alright after all that we are finally going to do some Android installations.

The pattern is pretty similar to the previous two steps.

First we add the external Android SDK installation repository to the requirements.yml:

- name: hth-android-sdk
  scm: git
  version: "0.0.1"

Then the role is added to the task group inside the playbook.yml

- hosts: workstation
  tags: base
  become: true
    - hth-java
    - hth-gradle
    - hth-android-sdk

The options available for this role are a bit more extensive then for the two previous ones, and for full description of them see the README.

See, this role first bootstraps the Android SDK onto the system by downloading the command line tools package (find the latest versions here) then uses the bundled sdkmanager to download the requested packages on to the system. Since the sdkmanager can handle a lot of different types of package definitions, this role also needs to be able to handle them.

To handle the complexity of the options available, this role uses the python package jsonschema to validate the values it is given. That library can easily be installed by executing the following on the server:

pip install jsonschema

The role also comes with it’s own external Ansible dependencies, which as of writing this I have not yet figured out a way of having automatically install when the role is first used.

So in the meantime manually install the dependency on the server by executing:

ansible-galaxy collection install ansible.utils

Just like the Gradle role downloaded the artifacts directly, this one downloads the bootstrapping commandline package straight from Google, and thus requires also the checksum of the package being downloaded. Both the build number (the numbers in the archive name as seen on Google’s webpage) as well as the checksums can be found here.

As a part of the bootstrapping process, the cmdlinetools package gets updated to the latest version via the use of sdkmanager.

The following configuration uses build number 9123335 of cmdlinetools then downloads three different versions of the build tools from different channels, as well as two platform versions and the latest stable platform-tools (there is no version configuration option for it in sdkmanager, only channel selection):

android_user_home: ".android"
android_adb_to_path: true
android_sdkmanager_to_path: true
android_cmdlinetools_bootstrap_build: 9123335
android_cmdlinetools_bootstrap_checksum: "0bebf59339eaa534f4217f8aa0972d14dc49e7207be225511073c661ae01da0a"
    - version: "33.0.0"
      channel: 2
    - version: "31.0.1"
      channel: 0
    - version: "29.0.2"
      channel: 0
    - version: 33
      channel: 2
    - version. 29
      channel: 0
    channel: 0

The client now has a complete installation of the Android SDK ready to go!

The playbook repository at this point.

Step 5. Android Studio Link to heading

The last piece of this puzzle is to actually install Android Studio.

At this point we know the steps. The Android Studio installation role is found here and is added as a requirement to the playbook.

Like in previous steps this role downloads the artifacts (in this case Android Studio) from Google directly and thus requires the associated checksum for the version. These can be found here.

The following configuration downloads two different Android Studio versions (the only limit on how many it can download is disk and network bandwidth) into the predefined path and then assigns one of them as the primary studio client and the other one as the canary client, along with creating desktop entries and symlinks added to the global path.

See the README for more detail on each option available in the role.

android_studio_path: "/opt/android-studio"
  - version: "2022.2.1.9"
    checksum: "7e7868b83bca8255f690e6c7fe026a3f3be1e140d2f09682c2b150af8cf93550"
  - version: "2021.3.1.17"
    checksum: "89adb0ce0ffa46b7894e7bfedb142b1f5d52c43c171e6a6cb9a95a49f77756ca"
  version: "2021.3.1.17"
    create: yes
    name: "AStudio Stable"
  version: "2022.2.1.9"
    create: yes
    name: "AStudio Canary"

Note: Primary and canary clients are nothing special. I personally always have two versions of studio on my system, stable and canary. In the role there is no mechanism for enforcing that the primary client is actually a stable version or that the canary version is not a stable client. It is up to you to configure, and the names could be set to Client1 and Client2 for all that the role cares.

Execute the script and if everything went well the system should now have a fully functioning Android Studio ready to go.

The playbook repository at this point.

Conclusion Link to heading

I like the feeling of being able to encapsulate and codify my system setup in the structured manner that Ansible allows for. Being able to easily setup a new system from scratch just the way I want it by running (almost) just a single command really tickles an itch.

Sure, between system setups these playbook’s will need to be updated to match the latest versions of the software that they are configured to use, and certain url’s might change over time.

But with every new configuration that gets added to the big playbook, the less attached I am to my system setup. It is a nice feeling knowing that it can all be recreated by a single command any time it is needed.

I was only able to abandon the previous system setup because all relevant data, both personal and system configurations, was already backed up both locally and remotely (remember the 3-2-1 guideline on backups).

Do you have your backups in order?