Deploying Tarantool Cartridge applications with zero effort (Part 2)
We have recently talked about how to deploy a Tarantool Cartridge application. However, an application’s life doesn’t end with deployment, so today we will update our application and figure out how to manage topology, sharding, and authorization, and change the role configuration.
Feeling interested? Please continue reading under the cut.
Where did we leave off?
Last time, we set up the following topology:
The sample repository has changed a bit: there are new files called getting-started-app-2.0.0-0.rpm
and hosts.updated.2.yml
. You do not have to pull the new version, you can just download the package by clicking this link, and you need hosts.updated.2.yml
only to look there if you have trouble changing the current inventory.
If you have followed all the steps from the previous part of this tutorial, you now have a cluster configuration with two storage
replica sets in the hosts.yml
file (hosts.updated.yml
in the repository).
First, start the virtual machines:
$ vagrant up
An up to date version of the Tarantool Cartridge Ansible role should already be installed. Just in care run the following command:
$ ansible-galaxy install tarantool.cartridge,1.0.2
So, the current cluster configuration:
---
all:
vars:
# common cluster variables
cartridge_app_name: getting-started-app
cartridge_package_path: ./getting-started-app-1.0.0-0.rpm # path to package
cartridge_cluster_cookie: app-default-cookie # cluster cookie
# common ssh options
ansible_ssh_private_key_file: ~/.vagrant.d/insecure_private_key
ansible_ssh_common_args: '-o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
# INSTANCES
hosts:
storage-1:
config:
advertise_uri: '172.19.0.2:3301'
http_port: 8181
app-1:
config:
advertise_uri: '172.19.0.3:3301'
http_port: 8182
storage-1-replica:
config:
advertise_uri: '172.19.0.3:3302'
http_port: 8183
storage-2:
config:
advertise_uri: '172.19.0.3:3303'
http_port: 8184
storage-2-replica:
config:
advertise_uri: '172.19.0.2:3302'
http_port: 8185
children:
# GROUP INSTANCES BY MACHINES
host1:
vars:
# first machine connection options
ansible_host: 172.19.0.2
ansible_user: vagrant
hosts: # instances to be started on the first machine
storage-1:
storage-2-replica:
host2:
vars:
# second machine connection options
ansible_host: 172.19.0.3
ansible_user: vagrant
hosts: # instances to be started on the second machine
app-1:
storage-1-replica:
storage-2:
# GROUP INSTANCES BY REPLICA SETS
replicaset_app_1:
vars: # replica set configuration
replicaset_alias: app-1
failover_priority:
- app-1 # leader
roles:
- 'api'
hosts: # replica set instances
app-1:
replicaset_storage_1:
vars: # replica set configuration
replicaset_alias: storage-1
weight: 3
failover_priority:
- storage-1 # leader
- storage-1-replica
roles:
- 'storage'
hosts: # replica set instances
storage-1:
storage-1-replica:
replicaset_storage_2:
vars: # replica set configuration
replicaset_alias: storage-2
weight: 2
failover_priority:
- storage-2
- storage-2-replica
roles:
- 'storage'
hosts: # replica set instances
storage-2:
storage-2-replica:
Go to http://localhost:8181/admin/cluster/dashboard and make sure that your cluster is operating correctly.
As before, we change this file step-by-step and watch how the cluster changes. You can always look up the final version in hosts.updated.2.yml
Let’s start!
Updating the application
First, we are going to update our application. Make sure you have the getting-started-app-2.0.0-0.rpm
file in your current directory (otherwise, download it from the repository).
Specify the path to a new version of the package:
---
all:
vars:
cartridge_app_name: getting-started-app
cartridge_package_path: ./getting-started-app-2.0.0-0.rpm # <==
cartridge_enable_tarantool_repo: false # <==
We have set cartridge_enable_tarantool_repo: false
so that the role does not include the repository with the Tarantool package that we had already installed last time. It slightly speeds up the deployment process but it isn’t obligatory.
Run the playbook with the cartridge-instances
tag:
$ ansible-playbook -i hosts.yml playbook.yml \
--tags cartridge-instances
And check that the package has been updated:
$ vagrant ssh vm1
[vagrant@svm1 ~]$ sudo yum list installed | grep getting-started-app
Check that the version is 2.0.0
:
getting-started-app.x86_64 2.0.0-0 installed
Now you can safely try out the new version of the application.
Enabling sharding
Let’s enable sharding so that we can later get to managing storage
replica sets. It’s an easy thing to do. Add the cartridge_bootstrap_vshard
variable to the all.vars
section:
---
all:
vars:
...
cartridge_cluster_cookie: app-default-cookie # cluster cookie
cartridge_bootstrap_vshard: true # <==
...
hosts:
...
children:
...
Run:
$ ansible-playbook -i hosts.yml playbook.yml \
--tags cartridge-config
Note that we have specified the cartridge-config
tag to run only the tasks related to the cluster configuration.
Open the Web UI http://localhost:8181/admin/cluster/dashboard and note that the buckets are distributed among storage replica sets as 2:3
(as you may recall, we specified these weights for the replica sets):
Enabling automatic failover
Now we are going to enable the automatic failover mode in order to find out what it is and how it works.
Add the cartridge_failover
flag to the configuration:
---
all:
vars:
...
cartridge_cluster_cookie: app-default-cookie # cluster cookie
cartridge_bootstrap_vshard: true
cartridge_failover: true # <==
...
hosts:
...
children:
...
Start cluster management tasks again:
$ ansible-playbook -i hosts.yml playbook.yml \
--tags cartridge-config
When the playbook finishes successfully, you can go to the Web UI and make sure that the Failover
switch in the top right corner is now switched on. To disable the automatic failover mode, simply change the value of cartridge_failover
to false
and run the playbook again.
Now let’s take a closer look at this mode and see why we enabled it.
Looking into failover
You have probably noticed the failover_priority
variable that we specified for each replica set. Let’s look into it.
Tarantool Cartridge provides an automatic failover mode. Each replica set has a leader, that is, the instance where the record is written. If anything happens to the leader, one of the replicas takes over its role. Which one? Look at the storage-2
replica set:
---
all:
...
children:
...
replicaset_storage_2:
vars:
...
failover_priority:
- storage-2
- storage-2-replica
In failover_priority
, we specified the storage-2
instance as the first one. In the Web UI, it is the first one in the replica set instance list and is marked with a green crown. This is the leader, or the first instance specified in failover_priority
:
Now let’s see what happens if something is wrong with the replica set leader. Go to the virtual machine and stop the storage-2
instance:
$ vagrant ssh vm2
[vagrant@vm2 ~]$ sudo systemctl stop getting-started-app@storage-2
Back to the Web UI:
The crown of the storage-2
instance turns red, which means that the assigned leader is unhealthy. But storage-2-replica
now has a green crown, so this instance took over the leader role until storage-2
comes back into operation. This is the automatic failover in action.
Let’s bring storage-2
back to life:
$ vagrant ssh vm2
[vagrant@vm2 ~]$ sudo systemctl start getting-started-app@storage-2
Everything is back to normal:
Now we change the instance order in failover priority. We make storage-2-replica
the leader and remove storage-2
from the list:
---
all:
vars:
...
hosts:
...
children:
...
replicaset_storage_2:
vars: # replica set configuration
...
failover_priority:
- storage-2-replica # <==
...
Run cartridge-replicasets
tasks for instances from the replicaset_storage_2
group:
$ ansible-playbook -i hosts.yml playbook.yml \
--limit replicaset_storage_2 \
--tags cartridge-replicasets
Go to http://localhost:8181/admin/cluster/dashboard and check that the leader has changed:
But we removed the storage-2
instance from the configuration, why is it still here? The fact is that when Cartridge receives a new failover_priority
value at the input, it arranges the instances as follows: the first instance from the list becomes the leader followed by the other specified instances. Instances left out from failover_priority
are arranged by UUID and added to the end.
Expelling instances
What if you want to expel an instance from the topology? It is straightforward: just assign the expelled
flag to it. Let’s expel the storage-2-replica
instance. It is the leader now, so Cartridge will not let us do this. But we’re not afraid so we’ll try:
---
all:
vars:
...
hosts:
storage-2-replica:
config:
advertise_uri: '172.19.0.2:3302'
http_port: 8185
expelled: true # <==
...
We specify the cartridge-replicasets
tag because expelling an instance is a change in topology:
$ ansible-playbook -i hosts.yml playbook.yml \
--limit replicaset_storage_2 \
--tags cartridge-replicasets
Run the playbook and observe the error:
Cartridge doesn’t let the current replica set leader be removed from the topology. This makes good sense because the replication is asynchronous, so expelling the leader is likely to cause data loss. We need to specify another leader and only then expel the instance. The role first applies the new replica set configuration and then proceeds to expelling the instance. So we change the failover_priority
and run the playbook again:
---
all:
vars:
...
hosts:
...
children:
...
replicaset_storage_2:
vars: # replica set configuration
...
failover_priority:
- storage-2 # <==
...
$ ansible-playbook -i hosts.yml playbook.yml \
--limit replicaset_storage_2 \
--tags cartridge-replicasets
And so storage-2-replica
disappears from the topology!
Please note that the instance is expelled permanently and irrevocably. After removing the instance from the topology, our Ansible role stops the systemd service and deletes all the files of this instance.
If you suddenly change your mind and decide that the storage-2
replica set still needs a second instance, you will not be able to restore it. Cartridge remembers the UUIDs of all the instances that have left the topology and will not allow the expelled one to return. You can start a new instance with the same name and configuration, but its UUID will obviously be different, so Cartridge will allow it to join.
Deleting replica sets
We have already found out that the replica set leader cannot be expelled. But what if we want to remove the storage-2
replica set permanently? Of course, there is a solution.
In order not to lose the data, we must first transfer all the buckets to storage-1
. For this purpose, we set the weight of the storage-2
replica set to 0
:
---
all:
vars:
...
hosts:
...
children:
...
replicaset_storage_2:
vars: # replica set configuration
replicaset_alias: storage-2
weight: 0 # <==
...
...
Start the topology control:
$ ansible-playbook -i hosts.yml playbook.yml \
--limit replicaset_storage_2 \
--tags cartridge-replicasets
Open the Web UI http://localhost:8181/admin/cluster/dashboard and watch all the buckets flow into storage-1
:
Assign the expelled
flag to the storage-2
leader and say goodbye to this replica set:
---
all:
vars:
...
hosts:
...
storage-2:
config:
advertise_uri: '172.19.0.3:3303'
http_port: 8184
expelled: true # <==
...
$ ansible-playbook -i hosts.yml playbook.yml \
--tags cartridge-replicasets
Note that we did not specify the limit
option this time since at least one of the instances with the running playbook must not be marked as expelled
.
So we’re back to the original topology:
Authorization
Let’s take our minds off replica set control and think about safety. Now any unauthorized user can manage the cluster via Web UI. We have to admit; it doesn’t look too good.
With Cartridge, you can connect your own authorization module, such as LDAP (or whatever), and use it to manage users and their access to the application. But here we’ll be using the built-in authorization module that Cartridge uses by default. This module allows you to perform basic operations with users (delete, add, edit) and implements password verification.
Please note that our Ansible role requires the authorization backend to implement all these functions.
Okay, we need to put theory into practice now. First, we are going to make authorization mandatory, set the session parameters, and add a new user:
---
all:
vars:
...
# authorization
cartridge_auth: # <==
enabled: true # enable authorization
cookie_max_age: 1000
cookie_renew_age: 100
users: # cartridge users to set up
- username: dokshina
password: cartridge-rullez
fullname: Elizaveta Dokshina
email: dokshina@example.com
# deleted: true # uncomment to delete user
...
Authorization is managed within the cartridge-config
tasks, so specify this tag:
$ ansible-playbook -i hosts.yml playbook.yml \
--tags cartridge-config
Now http://localhost:8181/admin/cluster/dashboard has a surprise for you:
You can log in with the username
and password
of the new user, or as admin
, the default user. The password is a cluster cookie; we have specified this value in the cartridge_cluster_cookie
variable (it is app-default-cookie
, don’t bother to check).
After a successful login, we open the Users
tab to make sure that everything goes well:
Try adding new users and changing their parameters. To delete a user, specify the deleted: true
flag for that user. The email
and fullname
values are not used by Cartridge, but you can specify them for your convenience.
Application configuration
Let’s step back and skim through the whole story.
We have deployed a small application that stores data about customers and their bank accounts. As you may recall, this application has two implemented roles: api
and storage
. The storage
role deals with data storage and sharding using the integrated vshard-storage
role. The second role (or api
) implements an HTTP server with an API for data management. It also has another integral standard role (vshard-router
) that controls sharding.
So, we send the first request to the application API to add a new client:
$ curl -X POST -H "Content-Type: application/json" \
-d '{"customer_id":1, "name":"Elizaveta", "accounts":[{"account_id": 1}]}' \
http://localhost:8182/storage/customers/create
In return, we get something like this:
{"info":"Successfully created"}
Note that in the URL we have specified the 8082
port of the app-1
instance as this is the port for the API.
Now we update the balance of the new user:
$ curl -X POST -H "Content-Type: application/json" \
-d '{"account_id": 1, "amount": ^_^quotϨquot^_^}' \
http://localhost:8182/storage/customers/1/update_balance
We see the updated balance in the response:
{"balance":"1000.00"}
All right, it works! The API is implemented, Cartridge takes care of data sharding, we have already configured the failover priority in case of emergency and enabled authorization. It’s time to get down to configuring the application.
The current cluster configuration is stored in a distributed configuration file. Each instance stores a copy of this file, and Cartridge ensures that it is synchronized among all the nodes in the cluster. We can specify the role configuration of our application in this file, and Cartridge will make sure that the new configuration is distributed across all the instances.
Let’s take a look at the current contents of this file. Go to the Configuration files
tab and click on the Download
button:
In the downloaded config.yml
file, we find an empty table. It’s no surprise because we haven’t specified any parameters yet:
--- []
...
In fact, the cluster configuration file is not empty: it stores the current topology, authorization settings, and sharding parameters. Cartridge does not share this information so easily; the file is intended for internal use, and therefore stored in hidden system sections that you cannot edit.
Each application role can use one or more configuration sections. The new configuration is loaded in two steps. First, all the roles verify that they are ready to accept the new parameters. If there are no problems, the changes are applied; otherwise, the changes are rolled back.
Now get back to the application. The api
role uses the max-balance
section, where the maximum allowed balance for a single client account is stored. Let’s configure this section using our Ansible role (not manually, of course).
So now the application configuration (more precisely, the available part) is an empty table. Now add a max-balance
section there with a value of 100,000
, and specify the cartridge_app_config
variable in the inventory file:
---
all:
vars:
...
# cluster-wide config
cartridge_app_config: # <==
max-balance: # section name
body: 1000000 # section body
# deleted: true # uncomment to delete section max-balance
...
We have specified a section name (max-balance
) and its contents (body
). The content of the section can be more than just a number; it can also be a table or a string depending on how the role is written and what type of value you want to use.
Run:
$ ansible-playbook -i hosts.yml playbook.yml \
--tags cartridge-config
And check that the maximum allowed balance has indeed changed:
$ curl -X POST -H "Content-Type: application/json" \
-d '{"account_id": 1, "amount": "1000001"}' \
http://localhost:8182/storage/customers/1/update_balance
In return, we get an error, just as we wanted:
{"info":"Error","error":"Maximum is 1000000"}
You can download the configuration file from the Configuration files
tab once again to make sure the new section is there:
---
max-balance: 1000000
...
Try adding new sections to the application configuration, change their contents, or delete them altogether (to do this, you need to set the deleted: true
flag in the section):
For more information on using the distributed configuration in roles, see the Tarantool Cartridge documentation.
Don’t forget to run vagrant halt
to stop the virtual machines when you’re done.
Summary
Last time we learned how to deploy distributed Tarantool Cartridge applications using a special Ansible role. Today we updated the application and learned how to manage application topology, sharding, authorization, and configuration.
As a next step, you can try different approaches to writing Ansible Playbook and use your apps in the most convenient way.
If something doesn’t work or you have ideas on how to improve our Ansible role, please feel free to create a ticket. We are always happy to help and open to any ideas and suggestions!