It feels like last months I focused way too much on debugging and .NET Core and stopped paying attention to topic I enjoyed to blog about over the last year – DevOps and distributed applications. That doesn’t feel right and in order to fix that I’ll play with something new today. For instance, with etcd.
During my romance with distributed apps, etcd always was somewhere near. It came up as alternative to Consul when I was experimenting with service discovery and configuration management. At some point etcd also comes up as a storage where Kubernetes stores its cluster data. Etcd is everywhere! So I think it worth understanding what that is and how it looks like.
What’s etcd
etcd is primitive but highly available key-value storage. “Primitive” means that key-value is the only thing it can store, and “highly available” implies that even if one or few of etcd cluster nodes dies, the data will live. No more, no less. Comparing to Consul it has lesser amount of features, but sometimes in order to crack a nut you don’t need a sledgehammer and regular hammer will get the job done.
However, being a key-value storage doesn’t mean there’re just three etcd commands – get/set/remove – and the story is over. Far from that. It supports transactional assignment of values, ability to watch for value change, assign user roles, use synchronization primitives and even set Time To Live duration.
So let’s have a closer look.
Installation
The best of installation approaches – download to install – works for etcd too. As usual, because I’m lazy and don’t want to pollute my laptop with temporary stuff, I’ll create a Vagrantfile with download and unpacking instructions, so creating disposable etcd machine can happen in one command.
Here’s the Vagrantfile I ended up with.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
Vagrant.configure("2") do |config| config.vm.box = "ubuntu/xenial64" etcd_version = "v3.3.1" download_url = "https://storage.googleapis.com/etcd" install_dir = "/tmp/etcd-download" config.vm.provision "shell", inline: %{ mkdir -p #{install_dir} curl -L #{download_url}/#{etcd_version}/etcd-#{etcd_version}-linux-amd64.tar.gz -o /tmp/etcd-${etcd_version}-linux-amd64.tar.gz tar xzvf /tmp/etcd-${etcd_version}-linux-amd64.tar.gz -C #{install_dir} --strip-components=1 rm -f /tmp/etcd-${etcd_version}-linux-amd64.tar.gz echo 'PATH=$PATH:#{install_dir}' >> /home/vagrant/.bashrc } end |
The script is reasonably simple. The first highlighted block declares target version with download and install paths, and the second one simply downloads and extracts etcd itself and adds path to it to PATH. vagrant up
will do its usual stuff and after a minute or so of downloading and preparing new virtual machine we should be able to vagrant ssh
and start doing something. For instance, checking etcd version number:
1 2 3 4 5 |
etcd --version #etcd Version: 3.3.1 #Git SHA: 28f3f26c0 #Go Version: go1.9.4 #Go OS/Arch: linux/amd64 |
Starting one node etcd cluster
Official documentation provides ridiculously long command for starting even one node cluster. However, if we just want to play on local machine, starting etcd
without any parameter at all works just as fine:
1 2 3 |
etcd # 2018-03-07 04:28:55.591939 I | etcdmain: etcd Version: 3.3.1 # 2018-03-07 04:28:55.592331 I | etcdmain: Git SHA: 28f3f26c0 |
We can confirm that cluster is operational by asking for its members list with etcdctl
command:
1 2 |
etcdctl member list # 8e9e05c52164694d: name=default peerURLs=http://localhost:2380 clientURLs=http://localhost:2379 isLeader=true |
If I was going to use more than one machine or connect to remote etcd cluster I’d have to provide etcdctl
with cluster URL (for instance -c http://localhost:2379
). But again, it’s localhost, so who needs that.
Setting, getting and removing values
We can use very same etcdctl
for manipulating key-value pairs. However, it supports more than one API version so ideally we should be specific about which one we’re going to use. For instance, in order to use the latest version (3
) we need to set ETCDCTL_API environmental variable (export ETCDCTL_API=3
).
That affects quite a lot. For instance, legacy API uses set
command to set key value, whereas v3 uses put
for that. In fact, the whole commands lists are totally different.
Speaking about put
, here’s how we can add, query and remove key-value pairs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
etcdctl put debug-mode-server on # create debug-mode-server key with value 'on' #OK etcdctl put debug-mode-client off # create another k-v pair #OK etcdctl get debug-mode-server # get one value #debug-mode-client #on etcdctl get debug --prefix # list all keys starting with 'debug' #debug-mode-client #off #debug-mode-server #on etcdctl del debug-mode-server # delete debug-mode-server #1 |
Using HTTP endpoints
etcdctl
obviously is not the only way to manipulate key-values. Despite under the hood etcd speaks gRPC, it’s still able to accept HTTP API calls via builtin grpc-gateway. Sometimes it feels a little bit weird and definitely HTTP API stuck somewhere at the bottom of Richardson Maturity Model, but it works. Even if it needs keys and values to be base64 encoded.
Here’s how adding mykey:myvalue
key-value pair would looks like:
1 2 |
curl -L http://localhost:2379/v3beta/kv/put -X POST -d "{\"key\":\"`echo 'mykey' | base64`\",\"value\":\"`echo 'myvalue' | base64`\"}" #{"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"7","raft_term":"6"}} |
And now getting it back:
1 2 3 |
curl -L http://localhost:2379/v3beta/kv/range -X POST -d "{\"key\":\"`echo 'mykey' | base64`\"}" #{"header":{"cluster_id":"14841639068965178418","member_id":"10276657743932975437","revision":"7","raft_term":"6"}, #"kvs":[{"key":"bXlrZXkK","create_revision":"7","mod_revision":"7","version":"1","value":"bXl2YWx1ZQo="}],"count":"1"} |
Looks… exotic. But again, it works.
Some other nice features
Watch for value change
What if we want to be notified when key’s value changes? No problem. We can even wait for value change of a key that doesn’t exist yet:
1 2 |
etcdctl watch shutdown-requested #... |
I remember Consul was doing something like this. Request will block until somebody updates shutdown-requested
value..
1 |
etcdctl put shutdown-requested now |
…just to keep blocking again.
1 2 3 4 |
etcdctl watch shutdown-requested #PUT #shutdown-requested #now |
Transactions
Imagine you need to update a key only if some other conditions are met. For instance, a host will put its name in a leader
key, but only of there’s no other leader yet. We can do that with txn
command in interactive mode by explicitly saying what key needs to be tested, what to do if its value is ‘correct’, and what to do if it doesn’t.
Let’s start with scenario, when there leader
key is already set.
1 2 3 4 5 6 7 8 9 10 11 12 |
etcdctl put leader me etcdctl txn --interactive #compares: #value("leader") = "none" # #success requests (get, put, del): #put leader myHostName # #failure requests (get, put, del): # #FAILURE |
When transaction hits the node, it obviously responds with FAILURE
, as key’s value is different from what we expected – none
. However, setting leader
value to none
will let the next transaction to succeed:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
etcdctl put leader none #OK etcdctl txn --interactive #compares: #value("leader") = "none" # #success requests (get, put, del): #put leader myHostName # #failure requests (get, put, del): # #SUCCESS # #OK etcdctl get leader #leader #myHostName |
Conclusion
That’s etcd
. Obviously, not all of it, but you’ve got the idea. I’m still going through its documentation and samples and it’s “Demos” section is probably the best one that I saw recently. The animation part is mostly useless and somewhat distracting, but having short code samples for every major feature really helps to get the idea of what this tool is about. I like that. Well, and etcd
itself – clearly can see how I can use it in production.