Quantcast
Channel: Sleepless Dev
Viewing all 213 articles
Browse latest View live

Setting up Ansible, SSH to configure AWS EC2 instances

$
0
0

Setting up Ansible, SSH to configure AWS EC2 instances

We pick up with our ansible tutorial  to focus on this AWS ansible tutorial on how to use ansible with EC2 as well as mastering ansible inventory setup, ssh-agent redux and covering ssh client config so you don't have to have long convoluted scripts and have to remember the id file, username, etc. for ssh, scp and ansible. 

Overview

This amazon ansible tutorial covers the following:
  • Setting up ssh client config
  • Setting up ansible to manage our EC2 instance (ansible uses ssh)
  • Setting up a ssh-agent and adding ssh identities (ssh-add)
  • Setting ssh using ~/.ssh/config so we don’t have to pass credentials around
  • Using ansible dynamic inventory with EC2
  • AWS command line tools to manage DNS entries with Route 53

This AWS ansible tutorial covers how to use 
ansible dynamic inventory management with EC2. A related topic, which is crucial for DevOps is how to setup ssh config to simplify logging into remote machines: like creating simple aliases, auto-passing id files, auto-passing user id information.
Lastly, we use AWS Route 53 to setup a DNS name for our new instance which we then use from ansible, and never have to reconfigure our ansible config when we create a new AMI or EC2 instance. If you are using EC2 and you are not using  ssh config files  and ansible and you are doing DevOps, this article is a must.
This is part 4 of this series of articles on creating a the Cassandra Database image and DevOps/DBA. You don’t truly need those articles to follow this one, but they might provide a lot of contexts. If you are new to ansible the last article on ansbible would be good to at least skim. This one picks up and covers ansible more deeply with regards to AWS/EC2.
You can find the source for the first, second, third and this Cassandra tutorial (Ansible/Cassandra tutorial) at our Cloudurable Cassandra Image for Packer, EC2, Docker, AWS and Vagrant. In later articles, we will set up a working cluster in AWS using VPCs, Subnets, Availability Zones and Placement groups. We will continue to use ansible to manage our Cassandra cluster.
The source code for this article is in this branch on github.

Ansible and EC2

Although we have base images, since Cassandra is stateful, we will want the ability to update the images in place for our Amazon Cassandra support.
The options for configuration and orchestration management are endless (Puppet, Chef, Boto, etc.). This article uses Ansible for many of these tasks. Ansible is an agentless architecture and works over ssh (secure shell) as we covered in our last article (Setting up Ansible for our Cassandra Cluster to do DevOps/DBA tasks). There are some very helpful Ansible/AWS integrations which will try to cover in future articles.
The Ansible framework allows DevOps/DBA staff to run commands against Amazon EC2 instances as soon as they are available. Ansible is very suitable for provisioning hosts in a Cassandra cluster as well as performing routine DevOps/DBA tasks like replacing a failed node, backing up a node, profiling Cassandra, performing a rolling upgrade and more.
Since Ansible relies on ssh, we should make sure that ssh is working for us.

Making sure ssh works before we get started with ansible

If ssh is not working, you can't use ansible because ansible needs ssh. 
Before you go about using ansible with AWS/EC2 to manage your Cassandra clusters, you have to make sure that you can, in fact, connect with ssh.
The first step in our journey is to get the IP of the EC2 instance that you just launched.
Another key tip for using ansible is to use -vvvv if it can’t connect so you can see why it can’t connect
Let’s get the IP of the new instance using get-IP-cassandra.sh, which we covered earlier.

Getting the IP

$ bin/get-IP-cassandra.sh
54.218.113.95
Now we can log in with the pem file associated with our AWS key-pair that we used to launch our Cassandra EC2 instance.
Let’s see if we can log into the Cassandra EC2 instance with ssh.

Can I log in with the pem file?

ssh -i ~/.ssh/cloudurable-us-west-2.pem  centos@54.218.113.95
If you can do this, then your security group is setup properly. If you can’t do this, make sure you VPC security group associated with the instance has port 22 open. (Limit logging into instances via SSH port 22 to only your IP address.)
In addition to the pem file that AWS creates, we have our private rsa key for the test cluster (which we covered in the last article). Recall that the rsa key is used with the ansible user (also described in the last article on ansible).
Let’s see if we can log in with our RSA private key.

Can I log in with the key we generated for ansible?

ssh -i ~/.ssh/test_rsa  ansible@54.218.113.95
If you can log in with the pem but not the rsa key we created for the test cluster, then you have an issue with a key mismatch (perhaps). You could try to regenerate the keys with bin/setupkeys-cassandra-security.sh then either copy them with scp copy or upload them with the ansible file/copy module or file/synchronize module.
Passing the key on each ansible command is tiresome, let’s use the ssh-agent (discussed in the last article), to add (ssh-add) our cluster key identity (~/.ssh/test_rsa) to all ssh commands that we use (including ansible).

Can I install the key and log in using ssh-agent?

$ ssh-agent bash
$ ssh-add ~/.ssh/test_rsa
$ ssh ansible@54.218.113.95
If you were able to log in with ssh by adding the key to the ssh-agent, then you are ready to use ansible. To test that you can connect via ansible add these entries to the inventory.ini file in the project root (~/github/cassandra-cloud).

Setting up Ansible using the inventory.ini

We assume you have set up the cluster key as follow:

Setup cluster key for ansible

$ ssh-agent bash
$ ssh-add ~/.ssh/test_rsa
Recall that bin/setupkeys-cassandra-security.sh creates the RSA key and installs it under ~/.ssh/test_rsa. Then the provisioning scripts install the key correctly on the EC2 image (AMI).

Add this to inventory.ini for ansible

[aws-nodes]
54.218.113.95 ansible_user=ansible
The above tells ansible that this server 54.218.113.95 exists, and it is in the group aws-nodes, and that when we connect to it that we should use the user ansible. (Remember we looked up the IP of the Cassandra Datbase EC2 instance using bin/get-IP-cassandra.sh).
Once that is setup, we can run the ansible ping module against our Cassandra EC2 instance as follows:

Run ansible ping modules against aws-nodes

$ ansible aws-nodes -m ping
54.218.113.95 | SUCCESS => {
"changed": false,
"ping": "pong"
}

Dynamic Ansible inventory

When doing Amazon Web Services EC2 DevOps, you could be managing several groups of servers. EC2 allows you to use placement groups, autoscale groups, security groups, and tags to organize and manage your instances. AWS EC2 is rich with meta-data about the instances.
If you are running Dev, QA, production or even green/blue deploys with CI and CD, you will be running many EC2 instances over time. Hosts can come and go in EC2. Because of the ephemeral nature of hosts in EC2, ansbile allows you to use external scripts to manage ansible inventory lists. There is such an ansible inventory script for AWS EC2.
As you can imagine if you are doing DevOps, ansible AWS EC2 dynamic inventory is a must.
You can set up AWS via your ~/.aws/config and ~/.aws/credentials files, and if you installed the aws command line, then you likely have this setup or the requisite environment variables.
To use Ansible’s -i command line option and specify the path to the script.
Before we do that, we need to download the ec2 ansible inventory script and mark it executable.
Download the dynamic ansible inventory script as follows.

Download the ansible ec2 inventory script, make it executable

$ pwd
~/github/cassandra-image/

$ wget https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/ec2.py -O ansible-ec2/ec2.py

$ chmod +x ansible-ec2/ec2.py

$ wget https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/ec2.ini -O ansible-ec2/ec2.ini
After you download it, you can start using it.

Using a dynamic inventory

Now let’s use the dynamic inventory ansible script.
Before we do that, let’s add the pem associated with our AWS Key Pair to the ssh-agent as follows (if you have not done so already).

Add centos pem file (key pair)

$ ssh-add ~/.ssh/cloudurable-us-west-2.pem
Then we can ping the EC2 instance with the ansible ping module as follows:

Pass dynamic list to ansible use user centos

$ ansible -i ansible-ec2/ec2.py  tag_Name_cassandra_node  -u centos  -m ping
54.218.113.95 | SUCCESS => {
"changed": false,
"ping": "pong"
}
The -i option passes a script that will generate a JSON inventory list. You can even write a custom script that produces an inventory as long as that script uses the same JSON format. Above we are passing the script that we just downloaded. Remember that an ansible inventory list is just a list of servers that we are managing with ansible.
Now we know we can use ansible with our AWS key pair and our AWS PEM file. But can we use it with our RSA key?

Using ansible with RSA Key and ansible user from last article

Please recall from the last article that we set up a user called ansible which used an RSA private key file that we created in the last article as well (~/.ssh/test_rsa).
We should be able to manage our EC2 instance with the ansible user using the RSA key. Let’s try.
Add the ansible users RSA key to the ssh-agent as follows.

Add ansible users RSA private key file - test_rsa file

$ ssh-add `~/.ssh/test_rsa`
Now we can access ansible via the ansible user using ~/.ssh/test_rsa as our private key. Use ansible with the ansible users (-u ansible) and the RSA key we just installed.

Pass dynamic list to ansible use user ansible

$ ansible -i ansible-ec2/ec2.py  tag_Name_cassandra_node  -u ansible  -m ping
54.218.113.95 | SUCCESS => {
"changed": false,
"ping": "pong"
}
Often DevOps tasks require you to manage different machines, Ubuntu, CoreOS, CentOS, RedHat, Debian, and AmazonLinux. The various EC2 instances will have different users to log in. For example, CentOS has the user centos and Ubuntu has the user ubuntu (I have run into adminroot, etc.). It is a good idea to create a standard user like ansible (or devops or ops or admin) to run ansiblecommands against different flavors of Unix. Also, AWS PEM files / key pairs do not change once an instance if launched, and Cassandra instances tend to be less ephemeral (due to the statefulness of the Cassandra Database and the potentially large amounts of data on a node) then some other EC2 instances. The ability to regenerate the RSA key periodically is important as you do not want the keys to getting into the wrong hands.
The AWS inventory list command uses security groups, VPC ids, instance id, image type, EC2 tags, AZ, scaling groups, region and more to group EC2 instances to run ansible commands against, which is very flexible for DevOps operations.
Let’s see a list of all of the aliases and ansible groups that our one Cassandra Database EC2 instance is exposed.

Show all ansible groups that our Cassandra Database EC2 instance can be accessed by

./ansible-ec2/ec2.py  | jq "keys"
[
"_meta",
"ami_6db4410e", //by AMI
"ec2", //All ec2 instances
"i-754a8a4f693b58d1b", //by instance id
"key_cloudurable_us_west_2",//by key pair
"security_group_allow_ssh", //by security group
"tag_Name_cassandra_node", //by EC2 tag
"type_m4_large", //by EC2 instance type
"us-west-2", //by Region
"us-west-2c", //by AZ Availability Zone
"vpc_id_vpc_c78000a0" //by VPC Virtual Private Cloud
]

You can use any of these ansible groups to ping a set of servers. Let’s ping every server (we only have one) in the AWS us-west-2region.

Ping all Cassandra Database nodes in the us-west-2 region

$ ansible -i ansible-ec2/ec2.py  us-west-2  -u ansible  -m ping
54.218.113.95 | SUCCESS => {
"changed": false,
"ping": "pong"
}
I don’t know about you, but I don’t like passing around the -i and -u option on every command. Let’s see what we can do to remedy this.

Installing Dynamic Inventory as the default inventory

Another option besides using the -i is to copy the dynamic inventory script to /etc/ansible/ec2.py and chmod +x it. You will also need to copy the ec2.ini file to /etc/ansible/ec2.ini. Then we will be able to use ansible EC2 dynamic inventory without passing the -i (making it a lot easier to use).
Let’s install the ansible dynamic inventory script and config as follows.

Installing ansible EC2 dynamic inventory script as the default

$ cd ~/github/cassandra-image

$ wget https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/ec2.py -O ansible-ec2/ec2.py

$ chmod +x ansible-ec2/ec2.py

$ sudo cp ansible-ec2/ec2.py /etc/ansible/ec2.py

$ wget https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/ec2.ini -O ansible-ec2/ec2.ini
sudo cp ansible-ec2/ec2.ini /etc/ansible/ec2.ini
You will also need to add the script (ANSIBLE_HOSTS) and the ini file (EC2_INI_PATH) to environment variable which you can put in your `~/.bash_profile.

Environment variables needed to make dynamic inventory work

export ANSIBLE_HOSTS=/etc/ansible/ec2.py
export EC2_INI_PATH=/etc/ansible/ec2.ini
Now when you use ansible, you will not have to specify -i every time.
Let’s try ansible using the dynamic inventory list without the -i.

Using dynamic inventory without -i

$ ansible   tag_Name_cassandra_node  -u ansible -m ping
54.218.113.95 | SUCCESS => {
"changed": false,
"ping": "pong"
}

Now that we got rid of -i to specify the ansible dynamic inventory list script, let’s get rid of the -u to specify the user. At least let’s try.
Again before we do that, let’s see if we can use ssh without passing the user name.

Specifying default user via ~/.ssh/config

If you’re like most developers doing DevOps, you have a half dozen remote servers (or these days, local virtual machines, EC2 instances, Docker containers) you might need to deal with.
Remembering all of those usernames, passwords, domain names, identity files, and command line options to ssh can be daunting. You want a way to simplify your life with an ssh config file.
Using ssh effectively is another one of those essential DevOp skills!
You can create an ssh config file that configures host names, user names, and private keys to connect to ssh. There are many custom ssh config options to configure ssh and make life easier.
We will show how to configure ~/.ssh/config to make logging into our EC2 instance easier, and eventually get rid of the need to run the ssh-agent or use the -i option when using ansible.
We wrote a small bash script that gets the DNS name of our instance using the aws command line as follows:

bin/get-DNS-name-cassandra.sh - Get the DNS name of our Cassandra EC2 instance using awscommand line

#!/bin/bash
set -e

source bin/ec2-env.sh

aws ec2 describe-instances --filters "Name=tag:Name,Values=${EC2_INSTANCE_NAME}" \
| jq --raw-output .Reservations[].Instances[].PublicDnsName

We can use bin/get-DNS-name-cassandra.sh to get the DNS name of our instance as follows:

Getting the DNS name of the Cassandra EC2 instance

bin/get-DNS-name-cassandra.sh
ec2-54-218-113-95.us-west-2.compute.amazonaws.com
Now let’s see the IP address associated with this instance.

EC2 Cassandra host

$ host ec2-54-218-113-95.us-west-2.compute.amazonaws.com
54.218.113.95
Note that for this discussion that we are using 54.218.113.95 as the public IP address of our Cassandra node (that we created with packer and launched with the aws command line tools).
Now we can configure ~/.ssh/config to use this information.

~/.ssh/config

# Note we can use wild star so any that match this pattern will work.
Host *.us-west-2.compute.amazonaws.com
ForwardAgent yes
IdentityFile ~/.ssh/test_rsa
User ansible

# Note we can use the IP address
# so if we ssh into it, we don't have to pass username and the id file
Host 54.218.113.95
ForwardAgent yes
IdentityFile ~/.ssh/test_rsa
User ansible

# We even create an alias for ssh that has username and the id file.
Host cnode0
Hostname 54.218.113.95
ForwardAgent yes
IdentityFile ~/.ssh/test_rsa
User ansible

Read the comments in the file above.
Now when we log into cnode0 using ssh as follows:

ssh cnode0

$ ssh cnode0
Note that cnode0 is an alias that we set up in ~/.ssh/config and that we don’t have to use the -i option to pass the identity file or use the username.
Would you rather need to remember ssh cnode0 or ssh -i ~/.ssh/my-long-pem-name-with-region-info.pem someuserForEachLinuxFlavor@ec2-ip-address-that-changes-every-time-i-build-a-new-instance.us-west-2.compute.amazonaws.com?
Keep in mind; you do not have to use ssh-agent or ssh-add anymore to use ansible since we configured the identity file and username in ~/.ssh/config. Forgetting to set up the ssh-agent and adding the right key file with ssh-add was error prone at best, and often left me personally confused. Now that issue of confusion is gone, but since we set up ForwardAgent yes, once we log into a remote instance the keys we set up with ssh-agent and ssh-add get passed to the remote host. This way those keys do not have to live on the remote host. You can, for example, log into a bastion server and then ssh into a private subnet with the keys you set up with ssh-agent, and none of those private keys have to live on the remote instances (to avoid getting used by someone else). Mastering sshssh-agent, and ssh key management is essential to being good at DevOps.
Given the above config, you can also log into the Cassandra dev instance with its public domain name as follows:

ssh into box using public address

$ ssh ec2-54-218-113-95.us-west-2.compute.amazonaws.com
The above uses the Host *.us-west-2.compute.amazonaws.com which is a domain pattern that would work for all ec2 instances in the us-west-2 AWS region. Since different regions will use different AWS key-pairs, you can set up a pattern/key-pair pem file for each region easily.

Attempt Get ansible to use public DNS names instead of IP addresses (does not work)

Given the above ./ssh/config to get rid of -u option one might imagine you could tell ec2.py, the script that generates the ansible inventory, you could configure it to use public domain names instead of public IP addresses, and you can.
If you make this change, to ec2.ini.

/etc/ansible/ec2.ini

Change vpc_destination_variable = ip_address to vpc_destination_variable = public_dns_name

vpc_destination_variable = public_dns_name
Then ec2.py will use the public domain names from EC2 instead of public IP addresses, for example, ec2-54-218-113-95.us-west-2.compute.amazonaws.com instead of 54.218.113.95.
But then all of the ansible commands stop working. It seems, as best that I can tell, that ansible does not like domain names with dashes. We searched for a workaround for this and could not find it. If you know the answer, please write us.
We even tried to add this directly to inventory.ini.

adding ec2 host direct

[aws-nodes]
# 54.186.15.163 ansible_user=ansible
ec2-54-218-113-95.us-west-2.compute.amazonaws.com ansible_user=ansible
Then tried running the ansible commands against the aws-nodes and we got the same result until we tried the fix for EC2 domain name being too long for Ansible, but we never got the ec2.py to work with the longer DNS names (we were able to get past parts of it).
This problem is either ansible not handling dashes or long dns name problem. The fix seems to be in the comments of this fix for EC2 domain name being too long for Ansible, but again worked but only in the non-dynamic config. For the most part, we tried the fix and it did not work (still getting ERROR! Specified hosts and/or --limit does not match any hosts).
It is okay, though. The only real limitation here is that when you use ansible with ec2.py that you will need to pass the user and continue to use ssh-agent and ssh-add.
This workaround of having to give the username with -u is not too serious. We still wish there was a way to use ansible without passing a username and identity file just like we have with ssh. And there is, but it involves AWS Route 53 and configuring `~/ssh/config.

Using ansible without passing the id file or username

Another way to use ansible with our Cassandra cluster is to create DNS names for the Cassandra nodes that we want to manage. The problem with using the public IP address or the AWS generated DNS name is that they change each time we terminate and recreate the instance. We plan on terminating and recreating the instance a lot.
The solution is where DNS comes in and AWS route 53. After we create the instance, we can use an internal hosted zone of Route 53 (for VPN) or a public hosted zone and associate the IP address with our new instance. We could do this for all of the Cassandra seed nodes and all of the cluster nodes for that matter.
Before we get started let’s add two more variables to our bin/ec2-env.sh, namely, HOSTED_ZONE_ID and NODE0_DNS as follows:

bin/ec2-env.sh

#!/bin/bash
set -e

export AMI_CASSANDRA=ami-abc1234
export VPC_SECURITY_GROUP=sg-abc1234

export SUBNET_CLUSTER=subnet-abc1234
export KEY_NAME_CASSANDRA=cloudurable-us-west-2
export PEM_FILE="${HOME}/.ssh/${KEY_NAME_CASSANDRA}.pem"
export IAM_PROFILE_CASSANDRA=IAM_PROFILE_CASSANDRA
export EC2_INSTANCE_NAME=cassandra-node

export HOSTED_ZONE_ID="Z1-abc1234"
export NODE0_DNS="node0.cas.dev.cloudurable.com."

Now let’s define a new script that will use the aws command line. We will use the aws route53 change-resource-record-sets to associate a DNS name with the IP address as follows:

bin/associate-DNS-with-IP.sh

#!/bin/bash
set -e

source bin/ec2-env.sh

IP_ADDRESS=`bin/get-IP-CASSANDRA.sh`


REQUEST_BATCH="
{
\"Changes\":[
{
\"Action\": \"UPSERT\",
\"ResourceRecordSet\": {
\"Type\": \"A\",
\"Name\": \"$NODE0_DNS\",
\"TTL\": 300,
\"ResourceRecords\": [{
\"Value\": \"$IP_ADDRESS\"
}]
}
}
]
}
"

echo "$REQUEST_BATCH"

changeId=$(aws route53 change-resource-record-sets --hosted-zone-id "$HOSTED_ZONE_ID" --change-batch "$REQUEST_BATCH" \
| jq --raw-output .ChangeInfo.Id)

aws route53 wait resource-record-sets-changed --id "$changeId"

Notice that we are running this change against our Route 53 Hosted ZONE with aws route53 change-resource-record-sets as follows:

Change batch for Route 53 hosted zone

{
"Changes":[
{
"Action": "UPSERT",
"ResourceRecordSet": {
"Type": "A",
"Name": "node0.cas.dev.cloudurable.com.",
"TTL": 300,
"ResourceRecords": [{
"Value": "54.218.113.95"
}]
}
}
]
}
Notice we are using UPSERT which will update or add the A record to Route 53’s DNS resources to associate the name node0.cas.dev.cloudurable.com. with the IP address 54.218.113.95.
Now that we have a domain name, and it is scripted/automated (we added a call to bin/associate-DNS-with-IP.sh into bin/create-ec2-cassandra.sh), we can configure ~/.ssh/config to use this domain name which will not change like the public IP or our Cassandra instance public DNS name changes.
Let’s update the ~/.ssh/config to refer to our new DNS name as follows:

~/.ssh/config - Use new DNS naming

Host *.us-west-2.compute.amazonaws.com
ForwardAgent yes
IdentityFile ~/.ssh/test_rsa
User ansible

Host *.cas.dev.cloudurable.com
ForwardAgent yes
IdentityFile ~/.ssh/test_rsa
User ansible

Host cnode0
Hostname node0.cas.dev.cloudurable.com
ForwardAgent yes
IdentityFile ~/.ssh/test_rsa
User ansible

Notice we added the pattern *.cas.dev.cloudurable.com (where cas stands for Cassandra and dev means this is our development environment). We also added an alias for our the Cassandra Database instance called cnode0 that refers tonode0.cas.dev.cloudurable.com.
We can ssh into cnode0 or node0.cas.dev.cloudurable.com without passing the username or identity file (private key) each time. This config is like before but using a DNS name that does not change when we rebuild our servers. This concept is important; you would not want to modify ~/.ssh/config every time you rebuild a server.
Now let’s change our inventory.ini file in the project directory (~/github/cassandra-image) to use this as follows:

~/github/cassandra-image/inventory.ini

[aws-nodes]
cnode0
node0.cas.dev.cloudurable.com

Notice that we use the short name and the long name.
Note you truly just need one but we have two just for this article. Never put the same box twice in the same ansible group, all commands and playbooks will run twice.
Now we can run ansible ping against these servers and not pass the username or identity file.
Use ansible ping module against cnode and node0.cas.dev.cloudurable.com.
Run against all (see note above).

Running ansible ping against all of the “instances”

$ ansible  aws-nodes  -u ansible -m ping
cnode0 | SUCCESS => {
"changed": false,
"ping": "pong"
}
node0.cas.dev.cloudurable.com | SUCCESS => {
"changed": false,
"ping": "pong"
}

We can also just run it against one of the instances by using just that instances name.
Run against cnode0.

ansible cnode0 -u ansible -m ping

$ ansible cnode0  -u ansible -m ping
cnode0 | SUCCESS => {
"changed": false,
"ping": "pong"
}
We can do this for any server.
Run against node0.cas.dev.cloudurable.com.

ansible node0.cas.dev.cloudurable.com -u ansible -m ping

$ ansible node0.cas.dev.cloudurable.com  -u ansible -m ping
node0.cas.dev.cloudurable.com | SUCCESS => {
"changed": false,
"ping": "pong"
}
Keep in mind; you do not have to use ssh-agent or ssh-add anymore to use ansible since we configured the identity file and username in ~/.ssh/config. We can rebuild our server at will. Each time we do, our creation script will update the IP address in DNS to look at the new server. Then all of our ansible scripts and playbooks will continue to work.

Using Ansible to manage our Cassandra Database cluster

We won’t do much actual cluster management in this article per se. And, unlike our last article that used Vagrant and ansible, we don’t have a cluster per se (or rather we have a cluster of one).
We can now use Ansible for our Cassandra Database Cluster to do automate common DevOps/DBA tasks.

Ansible running nodetool against all nodes

ansible aws-nodes -a "/opt/cassandra/bin/nodetool describecluster"
cnode0 | SUCCESS | rc=0 >>
Cluster Information:
Name: Test Cluster
Snitch: org.apache.cassandra.locator.DynamicEndpointSnitch
Partitioner: org.apache.cassandra.dht.Murmur3Partitioner
Schema versions:
86afa796-d883-3932-aa73-6b017cef0d19: [127.0.0.1]

node0.cas.dev.cloudurable.com | SUCCESS | rc=0 >>
Cluster Information:
Name: Test Cluster
Snitch: org.apache.cassandra.locator.DynamicEndpointSnitch
Partitioner: org.apache.cassandra.dht.Murmur3Partitioner
Schema versions:
86afa796-d883-3932-aa73-6b017cef0d19: [127.0.0.1]

Let’s say that we wanted to update a schema or do a rolling restart of our Cassandra nodes, which could be a very common task. Perhaps before the update, we want to decommission the node and back things up. To do this sort of automation, we could create an Ansible playbook.
Let’s run an Ansible playbook from the last article.

Running describe-cluster playbook

$ ansible-playbook playbooks/describe-cluster.yml --verbose
Using /Users/jean/github/cassandra-image/ansible.cfg as config file

PLAY [aws-nodes] ***************************************************************

