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.