How to deploy a Docker host with Tuono
This article describes how to build a Docker host with a single Tuono Blueprint. Using the same Blueprint, this can easily be extrapolated out to a whole Docker Swarm.
During a recent discussion with a customer at a software development company, they explained that they wanted to fully automate a Docker deployment and the required infrastructure in the public cloud. They had several Docker images, containing all the dependencies that they needed to work with a range of languages and frameworks. What they wanted to do was automate the deployment of the Docker infrastructure and pulling down the discrete container images based on the languages they wanted to work with.
This got me thinking…
Starting with the default Tuono “tutorial” Blueprint, we could easily modify this to become a Docker Blueprint using some standard Tuono functionality. We could automate the deployment of the correct Docker image from the container library based on the current requirement.
For this example, I created a Docker image from scratch, to demonstrate the principle. If you already have a few Docker images, you know how all this works and can make the appropriate changes in the “userdata” section, but if you don’t, you’ll have your own custom image at the end of this… the batteries are included.
Blueprint
Let’s dive straight in to the schema, and then we can talk about what it does.
# # This is an example blueprint which demonstrates the # creation of a Docker host with a public ip address. # # The machine can be accessed via: # # # ssh <admin_username>@<ip> # # And the Docker machine can be accessed via; # # # ssh <admin_username>@<ip> -p 8080 # --- variables: admin_username: description: The username for the administrative user. type: string default: adminuser container_password: description: The password for the Docker container type: string admin_public_key: description: The OpenSSH Public Key to use for administrative access. type: string number_of_cores: type: integer preset: true memory_in_gb: type: integer preset: true presets: venue: azure: number_of_cores: 1 memory_in_gb: 2 aws: number_of_cores: 2 memory_in_gb: 1 location: region: datacenter: aws: eu-west-1 azure: northeurope folder: docker: region: datacenter networking: network: testing: range: 10.0.0.0/16 public: true subnet: public: range: 10.0.0.0/24 network: testing firewall: only-secure-access public: true protocol: secure: ports: - port: 22 proto: tcp - port: 443 proto: tcp - port: 8080 proto: tcp firewall: only-secure-access: rules: - protocols: secure to: self compute: image: bionic: publisher: Canonical product: UbuntuServer sku: 18.04-LTS venue: aws: image_id: ami-06868ad5a3642e4d7 vm: docker-host: cores: ((number_of_cores)) memory: ((memory_in_gb)) GB image: bionic disks: data: size: 64 GB tags: tag: base_disk nics: external: ips: - private: type: dynamic public: type: static firewall: only-secure-access subnet: public tags: wicked: cool configure: admin: username: ((admin_username)) public_key: ((admin_public_key)) userdata: type: shell content: | #!/bin/sh ## Configure admin_username on the host machine userid=$(id -u ((admin_username))) if [ -z "$userid" ]; then set -e adduser --gecos "" --disabled-password ((admin_username)) cd ~((admin_username)) mkdir .ssh chmod 700 .ssh echo "((admin_public_key))" > .ssh/authorized_keys chmod 600 .ssh/authorized_keys chown -R ((admin_username)).((admin_username)) .ssh usermod -aG sudo ((admin_username)) echo "((admin_username)) ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers set +e fi ## Update the repositories and upgrade sudo apt update sudo apt upgrade -y ## Install and configure Docker sudo apt install docker.io -y sudo usermod -aG docker (( admin_username )) sudo systemctl enable --now docker ## Configure the Docker instance cd /home/((admin_username))/dockerbuild mkdir dockerbuild ## Create Dockerfile and do some bootsrapping on the container machine echo "FROM ubuntu:20.04 # Install the required dependencies RUN apt-get update && apt-get install -y openssh-server RUN mkdir /var/run/sshd # Add the admin_username RUN useradd ((admin_username)) RUN echo '((admin_username)):((container_password))' | chpasswd # Fix some ENV issues ENV NOTVISIBLE='in users profile' RUN echo 'export VISIBLE=now' >> /etc/profile # Open up port 22 EXPOSE 22 # This is a workaround to start (and keep up) SSHd CMD service ssh start && while true; do sleep 3000; done" > dockerbuild/dockerfile ## Build and run the Docker image docker build -t dockerfile dockerbuild docker run -d -p 8080:22 -t -i dockerfile
As you can see, this is a fairly standard VM Blueprint. In this case, I decided to create a preset for Azure and AWS. The only reason for this is so that it’s possible to deploy it in the free-tier of AWS (2 CPU and 1 GB RAM, or t3.micro). This size has no cognate in Azure. In real life, you’d remove the presets altogether, set it to a sensible amount of CPU and memory and it will deploy to Azure and AWS just fine. The other non-standard part of the base template is the use of port 8080. This is the port that will be mapped to the Docker image – container port 22 in this case. With the explanation complete, let’s talk about the interesting bit.
Userdata
For this example, it’s the “userdata” that does most of the heavy lifting. Userdata is interesting in that it allows you to use any shell-supported scripting language to bootstrap your infrastructure. It’s extremely useful for doing the initial infrastructure configuration (see the webservice example), or as in this case, for doing the initial configuration where state is not a concern. I should also add that cloud-init is fully supported too, but I specifically used bash in this example on the assumption that it may be more familiar to people if they wanted to extend this example.
So, what’s going on in the userdata? The first section is simply about configuring the admin_username that was defined as a variable. If we use userdata, it is expected that we do all the initial configuration in here, so we add the user, configure sudo and add them to the sudoers file and enable SSH key authentication. You can consider this section as boilerplate and should be present in some form in almost every case where you leverage userdata. From here, we:
- Update the Ubuntu instance
- Install Docker
- Add our admin_username to the Docker group. This ensures that the containers are accessible to this user.
The next section is where we write out our Dockerfile. In this example we do only the very minimum to make it useful:
- Update the Ubuntu container
- Install OpenSSH
- Add the admin_username
- Update /etc/profile
- Finally, start SSH at runtime using a workaround to keep it up and accessible.
If you want to take it for a test-drive:
To access the Docker host
# ssh <admin_username>@<ip>
And to access the container:
# ssh <admin_username>@<ip> -p 8080
All the IP details can be viewed by clicking the “Inventory” button on the Environment Screen.
If you want to go ahead and try this out, feel free to take Community Edition for a spin. It’s entirely free, with no credit card required, so it’s a good way to have some fun with this example. Stay tuned for more fun examples.