Using Ansible to Update Kamal Servers

I've been using Kamal recently and it's been a great experience. It takes a lot of the headaches out of having to wait for a deployment to finish. Previously, I was hosting Drifting Ruby on AWS with Elastic Beanstalk. This was a great solution for a long time as I didn't have to worry about the servers and it set up a lot of the networking for me. However, there were some pain points with this process and I have been wanting to switch to something new for a while. When I first heard about MRSK (now Kamal), I was excited! Ruby on Rails applications were getting a first class citizen deployment solution. The networking and infrastructure side of things hasn't ever been an issue for me since I initially came from that kind of background. However, I was initially looking for something easy to use that gave a bit of flexibility. I didn't want to go the Heroku route since pricing was a concern early on and AWS offers Reserved Instances which can save a significant amount.

One of the drawbacks with Kamal is that it doesn't have a built in way to update the servers. This is where Ansible comes in. Ansible is a tool that allows you to automate server configuration and deployment. But, in our case, we are going to use it to update our servers. This is a great solution for me since I already had some familiarity with Ansible and it's a great tool to have in your toolbelt. I need to look up some Ansible tricks a bit more to look at full server provisioning, but this will be a good start.

Note I am performing these steps with the assumption that you have already set up your Kamal servers and have them running. You can check out a recent episode on Kamal which covers deploying with Github Actions, but I also go through the steps of setting up the servers. I also have an older episode when Kamal was called MRSK which covers setting up the servers with a managed database and load balancer.

Note This Ansible playbook is also assuming that you'll be running this on a computer that has shell access to the servers that you're wanting to manage.

Setting up Ansible

Since I am on an Apple computer, I'm going to use Homebrew to install Ansible. If you're on a different platform, you can check out the Ansible installation documentation for more information.

brew install ansible

Understanding Ansible

This Ansible setup is going to have two different files. The first is the inventory file which will contain the list of servers that we want to manage. The second is the playbook.yml file which will contain the instructions for Ansible to run.

The inventory file will look something like this:

[server_group]
app1            ansible_host=PUBLIC_IP_ADDRESS_OF_SERVER
app2            ansible_host=PUBLIC_IP_ADDRESS_OF_SERVER

The inventory has different groups of servers. In this case, we only have one group called server_group. This group contains two servers. The first server is called app1 and the second server is called app2. You can add as many servers as you want to this group. You can also create multiple groups if you want to manage different servers with different playbooks. You will want to replace the PUBLIC_IP_ADDRESS_OF_SERVER with the public IP address of your server. This should be the same IP address that you use to SSH into your server and that you have set up in the Kamal deploy.yml file.

The playbook.yml file will look something like this:

---
- name: Update and Upgrade Packages on Servers Sequentially
  hosts: all
  serial: 1
  become: yes

  tasks:
    - name: Run apt update and apt upgrade on servers
      apt:
        update_cache: yes
        upgrade: yes
        cache_valid_time: 3600
      register: apt_update

    - name: Pause for 2 minutes before next server update
      pause:
        minutes: 2
      when: apt_update.changed

This is a bit more complicated because we have some additional things to take into consideration. The first thing to note is that we are running the playbook on all servers in the inventory. This is because we want to update all of the servers. However, we don't want to update them all at the same time. This is where the serial option comes in. This will tell Ansible to run the playbook on one server at a time. This is important because we don't want to take down our application while we are updating the servers. This could happen if there was an update to Docker. If we updated Docker on all of the servers at the same time, then our application would be down until the update was complete. This is why we want to update the servers one at a time.

The become option tells Ansible to run the commands as the root user. This is important because we need to be able to update the packages on the server. If we didn't have this option, then we would get an error when trying to update the packages.

The tasks section is where we define the tasks that we want Ansible to run. The first task is to run the apt update and apt upgrade commands. This will update all of the packages on the server. The register option tells Ansible to store the output of the command in a variable called apt_update. This will be used in the next task.

The second task is to pause for 2 minutes before running the next server update. This is important because we want to make sure that the server is fully updated before we update the next server. The when option tells Ansible to only run this task if the apt_update variable has changed. This will only happen if the server was updated. If the server was already up to date, then this task will not run.

Running the Ansible Playbook

Now that we have our playbook and inventory files set up, we can run the playbook using the following command:

ansible-playbook -i inventory playbook.yml

This will run the playbook on all of the servers in the inventory file. You should see something like this:

PLAY [Update and Upgrade Packages on Servers Sequentially] *********

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

TASK [Run apt update and apt upgrade on servers] *******************
ok: [app1]

TASK [Pause for 2 minutes before next server update] ***************
skipping: [app1]

PLAY [Update and Upgrade Packages on Servers Sequentially] *********

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

TASK [Run apt update and apt upgrade on servers] *******************
ok: [app2]

TASK [Pause for 2 minutes before next server update] ***************
skipping: [app2]

PLAY RECAP *********************************************************
app1                       : ok=2    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
app2                       : ok=2    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

You can see that the playbook ran on both servers and that the apt update and apt upgrade commands were run. You can also see that the Pause for 2 minutes before next server update task was skipped. This is because the server was already up to date.

I hope that this helps close another gap in the Kamal deployment process. I'm looking forward to seeing what else is in store for Kamal and I'm excited to see what the future holds for Ruby on Rails deployments.