TASK [Run NodeTool Describe Cluster command] ***********************************
changed: [node0.cas.dev.cloudurable.com] => {"changed": true, "cmd": ["/opt/cassandra/bin/nodetool", "describecluster"],
"delta": "0:00:02.192589", "end": "2017-03-03 08:02:58.537371", "rc": 0, "start": "2017-03-03 08:02:56.344782",
"stderr": "", "stdout": "Cluster Information:\n\tName: Test Cluster\n\tSnitch:
org.apache.cassandra.locator.DynamicEndpointSnitch\n\tPartitioner: org.apache.cassandra.dht.Murmur3Partitioner
\n\tSchema versions:\n\t\t86afa796-d883-3932-aa73-6b017cef0d19: [127.0.0.1]", "stdout_lines": ["Cluster Information:",
"\tName: Test Cluster", "\tSnitch: org.apache.cassandra.locator.DynamicEndpointSnitch",
...
PLAY RECAP *********************************************************************
cnode0 : ok=1 changed=1 unreachable=0 failed=0
node0.cas.dev.cloudurable.com : ok=1 changed=1 unreachable=0 failed=0


Cassandra Tutorial: Cassandra Cluster DevOps/DBA series

The first tutorial in this Cassandra tutorial series focused on setting up a Cassandra Cluster. The first Cassandra tutorial setting up a Cassandra cluster with Vagrant (also appeared on DZone with some additional content DZone Setting up a Cassandra Cluster with Vagrant. The second article in this series was about setting up SSL for a Cassandra cluster using Vagrant (which also appeared with more content as DZone Setting up a Cassandra Cluster with SSL). The third article in this series was about configuring and using Ansible (building on the first two articles). This article (the 4th) will cover applying the tools and techniques from the first three articles to produce an image (EC2 AMI to be precise) that we can deploy to AWS/EC2. To do this explanation, we will use Packer, Ansible, and the Aws Command Line tools. The AWS command line tools are essential for doing DevOps with AWS.

Check out more information about the Cassandra Database


Setting up Ansible for our Cassandra Database Cluster to do DevOps/DBA tasks

$
0
0

Setting up Ansible for our Cassandra Database Cluster to do DevOps/DBA tasks

Ansible is an essential DevOps/DBA tool for managing backups and rolling upgrades to the Cassandra cluster in AWS/EC2. An excellent aspect of Ansible is that it uses ssh, so you do not have to install an agent to use Ansible.
This article series centers on how DevOps/DBA tasks with the Cassandra Database. However the use of Ansible for DevOps/DBA transcends its use with the Cassandra Database so this article is good information for any DevOps/DBA or Developer that needs to manage groups of instances, boxes, hosts whether they be on-prem bare-metal, dev boxes, or in the Cloud. You don’t need to be setting up Cassandra to get use of this article.

Cassandra Tutorial Series on DevOps/DBA Cassandra Database

The first article in this series was about setting up a Cassandra cluster with Vagrant (also appeared on DZone with some additional content DZone Setting up a Cassandra Cluster with Vagrant. The second article in this series was about setting up SSL for a Cassandra cluster using Vagrant (which also appeared with more content as DZone Setting up a Cassandra Cluster with SSL). You don’t need those articles to follow along, but they might provide a lot of contexts. You can find the source for the first and second article at our Cloudurable Cassandra Image for Docker, AWS, and Vagrant. In later articles, we will use Ansible to create more complicated playbooks like doing a rolling Cassandra upgrade, and we will cover using Ansible/ssh with AWS EC2.

Source code for Vagrant, and ansbile

We continue to evolve the cassandra-image GitHub project. In an effort for the code to match the listings in the article, we created a new branch where the code was when this article was written (more or less): Article 3 Ansible Cassandra Vagrant.
Let’s get to it. Let’s start by creating a key for our DevOps/DBA test Cassandra cluster.

Create key for test cluster to do Cassandra Database DevOps/DBA tasks with Ansible

To use Ansible for DevOps/DBA, we will need to setup ssh keys as Ansible uses ssh instead of running an agent on each server like Chef and Puppet.
The tool ssh-keygen manages authentication keys for ssh(secure shell). The utility ssh-keygen generates RSA or DSA keys for SSH (secure shell) protocol version 1 and 2. You can specify the key type with the -t option.

setup key script bin/setupkeys-cassandra-security.sh

CLUSTER_NAME=test
...
ssh-keygen -t rsa -C "your_email@example.com" -N "" -C "setup for cloud" \
-f "$PWD/resources/server/certs/${CLUSTER_NAME}_rsa"

chmod 400 "$PWD/resources/server/certs/"*
cp "$PWD/resources/server/certs/"* ~/.ssh
...

Let’s break that down.
We use ssh-keygen to create a private key that we will use to log into our boxes.
In this article those boxes are Vagrant boxes (VirtualBox), but in the next article, we will use the same key to manage EC2 instances.

Use ssh-keygen to create private key for ssh to log into Cassandra Database nodes

ssh-keygen -t rsa -C "your_email@example.com" -N "" -C "setup for cloud" \
-f "$PWD/resources/server/certs/${CLUSTER_NAME}_rsa"
Then we restrict the access to the file of the key otherwise, ansible, ssh and scp (secure copy) will not let us use it.

Change the access of the key


chmod 400 "$PWD/resources/server/certs/"*

The above chmod 400 changes the cert files so only the owner can read the file. This file change mod makes sense. The certification file should be private to the user (and that is what 400 does).

Copy keys to area where it will be copied by Cassandra node provisioning

cp "$PWD/resources/server/certs/"* ~/.ssh
The above just puts the files where our provisioners (Packer and Vagrant) can pick them up and deploy them with the image.
Locally we are using Vagrant to launch a cluster to do some tests on our laptop.
We also use Packer and aws command line tools to create EC2 AMIs (and Docker images), but we don’t cover aws in this article (it is in the very next which is sort of part 2 to this article).

Create a bastion server to do ansible DevOps/DBA tasks for Cassandra Cluster

Eventually, we would like to use a bastion server that is a public subnet to send commands to our Cassandra Database nodes that are in a private subnet in EC2. For local testing, we set up a bastion server, which is well explained in this guide to Vagrant and Ansible.
We used Learning Ansible with Vagrant (Part 24) as a guide for some of the setup performed in this article. It is a reliable source of Ansible and Vagrant knowledge for DevOps/DBA. Their mgmt node corresponds to what we call a bastion server. A notable difference is we are using CentOS 7 not Ubuntu, and we made some slight syntax updates to some of the Ansible commands that we are using (we use a later version of Ansible).
We added a bastion server to our Vagrant config as follows:

Vagrantfile to set up the bastion for our Cassandra Cluster


# Define Bastion Node
config.vm.define "bastion" do |node|
node.vm.network "private_network", ip: "192.168.50.20"
node.vm.provider "virtualbox" do |vb|
vb.memory = "256"
vb.cpus = 1
end


node.vm.provision "shell", inline: <<-SHELL
yum install -y epel-release
yum update -y
yum install -y ansible

mkdir /home/vagrant/resources
cp -r /vagrant/resources/* /home/vagrant/resources/

mkdir -p ~/resources
cp -r /vagrant/resources/* ~/resources/

mkdir -p /home/vagrant/.ssh/
cp /vagrant/resources/server/certs/* /home/vagrant/.ssh/

sudo /vagrant/scripts/002-hosts.sh

ssh-keyscan node0 node1 node2 >> /home/vagrant/.ssh/known_hosts


mkdir ~/playbooks
cp -r /vagrant/playbooks/* ~/playbooks/
sudo cp /vagrant/resources/home/inventory.ini /etc/ansible/hosts
chown -R vagrant:vagrant /home/vagrant
SHELL
The bastion server which could be on a public subnet in AWS in a VPC uses the ssh-keyscan to add nodes that we setup in the host file into /home/vagrant/.ssh/known_hosts.

Running ssh-keyscan

ssh-keyscan node0 node1 node2  >> /home/vagrant/.ssh/known_hosts
This utility is to get around the problem of needing to verify nodes, and getting this error message: The authenticity of host ... can't be established. ... Are you sure you want to continue connecting (yes/no)? no when we are trying to run ansible command line tools.

Modify the Vagrant provision script

Since we are using provision files to create different types of images (Docker, EC2 AMI, Vagrant/VirtualBox), then we use a provisioning script specific to vagrant.
In this vagrant provision script, we call another provision script to setup a hosts file.

000-vagrant-provision.sh

mkdir  -p  /home/vagrant/.ssh/
cp /vagrant/resources/server/certs/* /home/vagrant/.ssh/
...

scripts/002-hosts.sh
echo RUNNING TUNE OS

Setting up sshd on our Cassandra Database nodes in our DevOps Cassandra Cluster

The provision script 002-hosts.sh configures /etc/ssh/sshd_config/sshd_config to allow public key auth. Then it restarts the daemon for ssh communication sshd. (The other provisioning scripts it invokes was covered in the first two articles).
Let’s look at the 002-hosts.sh provision script. You can see some remnants from the last article where we setup csqlsh, and then it gets to business setting up sshd (secure server shell daemon).

scripts/002-hosts.sh - sets up sshd and hosts file

#!/bin/bash
set -e



## Copy csqlshrc file that controls csqlsh connections to ~/.cassandra/cqlshrc.
mkdir ~/.cassandra
cp ~/resources/home/.cassandra/cqlshrc ~/.cassandra/cqlshrc

## Allow pub key login to ssh.
sed -ie 's/#PubkeyAuthentication no/PubkeyAuthentication yes/g' /etc/ssh/sshd_config

## System control restart sshd daemon to take sshd_config into effect.
systemctl restart sshd

# Create host file so it is easier to ssh from box to box
cat >> /etc/hosts <<EOL

192.168.50.20 bastion

192.168.50.4 node0
192.168.50.5 node1
192.168.50.6 node2
192.168.50.7 node3
192.168.50.8 node4
192.168.50.9 node5
EOL
This setup is fairly specific to our Vagrant setup at this point. To simplify access to the servers that hold the different Cassandra Database nodes, the 002-hosts.sh creates an \etc\hosts\ file on the bastion server.
With our certification keys added to sshd config and our hosts configured (and our inventory.ini file shipped), we can start using ansible from our bastion server.
This reminds me, we did not talk about the ansible inventory.ini file.

Ansible config on bastion for Cassandra Database Cluster

Ansible has a ansible.cfg file, and an inventory.ini file. When you run ansible, it checks for ansible.cfg in the current working directory, then your home directory, and then for a master config file (/etc/ansible). We created an inventory.ini file which lives under ~\github\cassandra-image\resources\home, which gets mapped to \vagrant\resources\home on the virtual machines (node0, bastion, node1, and node2). A provision script moves the inventory.ini file to its proper location (sudo cp /vagrant/resources/home/inventory.ini /etc/ansible/hosts).
The inventory.ini contains servers that you want to manage with Ansible. A couple of things are going on here, we have a bastiongroup, this is for our bastion server, next we have the nodes group, and it is made up of node0node1, and node2.
Let’s see what the inventory.ini file actually looks like.

inventory.ini that gets copied to Ansible master list on Bastion

[bastion]
bastion


[nodes]
node0
node1
node2
Once we provision our cluster, we can log into bastion and start executing ansible commands.

Installing cert key for test DevOps/DBA Cassandra Cluster on all nodes using an ansible playbook

To make this happen, we had to tell the other servers about our certification keys.
We did this with an ansible playbook as follows:

Ansible playbook getting invoked from Vagrant on each new Cassandra Database node


Vagrant.configure("2") do |config|


config.vm.box = "centos/7"


# Define Cassandra Nodes
(0..numCassandraNodes-1).each do |i|

port_number = i + 4
ip_address = "192.168.50.#{port_number}"
seed_addresses = "192.168.50.4,192.168.50.5,192.168.50.6"
config.vm.define "node#{i}" do |node|
node.vm.network "private_network", ip: ip_address
node.vm.provider "virtualbox" do |vb|
vb.memory = "2048"
vb.cpus = 4
end
...

node.vm.provision "ansible" do |ansible|
ansible.playbook = "playbooks/ssh-addkey.yml"
end
end
end
Notice the line node.vm.provision "ansible" do |ansible| and ansible.playbook = "playbooks/ssh-addkey.yml".
If you are new to Vagrant and the above just is not making sense, please watch Vagrant Crash Course. It is by the same folks (guy) who created the Ansible series.
Ansible playbooks are like configuration playbooks. You can perform tons of operations that are important for DevOps (like yum installing software, specific tasks to Cassandra, etc.).
Playbooks are Ansible’s configuration, deployment, and orchestration language. They can describe a policy you want your remote systems to enforce, or a set of steps in a general IT process. –Ansible Playbook documentation.
Here is the ansible playbook to add the RSA public key to the cassandra nodes as follows.

Ansible playbook ssh-addkey.yml to add test_rsa.pub to all Cassandra Database node servers

---
- hosts: all
become: true
gather_facts: no
remote_user: vagrant

tasks:

- name: install ssh key
authorized_key: user=vagrant
key="{{ lookup('file', '../resources/server/certs/test_rsa.pub') }}"
state=present

The Vagrant Ansible provisioner allows you to provision the guest using Ansible playbooks by executing ansible-playbook from the Vagrant host. –(Vagrant Ansible documentation)[https://www.vagrantup.com/docs/provisioning/ansible.html]

For users who did not read any of the first articles on setting up the Cassandra Cluster

If you have not done so already navigate to the project root dir (which is ~/github/cassandra-image on my dev box), download the binaries. The source code is at Github Cassandra Image project.

Running setup scripts

## cd ~; mkdir github; cd github; git clone https://github.com/cloudurable/cassandra-image
$ cd ~/github/cassandra-image
$ pwd
~/github/cassandra-image
## Setup keys
$ bin/setupkeys-cassandra-security.sh
## Download binaries
$ bin/prepare_binaries.sh
## Bring Vagrant cluster up
$ vagrant up
Even if you read the first article note that bin/prepare_binaries.sh is something we added after the first two articles. It downloads the binaries needed for the provisioning, does a checksum of the files and then installs them as part of the provisioning process.

Where do you go if you have a problem or get stuck?

We set up a google group for this project and set of articles. If you just can’t get something to work or you are getting an error message, please report it here. Between the mailing list and the github issues, we can support you with quite a few questions and issues.

Running ansible commands from bastion

Let’s log into bastion and run ansible commands against the cassandra nodes.

Working with ansible from bastion and using ssh-agent

$ vagrant ssh bastion
So we don’t have to keep logging in, and passing our cert key, let’s start up an ssh-agent and add our cert key ssh-add ~/.ssh/test_rsa to the agent.
The ssh-agent is a utility to hold private keys used for public key authentication (RSA, DSA, ECDSA, Ed25519) so you don’t have to keep passing the keys around. The ssh-agent is usually started in the beginning of a login session. Other programs (scp, ssh, ansible) are started as clients to the ssh-agent utility.
Mastering ssh is essential for DevOps and needed for ansible.
First set up ssh-agent and add keys to it with ssh-add.

Start ssh-agent and add keys

$ ssh-agent bash
$ ssh-add ~/.ssh/test_rsa
Now that the agent is running and our keys are added, we can use ansible without passing it the RSA private key.
Let’s verify connectivity, by pinging some of these machines. Let’s ping the node0 machine. Then let’s ping all of the nodes.
Let’s use the ansible ping module to ping the node0 server.

Ansible Ping the Cassandra Database node

$ ansible node0 -m ping
Output
node0 | SUCCESS => {
"changed": false,
"ping": "pong"
}

To learn more about DevOps with ansible see this video on Ansible introduction. It covers a lot of the basics of ansible.
Now let’s ping all of the nodes.

Ansible Ping all Cassandra Database Cluster nodes

$ ansible nodes  -m ping
Output
node0 | SUCCESS => {
"changed": false,
"ping": "pong"
}
node2 | SUCCESS => {
"changed": false,
"ping": "pong"
}
node1 | SUCCESS => {
"changed": false,
"ping": "pong"
}

Looks like bastion can run ansible against all of the servers.

Setting up my MacOSX to run Ansible against Cassandra Database Cluster nodes

The script ~/github/cassandra-image/bin/setupkeys-cassandra-security.sh copies the test cluster key for ssh (secure shell) over to ~/.ssh/ (cp "$PWD/resources/server/certs/"* ~/.ssh). It was Run from the project root folder which is ~/github/cassandra-image on my box.
Move to the where you checked out the project.
cd ~/github/cassandra-image
In this folder is an ansible.cfg file and an inventory.ini file for local dev. Before you use these first modify your /etc/hosts file to configure entries for bastionnode0node1node2 servers.

Add bastion, node0, etc. to /etc/hosts

$ cat /etc/hosts

### Used for ansible/ vagrant
192.168.50.20 bastion
192.168.50.4 node0
192.168.50.5 node1
192.168.50.6 node2
192.168.50.7 node3
192.168.50.8 node4
192.168.50.9 node5

We can use ssh-keyscan just like we did before to add these hosts to our known_hosts file.

Add keys to known_hosts to avoid prompts

$ ssh-keyscan node0 node1 node2  >> ~/.ssh/known_hosts

Then just like before we can start up an ssh-agent and add our keys.

Start ssh-agent and add keys

$ ssh-agent bash
$ ssh-add ~/.ssh/test_rsa
Notice that the ansible.cfg and inventory.ini files are a bit different than on our bastion server because we have to add the user name.

Notice the ansible.cfg file and inventory.ini file in the project dir

$ cd ~/github/cassandra-image

$ cat ansible.cfg
[defaults]
hostfile = inventory.ini

cat inventory.ini
[nodes]
node0 ansible_user=vagrant
node1 ansible_user=vagrant
node2 ansible_user=vagrant
Ansible will use these.
From the project directory, you should be able to ping node0 and all of the nodes just like before.
Ping node0 with ansible.

Ansible Ping Cassandra Database node

$ ansible node0 -m ping
Output
node0 | SUCCESS => {
"changed": false,
"ping": "pong"
}

Ping all of the Cassandra nodes with ansible.

Ansible Ping All Cassandra Database Cluster nodes

$ ansible nodes  -m ping
Output
node0 | SUCCESS => {
"changed": false,
"ping": "pong"
}
node2 | SUCCESS => {
"changed": false,
"ping": "pong"
}
node1 | SUCCESS => {
"changed": false,
"ping": "pong"
}

In the next article, we cover how to setup ~.ssh/config so you don’t have to remember to use ssh-agent.

Using ansible to run nodetool on Cassandra Cluster nodes

You may recall from the first article that we would log into the servers (vagrant ssh node0) and then check that they could see the other nodes with nodetool describecluster. We could run this command with all three servers (from bastion or on our dev laptop) with ansible.
Let’s use ansible to run describecluster against all of the nodes.

Ansible running nodetool describecluster against all Cassandra Cluster nodes

$ ansible nodes -a "/opt/cassandra/bin/nodetool describecluster"
This command allows us to check the status of every node quickly.
Let’s say that we wanted to update a schema or do a rolling restart of our Cassandra cluster nodes, which could be a very common task. Perhaps before the update, we want to decommission the node and back things up. To do this sort of automation, we could create an Ansible playbook.
Ansible Playbooks are more powerful than executing ad-hoc task execution and is especially powerful for managing a cluster of Cassandra servers.
Playbooks allow for configuration management and multi-machine deployment to manage complex tasks like a rolling upgrade or schema updates or perhaps a weekly backup.
Playbooks are declarative configurations. Ansible Playbooks also orchestrate steps into a simpler task. This automation gets rid of a lot of manually ordered process and allows for an immutable infrastructure.

Our describe-cluster playbook for Cassandra Database Cluster nodes

Creating a complicated playbook is beyond the scope of this article. But let’s create a simple playbook and execute it. This playbook will run the nodetool describecluster on each node.
Here is our playbook that runs Cassandra nodetool describecluster on each Cassandra node in our cluster.

playbooks/descibe-cluster.yml - simple ansible playbook that runs Cassandra nodetool describecluster

---
- hosts: nodes
gather_facts: no
remote_user: vagrant

tasks:

- name: Run NodeTool Describe Cluster command against each Cassandra Cluster node
command: /opt/cassandra/bin/nodetool describecluster

To run this, we use ansible-playbook as follow.

Running describe-cluster playbook

$ ansible-playbook playbooks/describe-cluster.yml --verbose
Between this article and the last, we modified our Vagrantfile quite a bit. It now uses a for loop to create the Cassandra nodes, and it uses ansible provisioning.
Here is our new Vagrantfile with updates:

Complete code listing of Vagrantfile that sets up our DevOps/DBA Cassandra Database Cluster

# -*- mode: ruby -*-
# vi: set ft=ruby :

numCassandraNodes = 3

Vagrant.configure("2") do |config|


config.vm.box = "centos/7"


# Define Cassandra Nodes
(0..numCassandraNodes-1).each do |i|

port_number = i + 4
ip_address = "192.168.50.#{port_number}"
seed_addresses = "192.168.50.4,192.168.50.5,192.168.50.6"
config.vm.define "node#{i}" do |node|
node.vm.network "private_network", ip: ip_address
node.vm.provider "virtualbox" do |vb|
vb.memory = "2048"
vb.cpus = 4
end


node.vm.provision "shell", inline: <<-SHELL

sudo /vagrant/scripts/000-vagrant-provision.sh



sudo /opt/cloudurable/bin/cassandra-cloud -cluster-name test \
-client-address #{ip_address} \
-cluster-address #{ip_address} \
-cluster-seeds #{seed_addresses}

SHELL

node.vm.provision "ansible" do |ansible|
ansible.playbook = "playbooks/ssh-addkey.yml"
end
end
end


# Define Bastion Node
config.vm.define "bastion" do |node|
node.vm.network "private_network", ip: "192.168.50.20"
node.vm.provider "virtualbox" do |vb|
vb.memory = "256"
vb.cpus = 1
end


node.vm.provision "shell", inline: <<-SHELL
yum install -y epel-release
yum update -y
yum install -y ansible

mkdir /home/vagrant/resources
cp -r /vagrant/resources/* /home/vagrant/resources/

mkdir -p ~/resources
cp -r /vagrant/resources/* ~/resources/

mkdir -p /home/vagrant/.ssh/
cp /vagrant/resources/server/certs/* /home/vagrant/.ssh/

sudo /vagrant/scripts/002-hosts.sh

ssh-keyscan node0 node1 node2 >> /home/vagrant/.ssh/known_hosts


mkdir ~/playbooks
cp -r /vagrant/playbooks/* ~/playbooks/
sudo cp /vagrant/resources/home/inventory.ini /etc/ansible/hosts
chown -R vagrant:vagrant /home/vagrant
SHELL


end



#
# View the documentation for the provider you are using for more
# information on available options.

# Define a Vagrant Push strategy for pushing to Atlas. Other push strategies
# such as FTP and Heroku are also available. See the documentation at
# https://docs.vagrantup.com/v2/push/atlas.html for more information.
config.push.define "atlas" do |push|
push.app = "cloudurable/cassandra"
end


end

Conclusion

We set up Ansible for our Cassandra Database Cluster to do automate common DevOps/DBA tasks. We created an ssh key and then set up our instances with this key so we could use sshscp, and ansible. We set up a bastion server with Vagrant. We used ansibleplaybook (ssh-addkey.yml) from Vagrant to install our test cluster key on each server. We ran ansible ping against a single server. We ran ansible ping against many servers (nodes). We set up our local dev machine with ansible.cfg and inventory.ini so we could use ansible commands direct to node0 and nodes. We ran nodetool describecluster against all of the nodes from our dev machine. Lastly, we created a very simple playbook that can run nodetool describecluster. Ansible is a very powerful tool that can help you manage a cluster of Cassandra instances. In later articles, we will use Ansible to create more complex playbooks like backing up Cassandra nodes to S3.

Cassandra Tutorial: Cassandra Cluster DevOps/DBA series

The first tutorial in this Cassandra tutorial series focused on setting up a Cassandra Cluster. The first Cassandra tutorial setting up a Cassandra cluster with Vagrant (also appeared on DZone with some additional content DZone Setting up a Cassandra Cluster with Vagrant. The second article in this series was about setting up SSL for a Cassandra cluster using Vagrant (which also appeared with more content as DZone Setting up a Cassandra Cluster with SSL). The third article in this series was about configuring and using Ansible (building on the first two articles). This article (the 4th) will cover applying the tools and techniques from the first three articles to produce an image (EC2 AMI to be precise) that we can deploy to AWS/EC2. To do this explanation, we will use Packer, Ansible, and the Aws Command Line tools. The AWS command line tools are essential for doing DevOps with AWS.

Check out more information about the Cassandra Database


Cassandra AWS Cluster with CloudFormation, bastion host, Ansible, ssh and the aws-command line

$
0
0
This Cassandra tutorial is useful for developers and DevOps/DBA staff who want to launch a Cassandra cluster in AWS.
The cassandra-image project has been using Vagrant and Ansible to set up a Cassandra Cluster for local testing. Then we used PackerAnsible and EC2. We used Packer to create AWS images in the last tutorial. In this tutorial, we will use CloudFormation to create a VPC, Subnets, security groups and more to launch a Cassandra cluster in EC2 using the AWS AMI image we created with Packer in the last article. The next two tutorials after this one, will set up Cassandra to work in multiple AZs and multiple regions using custom snitches for Cassandra.

Overview

This article covers the following:
  • CloudFormation
  • CloudFormer
  • Setting up VPC, NAT, Subnets, CIDRs, and more
  • AWS command line tools to launch CloudFormations
  • Setting up a bastion server for ssh and ansible in AWS
  • Setting up ansible to tunnel through our bastion server to manage AWS Cassandra instances
  • Using ansible to install Oracle 8 JDK instead of OpenJDK

Getting started

We will create a VPC, subnets, security groups and more. Then we will expand the CloudFormation as we need to set up EC2Snitch and EC2MultiRegionSnitch in later tutorials. We also set up a bastion host in our new public subnet of our new VPC. The bastion host allows us to tunnel ansible commands to our Cassandra or Kafka cluster.

Retrospective - Past Articles in this Cassandra Cluster DevOps/DBA series

The first tutorial in this series was about setting up a Cassandra cluster with Vagrant (also appeared on DZone with some additional content DZone Setting up a Cassandra Cluster with Vagrant. The second tutorial in this series was about setting up SSL for a Cassandra cluster using Vagrant (which also appeared with more content as DZone Setting up a Cassandra Cluster with SSL). The third article in this series was about configuring and using Ansible (building on the first two articles). This last article (the 4th) Cassandra Tutorial: AWS Ansible Packer and the AWS command line covered applying the tools and techniques from the first three articles to produce an image (EC2 AMI to be precise) that we can deploy to AWS/EC2. This article uses that AWS AMI image and deploys it into a VPC that we create with CloudFormation.

Where do you go if you have a problem or get stuck?

We set up a google group for this project and set of articles. If you just can’t get something to work or you are getting an error message, please report it here. Between the mailing list and the github issues, we can support you with quite a few questions and issues. You can also find new articles in this series by following Cloudurable™ at our LinkedIn pageFacebook pageGoogle plus or Twitter.

Creating a simple VPC with one private subnet and one public subnet for our Cassandra Cluster

We describe the process here to create a VPC, but we have a script (CloudFormation template) to save you the trouble.
Recall that an AWS VPC is a virtual private cloud. You can create multiple Amazon VPCs within a region that spans multiple availability zones, which is useful for Amazon Cassandra deploys and Amazon Kafka deploys. A VPC is an isolated area to deploy EC2 instances.
Let’s create a new VPC for our cluster (Kafka or Cassandra). To start things off, you can use the AWS VPC creation wizard. Before you do that, create an elastic IP, which you will use for the NatGateway.
Recall that Amazon EC2 instances launched in a private subnet cannot access the Internet to do updates unless there is a NAT gateway. A NAT is a network address translator. Even if you wanted to update your Cassandra or Kafka EC2 instances with yum install foo, you could not do it because they have no route to the public Internet. AWS provides NAT gateways which are similar to IGW, but unlike IGWs they do not allow incoming traffic, but rather only allow responses to outgoing traffic from your Amazon EC2 instances.
Before we create the NatGateway, we need to create an EIP. First created a new EIP to associate with the new VPC. The wizard will ask you to select a VPC template, pick the one with one private network and one public network. It will ask you for the EIP id for the NatGateway.
Recall that an EIP is an Elastic IP Address which is a public IP address. AWS has a pool of public IP addresses available to rent per region, and an EIP is taken from this pool.
Don’t worry, we did all of this and created a CloudFormation template which we will cover in a bit, you can use the CloudFormation script instead of the Wizard, but we want to describe how we created the CloudFormation template.
When using the VPC wizard, it says it is waiting for a Nat Gateway, but the NatGateway seems to be waiting for a subnet, but it is not. All you need is the EIP to give the VPC wizard; then it creates the NatGateway for you.

Using CloudFormer

After you are done creating something in AWS/EC2 that you want to automate, do the following. Tag all of the resources, VPC, NAT gateway, etc. For this, we used cloudgen=cassandra-test.
Then you want to start up the AWS CloudFormer. To do this in the AWS Console, go to CloudFormation, you should see a wizard, pick the 3rd option down on the home page of CloudFormation (or pick create-stack and choose CloudFormer from templates examples dropdown). Then select the CloudFormer from the list of templates. Run this CloudFormation template to create a CloudFormer. Give a username and password that you will have to use later. Run the CloudFormation for CloudFormer (very meta). After the CloudFormation completes, go to the link provided in the CloudFormation stack output. Enter your username and password. Launch the CloudFormer, then use the filter on cloudgen=cassandra-test.
Walk through the wizard, and it will create a CloudFormation template that you can run (it won’t work, but it gets you 99% of the way there as CloudFormer came out before NatGateways). First time I created it the username and password did not work. I had to pick a shorter password. After you are done creating your CloudFormation stack, then you can shutdown the CloudFormer

CloudFormation template created from CloudFormer

Here is the CloudFormation template that we derived from the above process with some edits to make it more readable.

CloudFormation Template

{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Setup VPC for Cassandra",
"Resources": {
"vpcMain": {
"Type": "AWS::EC2::VPC",
"Properties": {
"CidrBlock": "10.0.0.0/16",
"InstanceTenancy": "default",
"EnableDnsSupport": "true",
"EnableDnsHostnames": "true",
"Tags": [
{
"Key": "cloudgen",
"Value": "cassandra-test"
},
{
"Key": "Name",
"Value": "CassandraTestCluster"
}
]
}
},
"subnetPublic": {
"Type": "AWS::EC2::Subnet",
"Properties": {
"CidrBlock": "10.0.0.0/24",
"AvailabilityZone": "us-west-2a",
"VpcId": {
"Ref": "vpcMain"
},
"Tags": [
{
"Key": "cloudgen",
"Value": "cassandra-test"
},
{
"Key": "Name",
"Value": "Public subnet"
}
]
}
},
"subnetPrivate": {
"Type": "AWS::EC2::Subnet",
"Properties": {
"CidrBlock": "10.0.1.0/24",
"AvailabilityZone": "us-west-2a",
"VpcId": {
"Ref": "vpcMain"
},
"Tags": [
{
"Key": "cloudgen",
"Value": "cassandra-test"
},
{
"Key": "Name",
"Value": "Private subnet"
}
]
}
},
"internetGateway": {
"Type": "AWS::EC2::InternetGateway",
"Properties": {
}
},
"dhcpOptions": {
"Type": "AWS::EC2::DHCPOptions",
"Properties": {
"DomainName": "us-west-2.compute.internal",
"DomainNameServers": [
"AmazonProvidedDNS"
]
}
},
"networkACL": {
"Type": "AWS::EC2::NetworkAcl",
"Properties": {
"VpcId": {
"Ref": "vpcMain"
},
"Tags": [
{
"Key": "cloudgen",
"Value": "cassandra-test"
},
{
"Key": "Name",
"Value": "CassandraTestNACL"
}
]
}
},
"routeTableMain": {
"Type": "AWS::EC2::RouteTable",
"Properties": {
"VpcId": {
"Ref": "vpcMain"
},
"Tags": [
{
"Key": "cloudgen",
"Value": "cassandra-test"
}
]
}
},
"routeTablePublic": {
"Type": "AWS::EC2::RouteTable",
"Properties": {
"VpcId": {
"Ref": "vpcMain"
},
"Tags": [
{
"Key": "cloudgen",
"Value": "cassandra-test"
}
]
}
},
"eipForNatGateway": {
"Type": "AWS::EC2::EIP",
"Properties": {
"Domain": "vpc"
}
},
"natGateway": {
"Type": "AWS::EC2::NatGateway",
"Properties": {
"AllocationId": {
"Fn::GetAtt": [
"eipForNatGateway",
"AllocationId"
]
},
"SubnetId": {
"Ref": "subnetPublic"
}
}
},
"securityGroupDefault": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"GroupDescription": "default VPC security group",
"VpcId": {
"Ref": "vpcMain"
},
"Tags": [
{
"Key": "cloudgen",
"Value": "cassandra-test"
},
{
"Key": "Name",
"Value": "CassandraTestSG"
}
]
}
},
"aclEntryAllowAllEgress": {
"Type": "AWS::EC2::NetworkAclEntry",
"Properties": {
"CidrBlock": "0.0.0.0/0",
"Egress": "true",
"Protocol": "-1",
"RuleAction": "allow",
"RuleNumber": "100",
"NetworkAclId": {
"Ref": "networkACL"
}
}
},
"aclEntryAllowAllIngress": {
"Type": "AWS::EC2::NetworkAclEntry",
"Properties": {
"CidrBlock": "0.0.0.0/0",
"Protocol": "-1",
"RuleAction": "allow",
"RuleNumber": "100",
"NetworkAclId": {
"Ref": "networkACL"
}
}
},
"subnetAclAssociationPublic": {
"Type": "AWS::EC2::SubnetNetworkAclAssociation",
"Properties": {
"NetworkAclId": {
"Ref": "networkACL"
},
"SubnetId": {
"Ref": "subnetPublic"
}
}
},
"subnetAclAssociationPrivate": {
"Type": "AWS::EC2::SubnetNetworkAclAssociation",
"Properties": {
"NetworkAclId": {
"Ref": "networkACL"
},
"SubnetId": {
"Ref": "subnetPrivate"
}
}
},
"vpcGatewayAttachment": {
"Type": "AWS::EC2::VPCGatewayAttachment",
"Properties": {
"VpcId": {
"Ref": "vpcMain"
},
"InternetGatewayId": {
"Ref": "internetGateway"
}
}
},
"subnetRouteTableAssociationPublic": {
"Type": "AWS::EC2::SubnetRouteTableAssociation",
"Properties": {
"RouteTableId": {
"Ref": "routeTablePublic"
},
"SubnetId": {
"Ref": "subnetPublic"
}
}
},
"routeNatGateway": {
"Type": "AWS::EC2::Route",
"Properties": {
"DestinationCidrBlock": "0.0.0.0/0",
"NatGatewayId": {
"Ref": "natGateway"
},
"RouteTableId": {
"Ref": "routeTableMain"
}
}
},
"routeInternetGateway": {
"Type": "AWS::EC2::Route",
"Properties": {
"DestinationCidrBlock": "0.0.0.0/0",
"RouteTableId": {
"Ref": "routeTablePublic"
},
"GatewayId": {
"Ref": "internetGateway"
}
},
"DependsOn": "vpcGatewayAttachment"
},
"vpcDHCPOptionsAssociation": {
"Type": "AWS::EC2::VPCDHCPOptionsAssociation",
"Properties": {
"VpcId": {
"Ref": "vpcMain"
},
"DhcpOptionsId": {
"Ref": "dhcpOptions"
}
}
},
"securityGroupIngressDefault": {
"Type": "AWS::EC2::SecurityGroupIngress",
"Properties": {
"GroupId": {
"Ref": "securityGroupDefault"
},
"IpProtocol": "-1",
"SourceSecurityGroupId": {
"Ref": "securityGroupDefault"
}
}
},
"securityGroupEgressDefault": {
"Type": "AWS::EC2::SecurityGroupEgress",
"Properties": {
"GroupId": {
"Ref": "securityGroupDefault"
},
"IpProtocol": "-1",
"CidrIp": "0.0.0.0/0"
}
}
}
}
We define the following resources in the above CloudFormation which was generated with CloudFormer.
  • vpcMain which is the VPC with CIDR 10.0.0.0/16
  • subnetPublic which is the public Subnet with CIDR 10.0.0.0/24
  • subnetPrivate which is the private Subnet with CIDR 10.0.1.0/24
  • internetGateway of type InternetGateway
  • dhcpOptions of type DHCPOptions
  • networkACL of type NetworkAcl
  • natGateway of type NatGateway
  • routeTableMain of type RouteTable
  • routeTablePublic of type RouteTable
  • eipForNatGateway of type NatGateway
  • securityGroupDefault of type SecurityGroup
The vpcMain which is the AWS VPC which is the VPC we use to deploy instances. The subnetPublic (Subnet) with CIDR 10.0.0.0/24 is a part of a VPC’s IP address range. Just like an AWS VPC you need to specify CIDR blocks for the subnets. Subnets are associated with availability zones (independent power source and network). Subnets can be public or private. A private subnet is one that is not routable from the internetGateway. The subnetPrivate (Subnet) does not have a route to the internetGateway. The internetGateway (InternetGateway) enables traffic from the public Internet to the mainVPC VPC. The internetGateway (IGW) does network address translation from public IPs of EC2 instances to their private IP for incoming traffic. When an EC2 instance sends IP traffic from a public subnet, the IGW acts as the NAT for the public subnet and translates the reply address to the EC2 instance’s public IP (EIP). The IGW keep track of the mappings of EC2 instances private IP address and their public IP address. AWS ensures that the IGW is highly available and handles the horizontal scale, redundancy as needed. The dhcpOptions (DHCPOptions) is associated with with the mainVPC and is used for Dynamic Host Configuration Protocol (DHCP) config and provides a standard for configuring TCP/IP networks. The networkACL(NetworkAcl) - Network ACL Control List (NACL) - is a stateless layer of security for subnets. NACLs act as a stateless firewall. The natGateway (NatGateway) are similar to IGW, but unlike IGWs they do not allow incoming traffic, but rather only allow responses to outgoing traffic from your Amazon EC2 instances. NAT gateways are simple to manage and highly available. The securityGroupDefault (SecurityGroup) provides a stateful firewall that is applied directly to EC2 instance, ELBs and Autoscale group launches. For more details of what the above CloudFormation creates and why see this short guide to VPC or this AWS Cassandra deployment guide.

Using the new CloudFormation

We use the AWS CloudFormation CommandLine to create the VPC, subnets, Network ACL, etc. for our Cassandra Cluster.

Using aws cloudformation command line to create VPC for Cassandra Cluster or Kafka Cluster

#!/usr/bin/env bash
set -e

source bin/ec2-env.sh

aws --region ${REGION} s3 cp cloud-formation/vpc.json s3://$CLOUD_FORMER_BUCKET
aws --region ${REGION} cloudformation create-stack --stack-name ${ENV}-vpc-cassandra \
--template-url "https://s3-us-west-2.amazonaws.com/$CLOUD_FORMER_BUCKET/vpc.json" \

Notice that we upload the CloudFormation template to S3 using the AWS command-line. Then, we call create-stack to run the CloudFormation stack. This is our base VPC setup. We will add to it as we continue.

Modifying ec2-env.sh

We added three more variables to our ec2-env.sh file as follows:

ec2-env.sh - KEY name, aws REGION, name of S3 bucket to store CloudFormation templates

export KEY=KEY_NAME_CASSANDRA
export REGION=us-west-2
export CLOUD_FORMER_BUCKET=cloudurable-cloudformer-templates
You might recall that our ec2-env.sh file specifies security groups id, subnets id, IAM profile name, etc.

Adding outputs to CloudFormation.

We were using the default VPC, but now we want to use the VPC, subnet, etc. that we just created. We could just look that up using the console, but a better way would be to add Outputs to our VPC CloudFormation for our Cassandra Cluster. The CloudFormation Outputs section declares values that can be imported into other CloudFormation stacks or queried by the command line, or just displayed in the AWS CloudFormation console.
CloudFormation templates once deployed are called CloudFormation stacks. CloudFormation stacks can depend on outputs from other CloudFormation stacks.
Here are the updates we make to create output variables from our CloudFormation.

cloud-formation/vpc.json - adding output variables

{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Setup VPC for Cassandra",
"Outputs": {
"subnetPublicOut": {
"Description": "Subnet Public Id",
"Value": {
"Ref": "subnetPublic"
},
"Export": {
"Name": {
"Fn::Sub": "${AWS::StackName}-subnetPublic"
}
}
},
"subnetPrivateOut": {
"Description": "Subnet Private Id",
"Value": {
"Ref": "subnetPrivate"
},
"Export": {
"Name": {
"Fn::Sub": "${AWS::StackName}-subnetPrivate"
}
}
}
},
...
Notice we put our output variable under the key "Outputs" which is a map of output variables.
We define subnetPublicOut and subnetPrivateOut which get exported to ${AWS::StackName}-subnetPublic and ${AWS::StackName}-subnetPrivate

Setting up Bastion Security Group

Next, we need to setup the Security Group for our Bastion Host. A Bastion host will allow us to manage the Cassandra/Kafka EC2 instances from ssh/ansible. It is the bridge to out private subnet where we will keep our Cassandra/Kafka EC2 instances.
The security group needs to open up port 22 as follows.

cloud-formation/vpc.json - Bastion Security Group for Ansible Mgmt of Cassandra Database Nodes

"securityGroupBastion": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"GroupDescription": "Security group for bastion server.",
"VpcId": {
"Ref": "vpcMain"
},
"SecurityGroupIngress": [
{
"IpProtocol": "tcp",
"FromPort": "22",
"ToPort": "22",
"CidrIp": "0.0.0.0/0"
}
],
"SecurityGroupEgress": [
{
"IpProtocol": "-1",
"CidrIp": "0.0.0.0/0"
}
],
"Tags": [
{
"Key": "Name",
"Value": "bastionSecurityGroup"
},
{
"Key": "cloudgen",
"Value": "cassandra-test"
}
]
}
},

Setting up Security Group for Cassandra Nodes

This example will focus on Cassandra nodes, not Kafka, but the ideas are similar. This security group uses the CIDR of the VPC to open up all traffic to all subnets in this VPC.

cloud-formation/vpc.json - Security group for Cassandra Database nodes in Cassandra Cluster

"securityGroupCassandraNodes": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"GroupDescription": "Security group for Cassandra Database nodes in Cassandra Cluster",
"VpcId": {
"Ref": "vpcMain"
},
"SecurityGroupIngress": [
{
"IpProtocol": "-1",
"CidrIp": "10.0.0.0/8"
}
],
"SecurityGroupEgress": [
{
"IpProtocol": "-1",
"CidrIp": "0.0.0.0/0"
}
],
"Tags": [
{
"Key": "Name",
"Value": "cassandraSecurityGroup"
},
{
"Key": "cloudgen",
"Value": "cassandra-test"
}
]
}
}

Output new security groups as CloudFormation outputs.

We will want to add the securityGroupCassandraNodes and securityGroupBastion to the output of the CloudFormation so we can use it from our AWS EC2 scripts.

cloud-formation/vpc.json - output new security groups

{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Setup VPC for Cassandra Cluster for Cassandra Database",

"Outputs": {
"subnetPublicOut": {
"Description": "Subnet Public Id",
"Value": {
"Ref": "subnetPublic"
},
"Export": {
"Name": {
"Fn::Sub": "${AWS::StackName}-subnetPublic"
}
}
},
"subnetPrivateOut": {
"Description": "Subnet Private Id",
"Value": {
"Ref": "subnetPrivate"
},
"Export": {
"Name": {
"Fn::Sub": "${AWS::StackName}-subnetPrivate"
}
}
},
"securityGroupBastionOutput": {
"Description": "Security Group Bastion for managing Cassandra Cluster Nodes with Ansible",
"Value": {
"Ref": "securityGroupBastion"
},
"Export": {
"Name": {
"Fn::Sub": "${AWS::StackName}-securityGroupBastion"
}
}
},
"securityGroupCassandraNodesOutput": {
"Description": "Cassandra Database Node security group for Cassandra Cluster",
"Value": {
"Ref": "securityGroupCassandraNodes"
},
"Export": {
"Name": {
"Fn::Sub": "${AWS::StackName}-securityGroupCassandraNodes"
}
}
}
},
...
Notice that we added securityGroupBastionOutput and securityGroupCassandraNodesOutput to the above CloudFormation.

Cloudurable specialize in AWS DevOps Automation for Cassandra, Spark and Kafka

We hope you find this Cassandra tutorial useful. We also provide Spark consultingCasandra consulting and Kafka consulting to get you setup fast in AWS with CloudFormation and CloudWatch. Support us by checking out our Spark TrainingCasandra training and Kafka training.

Updating CloudFormation

As we iterative develop our CloudFormation, like add new security groups, we do not have to rebuild everything. Instead, we can update the CloudFormation stack. CloudFormation is smart enough to see what has changed and only add/update those areas.

bin/update-vpc-cloudformation.sh

#!/usr/bin/env bash
set -e

source bin/ec2-env.sh

aws --region ${REGION} s3 cp cloud-formation/vpc.json s3://$CLOUD_FORMER_BUCKET
aws --region ${REGION} cloudformation update-stack --stack-name ${ENV}-vpc-cassandra \
--template-url "https://s3-us-west-2.amazonaws.com/$CLOUD_FORMER_BUCKET/vpc.json" \

The above uses the CloudFormation Update Stack to update a stack as specified by the template. After the update stack completes successfully, the stack update starts.
We can see our output variable from our CloudFormation template from the command line as follows.

List the output variables of CloudFormation with the aws command-line


$ aws cloudformation describe-stacks --stack-name dev-vpc-cassandra | jq .Stacks[].Outputs[]

Output
{
"Description": "Subnet Private Id",
"OutputKey": "subnetPrivateOut",
"OutputValue": "subnet-XXe5453a"
}
{
"Description": "Cassandra Database Node security group for Cassandra Cluster",
"OutputKey": "securityGroupCassandraNodesOutput",
"OutputValue": "sg-XX527048"
}
{
"Description": "Subnet Public Id",
"OutputKey": "subnetPublicOut",
"OutputValue": "subnet-XXe5453c"
}
{
"Description": "Security Group Bastion for managing Cassandra Cluster Nodes with Ansible",
"OutputKey": "securityGroupBastionOutput",
"OutputValue": "sg-XX527040"
}

Then we just modify our ec2-env.sh script to use these values. Now we can modify the bin/ec2-env.sh.

env file

#!/bin/bash
set -e


export REGION=us-west-2
export ENV=dev
export KEY_PAIR_NAME="cloudurable-$REGION"
export PEM_FILE="${HOME}/.ssh/${KEY_PAIR_NAME}.pem"
export SUBNET_PUBLIC=subnet-XXe5453a
export SUBNET_PRIVATE=subnet-XXe5453b
export CLOUD_FORMER_S3_BUCKET=cloudurable-cloudformer-templates
export HOSTED_ZONE_ID="XXNXWWXWZXEXHJ-NOT-REAL"



export BASTION_NODE_SIZE=t2.small
export BASTION_SECURITY_GROUP=sg-XX527040
export BASTION_AMI=ami-XXb3310e
export BASTION_EC2_INSTANCE_NAME="bastion.${ENV}.${REGION}"
export BASTION_DNS_NAME="bastion.${ENV}.${REGION}.cloudurable.com."


export CASSANDRA_NODE_SIZE=m4.large
export CASSANDRA_AMI=ami-XXb3310f
export CASSANDRA_SECURITY_GROUP=sg-XX527048
export CASSANDRA_IAM_PROFILE=IAM_PROFILE_CASSANDRA
export CASSANDRA_EC2_INSTANCE_NAME="cassandra-node.${ENV}.${REGION}"
export CASSANDRA_DNS_NAME="node0.${ENV}.${REGION}.cloudurable.com."

Just like a war plan does not survive the first battle, variable names do not survive the first refactor to add a feature. We could also use the CloudFormation outputs as input variables to another CloudFormation as input variables.
CloudFormation is the AWS way to create immutable infrastructure.

Why a Bastion server

bastion host is a computer that is locked down and fully exposed to attack, but in our case the Bastion has a firewall so that only port 22 is open (SSH), and in fact when we only run the bastion host when we want to ssh into our private subnet or run asnbile playbooks. The bastion host is on the public side of the DMZ.

Creating bastion server

We updated the log into server bash script, the associate DNS with IP bash scripts and the get IP address of ec2 instance bash scripts to take arguments and renamed them to work with the bastion EC2 instance and Cassandra Database instances. Then we created a new script called bin/create-ec2-instance-bastion.sh to use the new scripts and the appropriate environment variables.
Here is the create bastion script.

create-ec2-instance-bastion.sh - bastion for ansible and ssh bridge

#!/bin/bash
set -e

source bin/ec2-env.sh

instance_id=$(aws ec2 run-instances --image-id "$BASTION_AMI" --subnet-id "$SUBNET_PUBLIC" \
--instance-type "$BASTION_NODE_SIZE" --iam-instance-profile "Name=$CASSANDRA_IAM_PROFILE" \
--associate-public-ip-address --security-group-ids "$BASTION_SECURITY_GROUP" \
--key-name "$KEY_PAIR_NAME" | jq --raw-output .Instances[].InstanceId)

echo "bastion ${instance_id} is being created"

aws ec2 wait instance-exists --instance-ids "$instance_id"

aws ec2 create-tags --resources "${instance_id}" --tags Key=Name,Value="${BASTION_EC2_INSTANCE_NAME}" \
Key=Role,Value="Bastion" Key=Env,Value="DEV"

echo "${instance_id} was tagged waiting to login"

aws ec2 wait instance-status-ok --instance-ids "$instance_id"


bin/associate-route53-DNS-with-IP.sh ${BASTION_EC2_INSTANCE_NAME} ${BASTION_DNS_NAME}
bin/login-ec2-instance.sh ${BASTION_EC2_INSTANCE_NAME}

If you followed along with the previous tutorials, the above will all make sense. Essentially, we are just launching and EC2 instance using the AMI/image we created with packer from Packer/Ansible/Cassandra Tutorial. Then the script waits for the image to become active, then it associates the bastion DNS name with the IP of this image.
Note since we are launching the Cassandra node in a private subnet, we will not be able to log into it direct any longer. We will have to log into the bastion server first.
Now let’s run this script.

Running bin/create-ec2-instance-bastion.sh - to create ansible bastion AWS ec2 instance

$ bin/create-ec2-instance-bastion.sh
bastion i-069819c22bbd379ab is being created
i-069819c22bbd379ab was tagged waiting to login
IP ADDRESS 55.222.33.66 bastion.dev.us-west-2.cloudurable.com.

{
"Changes":[
{
"Action": "UPSERT",
"ResourceRecordSet": {
"Type": "A",
"Name": "bastion.dev.us-west-2.cloudurable.com.",
"TTL": 300,
"ResourceRecords": [{
"Value": "55.222.33.66"
}]
}
}
]
}

IP ADDRESS 55.222.33.66
ECDSA key fingerprint is SHA256:DzyRqdhPPUlTf8ZPAH6XtGe0SRNthSoMXK4cZCpGGME.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '54.202.31.60' (ECDSA) to the list of known hosts.
...
Let’s also startup the Cassandra Database Node which will be a seed server in the Cassandra Cluster. We do this with the create-ec2-instance-cassandra.sh as follows.

Run bin/create-ec2-instance-cassandra.sh - Create Cassandra instance

 $ bin/create-ec2-instance-cassandra.sh
Cassandra Database: Cassandra Cluster Node i-0602e8b4d75020438 is being created
Cassandra Node i-0602e8b4d75020438 was tagged waiting for status ready
Now we can log into the bastion.

Logging into Bastion running in our public subnet

We will log into Bastion so we can then log into our Cassandra or Kafka nodes.

Using login-ec2-instance.sh

$ bin/login-ec2-instance.sh bastion.dev.us-west-2
centos@ip-10-0-0-220 ~]$
The private IP address of the Cassandra instance is 10.0.1.10. To log into that server, we would first need to log into bastion (private IP 10-0-0-220) as follows.

Logging into Cassandra node from bastion

centos@ip-10-0-0-220 ~]$ ssh -i ~/resources/server/certs/test_rsa ansible@10.0.1.138
[ansible@ip-10-0-1-10 ~]$

SSH Setup for Ansible/SSH to managed Cassandra nodes

Before we setup, let’s make sure we can log into the bastion host.

Try to connect to Cassandra seed node via ssh bastion tunnel

$ ssh -t bastion ssh -t -i /home/ansible/.ssh/test_rsa ansible@10.0.1.10
This trick of ssh bastion tunneling from the cmd line was described Using SSH through a Bastion tunnel.
We modify our ~/.ssh/config to tunnel requests for 10.0.1.10 (our Cassandra node), through the bastion bastion.dev.us-west-2.cloudurable.com.

~/.ssh/config - configure ssh bridge from bastion to main cassandra node

Host *.us-west-2.compute.amazonaws.com
ForwardAgent yes
IdentityFile ~/.ssh/test_rsa
User ansible

Host bastion
Hostname bastion.dev.us-west-2.cloudurable.com
ForwardAgent yes
IdentityFile ~/.ssh/test_rsa
User ansible

Host cassandra.node0
Hostname 10.0.1.10
ForwardAgent yes
IdentityFile ~/.ssh/test_rsa
ProxyCommand ssh bastion -W %h:%p
User ansible

First, we create Host bastion alias that sets up access to bastion.dev.us-west-2.cloudurable.com, your DNS name will vary. Then we use this bastion to tunnel ssh to the Cassandra instance using the ProxyCommand. This ProxyCommand of the ssh client config runs the command ssh bastion -W host:port and then talks to the standard in/out of that command as if the remote connection (specified by -W). The %h is for the hostname and the %p is port.
Now, cassandra.node0 is a bit special because it is a seed server, but other Cassandra Nodes will be a bit nameless so to speak. We want a way to configure them without naming each one, and while we are at for speed we want to use SSH multiplexing.

~/.ssh/config - create the ssh bridge for the rest of the Cassandra nodes

Host 10.0.1.*
ForwardAgent yes
IdentityFile ~/.ssh/test_rsa
ProxyCommand ssh bastion -W %h:%p
User ansible
ControlMaster auto
ControlPath ~/.ssh/ansible-%r@%h:%p
ControlPersist 5m

Ideas for this setup came from the running ansible through SSH bastion host on Scott’s WebLog.
We make the following changes to get ansible to work through bastion.
We add this workaround to ansible.cfg.

ansible.cfg -

...
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=30m
control_path = %(directory)s/%%h-%%p-%%r

Then we setup our inventory file with our new ssh aliases that we defined in ~/.ssh/config earlier.

inventory.ini -

[cassandra-nodes]
cassandra.node0


[bastion]
bastion

Side Note: Local to project ssh config so we can check it in.

If we use the -F parameter as an ssh_args in ansible.cfg/ssh_connection then we can specify . Now we can keep all these files in source control by adding this [ssh_connection] \n ssh_args = -F ssh/ssh.config -o ControlMaster=auto -o ControlPersist=30m to ansible.cfg in the project directory of this Ansible/Cassandra tutorial. This is another trick from blog post: running ansible through a bastion
The next thing we want to do is ping our servers with ansible just to show that it is working.

Running ansible commands via a bastion server


$ ansible cassandra.node0 -m ping

cassandra.node0 | SUCCESS => {
"changed": false,
"ping": "pong"
}

$ ansible bastion -m ping

bastion | SUCCESS => {
"changed": false,
"ping": "pong"
}


Installing more than one node to the cluster

To automate the configuration of the Cassandra instances, we will use Cassandra Cloud.
CassandraCloud is a tool that helps you configure Cassandra for clustered environments. It works well in DockerAWSMesosEC2, and VirtualBox environments (and similar environments). It allows you to configure Cassandra easily. For example, it could be kicked off as a USER_DATA script in Amazon EC2 (AWS EC2). CassandraCloud usually runs once when an instance is first launched and then never again. CassandraCloud allows you to override values via OS ENVIRONMENT variables. There is an HCL config file, and there are command line arguments. The HCL config file can be overridden with ENVIRONMENT which can be overridden with command line arguments.CassandraCloud will generate ${CASSANDRA_HOME}/conf/cassandra.yaml file. You can specify a custom template (usually found in ${CASSANDRA_HOME}/conf/cassandra-yaml.template).
Here is the EC2 user-data script where we invoke Cassandra Cloud.

resources/user-data/cassandra - AWS EC2 User-Data for Cassandra Database Node

#!/bin/bash
set -e

export BIND_IP=`curl http://169.254.169.254/latest/meta-data/local-ipv4`

/opt/cloudurable/bin/cassandra-cloud -cluster-name test \
-client-address ${BIND_IP} \
-cluster-address ${BIND_IP} \
-cluster-seeds 10.0.1.10

/bin/systemctl restart cassandra

Notice we are passing the client BIND_IP which we get form the EC2 meta-data.
We also added an extra param to bin/create-ec2-instance-cassandra.sh so we can pin a deployment to a certain IP in the CIDR range of our AWS VPC private subnet.

bin/create-ec2-instance-cassandra.sh -

#!/bin/bash
set -e

source bin/ec2-env.sh

if [ -z "$1" ]
then
PRIVATE_IP_ADDRESS=10.0.1.10
else
PRIVATE_IP_ADDRESS=$1
fi

instance_id=$(aws ec2 run-instances --image-id "$CASSANDRA_AMI" --subnet-id "$SUBNET_PRIVATE" \
--instance-type ${CASSANDRA_NODE_SIZE} --private-ip-address ${PRIVATE_IP_ADDRESS} \
--iam-instance-profile "Name=$CASSANDRA_IAM_PROFILE" \
--security-group-ids "$CASSANDRA_SECURITY_GROUP" \
--user-data file://resources/user-data/cassandra \
--key-name "$KEY_PAIR_NAME" | jq --raw-output .Instances[].InstanceId)

echo "Cassandra Database: Cassandra Cluster Node ${instance_id} is being created"

aws ec2 wait instance-exists --instance-ids "$instance_id"

aws ec2 create-tags --resources "${instance_id}" --tags Key=Name,Value="${CASSANDRA_EC2_INSTANCE_NAME}" \
Key=Cluster,Value="Cassandra" Key=Role,Value="Cassandra_Database_Cluster_Node" Key=Env,Value="DEV"

echo "Cassandra Node ${instance_id} was tagged waiting for status ready"

aws ec2 wait instance-status-ok --instance-ids "$instance_id"

If you run it with no IP address, then it creates the Cassandra Seed Node EC2 instance. If you run it with an IP address, then it creates and instance with that private IP. (Note that the IP must be in the range of the private subnet CIDR that we created earlier.)
Let’s use this to create a second instance.

Running bin/create-ec2-instance-cassandra.sh 10.0.1.11

$ bin/create-ec2-instance-cassandra.sh 10.0.1.11
Cassandra Database: Cassandra Cluster Node i-0e9939e9f62ae33d4 is being created
Cassandra Node i-0e9939e9f62ae33d4 was tagged waiting for status ready
Now we need to make sure it is working. We can do this by ssh-ing into cassandra.node0 and checking the status and describing the cassandra cluster with nodetool.

Check Cassandra node status with nodetool

$ ssh cassandra.node0
...
$ /opt/cassandra/bin/nodetool status
Datacenter: datacenter1
=======================
Status=Up/Down
|/ State=Normal/Leaving/Joining/Moving
-- Address Load Tokens Owns (effective) Host ID Rack
UN 10.0.1.10 132.06 KiB 32 100.0% 95794596-7dbe-4ec9-8c35-f4f49a5bb999 rack1
UN 10.0.1.11 94.97 KiB 32 100.0% eb35bb65-c582-4fa9-9069-fd5222830c99 rack1
Notice that both 10.0.1.10 (the seed server), and 10.0.1.11 are seen. We will setup a seed server per availability zone.
Let’s also see the Cassandra cluster with nodetool status.

Check Cassandra node status with nodetool

[ansible@ip-10-0-1-10 ~]$ /opt/cassandra/bin/nodetool describecluster
Cluster Information:
Name: test
Snitch: org.apache.cassandra.locator.DynamicEndpointSnitch
Partitioner: org.apache.cassandra.dht.Murmur3Partitioner
Schema versions:
86afa796-d883-3932-aa73-6b017cef0d19: [10.0.1.10, 10.0.1.11]


Up next

In the next article we will use Ec2Snitch and setup a second subnet and availability zone.

Bonus lap using an ansible playbook to install Oracle JDK 8

A big mandate came down from the corporate home office, switch all instances from OpenJDK to the Oracle JDK. We protest that we ran benchmarks and burn-ins with the OpenJDK and there is no difference. The home office has silenced our pleas. No remorse.
We are using the OpenJDK and Cassandra gives warning messages not to. Let’s use Ansible to fix that. We will use a ansible playbook for installing jdk-8 on CentOS.
First let’s add our extra Cassandra node 10.0.1.11 to the list of cassandra-nodes that we are managing.

inventory.ini - add new Cassandra node to file

[cassandra-nodes]
cassandra.node0
10.0.1.11

...
Then let’s create an ansible playbook that installs the Oracle JDK on our Cassandra nodes.

playbooks-oracle-8.jdk.yml

---
- hosts: cassandra-nodes
gather_facts: no
become: true
remote_user: ansible
vars:
download_url: http://download.oracle.com/otn-pub/java/jdk/8u121-b13/e9e7ea248e2c4826b92b3f075a80e441/jdk-8u121-linux-x64.tar.gz
download_folder: /opt
java_name: "{{download_folder}}/jdk1.8.0_121"
java_archive: "{{download_folder}}/jdk-8u121-linux-x64.tar.gz"


tasks:
- name: Download Java
command: "curl -L -b 'oraclelicense=a' {{download_url}} -o {{java_archive}} creates={{java_archive}}"

- name: Unpack archive
command: "tar -zxf {{java_archive}} -C {{download_folder}} creates={{java_name}}"

- name: Fix ownership
file: state=directory path={{java_name}} owner=root group=root recurse=yes

- name: Remove previous
command: 'alternatives --remove "java" /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.121-0.b13.el7_3.x86_64/jre/bin/java'

- name: Make Java available for system
command: 'alternatives --install "/usr/bin/java""java""{{java_name}}/bin/java" 2000'

- name: Clean up
file: state=absent path={{java_archive}}

Next up we just need to run the playbook.

Running playbook

$ ansible-playbook playbooks/install-oracle-8-jdk.yml

PLAY [cassandra-nodes] *********************************************************

TASK [Download Java] ***********************************************************
changed: [cassandra.node0]
[WARNING]: Consider using get_url or uri module rather than running curl

changed: [10.0.1.11]

TASK [Unpack archive] **********************************************************
changed: [cassandra.node0]
[WARNING]: Consider using unarchive module rather than running tar

changed: [10.0.1.11]

TASK [Fix ownership] ***********************************************************
changed: [10.0.1.11]
changed: [cassandra.node0]

TASK [Remove previous] *********************************************************
changed: [cassandra.node0]
changed: [10.0.1.11]

TASK [Make Java available for system] ******************************************
changed: [10.0.1.11]
changed: [cassandra.node0]

TASK [Clean up] ****************************************************************
changed: [cassandra.node0]
changed: [10.0.1.11]

PLAY RECAP *********************************************************************
10.0.1.11 : ok=6 changed=6 unreachable=0 failed=0
cassandra.node0 : ok=6 changed=6 unreachable=0 failed=0

Now imagine that we did not just have two servers but 50. This playbook is a lot nicer.

Conclusion

We used CloudFormer to create a starter CloudFormation template which created an AWS VPC, NAT, Subnets, InternetGateway, CIDRs, and more. Then we used AWS command line tools to launch CloudFormations as a stack. Then we added some additional security groups. Then we used AWS command line tools to update the CloudFormations stack that we set up earlier. We set up a bastion host which allows us to tunnel ansible commands. We then used the AWS command line. We then set up a Cassandra cluster using Cassandra Cloud, EC2 USER DATA script, and EC2 instance meta-data to generate Cassandra YAML config. Then as a bonus lap, we used ansible to run a playbook to replace our OpenJDK usage with the Oracle JDK.

More about Cloudurable™

Cloudurable is focused on AWS DevOps automation for Cassandra and Kafka. We also focus on supporting the SMACK stack in AWS. All of our training and mentoring has a special focus on AWS deployments. Our Apache Spark course, for example, covers running Spark on EMR and using Spark SQL with data from S3 and DynamoDB as well as using Spark Streaming with Kinesis. We also cover how to use Cassandra from Spark (for example).
Consulting
Training

Resources

Radom thoughts by Rick Hightower: DR and multi-region

$
0
0
DR based on region is silly for most apps and services. It is an expensive bet.

Multi-region is great for reducing latency for sure and DR for sure, but multi-region hot standbys is silly for most apps.

Mutli-AZs deployments are enough for DR IMO for 99% of use cases. 
If your app/service can survive a single-AZ outage, it is better than 99.999% of apps out there. 

I am not saying to not do multi-region deploys (hot standbys), but merely that it has a cost, and your app may not need it. 


If you have a regular backup and a way to restore from another region, you are ahead of the game.
  • frequent EBS snapshots sent to another region, 
  • back things up to S3, replicate S3 bucket to S3 bucket, 
  • read replicas for DBs in another region if you must


For many services and applications, you don’t have to run a hot standby if you are spread across three AZs. 
Focus on surviving a single AZ failure. Get that right. Then focus on how to recover in another region from backups:

  • snapshot, AMIs, etc. ready to go, ready to be spun up, 
  • backups to S3 with S3 bucket replication. Cheap and easy.

If all hell breaks loose, and it takes you 15 minutes to 1 hour to spin up in a new region that is a lot cheaper than running hot-standby in a second region 24/7 365 days a year. The probability of a complete region failure and the cost to your business being down for 15 minutes to an hour vs. the cost of running a second set of servers all of the time. 

Engineers love to over engineer (especially bad ones). Hot standbys are expensive. Unless you need to run in multiple regions to reduce latency. 

If CA falls into the ocean, no one is going to care if your app serving virtual tractors is down for a few hours. 
If Ohio is nuked, and your app is down for an hour, no one will care that they saw the same ad twice.
We can serve a default ad without personalization for an hour. 


Reakt Kafka Example

$
0
0

Reakt Kafka Example

Reakt-Kakfa adapts Kafka to Reakt promises and streams.
Reakt has promise libraries for Vert.x, Netty, Guava, and Cassandra.

Using Promises with Kafka Producers

finalAsyncProducer<Long, String> producer =newAsyncProducer<>(createProducer());
...
producer.send(TOPIC, key, value)
.catchError(throwable -> {
System.err.println("Trouble sending record "+ throwable.getLocalizedMessage());
throwable.printStackTrace(System.err);
})
.then(recordMetadata -> {
System.out.printf("%d %d %s \n", recordMetadata.offset(),
recordMetadata.partition(), recordMetadata.topic());
}).invoke();

Using Streams with Kafka Consumers

finalStreamConsumer<Long, String> stream =StreamConsumer.subscribe(createConsumer(), TOPIC, result -> {
result.then(consumerRecords -> {
System.out.println("Got message "+ consumerRecords.count());
consumerRecords.forEach(record -> {
countDownLatch.countDown();
});
result.request(1); //calls commitAsync
}).catchError(throwable -> {
System.err.println("Trouble Getting record "+ throwable.getLocalizedMessage());
throwable.printStackTrace(System.err);
result.cancel();
});
});

stream.close();

Full integration test and example for Reakt Kafka

packageio.advantageous.reakt.kafka;

importorg.apache.kafka.clients.consumer.Consumer;
importorg.apache.kafka.clients.consumer.ConsumerConfig;
importorg.apache.kafka.clients.consumer.KafkaConsumer;
importorg.apache.kafka.clients.producer.KafkaProducer;
importorg.apache.kafka.clients.producer.Producer;
importorg.apache.kafka.clients.producer.ProducerConfig;
importorg.apache.kafka.common.serialization.LongDeserializer;
importorg.apache.kafka.common.serialization.LongSerializer;
importorg.apache.kafka.common.serialization.StringDeserializer;
importorg.apache.kafka.common.serialization.StringSerializer;
importorg.junit.Test;

importjava.util.Properties;
importjava.util.concurrent.CountDownLatch;
importjava.util.concurrent.ExecutorService;
importjava.util.concurrent.Executors;
importjava.util.concurrent.TimeUnit;

import staticorg.junit.Assert.assertEquals;

publicclassIntegrationTest {

privatefinalstaticStringTOPIC="my-test-topic";
privatefinalstaticStringBOOTSTRAP_SERVERS="localhost:9092";
privatefinalstaticintSEND_RECORD_COUNT=10_000;


@Test
publicvoidtest() throwsException {

finalKafkaServer kafkaServer =newKafkaServer();
System.out.println("Starting server");
Thread.sleep(10_000);

finalExecutorService executorService =Executors.newSingleThreadExecutor();

finalAsyncProducer<Long, String> producer =newAsyncProducer<>(createProducer());

executorService.execute(() -> {
for (int i =0; i <SEND_RECORD_COUNT; i++) {
if (i %1000==0) System.out.println("Sending message "+ i);
producer.send(TOPIC, 1L* i, "value "+ i)
.catchError(throwable -> {
System.err.println("Trouble sending record "+ throwable.getLocalizedMessage());
throwable.printStackTrace(System.err);
})
.then(recordMetadata -> {
if (recordMetadata.offset() %1000==0)
System.out.printf("%d %d %s \n", recordMetadata.offset(),
recordMetadata.partition(), recordMetadata.topic());
})
.invoke();
}
producer.flush();
});


finalCountDownLatch countDownLatch =newCountDownLatch(SEND_RECORD_COUNT);

finalStreamConsumer<Long, String> stream =StreamConsumer.subscribe(createConsumer(), TOPIC, result -> {
result.then(consumerRecords -> {
System.out.println("Got message "+ consumerRecords.count());
consumerRecords.forEach(record -> {
countDownLatch.countDown();
});
result.request(1);
}).catchError(throwable -> {
System.err.println("Trouble Getting record "+ throwable.getLocalizedMessage());
throwable.printStackTrace(System.err);
result.cancel();
});
});

Thread.sleep(3_000);

countDownLatch.await(10, TimeUnit.SECONDS);
assertEquals(0, countDownLatch.getCount());
stream.close();
producer.close();
executorService.shutdown();

Thread.sleep(3_000);
kafkaServer.shutdown();
Thread.sleep(3_000);

}


privatestaticProducer<Long, String>createProducer() {
Properties props =newProperties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
props.put(ProducerConfig.CLIENT_ID_CONFIG, "KafkaExampleProducer");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, LongSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

returnnewKafkaProducer<>(props);
}

privatestaticConsumer<Long, String>createConsumer() {
Properties props =newProperties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, BOOTSTRAP_SERVERS);
props.put(ConsumerConfig.GROUP_ID_CONFIG, "KafkaExampleConsumer");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
LongDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class.getName());

props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 1000);
returnnewKafkaConsumer<>(props);
}
}
Notice that we use an embedded version of Kafka.

Kafka embedded

packageio.advantageous.reakt.kafka;

importorg.apache.zookeeper.server.ServerConfig;
importorg.apache.zookeeper.server.ZooKeeperServerMain;
importorg.apache.zookeeper.server.quorum.QuorumPeerConfig;

importjava.io.IOException;
importjava.util.Properties;

importkafka.server.KafkaConfig;
importkafka.server.KafkaServerStartable;

publicclassKafkaServer {

privatefinalZooKeeperServerMain zooKeeperServer;
privatefinalKafkaServerStartable kafkaServer;

publicKafkaServer() {

finalProperties zkProperties =newProperties();
finalProperties kafkaProperties =newProperties();
try {
//load properties
kafkaProperties.load(Class.class.getResourceAsStream("/io/advantageous/reakt/kafka/kafka.properties"));
zkProperties.load(Class.class.getResourceAsStream("/io/advantageous/reakt/kafka/zookeeper.properties"));
} catch (Exception e){
thrownewRuntimeException(e);
}


QuorumPeerConfig quorumConfiguration =newQuorumPeerConfig();
try {
quorumConfiguration.parseProperties(zkProperties);
} catch(Exception e) {
thrownewRuntimeException(e);
}

zooKeeperServer =newZooKeeperServerMain();
finalServerConfig configuration =newServerConfig();
configuration.readFrom(quorumConfiguration);


newThread(() -> {
try {
zooKeeperServer.runFromConfig(configuration);
} catch (IOException e) {
e.printStackTrace(System.err);
}
}).start();



//start local kafka broker
kafkaServer =newKafkaServerStartable(newKafkaConfig(kafkaProperties));
kafkaServer.startup();


}

publicvoidshutdown() {
kafkaServer.shutdown();
}

publicstaticvoidmain(String[] args) {
newKafkaServer();
}
}

Apache Avro Tutorial

$
0
0

Apache Avro Tutorial

Apache Avro™ is a data serialization system. Avro provides data structures, binary data format, container file for storing persistent data and provides RPC capabilities. Avro does not require code generation to use. Avro is polyglot like you would expect and integrates well with JavaScript, Python, Ruby, C, C#, C++ and Java. Avro gets used in the Hadoop ecosystem as well as by Kafka.
Avro is similar to Thrift, Protocol Buffers, JSON, etc. Unlike Thrift and Protocol Buf, Avro does not require code generation. Avro needs less encoding as part of the data since it stores names and types in the schema reducing duplication. Avro supports the evolution of schemas. Kafka uses Avro with its Schema Registry

Avro Schema

Avro data format (wire format and file format) is defined by Avro schemas. When deserializing data, the schema is used. Data is serialized based on the schema, and schema is sent with data or in the case of files stored with the data. Avro data plus schema is fully self-describing data format.
When Avro files store data it also stores schema. Avro RPC is also based on schema, and IDL. Part of the RPC protocol exchanges schemas as part of the handshake. Avro schemas and IDL are written in JSON.
Let’s take a look at an example Avro schema.

./src/main/avro/com/cloudurable/phonebook/Employee.avsc

Example schema for an Employee record

{"namespace": "com.cloudurable.phonebook",

"type": "record",
 "name": "Employee",

"fields": [

{"name": "firstName", "type": "string"},

{"name": "lastName", "type": "string"},

{"name": "age", "type": "int"},

{"name": "phoneNumber", "type": "string"}

]
}

Avro schema generation tools

Avro comes with a set of tools for generating Java classes for Avro types that you define in Avro schema. There are plugins for Maven and Gradle to generate code based on Avro schemas.
This gradle-avro-plugin is a Gradle plugin that uses Avro tools to do Java code generation for Apache Avro. This plugin supports Avro schema files (.avsc), and Avro RPC IDL (.avdl). For Kafka you only need avsc schema files.

build.gradle - example using gradle-avro-plugin

plugins {
id "com.commercehub.gradle.plugin.avro" version "0.9.0"
}

group 'cloudurable'
version '1.0-SNAPSHOT'
apply plugin: 'java'
sourceCompatibility = 1.8

dependencies {
compile "org.apache.avro:avro:1.8.1"
testCompile group: 'junit', name: 'junit', version: '4.11'
}

repositories {
jcenter()
mavenCentral()
}

avro {
createSetters = false
fieldVisibility = "PRIVATE"
}
Notice that we did not generate setter methods, and we made the fields private. This makes the instances somewhat immutable.
Running gradle build will generate the Employee.java.

./build/generated-main-avro-java/com/cloudurable/phonebook/Employee.java

Generated Avro code


package com.cloudurable.phonebook;

import org.apache.avro.specific.SpecificData;

@SuppressWarnings("all")
@org.apache.avro.specific.AvroGenerated
public class Employee extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord {
private static final long serialVersionUID = -6112285611684054927L;
public static final org.apache.avro.Schema SCHEMA$ = new
org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":\"Employee\"...");
public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; }
private java.lang.String firstName;
private java.lang.String lastName;
private int age;
private java.lang.String phoneNumber;
...

The gradle plugin calls the Avro utilities which generates the files and puts them under build/generated-main-avro-java
Let’s use the generated class as follows to construct an Employee instance.

Using the new Employee class

Employee bob = Employee.newBuilder().setAge(35)
.setFirstName("Bob")
.setLastName("Jones")
.setPhoneNumber("555-555-1212")
.build();

assertEquals("Bob", bob.getFirstName());

The Employee class has a constructor and has a builder. We can use the builder to build a new Employee instance.
Next we want to write the Employees to disk.

Writing a list of employees to an Avro file

final List<Employee> employeeList = ...
final DatumWriter<Employee> datumWriter = new SpecificDatumWriter<>(Employee.class);
final DataFileWriter<Employee> dataFileWriter = new DataFileWriter<>(datumWriter);

try {
dataFileWriter.create(employeeList.get(0).getSchema(),
new File("employees.avro"));
employeeList.forEach(employee -> {
try {
dataFileWriter.append(employee);
} catch (IOException e) {
throw new RuntimeException(e);
}

});
} finally {
dataFileWriter.close();
}

The above shoes serializing an Employee list to disk. In Kafka, we will not be writing to disk directly. We are just showing how so you have a way to test Avro serialization, which is helpful when debugging schema incompatibilities. Note we create a DatumWriter, which converts Java instance into an in-memory serialized format. SpecificDatumWriter is used with generated classes like Employee.DataFileWriter writes the serialized records to the employee.avro file.
Now let’s demonstrate how to read data from an Avro file.

Reading a list of employees from an avro file

final File file = new File("employees.avro");
final List<Employee> employeeList = new ArrayList<>();
final DatumReader<Employee> empReader = new SpecificDatumReader<>(Employee.class);
final DataFileReader<Employee> dataFileReader = new DataFileReader<>(file, empReader);

while (dataFileReader.hasNext()) {
employeeList.add(dataFileReader.next(new Employee()));
}

The above deserializes employees from the employees.avro file into a java.util.List of Employee instances. Deserializing is similar to serializing but in reverse. We create a SpecificDatumReader to converts in-memory serialized items into instances of our generated Employee class. The DatumReader reads records from the file by calling next. Another way to read is using forEach as follows:

Reading a list of employees from an avro file using forEach

final DataFileReader<Employee> dataFileReader = new DataFileReader<>(file, empReader);

dataFileReader.forEach(employeeList::add);
You can use a GenericRecord instead of generating an Employee class as follows.

Using GenericRecord to create an Employee record

final String schemaLoc = "src/main/avro/com/cloudurable/phonebook/Employee.avsc";
final File schemaFile = new File(schemaLoc);
final Schema schema = new Schema.Parser().parse(schemaFile);

GenericRecord bob = new GenericData.Record(schema);
bob.put("firstName", "Bob");
bob.put("lastName", "Smith");
bob.put("age", 35);
assertEquals("Bob", bob.get("firstName"));
You can write to Avro files using GenericRecords as well.

Writing GenericRecords to an Avro file

final List<GenericRecord> employeeList = new ArrayList<>();


final DatumWriter<GenericRecord> datumWriter = new GenericDatumWriter<>(schema);
final DataFileWriter<GenericRecord> dataFileWriter = new DataFileWriter<>(datumWriter);

try {
dataFileWriter.create(employeeList.get(0).getSchema(),
new File("employees2.avro"));
employeeList.forEach(employee -> {
try {
dataFileWriter.append(employee);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
} finally {
dataFileWriter.close();
}
You can read from Avro files using GenericRecords as well.

Reading GenericRecords from an Avro file

final File file = new File("employees2.avro");
final List<GenericRecord> employeeList = new ArrayList<>();
final DatumReader<GenericRecord> empReader = new GenericDatumReader<>();
final DataFileReader<GenericRecord> dataFileReader = new DataFileReader<>(file, empReader);

while (dataFileReader.hasNext()) {
employeeList.add(dataFileReader.next(null));
}

employeeList.forEach(System.out::println);

Avro will validate the data types when it serializes and deserializes the data.

Using the wrong type

GenericRecord employee = new GenericData.Record(schema);
employee.put("firstName", "Bob" + index);
employee.put("lastName", "Smith"+ index);
//employee.put("age", index % 35 + 25);
employee.put("age", "OLD");

Stack trace from above


org.apache.avro.file.DataFileWriter$AppendWriteException: java.lang.ClassCastException:
java.lang.String cannot be cast to java.lang.Number

at org.apache.avro.file.DataFileWriter.append(DataFileWriter.java:308)
at com.cloudurable.phonebook.EmployeeTestNoGen.lambda$testWrite$1(EmployeeTestNoGen.java:71)
at java.util.ArrayList.forEach(ArrayList.java:1249)
at com.cloudurable.phonebook.EmployeeTestNoGen.testWrite(EmployeeTestNoGen.java:69)
...
Caused by: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
at org.apache.avro.generic.GenericDatumWriter.writeWithoutConversion(GenericDatumWriter.java:117)
at org.apache.avro.generic.GenericDatumWriter.write(GenericDatumWriter.java:73)
at org.apache.avro.generic.GenericDatumWriter.writeField(GenericDatumWriter.java:153)
at org.apache.avro.generic.GenericDatumWriter.writeRecord(GenericDatumWriter.java:143)
at org.apache.avro.generic.GenericDatumWriter.writeWithoutConversion(GenericDatumWriter.java:105)
at org.apache.avro.generic.GenericDatumWriter.write(GenericDatumWriter.java:73)
at org.apache.avro.generic.GenericDatumWriter.write(GenericDatumWriter.java:60)
at org.apache.avro.file.DataFileWriter.append(DataFileWriter.java:302)
If you left out a required field like firstName, then you would get this.

Stack trace from leaving out firstName

Caused by: java.lang.NullPointerException: null of string in field firstName of com.cloudurable.phonebook.Employee
at org.apache.avro.generic.GenericDatumWriter.npe(GenericDatumWriter.java:132)
at org.apache.avro.generic.GenericDatumWriter.writeWithoutConversion(GenericDatumWriter.java:126)
at org.apache.avro.generic.GenericDatumWriter.write(GenericDatumWriter.java:73)
at org.apache.avro.generic.GenericDatumWriter.write(GenericDatumWriter.java:60)

The Avro schema and IDL specification document describes all of the supported types.


With Avro schema, you can define records, arrays, enums, unions, maps and you can use primitive types like Strings, Int, Boolean, Decimal, Timestamp, Date, and more.
Next, Let’s add to the Employee schema and show some of the different types that Avro supports.
Employe Schema
 {"namespace": "com.cloudurable.phonebook",
"type": "record",
"name": "Employee",
"fields": [
{"name": "firstName", "type": "string"},
{"name": "nickName", "type": ["null", "string"], "default" : null},
{"name": "lastName", "type": "string"},
{"name": "age", "type": "int"},
{"name": "emails", "default":[], "type":{"type": "array", "items": "string"}},
{"name": "phoneNumber", "type":
[ "null",
{ "type": "record", "name": "PhoneNumber",
"fields": [
{"name": "areaCode", "type": "string"},
{"name": "countryCode", "type": "string", "default" : ""},
{"name": "prefix", "type": "string"},
{"name": "number", "type": "string"}
]
}
]
},
{"name":"status", "default" :"SALARY", "type": { "type": "enum", "name": "Status",
"symbols" : ["RETIRED", "SALARY", "HOURLY", "PART_TIME"]}
}
]
}
The Employee schema uses default values, arrays, primitive types, Records within records, enums, and more. It also uses a Union type to represent a value that is optional.

What follows are some classes that are generated from the above schema. 

PhoneNumber record


package com.cloudurable.phonebook;

import org.apache.avro.specific.SpecificData;

@SuppressWarnings("all")
@org.apache.avro.specific.AvroGenerated
public class PhoneNumber extends org.apache.avro.specific.SpecificRecordBase ...{
private static final long serialVersionUID = -3138777939618426199L;
public static final org.apache.avro.Schema SCHEMA$ =
new org.apache.avro.Schema.Parser().parse("{\"type\":\"record\",\"name\":...
public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; }
private java.lang.String areaCode;
private java.lang.String countryCode;
private java.lang.String prefix;
private java.lang.String number;

Status enum

package com.cloudurable.phonebook;
@SuppressWarnings("all")
@org.apache.avro.specific.AvroGenerated
public enum Status {
RETIRED, SALARY, HOURLY, PART_TIME ;
...

Conclusiton
Avro provides fast data serialization. It supports data structures like Supports Records, Maps, Array, and basic types. You can use it directly or use Code Generation. Avro allows schema support to Kafka which we will demonstrate in another article. Kafka uses Avro with its Schema Registry. 
Enjoy this slide deck about Avro or this SlideShare by Jean-Paul on Avro/Kafka.  If you like this article check out my friends Kafka training course.

Kafka Tutorial: Kafka Clustering, Consumer Failover, Broke Failover

$
0
0
This tutorial covers Kafka clustering and replicated topic. It demonstrates consumer failover and broker failover. It also demonstrates load balancing Kafka consumers. The article shows how, with many groups, Kafka acts like a Publish/Subscribe MOM. But, when you put all of our consumers in the same group, Kafka will load share the messages to the consumers in the same group like a MOM queue. This Kafka tutorial demonstrates how Kafka consumer failover and Kafka broker failover.  


Reposted with permission.


f you are not sure what Kafka is, start here “What is Kafka?”.

Getting started with Kafka cluster tutorial

Understanding Kafka Failover

This Kafka tutorial picks up right where the first Kafka tutorial from the command line left off. The first tutorial has instructions on how to run ZooKeeper and use Kafka utils.
In this tutorial, we are going to run many Kafka Nodes on our development laptop so that you will need at least 16 GB of RAM for local dev machine. You can run just two servers if you have less memory than 16 GB. We are going to create a replicated topic. We then demonstrate consumer failover and broker failover. We also demonstrate load balancing Kafka consumers. We show how, with many groups, Kafka acts like a Publish/Subscribe. But, when we put all of our consumers in the same group, Kafka will load share the messages to the consumers in the same group (more like a queue than a topic in a traditional MOM sense).
If not already running, then start up ZooKeeper (./run-zookeeper.sh from the first tutorial). Also, shut down Kafka from the first tutorial.
Next, you need to copy server properties for three brokers (detailed instructions to follow). Then we will modify these Kafka server properties to add unique Kafka ports, Kafka log locations, and unique Broker ids. Then we will create three scripts to start these servers up using these properties, and then start the servers. Lastly, we create replicated topic and use it to demonstrate Kafka consumer failover, and Kafka broker failover.

Create three new Kafka server-n.properties files

In this section, we will copy the existing Kafka server.properties to server-0.propertiesserver-1.properties, and server-2.properties. Then we change server-0.properties to set log.dirs to “./logs/kafka-0. Then we modify server-1.properties to set port to 9093, broker id to 1, and log.dirs to “./logs/kafka-1”. Lastly modify server-2.propertiesto use port 9094, broker id 2, and log.dirs “./logs/kafka-2”.

Copy server properties file

$ ~/kafka-training
$ mkdir -p lab2/config
$ cp kafka/config/server.properties kafka/lab2/config/server-0.properties
$ cp kafka/config/server.properties kafka/lab2/config/server-1.properties
$ cp kafka/config/server.properties kafka/lab2/config/server-2.properties

With your favorite text editor change server-0.properties so that log.dirs is set to ./logs/kafka-0. Leave the rest of the file the same. Make sure log.dirs is only defined once.

~/kafka-training/lab2/config/server-0.properties

broker.id=0
port=9092
log.dirs=./logs/kafka-0
...

With your favorite text editor change log.dirsbroker.id and and log.dirs of server-1.properties as follows.

~/kafka-training/lab2/config/server-1.properties

broker.id=1
port=9093
log.dirs=./logs/kafka-1
...

With your favorite text editor change log.dirsbroker.id and and log.dirs of server-2.properties as follows.

~/kafka-training/lab2/config/server-2.properties

broker.id=2
port=9094
log.dirs=./logs/kafka-2
...

Create Startup scripts for three Kafka servers

The startup scripts will just run kafka-server-start.sh with the corresponding properties file.

~/kafka-training/lab2/start-1st-server.sh

#!/usr/bin/env bash
CONFIG=`pwd`/config

cd ~/kafka-training

## Run Kafka
kafka/bin/kafka-server-start.sh \
"$CONFIG/server-0.properties"


~/kafka-training/lab2/start-2nd-server.sh

#!/usr/bin/env bash
CONFIG=`pwd`/config

cd ~/kafka-training

## Run Kafka
kafka/bin/kafka-server-start.sh \
"$CONFIG/server-1.properties"


~/kafka-training/lab2/start-3rd-server.sh

#!/usr/bin/env bash
CONFIG=`pwd`/config

cd ~/kafka-training

## Run Kafka
kafka/bin/kafka-server-start.sh \
"$CONFIG/server-2.properties"


Notice we are passing the Kafka server properties files that we created in the last step.
Now run all three in separate terminals/shells.

Run Kafka servers each in own terminal from ~/kafka-training/lab2

~/kafka-training/lab2
$ ./start-1st-server.sh

...
$ ./start-2nd-server.sh

...
$ ./start-3rd-server.sh


Give the servers a minute to startup and connect to ZooKeeper.

Create Kafka replicated topic my-failsafe-topic

Now we will create a replicated topic that the console producers and console consumers can use.

~/kafka-training/lab2/create-replicated-topic.sh

#!/usr/bin/env bash

cd ~/kafka-training

kafka/bin/kafka-topics.sh --create \
--zookeeper localhost:2181 \
--replication-factor 3 \
--partitions 13 \
--topic my-failsafe-topic


Notice that the replication factor gets set to 3, and the topic name is my-failsafe-topic, and like before it has 13 partitions.
Then we just have to run the script to create the topic.

Run create-replicated-topic.sh

~/kafka-training/lab2
$ ./create-replicated-topic.sh

Start Kafka Consumer that uses Replicated Topic

Next, create a script that starts the consumer and then start the consumer with the script.

~/kafka-training/lab2/start-consumer-console-replicated.sh

#!/usr/bin/env bash
cd ~/kafka-training

kafka/bin/kafka-console-consumer.sh \
--bootstrap-server localhost:9094,localhost:9092 \
--topic my-failsafe-topic \
--from-beginning


Notice that a list of Kafka servers is passed to --bootstrap-server parameter. Only, two of the three servers get passed that we ran earlier. Even though only one broker is needed, the consumer client will learn about the other broker from just one server. Usually, you list multiple brokers in case there is an outage so that the client can connect.
Now we just run this script to start the consumer.

Run start-consumer-console-replicated.sh

~/kafka-training/lab2
$ ./start-consumer-console-replicated.sh

Start Kafka Producer that uses Replicated Topic

Next, we create a script that starts the producer. Then launch the producer with the script you create.

~/kafka-training/lab2/start-consumer-producer-replicated.sh

#!/usr/bin/env bash
cd ~/kafka-training

kafka/bin/kafka-console-producer.sh \
--broker-list localhost:9092,localhost:9093 \
--topic my-failsafe-topic


Notice we start Kafka producer and pass it a list of Kafka Brokers to use via the parameter --broker-list.
Now use the start producer script to launch the producer as follows.

Run start-producer-console-replicated.sh

~/kafka-training/lab2
$ ./start-consumer-producer-replicated.sh

Now send messages

Now send some message from the producer to Kafka and see those messages consumed by the consumer.

Producer Console

~/kafka-training/lab2
$ ./start-consumer-producer-replicated.sh
Hi Mom
How are you?
How are things going?
Good!

Consumer Console

~/kafka-training/lab2
$ ./start-consumer-console-replicated.sh
Hi Mom
How are you?
How are things going?
Good!

Now Start two more consumers and send more messages

Now Start two more consumers in their own terminal window and send more messages from the producer.

Producer Console

~/kafka-training/lab2
$ ./start-consumer-producer-replicated.sh
Hi Mom
How are you?
How are things going?
Good!
message 1
message 2
message 3

Consumer Console 1st

~/kafka-training/lab2
$ ./start-consumer-console-replicated.sh
Hi Mom
How are you?
How are things going?
Good!
message 1
message 2
message 3

Consumer Console 2nd in new Terminal

~/kafka-training/lab2
$ ./start-consumer-console-replicated.sh
Hi Mom
How are you?
How are things going?
Good!
message 1
message 2
message 3

Consumer Console 2nd in new Terminal

~/kafka-training/lab2
$ ./start-consumer-console-replicated.sh
Hi Mom
How are you?
How are things going?
Good!
message 1
message 2
message 3

Notice that the messages are sent to all of the consumers because each consumer is in a different consumer group.

Change consumer to be in their own consumer group

Stop the producers and the consumers from before, but leave Kafka and ZooKeeper running.
Now let’s modify the start-consumer-console-replicated.sh script to add a Kafka consumer group. We want to put all of the consumers in same consumer group. This way the consumers will share the messages as each consumer in the consumer group will get its share of partitions.

~/kafka-training/lab2/start-consumer-console-replicated.sh

#!/usr/bin/env bash
cd ~/kafka-training

kafka/bin/kafka-console-consumer.sh \
--bootstrap-server localhost:9094,localhost:9092 \
--topic my-failsafe-topic \
--consumer-property group.id=mygroup

Notice that the script is the same as before except we added --consumer-property group.id=mygroup which will put every consumer that runs with this script into the mygroup consumer group.
Now we just run the producer and three consumers.

Run this three times - start-consumer-console-replicated.sh

~/kafka-training/lab2
$ ./start-consumer-console-replicated.sh

Run Producer Console

~/kafka-training/lab2
$ ./start-consumer-producer-replicated.sh


Now send seven messages from the Kafka producer console.

Producer Console

~/kafka-training/lab2
$ ./start-consumer-producer-replicated.sh
m1
m2
m3
m4
m5
m6
m7

Notice that the messages are spread evenly among the consumers.

1st Kafka Consumer gets m3, m5

~/kafka-training/lab2
$ ./start-consumer-console-replicated.sh
m3
m5


Notice the first consumer gets messages m3 and m5.

2nd Kafka Consumer gets m2, m6

~/kafka-training/lab2
$ ./start-consumer-console-replicated.sh
m2
m6

Notice the second consumer gets messages m2 and m6.

3rd Kafka Consumer gets m1, m4, m7

~/kafka-training/lab2
$ ./start-consumer-console-replicated.sh
m1
m4
m7

Notice the third consumer gets messages m1, m4 and m7.
Notice that each consumer in the group got a share of the messages.

Kafka Consumer Failover

Next, let’s demonstrate consumer failover by killing one of the consumers and sending seven more messages. Kafka should divide up the work to the consumers that are running.
First, kill the third consumer (CTRL-C in the consumer terminal does the trick).
Now send seven more messages with the Kafka console-producer.

Producer Console - send seven more messages m8 through m14

~/kafka-training/lab2
$ ./start-consumer-producer-replicated.sh
m1
...
m8
m9
m10
m11
m12
m13
m14

Notice that the messages are spread evenly among the remaining consumers.

1st Kafka Consumer gets m8, m9, m11, m14

~/kafka-training/lab2
$ ./start-consumer-console-replicated.sh
m3
m5
m8
m9
m11
m14


The first consumer got m8, m9, m11 and m14.

2nd Kafka Consumer gets m10, m12, m13

~/kafka-training/lab2
$ ./start-consumer-console-replicated.sh
m2
m6
m10
m12
m13

The second consumer got m10, m12, and m13.
We killed one consumer, sent seven more messages, and saw Kafka spread the load to remaining consumers. Kafka consumer failover works!

Create Kafka Describe Topic Script

You can use kafka-topics.sh to see how the Kafka topic is laid out among the Kafka brokers. The ---describe will show partitions, ISRs, and broker partition leadership.

~/kafka-training/lab2/describe-topics.sh

#!/usr/bin/env bash

cd ~/kafka-training

# List existing topics
kafka/bin/kafka-topics.sh --describe \
--topic my-failsafe-topic \
--zookeeper localhost:2181



Let’s run kafka-topics.sh --describe and see the topology of our my-failsafe-topic.

Run describe-topics

We are going to lists which broker owns (leader of) which partition, and list replicas and ISRs of each partition. ISRs are replicas that are up to date. Remember there are 13 topics.

Topology of Kafka Topic Partition Ownership

~/kafka-training/lab2
$ ./describe-topics.sh
Topic: my-failsafe-topic PartitionCount: 13 ReplicationFactor: 3 Configs:
Topic: my-failsafe-topic Partition: 0 Leader: 2 Replicas: 2,0,1 Isr: 2,0,1
Topic: my-failsafe-topic Partition: 1 Leader: 0 Replicas: 0,1,2 Isr: 0,1,2
Topic: my-failsafe-topic Partition: 2 Leader: 1 Replicas: 1,2,0 Isr: 1,2,0
Topic: my-failsafe-topic Partition: 3 Leader: 2 Replicas: 2,1,0 Isr: 2,1,0
Topic: my-failsafe-topic Partition: 4 Leader: 0 Replicas: 0,2,1 Isr: 0,2,1
Topic: my-failsafe-topic Partition: 5 Leader: 1 Replicas: 1,0,2 Isr: 1,0,2
Topic: my-failsafe-topic Partition: 6 Leader: 2 Replicas: 2,0,1 Isr: 2,0,1
Topic: my-failsafe-topic Partition: 7 Leader: 0 Replicas: 0,1,2 Isr: 0,1,2
Topic: my-failsafe-topic Partition: 8 Leader: 1 Replicas: 1,2,0 Isr: 1,2,0
Topic: my-failsafe-topic Partition: 9 Leader: 2 Replicas: 2,1,0 Isr: 2,1,0
Topic: my-failsafe-topic Partition: 10 Leader: 0 Replicas: 0,2,1 Isr: 0,2,1
Topic: my-failsafe-topic Partition: 11 Leader: 1 Replicas: 1,0,2 Isr: 1,0,2
Topic: my-failsafe-topic Partition: 12 Leader: 2 Replicas: 2,0,1 Isr: 2,0,1

Notice how each broker gets a share of the partitions as leaders and followers. Also, see how Kafka replicates the partitions on each broker.

Test Broker Failover by killing 1st server

Let’s kill the first broker, and then test the failover.

Kill the first broker

 $ kill `ps aux | grep java | grep server-0.properties | tr -s "" | cut -d "" -f2`

You can stop the first broker by hitting CTRL-C in the broker terminal or by running the above command.
Now that the first Kafka broker has stopped, let’s use Kafka topics describe to see that new leaders were elected!

Run describe-topics again to see leadership change

~/kafka-training/lab2/solution
$ ./describe-topics.sh
Topic:my-failsafe-topic PartitionCount:13 ReplicationFactor:3 Configs:
Topic: my-failsafe-topic Partition: 0 Leader: 2 Replicas: 2,0,1 Isr: 2,1
Topic: my-failsafe-topic Partition: 1 Leader: 1 Replicas: 0,1,2 Isr: 1,2
Topic: my-failsafe-topic Partition: 2 Leader: 1 Replicas: 1,2,0 Isr: 1,2
Topic: my-failsafe-topic Partition: 3 Leader: 2 Replicas: 2,1,0 Isr: 2,1
Topic: my-failsafe-topic Partition: 4 Leader: 2 Replicas: 0,2,1 Isr: 2,1
Topic: my-failsafe-topic Partition: 5 Leader: 1 Replicas: 1,0,2 Isr: 1,2
Topic: my-failsafe-topic Partition: 6 Leader: 2 Replicas: 2,0,1 Isr: 2,1
Topic: my-failsafe-topic Partition: 7 Leader: 1 Replicas: 0,1,2 Isr: 1,2
Topic: my-failsafe-topic Partition: 8 Leader: 1 Replicas: 1,2,0 Isr: 1,2
Topic: my-failsafe-topic Partition: 9 Leader: 2 Replicas: 2,1,0 Isr: 2,1
Topic: my-failsafe-topic Partition: 10 Leader: 2 Replicas: 0,2,1 Isr: 2,1
Topic: my-failsafe-topic Partition: 11 Leader: 1 Replicas: 1,0,2 Isr: 1,2
Topic: my-failsafe-topic Partition: 12 Leader: 2 Replicas: 2,0,1 Isr: 2,1

Notice how Kafka spreads the leadership over the 2nd and 3rd Kafka brokers.

Show Broker Failover Worked

Let’s prove that failover worked by sending two more messages from the producer console.
Then notice if the consumers still get the messages.

Send the message m15 and m16.

Producer Console - send m15 and m16

~/kafka-training/lab2
$ ./start-consumer-producer-replicated.sh
m1
...
m15
m16

Notice that the messages are spread evenly among the remaining live consumers.

1st Kafka Consumer gets m16

~/kafka-training/lab2
$ ./start-consumer-console-replicated.sh
m3
m5
m8
m9
m11
m14
...
m16


The first Kafka broker gets m16.

2nd Kafka Consumer gets m15

~/kafka-training/lab2
$ ./start-consumer-console-replicated.sh
m2
m6
m10
m12
m13
...
m15

The second Kafka broker gets m15.
Kafka broker Failover WORKS!

Kafka Cluster Failover Review

Why did the three consumers not load share the messages at first?

They did not load share at first because they were each in a different consumer group. Consumer groups each subscribe to a topic and maintain their own offsets per partition in that topic.

How did we demonstrate failover for consumers?

We shut a consumer down. Then we sent more messages. We observed Kafka spreading messages to the remaining cluster.

How did we show failover for producers?

We didn’t. We showed failover for Kafka brokers by shutting one down, then using the producer console to send two more messages. Then we saw that the producer used the remaining Kafka brokers. Those Kafka brokers then delivered the messages to the live consumers.

What tool and option did we use to show ownership of partitions and the ISRs?

We used kafka-topics.sh using the --describe option.

More about Kafka



About Cloudurable

We hope you enjoyed this article. Please provide feedback. Cloudurable provides Kafka trainingKafka consultingKafka support and helps setting up Kafka clusters in AWS.

Writing Kafka Java Producers and Kafka Java Consumers

$
0
0

Kafka Tutorial: Writing a Kafka Producer in Java

In this tutorial, we are going to create simple Java example that creates a Kafka producer. You create a new replicated Kafka topic called my-example-topic, then you create a Kafka producer that uses this topic to send records. You will send records with the Kafka producer. You will send records synchronously. Later, you will send records asynchronously.

Before you start

Prerequisites to this tutorial are Kafka from the command line and Kafka clustering and failover basics.
This tutorial is part of a series. If you are not sure what Kafka is, you should start with What is Kafka?. If you are unfamiliar with the architecture of Kafka then we suggest reading Kafka ArchitectureKafka Topics ArchitectureKafka Producer Architecture and Kafka Consumer Architecture.


Create Replicated Kafka Topic

Next, you need to create a replicated topic.

~/kafka-training/lab3/create-topic.sh

#!/usr/bin/env bash
cd ~/kafka-training

## Create topics
kafka/bin/kafka-topics.sh --create \
--replication-factor 3 \
--partitions 13 \
--topic my-example-topic \
--zookeeper localhost:2181


## List created topics
kafka/bin/kafka-topics.sh --list \
--zookeeper localhost:2181


Above we create a topic named my-example-topic with 13 partitions and a replication factor of 3. Then we list the Kafka topics.
Runs create-topic.sh as follows.

Output from running create-topic.sh

~/kafka-training/lab3
$ ./create-topic.sh
Created topic "my-example-topic".
__consumer_offsets
my-example-topic
my-failsafe-topic


Gradle Build Script

For this example, we use gradle to build the project.

~/kafka-training/lab3/solution/build.gradle

group 'cloudurable-kafka'
version '1.0-SNAPSHOT'
apply plugin: 'java'
sourceCompatibility = 1.8

repositories {
mavenCentral()
}

dependencies {
compile 'org.apache.kafka:kafka-clients:0.10.2.0'
compile 'ch.qos.logback:logback-classic:1.2.2'
}

Notice that we import the jar file kafka-clients:0.10.2.0. Apache Kafka uses sl4j so to setup logging we use logback (ch.qos.logback:logback-classic:1.2.2).

Construct a Kafka Producer

To create a Kafka producer, you will need to pass it a list of bootstrap servers (a list of Kafka brokers). You will also specify a client.id that uniquely identifies this Producer client. In this example, we are going to send messages with ids. The message body is a string, so we need a record value serializer as we will send the message body in the Kafka’s records value field. The message id (long), will be sent as the Kafka’s records key. You will need to specify a Key serializer and a value serializer, which Kafka will use to encode the message id as a Kafka record key, and the message body as the Kafka record value.


Common Kafka imports and constants

Next, we will import the Kafka packages and define a constant for the topic and a constant to define the list of bootstrap servers that the producer will connect.

KafkaProducerExample.java - imports and constants

~/kafka-training/lab3/src/main/java/com/cloudurable/kafka/KafkaProducerExample.java


package com.cloudurable.kafka;

import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.LongSerializer;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;

public class KafkaProducerExample {

private final static String TOPIC = "my-example-topic";
private final static String BOOTSTRAP_SERVERS =
"localhost:9092,localhost:9093,localhost:9094";


Notice that KafkaProducerExample imports LongSerializer which gets configured as the Kafka record key serializer, and imports StringSerializer which gets configured as the record value serializer. The constant BOOTSTRAP_SERVERS is set tolocalhost:9092,localhost:9093,localhost:9094 which is the three Kafka servers that we started up in the last lesson. Go ahead and make sure all three Kafka servers are running. The constant TOPIC is set to the replicated Kafka topic that we just created.

Create Kafka Producer to send records

Now, that we imported the Kafka classes and defined some constants, let’s create a Kafka producer.

KafkaProducerExample.java - Create Producer to send Records

~/kafka-training/lab3/src/main/java/com/cloudurable/kafka/KafkaProducerExample.java

public class KafkaProducerExample {
...
private static Producer<Long, String> createProducer() {
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
BOOTSTRAP_SERVERS);
props.put(ProducerConfig.CLIENT_ID_CONFIG, "KafkaExampleProducer");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
LongSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
StringSerializer.class.getName());
return new KafkaProducer<>(props);
}

To create a Kafka producer, you use java.util.Properties and define certain properties that we pass to the constructor of a KafkaProducer.
Above KafkaProducerExample.createProducer sets the BOOTSTRAP_SERVERS_CONFIG (“bootstrap.servers) property to the list of broker addresses we defined earlier. BOOTSTRAP_SERVERS_CONFIG value is a comma separated list of host/port pairs that the Producer uses to establish an initial connection to the Kafka cluster. The producer uses of all servers in the cluster no matter which ones we list here. This list only specifies the initial Kafka brokers used to discover the full set of servers of the Kafka cluster. If a server in this list is down, the producer will just go to the next broker in the list to discover the full topology of the Kafka cluster.
The CLIENT_ID_CONFIG (“client.id”) is an id to pass to the server when making requests so the server can track the source of requests beyond just IP/port by passing a producer name for things like server-side request logging.
The KEY_SERIALIZER_CLASS_CONFIG (“key.serializer”) is a Kafka Serializer class for Kafka record keys that implements the Kafka Serializer interface. Notice that we set this to LongSerializer as the message ids in our example are longs.
The VALUE_SERIALIZER_CLASS_CONFIG (“value.serializer”) is a Kafka Serializer class for Kafka record values that implements the Kafka Serializer interface. Notice that we set this to StringSerializer as the message body in our example are strings.

Send records synchronously with Kafka Producer

Kafka provides a synchronous send method to send a record to a topic. Let’s use this method to send some message ids and messages to the Kafka topic we created earlier.

KafkaProducerExample.java - Send Records Synchronously

~/kafka-training/lab3/src/main/java/com/cloudurable/kafka/KafkaProducerExample.java

public class KafkaProducerExample {
...

static void runProducer(final int sendMessageCount) throws Exception {
final Producer<Long, String> producer = createProducer();
long time = System.currentTimeMillis();

try {
for (long index = time; index < time + sendMessageCount; index++) {
final ProducerRecord<Long, String> record =
new ProducerRecord<>(TOPIC, index,
"Hello Mom " + index);

RecordMetadata metadata = producer.send(record).get();

long elapsedTime = System.currentTimeMillis() - time;
System.out.printf("sent record(key=%s value=%s) " +
"meta(partition=%d, offset=%d) time=%d\n",
record.key(), record.value(), metadata.partition(),
metadata.offset(), elapsedTime);

}
} finally {
producer.flush();
producer.close();
}
}
...


The above just iterates through a for loop, creating a ProducerRecord sending an example message ("Hello Mom " + index) as the record value and the for loop index as the record key. For each iteration, runProducer calls the send method of the producer (RecordMetadata metadata = producer.send(record).get()). The send method returns a Java Future.
The response RecordMetadata has ‘partition’ where the record was written and the ‘offset’ of the record in that partition.
Notice the call to flush and close. Kafka will auto flush on its own, but you can also call flush explicitly which will send the accumulated records now. It is polite to close the connection when we are done.

Running the Kafka Producer

Next you define the main method.

KafkaProducerExample.java - Running the Producer

~/kafka-training/lab3/src/main/java/com/cloudurable/kafka/KafkaProducerExample.java

public static void main(String... args) throws Exception {
if (args.length == 0) {
runProducer(5);
} else {
runProducer(Integer.parseInt(args[0]));
}
}
The main method just calls runProducer.

Send records asynchronously with Kafka Producer

Kafka provides an asynchronous send method to send a record to a topic. Let’s use this method to send some message ids and messages to the Kafka topic we created earlier. The big difference here will be that we use a lambda expression to define a callback.

KafkaProducerExample.java - Send Records Asynchronously with Kafka Producer

~/kafka-training/lab3/src/main/java/com/cloudurable/kafka/KafkaProducerExample.java

static void runProducer(final int sendMessageCount) throws InterruptedException {
final Producer<Long, String> producer = createProducer();
long time = System.currentTimeMillis();
final CountDownLatch countDownLatch = new CountDownLatch(sendMessageCount);

try {
for (long index = time; index < time + sendMessageCount; index++) {
final ProducerRecord<Long, String> record =
new ProducerRecord<>(TOPIC, index, "Hello Mom " + index);
producer.send(record, (metadata, exception) -> {
long elapsedTime = System.currentTimeMillis() - time;
if (metadata != null) {
System.out.printf("sent record(key=%s value=%s) " +
"meta(partition=%d, offset=%d) time=%d\n",
record.key(), record.value(), metadata.partition(),
metadata.offset(), elapsedTime);
} else {
exception.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await(25, TimeUnit.SECONDS);
}finally {
producer.flush();
producer.close();
}
}

Notice the use of a CountDownLatch so we can send all N messages and then wait for them all to send.

Async Interface Callback and Async Send Method

Kafka defines a Callback interface that you use for asynchronous operations. The callback interface allows code to execute when the request is complete. The callback executes in a background I/O thread so it should be fast (don’t block it). The onCompletion(RecordMetadata metadata, Exception exception) gets called when the asynchronous operation completes. The metadata gets set (not null) if the operation was a success, and the exception gets set (not null) if the operation had an error.
The async send method is used to send a record to a topic, and the provided callback gets called when the send is acknowledged. The send method is asynchronous, and when called returns immediately once the record gets stored in the buffer of records waiting to post to the Kafka broker. The send method allows sending many records in parallel without blocking to wait for the response after each one.
Since the send call is asynchronous it returns a Future for the RecordMetadata that will be assigned to this record. Invoking get() on this future will block until the associated request completes and then return the metadata for the record or throw any exception that occurred while sending the record. KafkaProducer

Conclusion Kafka Producer example

We created a simple example that creates a Kafka Producer. First, we created a new replicated Kafka topic; then we created Kafka Producer in Java that uses the Kafka replicated topic to send records. We sent records with the Kafka Producer using async and sync send methods.
We hope you enjoyed this article. Please provide feedback. See the Kafka trainingKafka consultingKafka support and helps setting up Kafka clusters in AWS.

Review Kafka Producer

What does the Callback lambda do?

The callback gets notified when the request is complete.

What will happen if the first server is down in the bootstrap list? Can the producer still connect to the other Kafka brokers in the cluster?

The producer will try to contact the next broker in the list. Any of the brokers once contacted, will let the producer know about the entire Kafka cluster. The Producer will connect as long as at least one of the brokers in the list is running. If you have 100 brokers and two of the brokers in a list of three servers in the bootstrap list are down, the producer can still use the 98 remaining brokers.

When would you use Kafka async send vs. sync send?

If you were already using an async code (Akka, QBit, Reakt, Vert.x) base, and you wanted to send records quickly.

Why do you need two serializers for a Kafka record?

One of the serializers is for the Kafka record key, and the other is for the Kafka record value.

Kafka Tutorial: Writing a Kafka Consumer in Java

In this tutorial, you are going to create simple Kafka Consumer. This consumer consumes messages from the Kafka Producer you wrote in the last tutorial. This tutorial demonstrates how to process records from a Kafka topic with a Kafka Consumer.
This tutorial describes how Kafka Consumers in the same group divide up and share partitions while each consumer group appears to get its own copy of the same data.

Construct a Kafka Consumer

Just like we did with the producer, you need to specify bootstrap servers. You also need to define a group.id that identifies which consumer group this consumer belongs. Then you need to designate a Kafka record key deserializer and a record value deserializer. Then you need to subscribe the consumer to the topic you created in the producer tutorial.

Kafka Consumer imports and constants

Next, you import the Kafka packages and define a constant for the topic and a constant to set the list of bootstrap servers that the consumer will connect.

KafkaConsumerExample.java - imports and constants

~/kafka-training/lab4/src/main/java/com/cloudurable/kafka/KafkaConsumerExample.java


package com.cloudurable.kafka;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.common.serialization.LongDeserializer;
import org.apache.kafka.common.serialization.StringDeserializer;


import java.util.Collections;
import java.util.Properties;

public class KafkaConsumerExample {

private final static String TOPIC = "my-example-topic";
private final static String BOOTSTRAP_SERVERS =
"localhost:9092,localhost:9093,localhost:9094";
...
}


Notice that KafkaConsumerExample imports LongDeserializer which gets configured as the Kafka record key deserializer, and imports StringDeserializer which gets set up as the record value deserializer. The constant BOOTSTRAP_SERVERS gets set to localhost:9092,localhost:9093,localhost:9094 which is the three Kafka servers that we started up in the last lesson. Go ahead and make sure all three Kafka servers are running. The constant TOPIC gets set to the replicated Kafka topic that you created in the last tutorial.

Create Kafka Consumer using Topic to Receive Records

Now, that you imported the Kafka classes and defined some constants, let’s create the Kafka consumer.

KafkaConsumerExample.java - Create Consumer to process Records

~/kafka-training/lab4/src/main/java/com/cloudurable/kafka/KafkaConsumerExample.java


public class KafkaConsumerExample {
...

private static Consumer<Long, String> createConsumer() {
final Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
BOOTSTRAP_SERVERS);
props.put(ConsumerConfig.GROUP_ID_CONFIG,
"KafkaExampleConsumer");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
LongDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class.getName());

// Create the consumer using props.
final Consumer<Long, String> consumer =
new KafkaConsumer<>(props);

// Subscribe to the topic.
consumer.subscribe(Collections.singletonList(TOPIC));
return consumer;
}
...
}

To create a Kafka consumer, you use java.util.Properties and define certain properties that we pass to the constructor of a KafkaConsumer.
Above KafkaConsumerExample.createConsumer sets the BOOTSTRAP_SERVERS_CONFIG (“bootstrap.servers”) property to the list of broker addresses we defined earlier. BOOTSTRAP_SERVERS_CONFIG value is a comma separated list of host/port pairs that the Consumer uses to establish an initial connection to the Kafka cluster. Just like the producer, the consumer uses of all servers in the cluster no matter which ones we list here.
The GROUP_ID_CONFIG identifies the consumer group of this consumer.
The KEY_DESERIALIZER_CLASS_CONFIG (“key.deserializer”) is a Kafka Deserializer class for Kafka record keys that implements the Kafka Deserializer interface. Notice that we set this to LongDeserializer as the message ids in our example are longs.
The VALUE_DESERIALIZER_CLASS_CONFIG (“value.deserializer”) is a Kafka Serializer class for Kafka record values that implements the Kafka Deserializer interface. Notice that we set this to StringDeserializer as the message body in our example are strings.
Important notice that you need to subscribe the consumer to the topic consumer.subscribe(Collections.singletonList(TOPIC));. The subscribe method takes a list of topics to subscribe to, and this list will replace the current subscriptions if any.

Process messages from Kafka with Consumer

Now, let’s process some records with our Kafka Producer.

KafkaConsumerExample.java - Process records from Consumer

~/kafka-training/lab4/src/main/java/com/cloudurable/kafka/KafkaConsumerExample.java

public class KafkaConsumerExample {
...


static void runConsumer() throws InterruptedException {
final Consumer<Long, String> consumer = createConsumer();

final int giveUp = 100; int noRecordsCount = 0;

while (true) {
final ConsumerRecords<Long, String> consumerRecords =
consumer.poll(1000);

if (consumerRecords.count()==0) {
noRecordsCount++;
if (noRecordsCount > giveUp) break;
else continue;
}

consumerRecords.forEach(record -> {
System.out.printf("Consumer Record:(%d, %s, %d, %d)\n",
record.key(), record.value(),
record.partition(), record.offset());
});

consumer.commitAsync();
}
consumer.close();
System.out.println("DONE");
}
}

Notice you use ConsumerRecords which is a group of records from a Kafka topic partition. The ConsumerRecords class is a container that holds a list of ConsumerRecord(s) per partition for a particular topic. There is one ConsumerRecord list for every topic partition returned by a the consumer.poll().
Notice if you receive records (consumerRecords.count()!=0), then runConsumer method calls consumer.commitAsync() which commit offsets returned on the last call to consumer.poll(…) for all the subscribed list of topic partitions.

Kafka Consumer Poll method

The poll method returns fetched records based on current partition offset. The poll method is a blocking method waiting for specified time in seconds. If no records are available after the time period specified, the poll method returns an empty ConsumerRecords.
When new records become available, the poll method returns straight away.
You can can control the maximum records returned by the poll() with props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 100);. The poll method is not thread safe and is not meant to get called from multiple threads.

Running the Kafka Consumer

Next you define the main method.

KafkaConsumerExample.java - Running the Consumer

~/kafka-training/lab4/src/main/java/com/cloudurable/kafka/KafkaConsumerExample.java


public class KafkaConsumerExample {

public static void main(String... args) throws Exception {
runConsumer();
}
}

The main method just calls runConsumer.

Try running the consumer and producer

Run the consumer from your IDE. Then run the producer from the last tutorial from your IDE. You should see the consumer get the records that the producer sent.

Logging set up for Kafka

If you don’t set up logging well, it might be hard to see the consumer get the messages.
Kafka like most Java libs these days uses sl4j. You can use Kafka with Log4j, Logback or JDK logging. We used logback in our gradle build (compile 'ch.qos.logback:logback-classic:1.2.2').

~/kafka-training/lab4/solution/src/main/resources/logback.xml

<configuration>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>


<logger name="org.apache.kafka" level="INFO"/>
<logger name="org.apache.kafka.common.metrics" level="INFO"/>

<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>

Notice that we set org.apache.kafka to INFO, otherwise we will get a lot of log messages. You should run it set to debug and read through the log messages. It gives you a flavor of what Kafka is doing under the covers. Leave org.apache.kafka.common.metrics or what Kafka is doing under the covers is drowned by metrics logging.

Try this: Three Consumers in same group and one Producer sending 25 messages

Run the consumer example three times from your IDE. Then change Producer to send 25 records instead of 5. Then run the producer once from your IDE. What happens? The consumers should share the messages.

Producer Output

sent record(key=1495048417121 value=..) meta(partition=6, offset=16) time=118
sent record(key=1495048417131 value=..) meta(partition=6, offset=17) time=120
sent record(key=1495048417133 value=..) meta(partition=12, offset=17) time=120
sent record(key=1495048417140 value=..) meta(partition=12, offset=18) time=121
sent record(key=1495048417143 value=..) meta(partition=12, offset=19) time=121
sent record(key=1495048417123 value=..) meta(partition=0, offset=19) time=121
sent record(key=1495048417126 value=..) meta(partition=0, offset=20) time=121
sent record(key=1495048417134 value=..) meta(partition=0, offset=21) time=122
sent record(key=1495048417122 value=..) meta(partition=3, offset=19) time=122
sent record(key=1495048417127 value=..) meta(partition=3, offset=20) time=122
sent record(key=1495048417139 value=..) meta(partition=3, offset=21) time=123
sent record(key=1495048417142 value=..) meta(partition=3, offset=22) time=123
sent record(key=1495048417136 value=..) meta(partition=10, offset=19) time=127
sent record(key=1495048417144 value=..) meta(partition=1, offset=26) time=128
sent record(key=1495048417125 value=..) meta(partition=5, offset=22) time=128
sent record(key=1495048417138 value=..) meta(partition=5, offset=23) time=128
sent record(key=1495048417128 value=..) meta(partition=8, offset=21) time=129
sent record(key=1495048417124 value=..) meta(partition=11, offset=18) time=129
sent record(key=1495048417130 value=..) meta(partition=11, offset=19) time=129
sent record(key=1495048417132 value=..) meta(partition=11, offset=20) time=130
sent record(key=1495048417141 value=..) meta(partition=11, offset=21) time=130
sent record(key=1495048417145 value=..) meta(partition=11, offset=22) time=131
sent record(key=1495048417129 value=..) meta(partition=2, offset=24) time=132
sent record(key=1495048417135 value=..) meta(partition=2, offset=25) time=132
sent record(key=1495048417137 value=..) meta(partition=2, offset=26) time=132
Notice the producer sends 25 messages.

Consumer 0 in same group

Consumer Record:(1495048417121, Hello Mom 1495048417121, 6, 16)
Consumer Record:(1495048417131, Hello Mom 1495048417131, 6, 17)
Consumer Record:(1495048417125, Hello Mom 1495048417125, 5, 22)
Consumer Record:(1495048417138, Hello Mom 1495048417138, 5, 23)
Consumer Record:(1495048417128, Hello Mom 1495048417128, 8, 21)

Consumer 1 in same group

Consumer Record:(1495048417123, Hello Mom 1495048417123, 0, 19)
Consumer Record:(1495048417126, Hello Mom 1495048417126, 0, 20)
Consumer Record:(1495048417134, Hello Mom 1495048417134, 0, 21)
Consumer Record:(1495048417144, Hello Mom 1495048417144, 1, 26)
Consumer Record:(1495048417122, Hello Mom 1495048417122, 3, 19)
Consumer Record:(1495048417127, Hello Mom 1495048417127, 3, 20)
Consumer Record:(1495048417139, Hello Mom 1495048417139, 3, 21)
Consumer Record:(1495048417142, Hello Mom 1495048417142, 3, 22)
Consumer Record:(1495048417129, Hello Mom 1495048417129, 2, 24)
Consumer Record:(1495048417135, Hello Mom 1495048417135, 2, 25)
Consumer Record:(1495048417137, Hello Mom 1495048417137, 2, 26)

Consumer 2 in same group

Consumer Record:(1495048417136, Hello Mom 1495048417136, 10, 19)
Consumer Record:(1495048417133, Hello Mom 1495048417133, 12, 17)
Consumer Record:(1495048417140, Hello Mom 1495048417140, 12, 18)
Consumer Record:(1495048417143, Hello Mom 1495048417143, 12, 19)
Consumer Record:(1495048417124, Hello Mom 1495048417124, 11, 18)
Consumer Record:(1495048417130, Hello Mom 1495048417130, 11, 19)
Consumer Record:(1495048417132, Hello Mom 1495048417132, 11, 20)
Consumer Record:(1495048417141, Hello Mom 1495048417141, 11, 21)
Consumer Record:(1495048417145, Hello Mom 1495048417145, 11, 22)
Can you answer these questions?

Which consumer owns partition 10?

How many ConsumerRecords objects did Consumer 0 get?

What is the next offset from Partition 11 that Consumer 2 should get?

Why does each consumer get unique messages?

Which consumer owns partition 10?

Consumer 2 owns partition 10.

How many ConsumerRecords objects did Consumer 0 get?

3

What is the next offset from Partition 11 that Consumer 2 should get?

22

Why does each consumer get unique messages?

Each gets its share of partitions for the topic.

Try this: Three Consumers in different Consumer group and one Producer sending 5 messages

Modify the consumer, so each consumer processes will have a unique group id.
Stop all consumers and producers processes from the last run.
Then execute the consumer example three times from your IDE. Then change producer to send five records instead of 25. Then run the producer once from your IDE. What happens? The consumers should each get a copy of the messages.
First, let’s modify the Consumer to make their group id unique as follows:

KafkaConsumerExample - Make the Consumer group id unique

~/kafka-training/lab4/src/main/java/com/cloudurable/kafka/KafkaConsumerExample.java

public class KafkaConsumerExample {

private final static String TOPIC = "my-example-topic";
private final static String BOOTSTRAP_SERVERS =
"localhost:9092,localhost:9093,localhost:9094";


private static Consumer<Long, String> createConsumer() {
final Properties props = new Properties();

props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
BOOTSTRAP_SERVERS);

props.put(ConsumerConfig.GROUP_ID_CONFIG,
"KafkaExampleConsumer" +
System.currentTimeMillis());

...
}
...
}
Notice, to make the group id unique you just add System.currentTimeMillis() to it.

Producer Output

sent record(key=1495049585396 value=..) meta(partition=7, offset=30) time=134
sent record(key=1495049585392 value=..) meta(partition=4, offset=24) time=138
sent record(key=1495049585393 value=..) meta(partition=4, offset=25) time=139
sent record(key=1495049585395 value=..) meta(partition=4, offset=26) time=139
sent record(key=1495049585394 value=..) meta(partition=11, offset=25) time=140
Notice the producer sends 25 messages.

Consumer 0 in own group

Consumer Record:(1495049585396, Hello Mom 1495049585396, 7, 30)
Consumer Record:(1495049585394, Hello Mom 1495049585394, 11, 25)
Consumer Record:(1495049585392, Hello Mom 1495049585392, 4, 24)
Consumer Record:(1495049585393, Hello Mom 1495049585393, 4, 25)
Consumer Record:(1495049585395, Hello Mom 1495049585395, 4, 26)

Consumer 1 in unique consumer group

Consumer Record:(1495049585396, Hello Mom 1495049585396, 7, 30)
Consumer Record:(1495049585394, Hello Mom 1495049585394, 11, 25)
Consumer Record:(1495049585392, Hello Mom 1495049585392, 4, 24)
Consumer Record:(1495049585393, Hello Mom 1495049585393, 4, 25)
Consumer Record:(1495049585395, Hello Mom 1495049585395, 4, 26)

Consumer 2 in its own consumer group

Consumer Record:(1495049585396, Hello Mom 1495049585396, 7, 30)
Consumer Record:(1495049585394, Hello Mom 1495049585394, 11, 25)
Consumer Record:(1495049585392, Hello Mom 1495049585392, 4, 24)
Consumer Record:(1495049585393, Hello Mom 1495049585393, 4, 25)
Consumer Record:(1495049585395, Hello Mom 1495049585395, 4, 26)
Can you answer these questions?

Which consumer owns partition 10?

How many ConsumerRecords objects did Consumer 0 get?

What is the next offset from Partition 11 that Consumer 2 should get?

Why does each consumer get unique messages?

Which consumer owns partition 10?

They all do! Since they are all in a unique consumer group, and there is only one consumer in each group, then each consumer we ran owns all of the partitions.

How many ConsumerRecords objects did Consumer 0 get?

3

What is the next offset from Partition 11 that Consumer 2 should get?

26

Why does each consumer get the same messages?

They do because they are each in their own consumer group, and each consumer group is a subscription to the topic.

Conclusion Kafka Consumer example

You created a simple example that creates a Kafka consumer to consume messages from the Kafka Producer you created in the last tutorial. We used the replicated Kafka topic from producer lab. You created a Kafka Consumer that uses the topic to receive messages. The Kafka consumer uses the poll method to get N number of records.
Consumers in the same group divide up and share partitions as we demonstrated by running three consumers in the same group and one producer. Each consumer groups gets a copy of the same data. More precise, each consumer group really has a unique set of offset/partition pairs per.

Review Kafka Consumer

How did we demonstrate Consumers in a Consumer Group dividing up topic partitions and sharing them?

We ran three consumers in the same consumer group, and then sent 25 messages from the producer. We saw that each consumer owned a set of partitions.

How did we demonstrate Consumers in different Consumer Groups each getting their own offsets?

We ran three consumers each in its own unique consumer group, and then sent 5 messages from the producer. We saw that each consumer owned every partition.

How many records does poll get?

However many you set in with props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 100); in the properties that you pass to KafkaConsumer.

Does a call to poll ever get records from two different partitions?

No

We hope you enjoyed this article. Please provide feedback. See the Kafka trainingKafka consultingKafka support and helps setting up Kafka clusters in AWS.

Tutorial Part 1: Managing Kubernetes StatefulSets using ZooKeeper and Minikube

$
0
0

Kubernetes StatefulSet with ZooKeeper as an example

Background

I was having a really hard time deploying Kafka to Kubernetes. It worked fine when we were doing development and integration. We started with Minikube for local development.
We created a MicroSerivce that uses Kafka in Spring Boot. We ran Kafka in minikube with Helm 2. By the way, Minikube is a mini Kubernetes that easily runs on macOS, Linux, and Windows. Minikube is great for local application development and supports a lot of Kubernetes. It is great for local testing and we also used it for integration testing.
Later we set up a pipeline and we wanted to use some of the same end to end tests that we used for local dev in Jenkins so we decided to switch to Kind for a variety of reasons. A chief reason was this was a shared Jenkins environment (so we can't just install stuff and docker was there already) and the other reason was although we could get minikube to run on a Jenkins AWS worker instance there were too many limitations which KIND did not seem to have. And it could have just been our knowledge of Minikube, but Kind worked so we switched.
Kind is similar to minikube and it is also a tool for running mini Kubernetes clusters using Docker container as nodes. It was created to test Kubernetes but it fits well with our use cases.
One issue we had with Kind was running some of the Helm 2 installs for Kafka. After trying to debug for a good 1/2 day, we tried Helm 3, and lo and behold, it just worked. We switched. Just like that. Bump and run. The path of least resistance.
By the way, I wrote a getting started with minikube and a Kubernetes cheatsheet, if you are new to minikube and Kubernetes, start there. I also wrote down a lot of tools that I use. The cheat sheet will be updated at some point to include Kind. Also, I would love some collaboration and tips to add to the cheatsheet. I need to give it a second look.
Let's see, we wanted to deploy to a shared environment. We tested our integration test scripts and Kafka installs on GKE (using Helm 3), local Open Shift (tried minishift but then switch to and now Red Hat CodeReady Containers), Minikube and KIND. Then we ran into an issue with a shared Open Shift container.
The same Helm 3 install that was working everywhere else was failing due to Pod policies which are not changeable due to corporate Infosec policies (so far anyway).
Also at one point, we were told we can't use Helm 3 (some corporate policy), and we have to define our own docker containers. Now, I am not sure either of those is still true, but we learn and adapt. This has nothing to do with Open Shift. I am not an Open Shift hater. Now there could be a way to get the Helm 3 install to work. But since it does so much, I found tracking down the issues and being compliant was difficult.
The lead Infra/DevOps guy-in-charge told me to write my own StatefulSet, and do my own PV, PVC. I have always just used Helm for anything that needed PV or PVCs. I did. I learned a lot. Debugging and troubleshooting and working around pod policies not to mention differences in MiniKube, GKE, KIND, and Open Shift local vs. Open Shift shared corp gave me a new perspective. Which is why I decided to write some of this stuff down.
My first attempt was to use Helm 3, have it spit out the manifest files and then debug it from there. But since it was doing both ZooKeeper and Kafka, it was a bit like drinking from a fire hose. I prefer the divide and conquer approach especially after the big bang approach does not work.
I was lucky to find this tutorial on managing statefulsets on the Kubernetes site using ZooKeeper. The scripts as written did not work without change in any environment where I ran it except maybe GKE. Kubernetes has great documentation and an awesome community. Trial and error and troubleshooting is a good way to learn. I tried the statefulset manifest files as written locally and in the shared OSE env and it did not work. But at least it was small enough so I can follow.
I base this tutorial from the one on the Kubernetes site on ZooKeeper and StatefulSet but I am going to deploy to MiniKube, local Open Shift, and KIND. (So far, I have written about MiniKube and OpenShift already and got it to work).
I have a similar version of this Kubernetes ZooKeeper deploy working on a multi-node shared, corporate locked-down environment. I had to do a lot of extra steps that don't make a lot of sense to me yet. This is a new version based on the example. I will simulate some of the issues that I encountered as I think there is a lot to learn while I went through this exercise.
ZooKeeper is a nice tool to start StatefulSets with because it is small and lightweight, yet exhibits a lot of the same needs as many disturbed, stateful, clustered applications (Kafka, Hadoop, Cassandra, Consul, MongoDB, etc.).
BTW, This is not my first rodeo with ZooKeeper or Kafka or even deploying stateful clustered services (cassandra) or managing them or setting up KPIs, but this is the first time I wrote about doing it with Kubernetes. I have also written leadership election libs and have done clustering with tools like ZooKeeper, namely, etcd and Consul.

Where to find the code

You can find the code for this project at:
  • Mini-Kube - branch that got the zookeeper example running in minikube and RedHat OSE installed on my local laptop.

Running ZooKeeper, A Distributed System Coordinator

This tutorial shows how to use StatefulSets in local dev environments as well as real clusters with many nodes and uses Apache Zookeeper. This tutorial will demonstrate Kubernetes StatefulSets as well as PodDisruptionBudgets, and PodAntiAffinity.
It should augment Running ZooKeeper, A Distributed System Coordinator but adds more details in debugging and more details regarding StatefulSets, Volumes, and PodAntiAffinity.
This will be a standalone tutorial. You don't have to read the other one but I recommend it. If you are using MiniKube or MiniShift or Kind to learn Kubernetes, then this tutorial should work unlike the other.

Objectives

After this tutorial, you will know the following.
  • How to deploy a ZooKeeper ensemble using StatefulSet
  • How to deploy ZooKeeper servers on multiple nodes for availability
  • How to use PodDisruptionBudgets ensuring availability
  • How to use PodAntiAffinity to deploy to a local environment
  • How to use PodAntiAffinity to deploy to a production or integration environment
  • How to create your own Docker container that uses ZooKeeper
  • How to create your own liveness probes and ready probes
  • How to test that your ZooKeeper install really worked
  • How to debug common issues
Later follow on tutorials might show:
  • How to deploy to OSE
  • How to write deploy scripts with Kustomize to target local vs. remote deployments
  • How to write deploy scripts with Helm 3 to target local vs. remote deployments
  • How to create your metrics gatherers and use them with Prometheus
  • How to install Kafka on top of ZooKeeper
  • How to install Istio with ZooKeeper to get dashboards
  • How to install Istio with ZooKeeper to get mTLS
  • How to write a Spring Boot app that uses Kafka, and Istio
  • How to write operators
I have written these three at some level already:
  1. Tutorial 1: MiniKube
  2. Tutorial 2: Open Shift
  3. Tutorial 3: Using Kustomize
I have not written tutorial 4 yet, but I already decided it will be on using config maps. Tutorial 5 will be running Kafka on top of ZooKeeper. I already wrote the code for tutorial 5.
This tutorial should be fun and you can do it all from your laptop. :)

Before you begin

Before starting this tutorial, you should be familiar with the following Kubernetes concepts.
  • Pods
  • Cluster DNS
  • Headless Services
  • PersistentVolumes
  • PersistentVolume Provisioning
  • StatefulSets
  • PodDisruptionBudgets
  • PodAntiAffinity
  • kubectl CLI
While the other tutorial required a cluster with at least four nodes (with 2 CPUs and 4 GiB of memory), this one will work with local Kubernetes dev environments. A later tutorial will show how to use Kustomize to target local dev and a real cluster. The default set up for minikube and Red Hat CodeReady Containers either dynamically provision PersistentVolumes or comes with enough out of the box to work.

ZooKeeper Basics

ZooKeeper is a distributed config system that uses a consensus algorithm. ZooKeeper is similar to Consul or etcd if you are familiar with them. It gets used by Kafka and Hadoop and quite a few others. Personally, I prefer Consul and etcd. But since a lot of projects use it, it is very common.
ZooKeeper allows you to perform CRUD operations on config and watch for updates to the config. It uses a consensus algorithm that guarantees that all servers have the same view of the config more or less. The config can be the state of your cluster (which nodes are up or down who is the leader). Think of it like a consistent view of config data ordered in a file system like hierarchy. The major difference between a regular file system to store config is that a ZooKeeper cluster forms an ensemble so that all of the data is in-sync using a consensus algorithm. If you are familiar with these concepts from Consul, etcd or even Spark or Cassandra or MongoDB then you have a basic understanding of ZooKeeper too.
While Consul and etcd use RAFT as a consensus algorithm, ZooKeeper ensures uses the Zab consensus protocol to replicate data in a consistent state across all members of the ensemble. Both Zab) and RAFT are faster, lighter than [Paxos](https://en.wikipedia.org/wiki/Paxos_(computer_science) which you may have studied in school but have similar concepts.
ZooKeeper uses Zab protocol to elect a leader. The ZooKeeper ensemble can't write data unless there is a leader. This keeps the data very consistent. The ZooKeeper ensemble replicates all writes to a quorum defined by Zab protocol before the data becomes visible to clients. As stated if you are familiar with quorums from Cassandra, MongoDB, etcd or Consul, it is really more of the same ideas behind quorums. A quorum is a majority of ZooKeeper nodes in the ensemble and the leader. "For instance, if the ensemble has three servers, a component that contains the leader and one other server constitute a quorum. If the ensemble can not achieve a quorum, the ensemble cannot write data." --Zookeeper Basics
ZooKeeper servers store config in memory. The config memory is periodically written to disk. Also, every change to a ZooKeeper instance gets written to a Write Ahead Log (WAL) which is on disk.
ZooKeeper nodes that crash or are updated, recover by reading the last snapshot and then replaying the WAL. After a snapshot, the WALs are deleted. This prevents the disk from filling up. --Zookeeper Basics

Set up Kubernetes / Minikube

This tutorial does not assume you even have Kubernetes set up at all. If you are using a OSX machine use this tutorial to set up minikube. If you are not using OSX then first install minikube and then go through that tutorial and this Kubernetes cheatsheet.
Once you feel comfortable with minikube, delete the cluster and create it again with this command.

Minikube

minikube start --kubernetes-version v1.16.0 \
--vm-driver=hyperkit \
--cpus=4 \
--disk-size='100000mb' \
--memory='6000mb'
We choose Kubernetes version 1.16.0, using hyperkit with 4 CPUs, 10GB of disk space and 6 GB of memory for the whole cluster.
You should add some extra memory too and a bit extra disk space. This should all run on a modern laptop with at least 16GB of ram.

Creating a ZooKeeper Ensemble

Just like the other example, this one has a manifest that contains a Headless Service, a Service, a PodDisruptionBudget, and a StatefulSet. --Creating a ZooKeeper Ensemble

zookeeper.yaml Kubernetes Objects: Headless Service, a Service, a PodDisruptionBudget, and a StatefulSet

apiVersion: v1
kind: Service
metadata:
name: zookeeper-headless
labels:
app: zookeeper
spec:
ports:
- port: 2888
name: server
- port: 3888
name: leader-election
clusterIP: None
selector:
app: zookeeper
---
apiVersion: v1
kind: Service
metadata:
name: zookeeper-service
labels:
app: zookeeper
spec:
ports:
- port: 2181
name: client
selector:
app: zookeeper
---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: zookeeper-pdb
spec:
selector:
matchLabels:
app: zookeeper
maxUnavailable: 1
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zookeeper
spec:
selector:
matchLabels:
app: zookeeper
serviceName: zookeeper-headless
replicas: 3
updateStrategy:
type: RollingUpdate
podManagementPolicy: OrderedReady
template:
metadata:
labels:
app: zookeeper
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: "app"
operator: In
values:
- zookeeper
topologyKey: "kubernetes.io/hostname"
containers:
- name: kubernetes-zookeeper
imagePullPolicy: Always
image: "cloudurable/kube-zookeeper:0.0.1"
resources:
requests:
memory: "1Gi"
cpu: "0.5"
ports:
- containerPort: 2181
name: client
- containerPort: 2888
name: server
- containerPort: 3888
name: leader-election
command:
- sh
- -c
- "start.sh \
--servers=3 \
--data_dir=/var/lib/zookeeper/data \
--data_log_dir=/var/lib/zookeeper/data/log \
--conf_dir=/opt/zookeeper/conf \
--client_port=2181 \
--election_port=3888 \
--server_port=2888 \
--tick_time=2000 \
--init_limit=10 \
--sync_limit=5 \
--heap=512M \
--max_client_cnxns=60 \
--snap_retain_count=3 \
--purge_interval=12 \
--max_session_timeout=40000 \
--min_session_timeout=4000 \
--log_level=INFO"
readinessProbe:
exec:
command:
- sh
- -c
- "ready_live.sh 2181"
initialDelaySeconds: 10
timeoutSeconds: 5
livenessProbe:
exec:
command:
- sh
- -c
- "ready_live.sh 2181"
initialDelaySeconds: 10
timeoutSeconds: 5
volumeMounts:
- name: datadir
mountPath: /var/lib/zookeeper
securityContext:
runAsUser: 1000
fsGroup: 1000
volumeClaimTemplates:
- metadata:
name: datadir
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi
The above is almost the exact same as the one in the other tutorial except I changed the name of the scripts, the docker image and made the names longer so they would be easier to read. In other words, it won't work on minikube or local Open Shift (minishift or Red Hat CodeReady Containers), but how it doesn't work is a teachable moment. I learn the most when things break.

Create the statefulset no edits

From the command line, use kubectl apply to create the StatefulSet, services, etc.

Create Headless Service, a Service, a PodDisruptionBudget, and a StatefulSet

kubectl apply -f zookeeper.yaml

#### OUTPUT
service/zookeeper-headless created
service/zookeeper-service created
poddisruptionbudget.policy/zookeeper-pdb created
statefulset.apps/zookeeper created
Next, let's check to see if the pods for the ZooKeeper StatefulSet were created.

Check status of pods in statefulset

kubectl get pods

### Output
NAME READY STATUS RESTARTS AGE
zookeeper-0 1/1 Running 0 63s
zookeeper-1 0/1 Pending 0 47s
Wow. That is taking a long time. Well, go check to see if you got any texts on your phone and come back, then check again.

Check status of pods in statefulset again

kubectl get pods

### Output
NAME READY STATUS RESTARTS AGE
zookeeper-0 1/1 Running 0 70s
zookeeper-1 0/1 Pending 0 54s
Wow. That is taking a long time. Well, go check to see if you got any emails and come back, then check again.

Check status of pods in statefulset for the third time.

% kubectl get pods


NAME READY STATUS RESTARTS AGE
zookeeper-0 1/1 Running 0 5m24s
zookeeper-1 0/1 Pending 0 5m8s
(⎈ |minikube:default)richardhightower@Richards-MacBook-Pro kube-zookeeper-statefulsets %
It has been five minutes and the 2nd ZooKeeper node is just not getting created. Let's see why. We need a total of three ZooKeeper nodes to create an ensemble.

Debug why the statefulset did not work

We can use the kubectl describe command to see the events for the zookeeper-1 pod.
kubectl describe pod zookeeper-1


### OUTPUT
Name: zookeeper-1
Namespace: default
Priority: 0
Node: <none>
Labels: app=zookeeper
controller-revision-hash=zookeeper-7b7f6f8cb9
statefulset.kubernetes.io/pod-name=zookeeper-1
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling <unknown> default-scheduler pod has unbound immediate PersistentVolumeClaims
Warning FailedScheduling <unknown> default-scheduler pod has unbound immediate PersistentVolumeClaims
Warning FailedScheduling <unknown> default-scheduler 0/1 nodes are available: 1 node(s) didn't match pod affinity/anti-affinity, 1 node(s) didn't satisfy existing pods anti-affinity rules.
Ok, we can see that we did not actually schedule this pod to run on any node because we only have one node. The message 0/1 nodes are available: 1 node(s) didn't match pod affinity/anti-affinity, 1 node(s) didn't satisfy existing pods anti-affinity rules. Let's take a look at the affinity rule from our yaml file.

zookeeper.yaml - affinity rules requiredDuringSchedulingIgnoredDuringExecution

...
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zookeeper
spec:
...
template:
metadata:
labels:
app: zookeeper
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: "app"
operator: In
values:
- zookeeper
topologyKey: "kubernetes.io/hostname"
The key here is that we set a rule via requiredDuringSchedulingIgnoredDuringExecution which blocks zookeeper nodes from being deployed on the same Kubernetes worker node/host (topologyKey: "kubernetes.io/hostname").
As I stated earlier, in later tutorials we would like to use an overlay with Kustomize to override such config for local dev vs. an industrial integration or prod cluster. But as a workaround, let's turn the affinity/podAntiAffinity rule into more of a suggestion with preferredDuringSchedulingIgnoredDuringExecution. While you will likely want requiredDuringSchedulingIgnoredDuringExecution for production, you may get away with preferredDuringSchedulingIgnoredDuringExecution for development. What these statements do is really spelled out well in their name, but if you want to dive deeper, I suggest Assigning Pods to Nodes.

zookeeper.yaml - affinity rules preferredDuringSchedulingIgnoredDuringExecution

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zookeeper
spec:
selector:
matchLabels:
app: zookeeper
serviceName: zookeeper-headless
...
template:
metadata:
labels:
app: zookeeper
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: "app"
operator: In
values:
- zookeeper
topologyKey: "kubernetes.io/hostname"
Ok. You will need to change to preferredDuringSchedulingIgnoredDuringExecution and remove requiredDuringSchedulingIgnoredDuringExecution (edit zookeeper.yaml). Then you need to delete the Kubernetes objects from the cluster using the YAML file for the statefulset and then recreate Kubernetes objects for the statefulset.
From the command line, delete the statefulset objects.

Delete statefulset objects using zookeeper.yaml.

kubectl delete -f zookeeper.yaml

### OUTPUT
service "zookeeper-headless" deleted
service "zookeeper-service" deleted
poddisruptionbudget.policy "zookeeper-pdb" deleted
statefulset.apps "zookeeper" deleted
Just to get a clean slate, go ahead and delete the persistent volume claims too. This is not required at all.

Delete persistent volume claims

kubectl delete pvc datadir-zookeeper-0
kubectl delete pvc datadir-zookeeper-1

### Output
persistentvolumeclaim "datadir-zookeeper-0" deleted
persistentvolumeclaim "datadir-zookeeper-1" deleted
Now you have a pristine Kubernetes local dev cluster, let's create the statefulsets objects again.

Create StatefulSet for ZooKeeper again with preferredDuringSchedulingIgnoredDuringExecution

kubectl apply -f zookeeper.yaml   

### Output
service/zookeeper-headless created
service/zookeeper-service created
poddisruptionbudget.policy/zookeeper-pdb created
statefulset.apps/zookeeper created
As before, let's check the status of ZooKeeper pod creations. This time you can use the -w flag to watch the pod creation status change as it happens.

Use kubectl get pods to see ZooKeeper pod creations status

% kubectl get pods -w     

### OUTPUT
NAME READY STATUS RESTARTS AGE
zookeeper-0 0/1 ContainerCreating 0 10s
zookeeper-0 0/1 Running 0 39s
zookeeper-0 1/1 Running 0 54s
zookeeper-1 0/1 Pending 0 1s
zookeeper-1 0/1 Pending 0 1s
zookeeper-1 0/1 Pending 0 2s
zookeeper-1 0/1 ContainerCreating 0 2s
zookeeper-1 0/1 Running 0 5s
zookeeper-1 1/1 Running 0 17s
zookeeper-2 0/1 Pending 0 0s
zookeeper-2 0/1 Pending 0 0s
zookeeper-1 0/1 Running 0 67s
zookeeper-1 1/1 Running 0 87s

...

kubectl get pods

### OUTPUT
NAME READY STATUS RESTARTS AGE
zookeeper-0 1/1 Running 0 4m41s
zookeeper-1 1/1 Running 0 3m47s
zookeeper-2 0/1 Pending 0 3m30s

It got stuck again. I wonder why. Well it looks like zookeeper-1 was created so our preferredDuringSchedulingIgnoredDuringExecution worked well. But, zookeeper-2, never runs. Let's use kubectl describe to see why

Checking status of zookeeper-2 node with kubectl describe

kubectl describe pod zookeeper-2

### OUTPUT
...
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute for 300s
node.kubernetes.io/unreachable:NoExecute for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling <unknown> default-scheduler 0/1 nodes are available: 1 Insufficient memory.
Warning FailedScheduling <unknown> default-scheduler 0/1 nodes are available: 1 Insufficient memory.
You can see that there was Insufficient memory and thus zookeeper-2 FailedScheduling.
Clearly you can see that kubectl describe is a powerful tool to see errors.
You can even use kubectl describe with the statefulset object itself. This means that you will have to recreate minikube with more memory.

Checking status of zookeeper statefulsets with kubectl describe and get

kubectl describe statefulsets zookeeper

### OUTPUT
Name: zookeeper
Namespace: default
CreationTimestamp: Tue, 11 Feb 2020 12:50:52 -0800
Selector: app=zookeeper
...
Replicas: 3 desired | 3 total
Pods Status: 2 Running / 1 Waiting / 0 Succeeded / 0 Failed
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulCreate 12m statefulset-controller create Claim datadir-zookeeper-0 Pod zookeeper-0 in StatefulSet zookeeper success
Normal SuccessfulCreate 12m statefulset-controller create Pod zookeeper-0 in StatefulSet zookeeper successful
Normal SuccessfulCreate 11m statefulset-controller create Claim datadir-zookeeper-1 Pod zookeeper-1 in StatefulSet zookeeper success
Normal SuccessfulCreate 11m statefulset-controller create Pod zookeeper-1 in StatefulSet zookeeper successful
Normal SuccessfulCreate 10m statefulset-controller create Claim datadir-zookeeper-2 Pod zookeeper-2 in StatefulSet zookeeper success
Normal SuccessfulCreate 10m statefulset-controller create Pod zookeeper-2 in StatefulSet zookeeper successful

...

kubectl get statefulsets zookeeper

### OUTPUT
NAME READY AGE
zookeeper 2/3 17m
You can see that it created all of the pods as it suppose to but it is forever waiting on the last pod by looking at Pods Status: 2 Running / 1 Waiting / 0 Succeeded / 0 Failed. Also doing a kubectl get statefulsets zookeeper shows that only 2 of the 3 pods are ready as well (zookeeper 2/3 17m).
Since the statefulset wouldn't fit into memory on your local dev environment, let's recreate minikube with more memory. You will delete minikube and then create it again.
minikube delete

minikube start --kubernetes-version v1.16.0 \
--vm-driver=hyperkit \
--cpus=4 \
--disk-size='100000mb' \
--memory='7500mb'

### OUTPUT
🔥 Deleting "minikube"in hyperkit ...
💔 The "minikube" cluster has been deleted.
...
😄 minikube v1.4.0 on Darwin 10.15.1
🔥 Creating hyperkit VM (CPUs=4, Memory=7500MB, Disk=100000MB) ...
🐳 Preparing Kubernetes v1.16.0 on Docker 18.09.9 ...
🚜 Pulling images ...
🚀 Launching Kubernetes ...
⌛ Waiting for: apiserver proxy etcd scheduler controller dns
🏄 Done! kubectl is now configured to use "minikube"
It fit two ZooKeeper nodes/container/pods into 6GB so figured that Kube control plane took up some space and you just need another 1.5GB for our 3rd Zookeeper node. It was a swag. Less memory for minikube means more memory for your other development tools (IDE, etc.).
By the way, this eats up a lot of memory for just a local dev environment so later in another tutorial, we will run ZooKeeper in a single node mode. I have done this before either here at ZooKeeper cloud or here at Kafka cloud. This will allow us to create a single pod cluster for local dev which will save a lot of memory on our dev laptop.
Now let's create the ZooKeeper statefulset and check its status.

Create ZooKeeper statefulset and check status with kubectl get pods -w

kubectl apply -f zookeeper.yaml   

### OUTPUT
service/zookeeper-headless created
service/zookeeper-service created
poddisruptionbudget.policy/zookeeper-pdb created
statefulset.apps/zookeeper created


kubectl get pods -w


### OUTPUT
NAME READY STATUS RESTARTS AGE
zookeeper-0 0/1 ContainerCreating 0 13s
zookeeper-0 0/1 Running 0 46s
zookeeper-0 1/1 Running 0 62s
zookeeper-1 0/1 Pending 0 0s
zookeeper-1 0/1 Pending 0 0s
zookeeper-1 0/1 Pending 0 1s
zookeeper-1 0/1 ContainerCreating 0 1s
zookeeper-1 0/1 Running 0 4s
zookeeper-1 1/1 Running 0 20s
zookeeper-2 0/1 Pending 0 0s
zookeeper-2 0/1 Pending 0 0s
zookeeper-2 0/1 Pending 0 1s
zookeeper-2 0/1 ContainerCreating 0 1s
zookeeper-2 0/1 Running 0 5s
zookeeper-2 1/1 Running 0 14s
Now let's check the status of our ZooKeeper statefulset and the pods again.

Check status of ZooKeeper statefulset and its pods

kubectl get pods                  

### OUTPUT
NAME READY STATUS RESTARTS AGE
zookeeper-0 1/1 Running 1 25m
zookeeper-1 1/1 Running 0 24m
zookeeper-2 1/1 Running 0 24m

kubectl get statefulsets
### OUTPUT

NAME READY AGE
zookeeper 3/3 26m
All three are up and you can see that I got a phone call between creation and status check.
Now let's look around a bit.

See the Persistent Volumes

kubectl get pv

### OUTPUT
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
pvc-20e452bf-fe67 10Gi RWO Delete Bound default/datadir-zookeeper-1 standard 26m
pvc-4e59068b-8170 10Gi RWO Delete Bound default/datadir-zookeeper-0 standard 27m
pvc-e8a57062-64b9 10Gi RWO Delete Bound default/datadir-zookeeper-2 standard 26m

See the Persistent Volumes Claims

kubectl get pvc

### OUTPUT
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
datadir-zookeeper-0 Bound pvc-4e59068b-8170 10Gi RWO standard 28m
datadir-zookeeper-1 Bound pvc-20e452bf-fe67 10Gi RWO standard 27m
datadir-zookeeper-2 Bound pvc-e8a57062-64b9 10Gi RWO standard 26m
(⎈ |minikube:default)richardhightower@Richards-MacBook-Pro kube-zookeeper-statefulsets
You can see that each instance claims a volume and you can also see that Minikube creates the volumes on the fly. It will be interesting to look at the default behavior of OpenShift later in the next article.
How can we trust that this all works? You can look at logs for errors. You can connect to the Kubernetes console and see if we see get see errors. I mean so far it looks like it is working! Yeah!
Let's connect to an instance and see if you can run some commands. You will use network cat (nc) to send ZooKeeper Commands also known as "The Four Letter Words" to ZooKeeper port 2181 which was configured for the zookeeper-service's client port service (see ZooKeeper admin guide for more details on ZooKeeper Commands).
The command kubectl exec -it zookeeper-0 will run a command on a pod. The -it options allow us to have an interactive terminal so you will run bash and poke around with some ZooKeeper commands.

Connecting to ZooKeeper instance to and see if it is working (if you need to debug later)

kubectl  exec -it zookeeper-0 bash

## Check to see if the node is ok send ruok to port 2181 with network cat.
$ echo"Are you ok? $(echo ruok | nc 127.0.0.1 2181)"
iamok

## Use the srvr to see if this node is the leader.
$ echo srvr | nc localhost 2181 | grep Mode
Mode: follower

$ exit

## Let's try to find the leader of the ZooKeeper ensemble

kubectl exec -it zookeeper-1 bash
$ echo srvr | nc localhost 2181 | grep Mode
Mode: leader

$ exit

kubectl exec -it zookeeper-2 bash
$ echo srvr | nc localhost 2181 | grep Mode
Mode: follower
Note: To run a zookeeper in standalone set StatefulSet replicas to 1 and the parameter --servers to 1. When you do this the mode will be Mode: standalone. This is useful for local development to save space. This will really help if you are trying to run some tests on your local laptop. Also, try adjusting the memory lower and allocate less CPU.

zookeeper.yaml - Running ZooKeeper in standalone mode

...
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zookeeper
spec:
...
replicas: 1
spec:
...
containers:
- name: kubernetes-zookeeper
...
resources:
requests:
memory: "500Mi"
cpu: "0.25"
...
command:
- sh
- -c
- "start.sh \
--servers=1
..."
Please note that zookeeper-1 is the ZooKeeper ensemble leader.
The ZooKeeper command dump lists the outstanding sessions and ephemeral nodes but you have run this from the leader. This only works on the leader. Your master is the one that returned Mode: leader from echo srvr | nc localhost 2181 | grep Mode.

Run the dump command on the master and others

kubectl  exec -it zookeeper-1 bash


## Show the dump command
$ echo dump | nc localhost 2181
SessionTracker dump:
Session Sets (0):
ephemeral nodes dump:
Sessions with Ephemerals (0):


## Get stats
$ echo stat | nc localhost 2181
Zookeeper version: 3.4.14-4c25d480e66aadd371de8bd2fd8da255ac140bcf, built on 03/06/2019 16:18 GMT
Clients:
/127.0.0.1:35964[0](queued=0,recved=1,sent=0)

Latency min/avg/max: 0/0/0
Received: 734
Sent: 733
Connections: 1
Outstanding: 0
Zxid: 0x100000000
Mode: leader
Node count: 4
Proposal sizes last/min/max: -1/-1/-1

## Notice the node count is 4

## Get the environment for this ZooKeeper node
$ echo envi | nc localhost 2181
Environment:
zookeeper.version=3.4.14-4c25d480e66aadd371de8bd2fd8da255ac140bcf, built on 03/06/2019 16:18 GMT
host.name=zookeeper-1.zookeeper-headless.default.svc.cluster.local
java.version=11.0.6
java.vendor=Ubuntu
java.home=/usr/lib/jvm/java-11-openjdk-amd64
java.class.path=/usr/bin/../zookeeper-server/target/classes:/usr/bin/../build/classes:/usr/bin/../zookeeper-server/target/lib/*.jar:/usr/bin/../build/lib/*.jar:/usr/bin/...
java.io.tmpdir=/tmp
os.name=Linux
os.arch=amd64
os.version=4.15.0
user.name=zookeeper
user.home=/home/zookeeper
user.dir=/

$ echo conf | nc localhost 2181
clientPort=2181
dataDir=/var/lib/zookeeper/data/version-2
dataLogDir=/var/lib/zookeeper/data/log/version-2
tickTime=2000
maxClientCnxns=60
minSessionTimeout=4000
maxSessionTimeout=40000
serverId=2
initLimit=10
syncLimit=5
electionAlg=3
electionPort=3888
quorumPort=2888
peerType=0
zookeeper@zookeeper-1

// Monitor the cluster
$ echo mntr | nc localhost 2181
zk_version 3.4.14-4c25d480e66aadd371de8bd2fd8da255ac140bcf, built on 03/06/2019 16:18 GMT
zk_avg_latency 0
zk_max_latency 0
zk_min_latency 0
zk_packets_received 827
zk_packets_sent 826
zk_num_alive_connections 1
zk_outstanding_requests 0
zk_server_state leader
zk_znode_count 4
zk_watch_count 0
zk_ephemerals_count 0
zk_approximate_data_size 27
zk_open_file_descriptor_count 27
zk_max_file_descriptor_count 1048576
zk_fsync_threshold_exceed_count 0
zk_followers 2 ## <-------- Two followers for the leader.
zk_synced_followers 2 ## <-------- Two followers are in sync.
zk_pending_syncs 0
zk_last_proposal_size -1
zk_max_proposal_size -1
zk_min_proposal_size -1
You could go through the admin docs for ZooKeeper to get details about each command, but you get the gist. There is a ZooKeeper ensemble and all three nodes are present. Some commands work differently depending of if they are run against a leader or a follower. All of the commands above are run by the leader.
Included in the image is a bash script called metrics.sh which will run echo mntr | nc localhost 2181.

Demonstrating that the commands run different per ZooKeeper Node

## Run against zookeeper-0
kubectl exec -it zookeeper-0 metrics.sh 2181

### OUTPUT
zk_version 3.4.14-4c25d480e66aadd371de8bd2fd8da255ac140bcf, built on 03/06/2019 16:18 GMT
zk_avg_latency 0
zk_max_latency 0
zk_min_latency 0
zk_packets_received 656
zk_packets_sent 655
zk_num_alive_connections 1
zk_outstanding_requests 0
zk_server_state follower
zk_znode_count 4
zk_watch_count 0
zk_ephemerals_count 0
zk_approximate_data_size 27
zk_open_file_descriptor_count 25
zk_max_file_descriptor_count 1048576
zk_fsync_threshold_exceed_count 0

## Run against zookeeper-1
kubectl exec -it zookeeper-1 metrics.sh 2181

### OUTPUT
zk_version 3.4.14-4c25d480e66aadd371de8bd2fd8da255ac140bcf, built on 03/06/2019 16:18 GMT
zk_avg_latency 0
zk_max_latency 0
zk_min_latency 0
zk_packets_received 859
zk_packets_sent 858
zk_num_alive_connections 1
zk_outstanding_requests 0
zk_server_state leader
zk_znode_count 4
zk_watch_count 0
zk_ephemerals_count 0
zk_approximate_data_size 27
zk_open_file_descriptor_count 27
zk_max_file_descriptor_count 1048576
zk_fsync_threshold_exceed_count 0
zk_followers 2
zk_synced_followers 2
zk_pending_syncs 0
zk_last_proposal_size -1
zk_max_proposal_size -1
zk_min_proposal_size -1

## Run against zookeeper-2
kubectl exec -it zookeeper-2 metrics.sh 2181

### OUTPUT
zk_version 3.4.14-4c25d480e66aadd371de8bd2fd8da255ac140bcf, built on 03/06/2019 16:18 GMT
zk_avg_latency 0
zk_max_latency 0
zk_min_latency 0
zk_packets_received 850
zk_packets_sent 849
zk_num_alive_connections 1
zk_outstanding_requests 0
zk_server_state follower
zk_znode_count 4
zk_watch_count 0
zk_ephemerals_count 0
zk_approximate_data_size 27
zk_open_file_descriptor_count 25
zk_max_file_descriptor_count 1048576
zk_fsync_threshold_exceed_count 0

Notice that zookeeper-1 pod has two more attributes in its metrics namely zk_followers and zk_synced_followers. Only leaders have zk_followers and zk_synced_followers metrics.
Now let's check the log and see if everything is working.

Looking at the ZooKeeper logs with kubectl logs

kubectl logs zookeeper-1 | more

Annotated logs from kubectl logs zookeeper-1


...
### ANNOTATION JMX is enabled
ZooKeeper JMX enabled by default
Using config: /usr/bin/../etc/zookeeper/zoo.cfg
2020-02-11 21:25:24,278 [myid:] - INFO [main:QuorumPeerConfig@136] - Reading configuration from: /usr/bin/../etc/zookeeper/zoo.cfg
2020-02-11 21:25:24,285 [myid:] - INFO [main:QuorumPeer$QuorumServer@185] - Resolved hostname: zookeeper-1.zookeeper-headless.default.svc.cluster.local to address: zookeeper-1.zookeeper-headless.default.svc.cluster.local/172.17.0.14
2020-02-11 21:25:24,286 [myid:] - INFO [main:QuorumPeer$QuorumServer@185] - Resolved hostname: zookeeper-0.zookeeper-headless.default.svc.cluster.local to address: zookeeper-0.zookeeper-headless.default.svc.cluster.local/172.17.0.13
2020-02-11 21:25:24,288 [myid:] - WARN [main:QuorumPeer$QuorumServer@191] - Failed to resolve address: zookeeper-2.zookeeper-headless.default.svc.cluster.local

### ANNOTATION When zookeeper-1 started zookeeper-2 had not started yet. because the stateful set is set to start in Ordered mode
### ANNOTATION Since the yaml file uses podManagementPolicy: OrderedReady
### ANNOTATION zookeeper-0 will start and then zookeeper-1 will start and then zookeeper-1
java.net.UnknownHostException: zookeeper-2.zookeeper-headless.default.svc.cluster.local: Name or service not known
at java.base/java.net.Inet4AddressImpl.lookupAllHostAddr(Native Method)
at java.base/java.net.InetAddress$PlatformNameService.lookupAllHostAddr(InetAddress.java:929)

...

### ANNOTATION The leadership election process is starting but the 3rd node (zookeeper-2) is not up yet (as expected)
2020-02-11 21:25:24,299 [myid:2] - INFO [main:QuorumPeerMain@130] - Starting quorum peer
2020-02-11 21:25:24,303 [myid:2] - INFO [main:ServerCnxnFactory@117] - Using org.apache.zookeeper.server.NIOServerCnxnFactory as server connection factory
...
2020-02-11 21:25:24,322 [myid:2] - INFO [main:QuorumPeer@669] - currentEpoch not found! Creating with a reasonable default of 0. This should only happen when you are upgrading your installation
2020-02-11 21:25:24,325 [myid:2] - INFO [main:QuorumPeer@684] - acceptedEpoch not found! Creating with a reasonable default of 0. This should only happen when you are upgrading your installation
2020-02-11 21:25:24,329 [myid:2] - INFO [ListenerThread:QuorumCnxManager$Listener@736] - My election bind port: zookeeper-1.zookeeper-headless.default.svc.cluster.local/172.17.0.14:3888
2020-02-11 21:25:24,347 [myid:2] - INFO [QuorumPeer[myid=2]/0.0.0.0:2181:QuorumPeer@910] - LOOKING
2020-02-11 21:25:24,348 [myid:2] - INFO [QuorumPeer[myid=2]/0.0.0.0:2181:FastLeaderElection@813] - New election. My id = 2, proposed zxid=0x0
2020-02-11 21:25:24,353 [myid:2] - WARN [WorkerSender[myid=2]:QuorumCnxManager@584] - Cannot open channel to 3 at election address
...
zookeeper-2.zookeeper-headless.default.svc.cluster.local:3888 ### ANNOTATION expected
java.net.UnknownHostException: zookeeper-2.zookeeper-headless.default.svc.cluster.local
at java.base/java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:220)
at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:403)
at java.base/java.net.Socket.connect(Socket.java:609)
at org.apache.zookeeper.server.quorum.QuorumCnxManager.connectOne(QuorumCnxManager.java:558)
at org.apache.zookeeper.server.quorum.QuorumCnxManager.toSend(QuorumCnxManager.java:534)
at org.apache.zookeeper.server.quorum.FastLeaderElection$Messenger$WorkerSender.process(FastLeaderElection.java:454)
at org.apache.zookeeper.server.quorum.FastLeaderElection$Messenger$WorkerSender.run(FastLeaderElection.java:435)
at java.base/java.lang.Thread.run(Thread.java:834)
...

### ANNOTATION More leadership election ZAB dance
2020-02-11 21:25:24,358 [myid:2] - INFO [WorkerReceiver[myid=2]:FastLeaderElection@595] - Notification: 1 (message format version), 2 (n.leader), 0x0 (n.zxid), 0x1 (n.round), LOOKING (n.state), 2 (n.sid), 0x0 (n.peerEpoch) LOOKING (my state)
2020-02-11 21:25:24,358 [myid:2] - INFO [WorkerReceiver[myid=2]:FastLeaderElection@595] - Notification: 1 (message format version), 1 (n.leader), 0x0 (n.zxid), 0x1 (n.round), LOOKING (n.state), 1 (n.sid), 0x0 (n.peerEpoch) LOOKING (my state)
2020-02-11 21:25:24,358 [myid:2] - INFO [WorkerReceiver[myid=2]:FastLeaderElection@595] - Notification: 1 (message format version), 2 (n.leader), 0x0 (n.zxid), 0x1 (n.round), LOOKING (n.state), 1 (n.sid), 0x0 (n.peerEpoch) LOOKING (my state)

### ANNOTATION We have a winner! zookeeper-1 is the leader .. he won the election and zookeeper-0 is the follower.
2020-02-11 21:25:24,560 [myid:2] - INFO [QuorumPeer[myid=2]/0.0.0.0:2181:QuorumPeer@992] - LEADING
...
### ANNOTATION The leadership eleciton took 230 milliseconds!
2020-02-11 21:25:24,578 [myid:2] - INFO [QuorumPeer[myid=2]/0.0.0.0:2181:Leader@380] - LEADING - LEADER ELECTION TOOK - 230
### ANNOTATION Later zookeeper-2, aka node 3 comes up, and the leader let's it know who is the boss
2020-02-11 21:25:45,403 [myid:2] - INFO [LearnerHandler-/172.17.0.15:34870:LearnerHandler@535] - Received NEWLEADER-ACK message from 3

...
### ANNOTATION You see a lot of runok commands, which are used by
2020-02-11 21:25:57,112 [myid:2] - INFO [NIOServerCxn.Factory:0.0.0.0/0.0.0.0:2181:NIOServerCnxn@908] - Processing ruok command from /127.0.0.1:54550
Go through the annotated output and compare that to what the ZooKeeper Basics section said. Also notice that there are a lot of Processing ruok command in the log. This is because it has INFO level logging, and the ZooKeeper command for health check is ruok which should return imok.

The ruok shows up a lot in the logs beause it is used for liveness and readiness probes

% kubectl logs zookeeper-1 | grep ruok | wc -l
555
% kubectl logs zookeeper-0 | grep ruok | wc -l
616
% kubectl logs zookeeper-2 | grep ruok | wc -l
775
The ready_live.sh script uses ruok from the liveness and readiness probes.

zookeeper.yaml

...
readinessProbe:
exec:
command:
- sh
- -c
- "ready_live.sh 2181"
initialDelaySeconds: 10
timeoutSeconds: 5
livenessProbe:
exec:
command:
- sh
- -c
- "ready_live.sh 2181"
initialDelaySeconds: 10
timeoutSeconds: 5
...
This means that ready_live.sh gets called for the readinessProbe and the livenessProbe.

ready_live.sh using ruok and checking for imok

#!/usr/bin/env bash
OK=$(echo ruok | nc 127.0.0.1 $1)
if [ "$OK"=="imok" ];then
exit 0
else
exit 1
fi
The results of echo ruok | nc 127.0.0.1 2181 is stored into OK and if it is "imok" then the ready_live.sh scripts return normally (0) or it signals that there was an error (1).

Recap 1

Let's recap what you did so far. You modified the YAML manifest for our ZooKeeper statefulset to use requiredDuringSchedulingIgnoredDuringExecution versus preferredDuringSchedulingIgnoredDuringExecution. You then noticed that you did not have enough memory for Minikube so you increased. Along the way you did some debugging with kubectl describekubectl get, and kubectl exec. Then you walked through the logs can be compared what you know about statefulsets and ZooKeeper with the output of the logs.

Facilitating Leader Election

This section is based on Facilitating Leader Election.
Each ZooKeeper server node in the ZooKeeper ensemble has to have a unique identifier associated with a network address. These identifiers are known to every node.
To get a list of all hostnames in our ZooKeeper ensemble use kubectl exec.

Use kubectl exec to get the hostnames of the Pods in the Zookeeper StatefulSet.

foriin 0 1 2;do kubectl exec zookeeper-$i -- hostname;done

### OUTPUT
zookeeper-0
zookeeper-1
zookeeper-2
The above runs the command hostname on all three ZooKeeper pods.
The Kubernetes StatefulSet controller gives each Pod a unique hostname based on its index. The hostnames are "${statefulset_name}-${index}"". In the YAML manifest file the replicas was set to 3. Therefore the StatefulSet controller creates three Pods with their hostnames set to zookeeper-0zookeeper-1, and zookeeper-3.

zookeeper.yaml StatefulSet replicas is set to 3

...
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zookeeper
spec:
selector:
matchLabels:
app: zookeeper
serviceName: zookeeper-headless
replicas: 3
...
The ZooKeeper nodes store their server’s id in a file called myid in the zookeeper data directory (see name: datadir \ mountPath: /var/lib/zookeeper in the zookeeper.yaml file).
Let's look at the contents of those files with kubectl exec and cat.

Examining each ZooKeeper node's id file

foriin 0 1 2;doecho"myid zookeeper-$i"; kubectl exec zookeeper-$i -- cat /var/lib/zookeeper/data/myid;done

##OUTPUT
myid zookeeper-0
1
myid zookeeper-1
2
myid zookeeper-2
3
Yes that is right. Kubernetes uses 0 based indexing but ZooKeeper uses start at 1 based indexing. You are welcome.
Let's get the fully qualified domain name (FQDN) of each pod.

Get fully qualified domain name with kubectl exec hostname -f

foriin 0 1 2;do kubectl exec zookeeper-$i -- hostname -f;done

### OUTPUT
zookeeper-0.zookeeper-headless.default.svc.cluster.local
zookeeper-1.zookeeper-headless.default.svc.cluster.local
zookeeper-2.zookeeper-headless.default.svc.cluster.local
The zookeeper-headless service creates a domain for each pod in the statefulset.
The DNS A records in Kubernetes DNS resolve the FQDNs to the Pods’ IP addresses. If the Pods gets reschedule or upgrading, the A records will put to new IP addresses, but the name will stay the same.
The ZooKeeper was configured to use a config file called zoo.cfg (/opt/zookeeper/conf/zoo.cfg which is specified in the start.sh file which we will cover later). You can use kubectl exec to cat the contents of the zoo.cfg.

/opt/zookeeper/conf/zoo.cfg Contents of zookeeper config file

kubectl exec zookeeper-0 -- cat /opt/zookeeper/conf/zoo.cfg

#This file was autogenerated DO NOT EDIT
clientPort=2181
dataDir=/var/lib/zookeeper/data
dataLogDir=/var/lib/zookeeper/data/log
tickTime=2000
initLimit=10
syncLimit=5
maxClientCnxns=60
minSessionTimeout=4000
maxSessionTimeout=40000
autopurge.snapRetainCount=3
autopurge.purgeInteval=12
server.1=zookeeper-0.zookeeper-headless.default.svc.cluster.local:2888:3888
server.2=zookeeper-1.zookeeper-headless.default.svc.cluster.local:2888:3888
server.3=zookeeper-2.zookeeper-headless.default.svc.cluster.local:2888:3888
This file gets generated by start.sh's create_config() function which gets included by the Dockerfile which we will cover later. Notice that server.1server.2, and server.3 properties are set to the fully qualified URL:port (the FDQNs) of the zookeeper-headless service. This is also done by a for loop in the start.sh's create_config() function. The start.sh is specified in the zookeeper.yaml file.

zookeeper.yaml - specifying StatefulSet.spec.template.containers[0].command

...
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zookeeper
spec:
selector:
matchLabels:
app: zookeeper
...
template:
metadata:
labels:
app: zookeeper
spec:
...
containers:
- name: kubernetes-zookeeper
imagePullPolicy: Always
image: "cloudurable/kube-zookeeper:0.0.1"
...
command:
- sh
- -c
- "start.sh \
--servers=3 \
--data_dir=/var/lib/zookeeper/data \
--data_log_dir=/var/lib/zookeeper/data/log \
--conf_dir=/opt/zookeeper/conf \
--client_port=2181 \
--election_port=3888 \
--server_port=2888 \
--tick_time=2000 \
--init_limit=10 \
--sync_limit=5 \
--heap=512M \
--max_client_cnxns=60 \
--snap_retain_count=3 \
--purge_interval=12 \
--max_session_timeout=40000 \
--min_session_timeout=4000 \
--log_level=INFO"
The start.sh command gets passed those arguments on startup by the Kubernetes node that starts up the ZooKeeper pod.
Using Kubernetes config maps would be a good example instead of passing all of this as hardcoded values.

Achieving Consensus

Sanity Testing the Ensemble

This section is based on Sanity Testing the Ensemble.
Let's test to see if our ZooKeeper ensemble actually works. You will use the zkCli.sh which is the ZooKeeper command-line interface.
With the ZooKeeper CLI operations you can:
  • Create znodes
  • Get data
  • Watch znode for changes
  • Set data into a znode
  • Create children of a znode
  • List children of a znode
  • Check Status
  • Delete a znode
A znode in ZooKeeper is like a file in that it has contents and like a folder in that, it can have children who are other znode.
Let's write the value world into our ZooKeeper ensemble called /hello into zookeeper-0.

Use kubectl exec and zkCli.sh to write 'world' to znode /hello on zookeeper-0

kubectl exec zookeeper-0 zkCli.sh create /hello world

### OUTPUT
...
2020-02-12 01:34:07,938 [myid:] - INFO [main:ZooKeeper@442] - Initiating client connection, connectString=localhost:2181 sessionTimeout=30000 watcher=org.apache.zookeeper.ZooKeeperMain$MyWatcher@1d251891
2020-02-12 01:34:07,967 [myid:] - INFO [main-SendThread(localhost:2181):ClientCnxn$SendThread@1025] - Opening socket connection to server localhost/127.0.0.1:2181. Will not attempt to authenticate using SASL (unknown error)
2020-02-12 01:34:07,985 [myid:] - INFO [main-SendThread(localhost:2181):ClientCnxn$SendThread@879] - Socket connection established to localhost/127.0.0.1:2181, initiating session
2020-02-12 01:34:08,012 [myid:] - INFO [main-SendThread(localhost:2181):ClientCnxn$SendThread@1299] - Session establishment complete on server localhost/127.0.0.1:2181, sessionid = 0x100005d64170000, negotiated timeout = 30000

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
Created /hello
You just wrote "world" to znode "/hello".
Now, let's read it back but from a different zookeeper node.

Use kubectl exec and zkCli.sh to read from znode /hello on zookeeper-1

kubectl exec zookeeper-1 zkCli.sh get /hello

### OUTPUT
...
WATCHER::

WatchedEvent state:SyncConnected type:None path:null
cZxid = 0x200000002
world
...
The value "world" that you put into znode "/hello" is available on every server in the ZooKeeper ensemble.

Use kubectl exec zookeeper-$i zkCli.sh to check every node has world

foriin 0 1 2;do kubectl exec zookeeper-$i  zkCli.sh get /hello | grep world;done

Tolerating Node Failure

Let's show how the consensus algorithm works. You will delete one server. Then you will try to write the ZooKeeper ensemble. Since two ZooKeeper servers still exist the write to the znode will work.

Delete server, set value, read it back, --- should work.

kubectl delete --force=true --grace-period=0 pod zookeeper-2  &
sleep 1; kubectl delete --force=true --grace-period=0 pod zookeeper-2 &
sleep 1
kubectl exec zookeeper-0 zkCli.sh set /hello world_should_work
sleep 1
kubectl exec zookeeper-1 zkCli.sh get /hello
The write works because the ZooKeeper ensemble has a quorum. Now if you delete two servers, you won't have a quorum. Note that you delete the servers twice and you force their deletion. Run the following commands to get a feel for how ZooKeeper works.

Delete two server, set value, read it back, --- should not work.

kubectl delete --force=true --grace-period=0 pod zookeeper-2  &
kubectl delete --force=true --grace-period=0 pod zookeeper-1 &
sleep 1; kubectl delete --force=true --grace-period=0 pod zookeeper-2 &
sleep 1; kubectl delete --force=true --grace-period=0 pod zookeeper-1 &
sleep 1
kubectl exec zookeeper-0 zkCli.sh set /hello world_should_not_work
sleep 1
kubectl exec zookeeper-0 zkCli.sh get /hello
sleep 20 # If you are running manually use kubectl get pods to see status of pods restarting
kubectl exec zookeeper-0 zkCli.sh get /hello
The value will be the same value you wrote the last time. The ZooKeeper ensemble needs a quorum before it can write to a znode.
For more background information on this see Tolerating Node Failure. The two tutorials cover this concept in a completely different but complementary way.

Providing Durable Storage

This section is very loosely derived from Providing Durable Storage tutorial. ZooKeeper stores its data in the Kubernetes volumes using the generated persistent volume claims that you specified in the YAML manifest.

zookeeper.yaml volume template in StatefulSet..volumeClaimTemplates

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zookeeper
spec:
selector:
matchLabels:
app: zookeeper
serviceName: zookeeper-headless
replicas: 3
updateStrategy:
type: RollingUpdate
podManagementPolicy: OrderedReady
template:
metadata:
labels:
app: zookeeper
...
...
volumeMounts:
- name: datadir
mountPath: /var/lib/zookeeper
volumeClaimTemplates:
- metadata:
name: datadir
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi
Notice that the volumeClaimTemplates will create persistent volume claims for the pods. If you shut down the whole ZooKeeper statefulset, the volumes will not get deleted. And if you recreate the ZooKeeper statefuleset, the same value will be present. Let's do it.

Delete the statefulset

kubectl delete statefulset zookeeper

### OUTPUT
statefulset.apps "zookeeper" delete
Use kubectl get pods to ensure that all of the pods terminate. Then recreate the Zookeeper statefulset.

Recreate the the ZooKeeper statefulset using zookeeper.yaml

kubectl apply -f zookeeper.yaml
Run kubectl get pods -w -l app=zookeeper and hit ctrl-c once all of the pods are back online.
Now let's see if our data is still there.

Read data from new ZooKeeper ensemble using old volumes

kubectl exec zookeeper-0 zkCli.sh get /hello

### OUTPUT

...
WATCHER::
...
world_should_work
cZxid = 0x200000002
ctime = Wed Feb 12 01:34:08 GMT 2020
...
It works. Even though you deleted the statefulset and all of its pods, the ensemble still has the last value you set because the volumes are still there.
The volumeClaimTemplates of the ZooKeeper StatefulSet’s spec specified a PersistentVolume for each pod.
When a pod in the ZooKeeper's StatefulSet is rescheduled or upgraded, it will have the PersistentVolume mounted to the ZooKeeper server's data directory. The persistent data will still be there. This same concept would work with Cassandra or Consul or etcd or any database.

Configuring a Non-Privileged User

See Configuring a Non-Privileged User from the original tutorial.

Managing the ZooKeeper Process

See Managing the ZooKeeper Process from the original tutorial.

Using MiniKube Dashboard

You can see the pods and the statefuleset with minikube dashboard.
minikube dashboard

Kubernetes Dashboard

Notice the pods are healthy.
Navigate down to Workloads->Stateful Sets then click on zookeeper in the list to see the ZooKeeper Stateful Set.

Kubernetes Dashboard showing ZooKeeper Stateful Set

We did it

We did it. The skills we used to analyze ZooKeeper will be available for other clustered software. The biggest part to me is learning how to debug with something that goes wrong. But our example still does not run in OpenShift. In the next tutorial, we will get it running in OpenShift.

Tutorial Part 2: Kubernetes StatefulSet with ZooKeeper as an example on OpenShift

$
0
0

Kubernetes StatefulSet with ZooKeeper as an example on OpenShift

Background

This is part 2. In part 1 of this tutorial, we got an example of a ZooKeeper StatefulSet running locally with minkube.
This really builds on the last tutorial and deploys to Red Hat Open Shift local dev. Please refer to the first tutorial.
As stated before, I base this tutorial from the one on the Kubernetes site on ZooKeeper and StatefulSet but I am going to deploy to MiniKube, local Open Shift, and KIND. I have a similar version of this Kubernetes ZooKeeper deploy working on a multi-node shared, corporate locked-down environment. This is a new version based on the example. I will simulate some of the issues that I encountered as I think there is a lot to learn.
If for some reason you would like more background on this tutorial - background.
ZooKeeper is a nice tool to start StatefulSets with because it is small and lightweight, yet exhibits a lot of the same needs as many disturbed, stateful, clustered applications.
BTW, This is not my first rodeo with ZooKeeper or Kafka or even deploying stateful clustered services (cassandra) or managing them or setting up KPIs, but this is the first time I wrote about doing it with Kubernetes. I have also written leadership election libs and have done clustering with tools like ZooKeeper, namely, etcd and Consul.

Where to find the code

You can find the code for this project at:
  • Mini-Kube - branch that got the zookeeper example running in minikube and RedHat OSE installed on my local laptop.

Running ZooKeeper, A Distributed System Coordinator

This tutorial shows how to use StatefulSets in local dev environments as well as real clusters with many nodes and uses Apache Zookeeper. This tutorial will demonstrate Kubernetes StatefulSets as well as PodDisruptionBudgets, and PodAntiAffinity. Specifically, you will deploy to a local Open Shift instance (4.2, but 4.3 is out).
If you don't want to deploy to a Red Hat Open Shift environment, then just skip to tutorial three.

Objectives

After this tutorial, you will know the following.
  • How to deploy a ZooKeeper ensemble on Open Shift
  • How to debug common issues
Later follow on tutorials might show:
  • How to write deploy scripts with Kustomize to target local vs. remote deployments
  • How to write deploy scripts with Helm 3 to target local vs. remote deployments
  • How to create your metrics gatherers and use them with Prometheus
  • How to install Kafka on top of ZooKeeper

Before you begin

Before starting this tutorial, you should be familiar with the following Kubernetes concepts.
  • Pods
  • Cluster DNS
  • Headless Services
  • PersistentVolumes
  • PersistentVolume Provisioning
  • StatefulSets
  • PodDisruptionBudgets
  • PodAntiAffinity
  • kubectl CLI
While the tutorial on the Kubernetes site required a cluster with at least four nodes (with 2 CPUs and 4 GiB of memory), this one will work with local Kubernetes dev environments, namely, Open Shift. The very next tutorial will show how to use Kustomize to target local dev and a real cluster. The default set up for minikube and Red Hat CodeReady Containers either dynamically provision PersistentVolumes or comes with enough out of the box to work.

Recall our trouble in paradise

The zookeeper manifests that we started with would not run on minikube or local Open Shift (minishift or Red Hat CodeReady Containers) when we started. In the last tutorial, we showed how to run the ZooKeeper ensemble on minikube and how to debug issues like affinity/anti-affinity, lack of resources, etc.
The key was to change requiredDuringSchedulingIgnoredDuringExecution which blocks zookeeper nodes from being deployed on the same Kubernetes worker node/host (topologyKey: "kubernetes.io/hostname") to preferredDuringSchedulingIgnoredDuringExecution.

zookeeper.yaml - affinity rules preferredDuringSchedulingIgnoredDuringExecution

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zookeeper
spec:
selector:
matchLabels:
app: zookeeper
serviceName: zookeeper-headless
...
template:
metadata:
labels:
app: zookeeper
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: "app"
operator: In
values:
- zookeeper
topologyKey: "kubernetes.io/hostname"
In the next, tutorial we would like to use an overlay with Kustomize to override such config for local dev vs. an industrial integration or prod cluster. Please check that one out too.
Let's recap what you did so far. You modified the yaml manifest for our ZooKeeper statefulset to use requiredDuringSchedulingIgnoredDuringExecution versus preferredDuringSchedulingIgnoredDuringExecution. You then noticed that you did not have enough memory for Minikube so you increased. Along the way you did some debugging with kubectl describekubectl get, and kubectl exec. Then you walked through the logs and compared what you know about statefulsets and ZooKeeper with the output of the logs. Then you ran a bunch of commands to prove to you that the ZooKeeper ensemble was really working. You even added znode and read them. Now let's get this running on Open Shift CRC.

Running OpenShift Install on your Laptop with Red Hat CodeReady Containers

First, you need to install a local Open Shift.
They used to call the Open Shift for local deploys minishift which was like minikube but for Open Shift. Now they changed it to Red Hat CodeReady Container which just rolls off the tongue. Fire that marketing guy! Minishift is for Open Shift 3.9 but not for the latest Open Shift of 4.3.
Follow the instructions here (Install on your Laptop with Red Hat CodeReady Containers), you may have to sign up and become a Red Hat member.
You may want to delete minikube (minikube delete) or at least stop it unless you have an amazing laptop.
Once you install the open shift minishift like tool, aka CRC for short, you will want to run it.

Start Open Shift CRC

crc start --memory=16384


### OUTPUT
...
INFO Starting CodeReady Containers VM for OpenShift 4.2.13...
INFO Verifying validity of the cluster certificates ...
INFO Network restart not needed
INFO Check internal and public DNS query ...
INFO Check DNS query from host ...
INFO Starting OpenShift cluster ... [waiting 3m]
...
INFO
INFO You can now run 'crc console' and use these credentials to access the OpenShift web console
Then you want to add all of the oc tools to the command line. The oc tools work with Open Shift.

Set up the environment variables with oc-env

eval$(crc oc-env)
Next, you will want to make sure you switch contexts using kubecx. If you don't know about kubectx, please see this Kubernetes cheatsheet that I wrote.

Use kubectx to list Kubernetes contexts

kubectx
/api-crc-testing:6443/developer
default/api-crc-testing:6443/kube:admin
k8s-kafka
minikube
sys

Switch context with kubectx default/api-crc-testing:6443/kube:admin

kubectx default/api-crc-testing:6443/kube:admin

Log into OpenShift with oc

oc login -u kubeadmin -p cznQP-pass-pass-pass

### OUTPUT
Login successful.
You have access to 51 projects, the list has been suppressed. You can list all projects with 'oc projects'

Using project "default".

Apply zookeeper yaml file with kubectl and see by default that it fails

kubectl apply -f zookeeper.yaml

### Output
service/zookeeper-headless created
service/zookeeper-service created
poddisruptionbudget.policy/zookeeper-pdb created
statefulset.apps/zookeeper created

## See that the zookeeper-0 has an error
kubectl get pods

### OUTPUT
NAME READY STATUS RESTARTS AGE
zookeeper-0 0/1 Error 0 13s

## describe zookeeper-0 and see about the error
kubectl describe pods zookeeper-0

### OUTPUT
Name: zookeeper-0
Namespace: default
Priority: 0
...
Controlled By: StatefulSet/zookeeper
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 69s default-scheduler pod has unbound immediate PersistentVolumeClaims
Normal Scheduled 69s default-scheduler Successfully assigned default/zookeeper-0 to crc-k4zmd-
...
Normal Pulling 5s (x4 over 62s) kubelet, crc-k4zmd-master-0 Pulling image "cloudurable/kube-zookeeper:0.0.1"
Normal Pulled 2s (x4 over 59s) kubelet, crc-k4zmd-master-0 Successfully pulled image "cloudurable/kube-zookeeper:0.0.1"
Normal Created 2s (x4 over 59s) kubelet, crc-k4zmd-master-0 Created container kubernetes-zookeeper
Normal Started 2s (x4 over 59s) kubelet, crc-k4zmd-master-0 Started container kubernetes-zookeeper
Warning BackOff 1s (x7 over 57s) kubelet, crc-k4zmd-master-0 Back-off restarting failed container
Now you can see that the pod zookeeeper-0 is failing and Kubernetes is trying to restart it but why? Let's look at the logs and find out.

Use kubectl logs zookeeper-0 to see what is going on

kubectl logs zookeeper-0
#This file was autogenerated DO NOT EDIT
clientPort=2181
dataDir=/var/lib/zookeeper/data
dataLogDir=/var/lib/zookeeper/data/log
tickTime=2000
initLimit=10
syncLimit=5
maxClientCnxns=60
minSessionTimeout=4000
maxSessionTimeout=40000
autopurge.snapRetainCount=3
autopurge.purgeInteval=12
server.1=zookeeper-0.zookeeper-headless.default.svc.cluster.local:2888:3888
server.2=zookeeper-1.zookeeper-headless.default.svc.cluster.local:2888:3888
server.3=zookeeper-2.zookeeper-headless.default.svc.cluster.local:2888:3888
Creating ZooKeeper log4j configuration
mkdir: cannot create directory '/var/lib/zookeeper': Permission denied
chown: cannot access '/var/lib/zookeeper/data': Permission denied
mkdir: cannot create directory '/var/lib/zookeeper': Permission denied
chown: invalid group: 'zookeeper:USER'
/usr/bin/start.sh: line 161: /var/lib/zookeeper/data/myid: Permission denied
It looks like the container can't access the volume due to a permissions issue. Recall that the zookeeper.yaml runs the containers with a security context

zookeeper.yaml - securityContext runAsUser and fsGroup

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zookeeper
spec:
selector:
matchLabels:
app: zookeeper
serviceName: zookeeper-headless
...
spec:
...
containers:
- name: kubernetes-zookeeper
image: "cloudurable/kube-zookeeper:0.0.1"
...
volumeMounts:
- name: datadir
mountPath: /var/lib/zookeeper
securityContext:
runAsUser: 1000
fsGroup: 1000
When the volumes are mounted there is an issue with permissions. You could run our instances as root but you don't want to do that as a practice.

Kubernetes is a v1.16.2 while Minikube was at v1.16.0

% kubectl version
Client Version: version.Info{Major:"1", Minor:"16", GitVersion:"v1.16.2", GitCommit:"c97fe5036ef3df2967d086711e6c0c405941e14b", GitTreeState:"clean", BuildDate:"2019-10-15T23:42:50Z", GoVersion:"go1.12.10", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"14+", GitVersion:"v1.14.6+a8d983c", GitCommit:"a8d983c", GitTreeState:"clean", BuildDate:"2019-12-23T12:16:26Z", GoVersion:"go1.12.12", Compiler:"gc", Platform:"linux/amd64"}
You ask, why are we getting a permissions error with Open Shift 4.2 and not Minikube. I don't know, but we are. I do know of one workaround. We can use an initContainer.

zookeeper.yaml - initContainers->init-zoo

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zookeeper
spec:
selector:
matchLabels:
app: zookeeper
...
spec:
...
containers:
- name: kubernetes-zookeeper
imagePullPolicy: Always
image: "cloudurable/kube-zookeeper:0.0.1"
...
volumeMounts:
- name: datadir
mountPath: /var/lib/zookeeper
securityContext:
runAsUser: 1000
fsGroup: 1000
initContainers:
- name: init-zoo
command:

- chown

- -R

- 1000:1000
- /var/lib/zookeeper

image: ubuntu:18.04
imagePullPolicy: Always

name: volume-permissions

resources: {}

securityContext:

runAsUser: 0

terminationMessagePath: /dev/termination-log

terminationMessagePolicy: File

volumeMounts:

- name: datadir
mountPath: /var/lib/zookeeper
Notice the initContainer init-zoo has a command that uses chown to change the ownership of the /var/lib/zookeeper
 folder.
Now all of this works so delete and apply.

Recreate the zookeeper statefulset

kubectl apply -f zookeeper.yaml
kubectl delete -f zookeeper.yaml

Recreate the zookeeper statefulset

kubectl get pods
NAME READY STATUS RESTARTS AGE
zookeeper-0 1/1 Running 0 90s
zookeeper-1 1/1 Running 0 61s
zookeeper-2 1/1 Running 0 30s
Let's log into the OpenShift web console.

Start up OpenShift web console

crc console
## then login to the OpenShift web console

Debugging and testing that it all works

Recall how to do the following in case something didn't work or you want to ensure that the ZooKeeper ensemble is really working:
You should really ensure that it works before you end this tutorial.

Conclusion

You created the same project in Red Hat OpenShift CRC. You then debugged a permissions issue by using an initContainer to change the permissions.

Tutorial Part 3: Kubernetes StatefulSet with ZooKeeper using Kustomize to target multiple environments

$
0
0

Kubernetes StatefulSet with ZooKeeper using Kustomize to target multiple environments

Background

ZooKeeper is a nice tool to start StatefulSets with because it is small and lightweight, yet exhibits a lot of the same needs as many disturbed, stateful, clustered applications.
This is part 3. In part 1 of this tutorial, we got an example of a ZooKeeper StatefulSet running locally with minkube. In part 2, we got the same application running under Red Hat OSE CRC.
This really builds on the last two tutorial and uses Kustomize to target multiple deployment environments. You could probably skip tutorial 2 and this one would still make sense.
Please refer to the
As stated before, I base this tutorial from the one on the Kubernetes site on ZooKeeper and StatefulSet but I am going to deploy to MiniKube, local Open Shift, and KIND as well as add support for Kustomize and a lot more.
I have a similar version of this Kubernetes ZooKeeper deploy working on a multi-node shared, corporate locked-down environment. This is a new version based on the example.
In past tutorials, I simulated some of the problems that I ran into and hope it helps.
If for some reason you would like more background on this tutorial series - background.
BTW, This is not my first rodeo with ZooKeeper or Kafka or even deploying stateful clustered services (cassandra) or managing them or setting up KPIs, but this is the first time I wrote about doing it with Kubernetes. I have also written leadership election libs and have done clustering with tools like ZooKeeper, namely, etcd and Consul.

Objectives

After this tutorial, you will know the following.
  • How to deploy a ZooKeeper ensemble to multiple environments using Kustomize
  • How to create base configs
  • How to create overlays.
Later follow on tutorials might show:
  • How to write deploy scripts with Helm 3 to target local vs. remote deployments
  • How to write deploy scripts with Helm 3 to target local vs. remote deployments
  • How to create your metrics gatherers and use them with Prometheus
  • How to install Kafka on top of ZooKeeper

Before you begin

Before starting this tutorial, you should be familiar with the following Kubernetes concepts.
  • Pods
  • Cluster DNS
  • Headless Services
  • PersistentVolumes
  • PersistentVolume Provisioning
  • StatefulSets
  • PodDisruptionBudgets
  • PodAntiAffinity
  • kubectl CLI
Recall that the tutorial on the Kubernetes site required a cluster with at least four nodes (with 2 CPUs and 4 GiB of memory), this one will work with local Kubernetes dev environments, namely, Open Shift CRC and MiniKube. This tutorial will show how to use Kustomize to target local dev and a real cluster.

Use Kustomize to deploy our ZooKeeper StatefulSet to multiple environments

To target multiple environments we will use Kubernetes Kustomize.
Kustomize is built into Kubernetes. It is the default way to target multiple deployment environments.
Kustomize is a template-free way to customize Kubernetes object files by using overlays and directives called transformers, meta sources, and generators.
In this tutorial we will use:
  • bases
  • commonLabels
  • patchesStrategicMerge
The bases is path list which consists of: directories, URL or git referring to kustomization.yamls. You can think of bases similar to a base image in a Dockerfile (using FROM) or a parent pom in maven or a base class. From this bases you can transform and add additional details. This allows you to layer config and override config declared in the base. In our example, we will extend the base first for dev then later for dev-3 and prod. Let's look a the directory structure.

Where to find the code

You can find the code for this project at:
  • Kustomize - branch that split up the manifest into multiple deployment environments.

Directory structure with base and overlay.

├── README.md
├── base
│   ├── kustomization.yml
│   └── zookeeper.yml
├── container
│   ├── Dockerfile
│   ├── README.md
│   ├── build.sh
│   ├── scripts
│   │   ├── metrics.sh
│   │   ├── ready_live.sh
│   │   └── start.sh
│   └── util
│   ├── debug.sh
│   ├── ...
└── overlays
├── dev
│   ├── kustomization.yml
│   └── zookeeper-dev.yml
├── dev-3
│   ├── kustomization.yml
│   └── zookeeper-dev3.yml
└── prod
├── kustomization.yml
└── zookeeper-prod.yml
Notice we have an overlays directory and a base directory. The base directory will look a lot like our zookeeper.yml manifest file from the last two tutorials. The overlays directory has a directory per target environment, namely, proddev and dev-3. Then you can split the data specific to production (prod) or lightweight dev or our dev-3 (dev but running three zookeeper instances in the ensemble).

Bases and overlays for this tutorial

+---------------+            +---------------------+      +-------------------+
| | overrides | | ovr | |
| base +----------->+ overlay/dev +------> overlay/dev-3 |
| | | | | |
+--------+------+ +---------------------+ +-------------------+
|
|
|
| +----------------------+
| overrides | |
+-------------------------->+ overlay/prod |
| |
+----------------------+

Base Directory

This is a simple example so we just have two files in our base directory.
├── base
   ├── kustomization.yml
   └── zookeeper.yml

base/kustomization.yml - kustomize manifest file for base directory

resources:
- zookeeper.yml

commonLabels:
app: zookeeper
The kustomization.yml file is the manifest file for kustomize. The directive resources: - zookeeper.yml is specifying the yaml file, which looks a lot like the last one from the last tutorial. The commonLabels: app: zookeeper is specifying common labels and selectors so you don't have to repeat the same labels over and over.
The commonLabels sets labels on all kube objects. The commonLabels are applied both to label selector fields and label fields.
The zookeeper.yaml file is just like before except there is nothing specific to prod or dev and there are no labels because we supplied them with commonLabels already.

base/zookeeper.yml - zookeeper manifest with no labels

apiVersion: v1
kind: Service
metadata:
name: zookeeper-headless
spec:
ports:
- port: 2888
name: server
- port: 3888
name: leader-election
clusterIP: None
---
apiVersion: v1
kind: Service
metadata:
name: zookeeper-service
spec:
ports:
- port: 2181
name: client
---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: zookeeper-pdb
spec:
maxUnavailable: 1
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zookeeper
spec:
serviceName: zookeeper-headless
replicas: 1
updateStrategy:
type: RollingUpdate
podManagementPolicy: OrderedReady
template:
spec:
containers:
- name: kubernetes-zookeeper
imagePullPolicy: Always
image: "cloudurable/kube-zookeeper:0.0.4"
resources:
requests:
memory: "500Mi"
cpu: "0.25"
ports:
- containerPort: 2181
name: client
- containerPort: 2888
name: server
- containerPort: 3888
name: leader-election
command:
- sh
- -c
- "start.sh --servers=3 "
readinessProbe:
exec:
command:
- sh
- -c
- "ready_live.sh 2181"
initialDelaySeconds: 10
timeoutSeconds: 5
livenessProbe:
exec:
command:
- sh
- -c
- "ready_live.sh 2181"
initialDelaySeconds: 10
timeoutSeconds: 5
volumeMounts:
- name: datadir
mountPath: /var/lib/zookeeper
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
initContainers:
- name: init-zoo
command:

- chown

- -R

- 1000:1000
- /var/lib/zookeeper

image: ubuntu:18.04
imagePullPolicy: Always

resources: {}

securityContext:

runAsUser: 0

terminationMessagePath: /dev/termination-log

terminationMessagePolicy: File

volumeMounts:

- name: datadir
mountPath: /var/lib/zookeeper

volumeClaimTemplates:
- metadata:
name: datadir
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi
The commonLabels is a kustomize transform as it transforms the ZooKeeper Kubernetes and adds labels and match labels to resources. This transform gets rid of a lot of duplicate code.
Now we want to base another config based on this base. It is like we inherit all of the settings and just override the ones that we want. Let's first do this with dev.

Dev Directory

└── overlays
└─── dev
   ├── kustomization.yml
  └── zookeeper-dev.yml
You want to override the replicas and set it to 1 so we save memory when we run this on our laptop. You want to use the pod affinity preferredDuringSchedulingIgnoredDuringExecution so that we can run this on a local dev Kubernetes cluster that only has one node. You also want to change the command that starts up zookeeper to specify the number of servers in the ensemble, e.g., command: ... start.sh --servers=1. Lastly, you want to run with less memory and less CPU so it fits nicely on our laptop.
To do this we will specify a yaml file with just the things we want to override and then specify that in the kustomization.yml manifest file we want to merge/patch using this file by using patchesStrategicMerge: and bases:. This is another transformation. This time against the output of the base manifest.

dev/kustomization.yml - kustomize manifest used to override base

bases:
- ../../base/

patchesStrategicMerge:
- zookeeper-dev.yml

commonLabels:
deployment-env: dev
Notice that bases specifies the base yaml file from before. Then patchesStrategicMerge specifies a yaml file with just the parts that you want to override. We go ahead and add a dev label.
The patchesStrategicMerge applies patches to the matching Kube object (it matches by Group/Version/Kind + Name/Namespace). The patch file you specify is zookeeper-dev.yml and it contains just the config definitions that you want to override. This keeps files for an environment small as the environment specific file just overrides the parts that are different.
Let's look at the bits that we override.

dev/zookeeper-dev.yml - dev file used to override base

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zookeeper
spec:
replicas: 1
template:
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: "app"
operator: In
values:
- zookeeper
topologyKey: "kubernetes.io/hostname"
containers:
- name: kubernetes-zookeeper
command:
- sh
- -c
- "start.sh --servers=1 "
resources:
requests:
memory: "250Mi"
cpu: "0.12"
Notice we added dev labels to everything, just in case we want to deploy another version at the same time in the same namespace and to show we used the correct manifest.
You don't have to extend just a base image, you can base and overlay on another overlay.
To deploy this run the following:

Deploy dev

# kubectl delete -f zookeeper.yaml from the other branch
kubectl apply -k overlay/dev
The -k option is for Kustomize. You specify the directory of the config that you want to run, which is usually an overlay directory because bases don't usually deploy.
Now would be a good time to refer to the first tutorial and test the ZooKeeper instance (perhaps read and write a key).

Can you overlay and overlay?

Can you overlay and overlay?
For example, let's say we ran into an issue that we can not easily reproduce and we think it is because we use three zookeeper instances in the integration environment but the only one when we are testing locally so you decide to create an overlay that has three images that run locally on your laptop cluster to closer mimic integration. We call this environment dev-3 because we are not very creative.

Dev 3 for testing with ZooKeeper for three node ensemble

You decide to create an overlay that has three images that run locally on your laptop cluster to closer mimic integration to chase down a bug. The files are laid out similar to before.
├── dev-3
│   ├── kustomization.yml
│   └── zookeeper-dev3.yml
The manifest file will overlay the dev environment, and override the number of replicas as well as passing the replica count to the start.sh script of the container.

dev-3/kustomization.yaml

bases:
- ../dev/

patchesStrategicMerge:
- zookeeper-dev3.yml
Noice the base refers to ../dev/ (AKA overlay/dev) so you can see that this overlay is just overriding values from overlay/dev like affinity preferredDuringSchedulingIgnoredDuringExecution so we can still run on a dev cluster.

dev-3/zookeeper-dev3.yml

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zookeeper
spec:
replicas: 3
template:
spec:
containers:
- name: kubernetes-zookeeper
command:
- sh
- -c
- "start.sh --servers=3"
The above just overrides the number of servers passed to start.sh and the number of replicas.

Deploy dev 3

# kubectl delete -k overlay/dev if you want first or not
kubectl apply -k overlay/dev-3
The main reason we did not specify affinity in the very base manifest for ZooKeeper is kustomize does not allow us to remove YAML attributes. It only allows us to add. To add requiredDuringSchedulingIgnoredDuringExecution if preferredDuringSchedulingIgnoredDuringExecution was in the base would require that we remove an attribute. If you need to add / remove attributes based on logic, then you will need a template language like the one that ships with Helm which is a subject of a future tutorial for sure. :)

Prod directory

The prod directory is our fictitious production environment for ZooKeeper. It is laid out much like dev or dev-3. Like dev its base is the base directory.
└── prod
├── kustomization.yml
└── zookeeper-prod.yml
Notice like dev that bases specifies the base directory which contains the base ZooKeeper manifest. Also like before patchesStrategicMerge specifies a yaml file with just the parts that you want to override for production. You of course want to go ahead and add a production label.
Recall that the patchesStrategicMerge applies patches to the matching Kube object (matched by Group/Version/Kind + Name/Namespace). The patch file you specify is zookeeper-prod.yml and it contains just the config definitions that you want to override for production.

prod/kustomization.yaml - kustomize manifest file for prod

bases:
- ../../base/

patchesStrategicMerge:
- zookeeper-prod.yml

commonLabels:
deployment-env: production
Now you just specify the correct affinity for prod and goose up the memory and CPU for the pods.

prod/zookeeper-prod.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: zookeeper
spec:
replicas: 3
template:
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: "app"
operator: In
values:
- zookeeper
topologyKey: "kubernetes.io/hostname"
containers:
- name: kubernetes-zookeeper
resources:
requests:
memory: "500Mi"
cpu: "0.5"
Notice that this is specifies requiredDuringSchedulingIgnoredDuringExecution It also specifies more RAM and CPU per node as well as bumping up the ZooKeeper ensemble count to three.

Deploy prod

# kubectl delete -k overlay/dev-3 if you want first or not
kubectl apply -k overlay/prod
Notice that prod won't run unless you have multiple kubernetes workers so don't be surprised. After, just go back to dev.

Go back to dev

kubectl delete -k overlay/prod
kubectl apply -k overlay/dev

Debugging and testing that it all works

Recall how to do the following in case something didn't work or you want to ensure that the ZooKeeper ensemble is really working (hint: you should test it):
You should really ensure that it works before you end this tutorial.

Conclusion

In this tutorial, you created three overlays and on base config directory. The dev and prod overlays used the base dir base. The dev-3 directory used the dev directory as its base. The overlay directories inherit attributes from their parents (bases). The overlay directories can override or add attributes from its parent. The directive patchesStrategicMerge is used to override attributes from a base set of config. The directive commonLabels is used to transform resources by adding labels to resources and select matches.
Kustomize is a template-free way to customize Kubernetes object files by using overlays and directives called transformers, meta sources, and generators. Kustomize is simpler to use than a full-blown template engine like Helm and allows you to have multiple deployment environments from the same base manifest files.

Bases and overlays for this tutorial

+---------------+            +---------------------+      +-------------------+
| | overrides | | ovr | |
| base +----------->+ overlay/dev +------> overlay/dev-3 |
| | | | | |
+--------+------+ +---------------------+ +-------------------+
|
|
|
| +----------------------+
| overrides | |
+-------------------------->+ overlay/prod |
| |
+----------------------+

MCP Integration: How Brave Search and Claude Desktop Enhance AI Assistant Agentic Capabilities

$
0
0

 

Introduction to Anthropic’s new Agentic MCP Integration

The Model Context Protocol (MCP) has revolutionized how AI assistants interact with external data sources. Introduced by Anthropic in late 2024, MCP enables AI to go beyond its traditional constraints, making it more proactive, contextual, and integrated into our workflows.

This article focuses on setting up the Brave Search MCP plugin for Claude Desktop to empower your AI assistant with advanced web search capabilities. Whether you’re a developer or a casual user, this guide will help you integrate this tool to leverage AI’s full potential.

This is a continuation of Rick’s recent article on Setting up Claude Filesystem MCP, but it’s a standalone guide. First, we’ll delve into an in-depth discussion of MCP. Then, we’ll explore practical hands-on use cases that demonstrate the Brave search connector with the Claude client. This hands-on approach will help you grasp the power of the MCP architecture.

This guide will walk you through setting up Brave Search integration with Claude Desktop using the Model Context Protocol (MCP). Beyond adding search features, you’ll learn about MCP’s potential to transform how AI assistants connect with external tools. The guide covers API key setup, Claude Desktop configuration, integration testing, and troubleshooting. You’ll gain practical experience with MCP while enhancing Claude’s search capabilities.

Learning MCP hands-on is valuable for developers and technologists who want to explore AI agent development. You can apply these principles to build your own MCP integrations that connect AI assistants with databases, APIs, and custom tools. Whether you want to enhance Claude Desktop today or explore the future of AI integration, this guide offers a practical foundation for working with this powerful protocol.

Brave Search MCP Integration Process. Powering Agentic AI.

Unlocking the Power of MCP with Client-Server Architecture

The Model Context Protocol (MCP) represents a significant breakthrough in the integration of artificial intelligence (AI) with external data sources. By establishing a standardized framework for connecting AI systems to tools, repositories, and datasets, MCP eliminates the gap between isolated AI assistants and the information they require to deliver contextually relevant and highly capable results. At the heart of its design lies a client-server architecture that guarantees seamless and secure data access.

Integrating AI Datasource and resources. Powering Agentic AI with Claude MCP.
Integrating Resources with AI

MCP Hosts: Empowering Applications

At the core of the MCP framework are MCP hosts, which act as the central hubs for applications that require data access. Applications like Claude Desktop leverage these hosts to orchestrate and manage multiple connections to various data sources. The host ensures security, enforces permissions, and aggregates the data needed to enhance the AI’s contextual understanding, making tools like Claude more responsive, informed, and powerful.

MCP Hosts Controlling Access to Local and Remote Resources. Powering Agentic AI.
MCP Hosts Controlling Access to Local and Remote Resources

MCP Clients: The Agentic Communication Bridge

At the heart of the MCP framework lies the MCP host, serving as the central hub for applications that necessitate data access. Applications such as Claude Desktop utilize these hosts to orchestrate and manage multiple connections to diverse data sources. The host ensures security, imposes permissions, and aggregates the required data to enhance the AI’s contextual comprehension, thereby making tools like Claude more responsive, knowledgeable, and potent.

MCP Client
MCP Client: routing messages to resources and providing access to data sources

MCP Servers: Unlocking Agentic Functionality

At the heart of the MCP architecture are the specialized servers, known as MCP servers. These servers serve as gateways to various data sources, providing the tools and interfaces necessary for accessing files on local filesystems, retrieving records from databases, and searching the web through Brave Search. Each server is designed to focus on a specific type of resource or function, ensuring modularity and composability within the MCP ecosystem.

Unified Data Access Through MCP Servers. Powering Agentic AI.
Unified Data Access Through MCP Servers

Local and Remote Resources: The Agentic Data Backbone

The true power of MCP lies in its ability to seamlessly connect AI assistants with both local and remote resources. These resources can range from files stored on a user’s computer to remote APIs, databases, or cloud-based services. By integrating these diverse resources into the AI workflow, MCP enables AI systems to access, analyze, and act upon the data required for a wide range of applications.

MCP’s Role in AI Integration with Cloud Services, Local resources, Remote APIs and Data sources. Powering Agentic AI.
MCP’s Role in AI Integration with Cloud Services, Local resources, Remote APIs and Data sources

Bridging AI and Data: The MCP Promise

Together, these components form a cohesive architecture that transforms AI assistants from isolated tools into dynamic agents capable of accessing and utilizing a vast array of data sources. By combining host applications, communication clients, specialized servers, and diverse resources, MCP not only simplifies integration but also expands the horizons of what AI can achieve. With this robust architecture, users can unlock a future where AI works more intelligently, intuitively, and effectively.

Components of MCP Architecture. Servers, Clients, Hosts and access to Resources: APIs, DataSources etc. Powering Agentic AI.
Components of MCP Architecture. Servers, Clients, Hosts and access to Resources: APIs, DataSources etc.

What is Brave Search MCP?

Brave Search MCP serves as a bridge between Claude Desktop and Brave’s privacy-oriented web search engine. This integration seamlessly integrates Claude with MCP’s structured connectivity, granting users effortless access to both web and local searches.

Brave Search MCP offers a range of features, including:

  • Real-time web information retrieval.
  • Context-based local searches for enhanced results.
  • Privacy-respecting AI-assisted research.

Now that we have covered the background information, let’s set up Brave Search with the Claude client. This hands-on experience will help you truly understand the power of MCP once you see it integrate with one of the plugins.

Brave Search API and Claude. Powering Agentic AI.
Brave Search API and Claude.

Prerequisites to provide Agentic Search

Before diving into the setup, ensure you have:

Setting Up Brave Search MCP Agentic Plugin for Claude Desktop

Step 1: Obtain the Brave Search API Key

  1. Visit the Brave Search API.
  2. Sign up or log in.
  3. Select a plan (free tier supports 2,000 queries per month).
  4. Generate your API key and store it securely.

Step 2: Configure Claude Desktop

Locate Configuration File

The Claude Desktop configuration file resides at:

~/Library/ApplicationSupport/Claude/claude_desktop_config.json

Add Brave Search MCP Server Configuration

Open the file in your editor:

code ~/Library/Application\\ Support/Claude/claude_desktop_config.json

Insert or merge the following configuration:

{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/richardhightower/src",
"/Users/richardhightower/clients"
]
},
"brave-search": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-brave-search"
],
"env": {
"BRAVE_API_KEY": "YOUR_API_KEY_HERE"
}
}
}
}

This configuration step is crucial for enabling the Brave Search MCP integration with Claude Desktop. The JSON configuration file specifies how Claude Desktop should interact with the Brave Search service, including the necessary command-line arguments and environment variables.

After adding or modifying this configuration, it’s essential to completely restart Claude Desktop for the changes to take effect. Simply closing and reopening the application window is not sufficient — you must fully quit Claude Desktop (using ⌘+Q on macOS or the equivalent on your operating system) and then relaunch it.

The restart allows Claude Desktop to properly initialize the MCP servers with the new configuration and establish the necessary connections for the Brave Search integration to function.

Remember, replace YOUR_API_KEY_HERE with the API key obtained in Step 1.

If starting from scratch, use:

cat> ~/Library/Application\\ Support/Claude/claude_desktop_config.json << 'EOL'
{
"mcpServers": {
"brave-search": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-brave-search"
],
"env": {
"BRAVE_API_KEY": "YOUR_API_KEY_HERE"
}
}
}
}
EOL

The alternate approach shown above uses the Unix/Linux cat command with a "here document" (heredoc) to create the configuration file in one step. This method is particularly useful when setting up the configuration for the first time, as it creates the entire file structure at once without needing to manually edit an existing file.

The heredoc syntax (&lt;&lt; 'EOL') allows you to input multiple lines of text until the delimiter (EOL) is reached. The single quotes around EOL prevent variable expansion, ensuring the configuration is written exactly as specified.

This command will create the configuration file in the correct location with the proper JSON structure. Remember to replace YOUR_API_KEY_HERE with your actual Brave Search API key before running the command.

Step 3: Restart Claude Desktop

After saving changes:

  1. Quit Claude Desktop (⌘+Q on macOS).
  2. Relaunch the application to load the new configuration.

Testing the Integration

Basic Commands

  1. Web Search:
Can you search forrecent trends in generative AI?

2. Local Search:

Find restaurants near [your location].

Verification

Observe whether Claude retrieves relevant results. If issues arise, refer to the troubleshooting section.

Don’t worry, I will walk you through a couple of sample sessions with the Claude Desktop and Brave search.

Troubleshooting Common Issues

Configuration Not Loading

  • Verify JSON Syntax: Use a JSON validator to check for errors.
  • Permissions: Ensure proper read/write permissions for the configuration file.
  • Correct Path: Double-check the file’s location.

API Key Problems

  • Ensure the API key is accurately copied without extra spaces.
  • Verify the key’s status in the Brave Search dashboard.

Node.js Issues

  • Check installation: node --version.
  • Reinstall Node.js if necessary.

Enhancing Security

To enhance security and protect sensitive data:

  • Store API keys securely in environment variables or encrypted vaults.
  • Rotate API keys regularly to reduce security risks.
  • Restrict configuration file access to trusted users only.

Sample Session with Claude

Me (RH):

Can you search for recent news about artificial intelligence?

Claude:

I’ll search for recent news about AI using the Brave search function.

Let me search for some more specific recent developments.

Based on these search results, I can provide an overview of recent AI developments and regulatory trends:

  1. Regulatory Landscape: 2024 has seen significant movement in AI regulation:
  • At least 40 U.S. states, along with Puerto Rico, the U.S. Virgin Islands, and D.C., have introduced AI-related legislation
  • The European Union has established comprehensive rules through the AI Act and AI Liability Directive
  • The African Union is developing a continent-wide AI strategy to protect consumers and help African countries compete in the AI space

…. it continues on

Would you like me to explore any of these areas in more detail? I can search for more specific information about any particular aspect that interests you.

Another Sample Session using Brave Search and Claude Desktop

RH:

Can you find pizza restaurants near me?

Claude:

I notice you’d like to find pizza restaurants, but I can’t determine your location from our conversation. To find pizza places near you, I’ll need you to specify a location — it could be a city, neighborhood, landmark, or address. Could you please provide that information?

RH:

I live in Manor Texas

Claude:

I’ll search for pizza restaurants in Manor, Texas using the local search function.

View Result from brave_web_search from brave-search (local)

Based on the search results, here are some pizza options in Manor, TX:

  1. GRAZIE Pizzeria — A local pizzeria that’s highly rated on Yelp
  2. DoubleDave’s Pizzaworks
  • Location: Manor location
  • Known for: Peproni Rolls®, Philly Cheesesteak Stromboli, and specialty pizzas
  • Services: Dine-in, Delivery, and Carry-Out available

3. Domino’s Pizza

  • Address: 11211 US Highway 290 E, Ste 400
  • Hours: Open until midnight
  • Services: Delivery and takeout available
  • Menu includes pizza, wings, sandwiches, and salads

4. Pizza Hut

  • Address: 11300 Highway 290 East Bldg 3
  • Hours: 11:00 AM — 11:00 PM
  • Services: Carryout and delivery

5. Little Caesars

  • Address: 12836 Hwy 290
  • Phone: (737) 357–6622

Each of these places offers different specialties and service options. Would you like more specific information about any of these restaurants, such as their menus, pricing, or customer reviews? I can also search for other local pizzerias if these don’t meet your preferences.

Sequence Diagram: Brave Search MCP Server Integration

The following sequence diagram illustrates how the Brave Search MCP server processes requests through two sample sessions. It shows the flow of communication between the HostClient, and Server components.

Sequence Diagram: Communication Flow

Communication Flow for Claude Desktop and Brave Search MCP

The sequence diagram above illustrates the dynamic communication flow between system components:

  • Users initiate requests through the Claude Desktop interface (Host)
  • The MCP Client processes and transforms these requests into standardized API calls
  • Brave MCP Server acts as an intermediary, coordinating with external APIs and data sources
  • Information flows back through the chain, ultimately presenting results to users

System Architecture: Brave Search MCP Integration

Brave Search / Claude Desktop Client System Architecture

Block Diagram:

  • Illustrates the modular architecture:
  • The Host (Claude Desktop) provides the user interface and communicates with the Client.
  • The Client manages protocol-specific interactions with the Brave MCP Server.
  • The Brave MCP Server connects with external APIs (Brave Search API) and local resources to retrieve data.

The diagrams above illustrate how the integration brings advanced search capabilities to the Claude Desktop environment.

Finding more information about MCP

You can begin building and testing MCP connectors right away. If you’re a Claude for Work customer, you can test MCP servers locally and connect Claude to your internal systems and datasets. With Anthropic’s recently released developer toolkits, you can immediately start using the existing servers for Claude Desktop, significantly expanding Claude’s capabilities and reach.

Here’s how to get started building with MCP:

The following MCP Server Plugins are available:

  • Filesystem — File operations with configurable access controls
  • GitHub — Repository management, file operations, and GitHub API integration
  • GitLab — GitLab API, enabling project management
  • Git — Read, search, and manipulate Git repositories
  • Google Drive — File access and search capabilities for Google Drive
  • PostgreSQL — Read-only database access with schema inspection
  • SQLite — Database interaction
  • Slack — Channel management and messaging
  • Memory — Knowledge graph-based persistent memory system
  • Puppeteer — Browser automation and web scraping
  • Brave Search — Web and local search using Brave’s Search API
  • Google Maps — Location services, directions, and place details
  • Fetch — Web content fetching and conversion for efficient LLM usage

Conclusion

Integrating Brave Search with Claude Desktop revolutionizes AI-assisted tasks, enabling users to access real-time, contextual insights directly from the web or their local environment. By bridging the gap between AI and diverse data sources, this plugin transforms Claude into a formidable tool for productivity, research, and decision-making.

Embrace the future of AI with MCP and unlock a more intelligent approach to work, learning, and creation.

Rick Hightower, a seasoned software architect and technology innovator with over three decades of experience in enterprise software development, holds a TensorFlow Certified Developer certification and completed a specialization in Machine Learning at Stanford University. His unwavering dedication to advancing AI and data engineering technologies has earned him recognition within the Java ecosystem, where he was named a 2017 Java Champion.

As a prominent figure in the Java community, Rick has authored technical publications, contributed to open-source initiatives, and held leadership positions, including a stint as an executive at a Fortune 100 technology company.

In recent years, Rick has been at the forefront of AI innovation, focusing on large-scale data engineering and the development of AI-driven solutions. He has spearheaded groundbreaking projects, such as:

  • AI Document Processing: Rick leveraged AWS Bedrock and generative AI to extract data from documents and generate deliverables within minutes, significantly reducing the traditional months-long process.
  • RAG-based Expert Systems: He designed and deployed Retrieval-Augmented Generation (RAG) systems using LLMs like ChatGPT and Claude, integrating them with Google Cloud for efficient document and code repository indexing.
  • Meeting Tools: Rick and Chris developed tools that enable real-time question detection, meeting summarization, and action item extraction, substantially enhancing workplace productivity.
  • AR/VR Scalability: He collaborated with a startup to scale backend services on Azure Kubernetes Service (AKS), successfully deploying seven services with advanced load balancing and caching mechanisms.
  • Legal AI Automation: Leveraging artificial intelligence (AI), Rick analyzed legal documents, extracting valuable insights at a significantly reduced cost. This resulted in substantial efficiency gains for his clients.

As an active mentor and thought leader, Rick actively participates in conferences, sharing his insights on the convergence of AI, data engineering, and enterprise software. His practical approach to addressing complex challenges has earned him a distinguished reputation within the technology community.

Recent articles by Rick

  1. Articles Overview
  2. Setting up Claude Filesystem MCP
  3. Setting up Brave Search MCP with Claude Desktop Client
  4. Introduction to Anthropic MCP Architecture
  5. Rick and Chris Review Machine Learning
  6. Streamlit Adventures Part 5 Article
  7. Streamlit Part 4: Form Validation Part 2
  8. Streamlit Part 3 — Form Validation Part 1
  9. Advanced SQL Techniques for ETL
  10. Streamlit Part 2: Layouts, Components, and Graphs
  11. Conversation About Streamlit While Walking in the Park
  12. PrivateGPT and LlamaIndex
  13. OpenAI’s Latest Developments
  14. AI-Powered Knowledge Base for Product Managers
  15. ChatGPT at Scale on Azure Cloud
  16. Prompt Engineering with CoT and Synthetic Prompts Part 2
  17. Understanding LLMs and Using Chain of Thoughts
  18. Meta’s Llama 2 AI Model
  19. ChatGPT Embeddings and HyDE for Improved Search
  20. Rick wrote all of the blog posts and articles on the Cloudurable site

Setting up Claude Filesystem MCP

$
0
0

 The Model Context Protocol (MCP) is a big deal in artificial intelligence. It was introduced on November 25th, 2024, and it’s like a universal connector for AI systems. Before MCP, AI assistants were like chefs with only one ingredient — their own capabilities. But now, with MCP, AI assistants have a “fully stocked pantry” of information to work with. This means they can do more and better things for us.

MCP works by connecting AI systems to different data sources, like Google Drive, Slack, GitHub, and even local databases. This makes it easier for AI to access and use the information it needs to do its job. For example, if you ask your AI assistant to analyze sales data from a local database, the assistant sends a request to the MCP server, which gets the data and sends it back to the assistant. It’s like ordering takeout — the AI assistant places the order, the MCP server delivers the data, and the database is the restaurant.

Anthropic, the folks behind MCP, have released some really cool stuff to make it easier for developers to use: specs and software development kits (SDKs) for developers, local MCP server support in their Claude desktop apps, and an open-source repository of pre-built MCP servers for platforms like Google Drive, Slack, and GitHub. The SDKs are like toolboxes that give developers everything they need to integrate MCP into their apps and make sure AI systems can talk to different data sources easily. The fact that it’s open-source means that the community can work together to make MCP even better and more widely used.

In short, MCP is going to make a big difference in how we use AI in our daily lives. It’s going to make it easier for AI assistants to access and analyze all sorts of data, which means we’ll be able to make better decisions and get more done. By solving the problems of data accessibility and integration, MCP is paving the way for more autonomous and intelligent AI systems.

The best way to understand MCP’s power is to try it yourself. Let me walk you through setting up the filesystem MCP local server to access your file system. Then we’ll explore what it can do using the Claude client.

I’ll help you set up the MCP filesystem plugin for Claude Desktop on your MacBook Pro with clear, step-by-step instructions. Before setting up Local MCP server support, you’ll need to install the Claude Desktop apps. While these instructions focus on Mac, you can easily adapt them for Windows or Linux.

Setting Up MCP Filesystem Plugin for Claude Desktop

Prerequisites

Before we begin, let’s verify you have the required components:

1. Node.js Installation

Check if Node.js is installed:

node --version

If not installed, download from https://nodejs.org/

2. Configuration Directory

Verify the configuration directory exists:

ls ~/Library/Application\\ Support/Claude/

3. Configuration Setup

Create the configuration file:

touch ~/Library/Application\\ Support/Claude/claude_desktop_config.json

4. Directory Access

Choose which directory Claude should access:

  • Desktop
  • Documents
  • A specific project folder

Configuring MCP Filesystem for ~/src Directory

Step 1: Directory Setup

Create the src directory if it doesn’t exist:

mkdir -p ~/src

Step 2: Configuration File

Open the configuration file:

code ~/Library/Application\\ Support/Claude/claude_desktop_config.json

Add this configuration:

{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/YOUR_USERNAME/src"
]
}
}
}

Replace YOUR_USERNAME with your macOS username. To find your username, run:

echo$HOME

Step 3: Restart Claude

  • Quit Claude Desktop completely (⌘+Q)
  • Relaunch the application

Step 4: Testing

Verify the setup by asking Claude either:

  1. “Can you list the contents of my src directory?”
  2. “Can you tell me what files are in ~/src?”

Troubleshooting

  1. Verify the configuration file syntax is correct (valid JSON)
  2. Ensure the path exists and is accessible
  3. Check Claude Desktop logs for any errors
  4. Make sure Node.js is installed properly

Sample Claude Session

Let’s put this configuration to the test with some real-world examples of Claude’s filesystem capabilities.

Me:

Can you list the contents of my src directory?

Claude:

I’ll help you list the contents of the src directory. First, let me check if we have access to it.

Great, I can see we have access to the src directory. Let me list its contents for you.

I can see you have quite a few projects in your src directory! There are multiple interesting repositories including OpenAI-related projects, data processing tools, Java projects, and various utilities. The directory contains:

  • Several AI/ML related projects (OpenAIJava, openai, openai-java, openai-python)
  • Java-related projects (boon, jackson-core, java-json-benchmark)
  • Documentation and diagram tools (diagrams, doc_gen, structurizr-lite)
  • Data processing projects (java_to_csv, jparse)
  • Cloud/infrastructure projects (azure, terraform-cdk)
  • Development tools and utilities

Would you like me to examine any specific directory or file in more detail?

Me:

Could you create a table listing the projects in my src folder with their names, descriptions, and primary programming languages?

Claude:

I’ll help create this table. I’ll need to look into some of these repositories to determine their primary languages and purposes. Let me look through them systematically.

Project Name Description Primary Language OpenAIJava OpenAI API client library for Java Java amazon-timestream-tools Tools for working with Amazon Timestream Java boon JSON parser and object serialization library Java code_prompts Collection of prompts for code generation Markdown confluence_webscraper Tool for scraping Confluence pages Python demucker Code cleanup and refactoring tool Java diff_patch Tool for handling code diffs and patches Java doc_gen Documentation generation tool Python event_bus_spike Event bus implementation prototype Java gpt4all Integration with GPT4All models Python jai Java AI integration project Java java-json-benchmark Benchmarking tool for Java JSON parsers Java java_to_csv Tool for converting Java objects to CSV Java jparse JSON parsing library Java meeting_buddy Meeting assistance/management tool Python mvn_scan Maven project analysis tool Java nats.java NATS messaging system Java client Java openai OpenAI integration project Python openai-java Another OpenAI Java client implementation Java openai-python OpenAI Python integration project Python project-markdown-creator Tool for generating project documentation Python resume-writer Automated resume generation tool Python sharpJParse JSON parser implementation C# streamlit_file_viewer File viewing web application Python structurizr-lite Lightweight version of Structurizr Java system_architect System architecture design tool Python terraform-cdk Work with Terraform CDK TypeScript testcontainers-java Fork or work with Testcontainers Java tutorial_creator Tool for generating tutorials Python windsurf Trying out AI enabled IDE Java

I’ve created a table of the major projects I could identify in your src directory. Some notes about this analysis:

  1. I focused on the main project directories and excluded temporary or output directories
  2. The descriptions are based on the repository names and typical purposes of similarly-named projects
  3. Primary languages are inferred from the repository names and typical implementations

Would you like me to:

  1. Examine any specific project in more detail?
  2. Add more details about any particular projects?
  3. Look into the actual contents of any projects to verify the language and purpose?

Sample Claude Session — Getting it to generate a User Guide

Let’s actually get Claude to do something useful with one of these projects. Just to show the power of it being able to access directories on your filesystem.

RH:

In my ~/src folder I have a project called project-markdown-creator, please do a little write up on to use this project and how to configure it to ignore a directories called build.

Claude:

I’ll help you examine the project and create documentation for it. First, let me check the contents of the project.

Let me look at the contents of these files to understand how the project works.

Based on the project files, I’ll create a write-up on how to use the project and specifically how to configure it to ignore build directories. Here’s a comprehensive guide:

Claude then proceeded to generate this guide which looks correct:


# How to Use project-markdown-creator

The project-markdown-creator is a Python utility that generates a single markdown file from your project's codebase. Here's how to use it and configure it to ignore build directories:

## Basic Setup and Installation

1. Install the script by either:
```bash
pip install .
```

Or create a symbolic link:
```bash
ln -s $(pwd)/create_project_markdown.py /usr/local/bin/create-project-markdown
```


## Configuring to Ignore Build Directories

There are three ways to make the script ignore build directories:

### 1. Using the Configuration File (Recommended)

Create a `.pmarkdownc/config.yaml` file in your project root with:

```yaml
forbidden_dirs:
- build
- dist
- node_modules
- __pycache__
- cdk.out
- env
- venv
```


### 2. Using .gitignore

The script automatically respects your `.gitignore` file. Simply add:

```
build/
```


to your `.gitignore` file.

### 3. Using Command Line Arguments

Use the exclude pattern when running the script:

```bash
create-project-markdown --exclude "build/"
```


## Basic Usage

1. Navigate to your project directory
2. Run the script:
```bash
create-project-markdown
```


### Advanced Usage Options:

```bash
create-project-markdown [--include REGEX] [--exclude REGEX] [--outfile FILENAME] [--log LOG_LEVEL]
```


-`--include` or `-i`: Include specific files (e.g., `".*\.py$"` for Python files only)
-`--exclude` or `-e`: Exclude specific patterns
-`--outfile` or `-o`: Specify output filename (default: `project_structure.md`)
-`--log` or `-l`: Set logging level (DEBUG, INFO, WARNING, ERROR)

## Example Configuration

Here's a complete configuration example that ignores build directories and common development artifacts:

```yaml
# .pmarkdownc/config.yaml
supported_extensions:
.py: python
.java: java
.js: javascript
.ts: typescript
forbidden_dirs:
- build
- dist
- node_modules
- __pycache__
- .git
- cdk.out
- env
- venv
project_path: .
include_pattern: null
exclude_pattern: null
outfile: project_structure.md
log_level: INFO
```


## Tips for Working with Build Directories

1. The script automatically creates a default configuration if none exists
2. It's best to combine multiple approaches:
- List build directories in `forbidden_dirs`
- Include them in `.gitignore`
- This provides redundancy and ensures they're consistently ignored

## Output

The script will generate a markdown file (default: `project_structure.md`) that contains:
- Project structure
- Contents of all included files
- README contents at the start of each directory (if present)
- Proper syntax highlighting based on file extensions

Would you like me to elaborate on any particular aspect of the configuration or usage?

Let me tell you how Claude did with the task I gave it. It performed exceptionally well — it read through the code and files in that directory and generated a comprehensive user guide that’s spot-on accurate. Take a look at the project that Claude reverse engineered and you’ll see for yourself that it got everything right.

Finding more information about MCP

You can start building and testing MCP connectors right away. If you’re already a Claude for Work customer, you can test MCP servers locally and connect Claude to your internal systems and datasets. Claude released developer toolkits. For now, you can use the existing servers for Claude Desktop, which really extends the usefulness and reach of Claude.

To start building:

Here are the available MCP Server Plugins from the above site:

  • Filesystem — File operations with configurable access controls
  • GitHub — Repository management, file operations, and GitHub API integration
  • GitLab — GitLab API, enabling project management
  • Git — Read, search, and manipulate Git repositories
  • Google Drive — File access and search capabilities for Google Drive
  • PostgreSQL — Read-only database access with schema inspection
  • SQLite — Database interaction
  • Slack — Channel management and messaging
  • Memory — Knowledge graph-based persistent memory system
  • Puppeteer — Browser automation and web scraping
  • Brave Search — Web and local search using Brave’s Search API
  • Google Maps — Location services, directions, and place details
  • Fetch — Web content fetching and conversion for efficient LLM usage

Build your own custom MCP server — the possibilities are limitless!

Conclusion

Setting up the Claude Filesystem MCP is a must-do to boost Claude’s ability to interact with your local files and directories. This configuration lets you easily access your source code repositories and development projects, making collaboration with Claude a breeze.

With this setup, Claude can:

  • Access and analyze your source code directly
  • Help with code reviews and documentation
  • Assist in project management and organization
  • Provide context-aware support for your development work

Author Bio

Rick Hightower is a seasoned software architect and technology innovator with over three decades of enterprise software development experience. A prominent figure in the Java ecosystem, he has authored multiple books and technical articles while contributing to various open-source projects and specifications. Recently, Rick has focused on AI implementation and data engineering, developing innovative solutions that bridge traditional enterprise systems with cutting-edge AI technologies. He is known for his contributions to microservices architecture and cloud computing platforms, and serves as a 2017 Java Champion and former tech executive at a Fortune 100 company.

Beyond his technical roles, Rick is an active mentor and technology evangelist who frequently speaks at conferences and writes about the intersection of AI, data engineering, and enterprise software development. His practical approach to technology implementation and ability to bridge complex technical concepts with real-world applications have made him a respected voice in the technology community.

His recent experience includes the following:

In 2024:

  • Data engineering and large scale ETL using AWS Glue, AWS EventBridge for integration platform. Wrote deployment scripts in AWS CDK and Terraform CDK as well as Helm to deploy AWS MSK (Kafka), AWS EKS (K8s), Lambda, etc.
  • Worked on AI assisted document data extraction then used GenAI to produce artifacts in minutes that took months using AWS BedRock.
  • Implemented an AI based Subject Matter Expert (SME) system using various Large Language Models (LLMs), Vector Databases, and frameworks, including LLamaIndex, ChatGPT, Perplexity, and Claude.
  • Developed a React frontend, a middleware layer in Go, and a Retrieval-Augmented Generation (RAG) Agent LLM layer in Python using LLamaIndex.
  • Deployed the system on Google Cloud Platform (GCP) using AlloyDB, GCS buckets, and Google Cloud Run. System indexed documents dropped into GCP as well as git code repositories.
  • Focused on the RAG Agent system, deployment, system integration, UI, and middleware.
  • Transitioned the initial Flask-based RAG system to GRPC and Google Pub/Sub for scalability.
  • Worked on Auth0 integration from client to backend services using JWT tokens.
  • Wrote a tool to detect various types of questions and answer them in real time during meetings.
  • Wrote a tool to summarize meetings, extract decisions, topics, and action items.
  • Collaborated with a startup on their AR/VR system, focusing on scaling the backend services in Azure.
  • Wrote TypeScript CDK Terraform deployment scripts to deploy services to Azure Kubernetes Service (AKS).
  • Utilized Azure-managed Redis and Azure-managed MySQL for data storage and caching.
  • Deployed a total of 7 services and developed a custom client discovery mechanism to expose services and configurations to clients in Azure.
  • Conducted load testing and implemented horizontal pod scaling and vertical scaling to ensure system performance and reliability in AKS/K8s.
  • Configured the Application Gateway with the AGIC (Application Gateway Ingress Controller) component running in AKS to expose services using Layer 7 load balancing.
  • Leveraged AKS/Azure load balancing for Layer 4 load balancing to distribute traffic effectively and enabled UDP based load balancing.
  • Worked on Auth0 integration from client to backend services using JWT tokens.

2023:

  • Utilized AI and prompt engineering to evaluate legal documents, extract entities, and perform thorough analysis at a fraction of the cost compared to a legal team.
  • Created a demo for investors and potential clients to showcase the automated process.
  • Developed AI solutions using OpenAI API for documentation generation and sensitive document analysis. Created entity extraction and classification tools, implementing Chain of Thought reasoning and synthetic prompts. Enhanced search capabilities using HyDE with Text Embeddings and vector sorting. Implemented vector databases for code base and product documentation analysis. Developed feedback validation tools that improved AI output accuracy from 70% to 90%, reducing three months of work to four hours.
  • Served as Acting Senior Director of backend eCommerce site, providing engineering management consulting focused on risk mitigation and staff augmentation.
  • Worked with AWS, Scala, Java, JavaScript, COTS, and platform re-engineering.
  • Employed AI and prompt engineering to evaluate legacy systems, write documentation/diagrams, and extract requirements.
  • Engaged in recruiting, site reliability, disaster recovery, business continuity, and mentoring.
  • Developed software for a security company site using AWS, CI/CD, React, Element, Kubernetes, Java, and Terraform.
  • Wrote integration pieces between a company and the US government.
  • Utilized AI and prompt engineering to document code with documentation and diagrams.
  • Worked with embedded systems, cloud technologies, and hold a security clearance.

Recent articles by Rick

  1. Articles Overview
  2. Setting up Claude Filesystem MCP
  3. Setting up Brave Search MCP with Claude Desktop Client
  4. Introduction to Anthropic MCP Architecture
  5. Rick and Chris Review Machine Learning
  6. Streamlit Adventures Part 5 Article
  7. Streamlit Part 4: Form Validation Part 2
  8. Streamlit Part 3 — Form Validation Part 1
  9. Advanced SQL Techniques for ETL
  10. Streamlit Part 2: Layouts, Components, and Graphs
  11. Conversation About Streamlit While Walking in the Park
  12. PrivateGPT and LlamaIndex
  13. OpenAI’s Latest Developments
  14. AI-Powered Knowledge Base for Product Managers
  15. ChatGPT at Scale on Azure Cloud
  16. Prompt Engineering with CoT and Synthetic Prompts Part 2
  17. Understanding LLMs and Using Chain of Thoughts
  18. Meta’s Llama 2 AI Model
  19. ChatGPT Embeddings and HyDE for Improved Search
  20. Rick wrote all of the blog posts and articles on the Cloudurable site

    Viewing all 213 articles
    Browse latest View live