Jumping Down The Rabbit Hole: IOTA, Docker, Kubernetes, and IBM Bluemix (Part 1)
In the first week of February I began my five month Blockchain internship at TheLedger in Amsterdam. The focus of the internship is to research the cryptocurrency IOTA and the Tangle, a directed acyclic graph (DAG) for storing transactions, which I’ll be using to develop an IoT related project. The amount I have learned since the internship began is more than I could have imagined, I’m very grateful for the opportunity! Part of the project I am working on required me to set up an IOTA private testnet for development, allowing complete control of the network difficulty and token distribution. Seeing 2,779,429,626,277,761 IOTA sitting in your wallet is a fun sight, even if they have no value.
For the private network to work, transactions would have to be confirmed. This meant running a Coordinator to create periodical milestones, lowering the Proof-of-Work minimum weight magnitude, and running a transaction generator to increase confirmation time and network transactions per second. After having that running locally with about six shells open in the background to make it work, I knew I needed to make it easier to use. A detailed list of commands needed to start up the various components usually sat open in the background while I worked. Not long after I showed it running, it was decided it should be run on IBM Bluemix using Kubernetes to be able to access it anywhere. At that point I had no experience with IBM Bluemix and had never heard of Kubernetes clusters beyond the occasional ad on the Docker homepage. It was advised to use Docker to accomplish that task, something I’d heard of before, but never learned much about. This two part article will detail the various difficulties and eureka! moments I had while figuring out how to make it work as intended, how the issues were overcome, and what reference material was useful. There’s also a basic introduction to each new component in case you, the reader, are new to Docker, Kubernetes, or IBM Bluemix. Unknown terms related to IOTA can probably be found in the IOTA glossary.
Throw the IOTA private testnet in Docker, wrap it together with docker-compose, test it, and finally push the images to IBM’s Bluemix and run them within a Kubernetes cluster. All the required sofware (IOTA’s IRI, TX generator, and the Coordinator) should run in the cloud and be accessible from anywhere. Improving my knowledge of Docker seemed like a great place to start!
Docker and Dockerfiles
So what is Docker and why would you need it? Visiting their website and taking a look at the What is Docker page, it is described as a container platform to “build, secure and manage the widest array of applications from development to production on both premises and in the cloud”. Or in layman’s terms: You can package software in ‘containers’ which include all the needed dependencies. This means that you can ensure that your software will work seamlessly in any environment as long as it supports Docker. No need to create a long README detailing the 37 steps needed to set up an environment for compatibility! For example, a Java jar can be packaged into a container and it will run anywhere without the need to install a JRE.
Sounds great! So how is it implemented? First you should install Docker on your system. Instructions can be found here. Docker images (which containers are based on) are built using a ‘Dockerfile’, a text file describing the steps, commands, and dependencies that a program requires to run. Dockerfiles should be in the root directory of your project. A very simple Dockerfile to containerize a Java jar looks like this:
FROM java:8 MAINTAINER Charlie van Rantwijk <email@example.com>COPY app.jar /usr/src/app.jarENTRYPOINT ["java", "-jar", "/usr/src/app.jar"]
To someone who has never used a Dockerfile before, it can quickly become overwhelming to try to understand the various keywords used. The example above has four keywords; the first is FROM, stating which base image the build should begin with. Since I want to run a jar file, I went with the java:8 image. You can find thousands of base images on the Docker Hub website, including interesting choices such as the Alpine Linux image, a minimal Docker image based on Alpine Linux with a complete package index, only 5MB in size! The second keyword, MAINTAINER, gives the ability to include your information. Next is COPY, used to (unsurprisingly) copy local files into the Docker image. In the example above COPY is used to include the local jar file (app.jar) into the image at the path /usr/src/app.jar. Finally, the ENTRYPOINT keyword is used to pass a command array to the image when it starts up. Each command argument should be in separate quotations with a command in between.
Command Line: java -jar /usr/src/app.jar
Dockerfile: ENTRYPOINT ["java", "-jar", "/usr/src/app.jar"]
Now the software, as well as the Dockerfile describing how to build an image for it, is ready. What’s next? Building the image! Open a terminal/command prompt and navigate to your project’s root directory where the Dockerfile is located. Use “docker build” to start the packaging process. More information on the supported arguments can be found at the Docker Build page.
Problems, Problems, Problems
Similar to the short example above, I followed dozens of tutorials during my Docker crash course. It’s wonderful when all the commands work without error. Of course, it’s never that easy. While setting up my work environment for Docker, even the simplest “Hello World” container refused to run. The very foundation of the enormous proverbial mountain ahead was not working correctly. How could I ever learn this technology well enough to complete the tasks ahead? What about the other words I had seen such as Kubernetes and Bluemix? Typical feelings of impostor syndrome (something most interns experience, I later found) quickly emerged during lunch as I sat silently on my phone Googling “Docker fails to run hello world” and other similar search terms. Eventually I stumbled upon an ancient Stack Overflow post listing the different Docker versions available. I suspected the one I installed via apt may be docker.io, an old and unsupported version. Checking my installation, docker.io was indeed the culprit! A quick uninstall and an update to the latest version of docker-ce later and I had the Hello World container running successfully.
With Docker installed and containers running as they should, I was ready to begin assembling the Dockerfile and image I would need for the IOTA private testnet. Working with virtual machines through software such as VirtualBox or VMware in previous work environments had given me a skewed view of what the core purpose of containers are. Instead of neatly containerizing the various parts of the testnet, I attempted to throw everything into one single image. Oh boy. Starting with a base Ubuntu image, I used the -it flag to invoke an interactive shell. The list of software and dependencies I would need was quite large, but nevertheless I went through the process of installing packages such as git, jdk, npm + node 8, mvn, and many more in addition to the IRI source and other project files. A monstrous behemoth of an image file was born, 2 GB in size and more build steps than I want to admit. I “didn’t know what I didn’t know”, this was progress! Due to it all running in one image I didn’t even have to worry about any networking configurations between the testnet components. Excellent.
What Was In That Documentation Anyways?
Excitement about my grand accomplishment soon faded when I started up a container based on the image for the first time. The Coordinator started up immediately, failed to make a connection to IRI, and promptly died. The TX generator suffered the same fate, leaving IRI to uselessly begin accepting connections about 20 seconds later. Without the Coordinator you can’t connect to the IRI node with a wallet! Sure, I could start up the container and interactively restart the failed programs every time, but monitoring what failed is difficult without having to access the shell each time. Better yet, each time I changed a single line of code somewhere in the testnet, IRI would have to chug along recompiling long enough for me to grab a coffee, and the Tangle history would be wiped clean (Volumes? Hadn’t heard of them). After spending a weekend watching hours upon hours of Docker and Dev-Ops videos I knew what I had to do: split the network back up into separate folders and use docker-compose to manage them in separate containers!
With docker-compose, you write a docker-compose.yaml configuration file detailing what to start up, which containers depend on others, what to do if one of the containers dies, in addition to many other features to precisely control deployment. The services defined in the yaml file can be accessed by other containers by using the service’s name (eg. http://iri:14265). Looking at the Getting Started guide and the example config file there shows a good use of various configuration settings. The config file I used to eventually get the testnet online ended up being about half the size of the example. Now Docker finally clicked for me! I quickly began trying out various options from Docker Hub and attempted installing it on other systems such as the ARM based Raspberry Pi 3, and an old Android phone running Android 4.4 by using Linux Deploy to configure Debian stretch. Unfortunately the phone’s kernel version was too low to run dockerd, Docker’s daemon.
The eventual combination of Udemy courses, online documentation, and help from colleagues helped me get to a point where I knew what Docker was and how to use it. Managing multiple containers and internal networking were no longer issues. Additionally, having seen well commented configuration examples allowed me to create a working local IOTA private testnet with a single command.
#: docker-compose upRecreating iri ... done
Recreating coordinator ... done
Recreating txgen ... done
When I started out learning about Docker, a few key things would have saved me hours of time and prevented occasional frustration. Here’s a list of what they were and where I found a helpful explanation.
- Installing the right version of Docker!
- The differences between virtual machines and docker containers.
- The difference between Docker images and Docker containers.
- What commands are available and how to use them.
- A clear explanation of how to create a Dockerfile and what the options mean in the syntax used.
- How to create a docker-compose.yaml file and what it can do.
- What the internal network looks like and how to expose ports.
- How to view the logs of running containers to see what’s going, especially the -f flag.
In Part 2 I will focus on the fun I had diving into Kubernetes and IBM Bluemix. I almost gave up and reverted to a virtual machine, and lost a lot of time to finding the correct combination of the NodePort’s IP and port number.
Thank you for taking the time to read! If you have any remarks or additions, I’d love to hear them.