
Update 05/25/2019: H A S K E L L
The application talked about in this article was originally hastily written using Node.js. I have ported it to Haskell+warp. The post has been updated as a result. I also made it even easier to build and run this application with some scripts, so all you have to do now is copy and paste 4 commands to get this running!
Before we get started, you should check out the functionality of this very simple example application that we'll be using as an example:
This link calls a relatively simple Haskell application that generates a secure password. The password is two words chosen at random from a predefined dictionary, 4 random numbers [0..9], and then a random character from the following: ['!', '@', '#', '$', '%', '&']. This password generator was ported from a Node.js implementation that I hastily threw together for work, and this Haskell version is way nicer:
{-# LANGUAGE OverloadedStrings #-}
module Main where
import System.IO
import System.Random
import Control.Monad
import Network.Wai
import Network.Wai.Handler.Warp
import Network.HTTP.Types (status200)
import Network.HTTP.Types.Header (hContentType)
import Data.ByteString.Lazy.Char8 as BS (pack)
import Data.Char (toLower,toUpper)
main :: IO ()
main = do
let port = 80
run port app
app :: Application
app req f = do
x <- genPass
f $ responseLBS status200 [(hContentType, "text/plain")] (BS.pack x)
rIndex :: [a] -> IO a
rIndex arr = do
i <- randomRIO (0, length arr - 1)
return $ arr !! i
dromedaryCase :: [Char] -> [Char]
dromedaryCase [] = []
dromedaryCase x = toUpper (head x) : map toLower (tail x)
gen4Num :: IO [Char] -- Generates four random numbers and concats them
gen4Num = do
a <- rIndex numList
b <- rIndex numList
c <- rIndex numList
d <- rIndex numList
return $ a ++ b ++ c ++ d
genPass :: IO [Char]
genPass = do
wrd0IO <- wordList
wrd1IO <- wordList
wrd0 <- rIndex wrd0IO
wrd1 <- rIndex wrd1IO
num0 <- gen4Num
sym0 <- rIndex symbolList
return $ (dromedaryCase wrd0) ++ (dromedaryCase wrd1) ++ num0 ++ sym0
readLines :: FilePath -> IO [[Char]]
readLines = fmap lines . readFile
wordList :: IO [[Char]]
wordList = readLines "./files/wordlist"
symbolList :: [[Char]]
symbolList = ["!", "@", "#", "$", "%", "&"]
numList :: [[Char]]
numList = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
Warning: Nonsensical Haskell Programming Explanation Incoming. Consult your doctor before reading the next paragraph.
Here's the short walkthrough on the above code: main
calls run port app
, which is a function from warp
that starts the webserver on a certain port (80, in this case) and runs an Application
. In this case, app :: Application
shoots back an HTTP 200 to the requester along with the results of the function genPass
. genPass
generates a secure password by reading from a word list (File IO), sending the contents of that word list to an array, and then rIndex
ing that array. rIndex
selects a random member of an array and returns it. This result is concatenated with another randomly selected word (after being modified by dromedaryCase
), which is then concatenated with the results of gen4Num
, which returns 4 random numbers inside of a [Char]
, and finally that is concatenated with a single symbol from rIndex symbolList
. genPass
returns an IO [Char]
, which is then "unpacked" by x <- genPass
in app
, which is then packed into a Lazy Bytestring so it can be sent off to the recipient.
Okay, we got through the Haskell part. Now we're going to talk about how easy it is to throw this on Docker and load balance it across multiple hosts that don't even have to be on the same network*1. If you were going to do something like this manually, you'd spend the better part of a week making your application scale across servers and networks automatically according to changing needs. With Docker, this is going to take approximately fifteen minutes to set up. Every time you need to change the total number of containers, you'll spend maybe fifteen seconds updating the configuration and deploying it. It's that easy.
*1 I'm not going to show you how to do it across multiple networks.
To get started, you'll want to have the following:
- The latest version of Docker for your Linux based operating system
- Your text editor of choice (which better be
vim
) - About 15 minutes to dedicate to implementing this, and about 5 minutes to dedicate to the childish excitement you'll have when completed
docker swarm init
If you don't already have a Docker Swarm defined, you'll want to start off by setting your main Docker machine as a manager with that command. It will also generate a worker token, so if you're planning to span this application across multiple hosts, you'll want to copy the command it provides to you and run it on your worker node. If they're not on the same network, you'll want to consult this documentation for more information on how to configure the networks for communication.
2. wget https://git.matri.cx/James/pwdGen/raw/branch/master/docker/autobuilder -O - | bash
This scary one-liner that I'm having you pipe to bash can be publicly examined at the wget link above. The script, as of this posting, looks like:
git clone https://git.matri.cx/James/pwdGen.git
cd pwdGen/docker
wget https://matri.cx/pwdGen.tar.gz
tar -xzvf pwdGen.tar.gz
mv pwdGen/pwdGenMusl ./
chmod +x buildDockerImage.sh
./buildDockerImage.sh
and then buildDockerImage.sh
looks like this:
#!/bin/sh
cp -r ../files/ ./
[ -f pwdGenMusl ] && docker build --tag=pwdgen . || echo "Please place your pwdGenMusl binary in this directory."
They're split up so that you can manually do the autobuilder
steps if you're not comfortable piping things directly to bash. If you are comfortable piping it directly to bash, the above one-liner after downloads everything you need and builds a Docker image named "pwdgen" for you! Super simple, super automated.
3. wget https://git.matri.cx/James/DockerIt/raw/branch/master/pwdgen/docker-compose.yml
This command will pull the docker-compose.yml for this application. Let's take a look at what that consists of:
version: "3"
services:
web:
image: docker.matri.cx/pwdgen
deploy:
replicas: 5
resources:
limits:
cpus: "0.2"
memory: 50M
restart_policy:
condition: on-failure
ports:
- "5002:80"
networks:
- webnet
networks:
webnet:
This is pretty simple! It's a version 3 docker-compose file that defines a service "web" using the docker.matri.cx/pwdgen
Docker image. It creates 5 replicas which will automatically load balance across manager and worker nodes. It limits each container to 0.2 of a CPU (what this means depends on your hardware) and 50M of RAM. It will restart the containers any time they fail. The port 5002 on the manager node that spins up the virtual webnet
network will be mapped to port 80 in the container.
4. docker stack deploy -c docker-compose.yml pwdgen
This is where the magic happens. The configuration file we talked about above is utilized to provision a network that load-balances requests across all of the nodes hosting the application, and the containers are spun up across nodes to share the load. That's all you have to do. If you then access the application at port 5002 on the manager node, each new request will be distributed to a new container instance across your hosts.
At this point, you're done. This probably didn't even take you 5 minutes, let alone 15. It's that easy to make magic happen with Docker Swarm. Who needs Kubernetes?