Hands-on: Kubernetes Pods. My first container

November 12, 2017 - - 0 Comment

On this third post of the series I’m going to talk about Kubernetes Pods. Kubernetes documentation defines a pod as “a group of one or more containers (such as Docker containers), with shared storage/network, and a specification for how to run the containers“. Kubernetes Pods are the smallest unit of computing that can be deployed and managed by Kubernetes.

Kubernetes Pods

Deep-diving into Kubernetes Pods

In the first place let’s walk through few key aspect of the Kubernetes Pods. Later on we will move on to the hands on labs.

Computing

A Kubernetes pod runs on a given node, it means a single pod cannot be stretched across multiple nodes. You can deploy more of the same pod, but it will be tied to a node. Also, a pod cannot be live migrated to another node like happens with virtual machines.

You cannot set resources to Kubernetes pods. The resource configuration happens at the container level in your pod definition. In order to successfully deploy a pod, your namespace must have enough resources.

Kubernetes Pods. Computing

Networking

From network point of view a routable IP address is assign to a given pod. Containers within a pod share an IP address and port space, and can find each other via localhost. You cannot have more than a container within the same pod listening on the same port. On the hands on section you will find an example.

In addition, containers in different pods have distinct IP addresses and can not communicate by IPC (inter-process communication)

Kubernetes Pods. Network.

Storage

The storage claimed by a pod is shared with all the containers within that pod. Once a persistent volume is claimed by a pod, it cannot be claimed/attached by another pod. Volumes enable data to survive container restarts and to be shared among the applications within the pod.

Kubernetes Pods. Storage.

Scheduling

By default the kube-scheduler service ensures that pods are only placed on nodes that have sufficient free resources. Also, it tries to balance out the resource utilisation of nodes.

Since Kubernetes 1.6 it offers advanced scheduling features: node affinity/anti-affinity, taints and tolerations, pod affinity/anti-affinity, and custom schedulers.

Kubernetes Pods. Scheduler.

Availability

The service kube-scheduler ensures a pod always is up and running in the event of a failure. If the Kubernetes node containing the pod fails, or your pod crashes, kube-scheduler will instantiate another pod for you.

With a single pod your availability is compromised if that fails. On a future post I’ll show you how to create replicas of your pod to improve its availability. ReplicaSet is the next-generation Replication Controller. A ReplicaSet ensures that a specified number of pod replicas are running at any given time. ReplicaSet should not be directly used, instead a deployment object as a high-level entity is recommended.

Kubernetes Pods. Availability.

Kubernetes Pods Lifecycle

Before we start with the creation, read, update, and delete (CRUD) of Kubernetes pods, I’d like to highlight an important recommendation. You should not directly instantiate a pod ever. I’m doing it for the sake of the post. You should always use a Kubernetes controller like Deployments, Job, or StatefulSet.

Creating Kubernetes Pods

First thing to remember is how works the Kubernetes Namespaces. If you are not familiar with namespaces, I suggest you to read my post Hands-on: Kubernetes Namespaces. We are going to work with two namespaces to showcase the connectivity between pods on different namespaces.

Unlike namespaces, to create a Kubernetes pod you must use a manifest. On our example we are going to use the namespaces production and development. Let’s create our first namespace (production) and a pod with Nginx at the same time.

cat <<EOF | kubectl create ‐f ‐
apiVersion: v1
kind: Namespace
metadata:
   name: production
‐‐‐
apiVersion: v1
kind: Pod
metadata:
   name: nginx
   namespace: production
spec:
   containers:
   ‐ name: nginx-app
     image: nginx
EOF
namespace “production” created
pod “nginx” created
kubectl -n production get all -o wide
NAME      READY  STATUS  RESTARTS  AGE  IP           NODE
po/nginx  1/1    Running 0         37s  10.244.2.12  node2

From any of your servers, master or nodes, you can check the Nginx container is accesible. Use curl and the IP address from the previous command.

curl 10.244.2.12
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed
and working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href=”http://nginx.org/”>nginx.org</a>.<br/>
Commercial support is available at
<a href=”http://nginx.com/”>nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>

Let’s create the second namespace development with a busybox container.

vim development.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: development
‐‐‐
apiVersion: v1
kind: Pod
metadata:
  name: busybox
  namespace: development
spec:
  containers:
  ‐ name: busybox-sleep
    image: busybox
    args:
    ‐ sleep
    ‐ “1000000”
kubectl create -f development.yaml
namespace “development” created
pod “busybox” create
kubectl -n development get all
NAME       READY  STATUS   RESTARTS  AGE
po/busybox 1/1    Running  0         30s

Now you will check the busybox container is able to open the Nginx site despite it is on a different namespace. The following command open a connection to a given container. Also, you will use wget to confirm the Nginx website is accessible from the busybox container. The IP address to use with wget can be gather listing the production namespace pods (kubectl -n production get pods -o wide)

kubectl -n development exec busybox -it /bin/bash
wget 10.244.2.12
Connecting to 10.244.2.12 (10.244.2.12:80)
index.html 100% |****************************************************|
612 0:00:00 ETA
cat index.html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed
and working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href=”http://nginx.org/”>nginx.org</a>.<br/>
Commercial support is available at
<a href=”http://nginx.com/”>nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>

Reading Kubernetes Pods

To read any given Kubernetes object you use the option describe.

kubectl -n production describe pod nginx
Name: nginx
Namespace: production
Node: node2/192.168.34.12
Start Time: Sun, 12 Nov 2017 16:25:03 +0000
Labels: <none>
Annotations: <none>
Status: Running
IP: 10.244.2.12
Containers:
  nginx-app:
    Container ID: docker://3c2846e5619f4203ed5d35e96818a71a9e71b
c5f3d442d25afae43f3af819766
    Image: nginx
    Image ID: docker-    pullable://[email protected]:9fca103a62af6db7f188ac3376c60927db41
f88b8d2354bf02d2290a672dc425
    Port: <none>
    State: Running
      Started: Sun, 12 Nov 2017 16:25:05 +0000
    Ready: True
    Restart Count: 0
    Environment: <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-q2dpl (ro)
Conditions:
  Type Status
  Initialized True
  Ready True
  PodScheduled True
Volumes:
  default-token-q2dpl:
  Type: Secret (a volume populated by a Secret)
  SecretName: default-token-q2dpl
  Optional: false
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.alpha.kubernetes.io/notReady:NoExecute for 300s
node.alpha.kubernetes.io/unreachable:NoExecute for 300s
Events:
Type   Reason                Age From              Message
‐‐‐‐   ‐‐‐‐‐‐                ‐‐‐ ‐‐‐‐              ‐‐‐‐‐‐
Normal Scheduled             31m default_scheduler Successfully assigned nginx to node2
Normal SuccessfulMountVolume 31m kubelet, node2    MountVolume.SetUp succeeded
for volume “default-token-q2dpl”
Normal Pulling               31m kubelet, node2    pulling image “nginx”
Normal Pulled                31m kubelet, node2    Successfully pulled image
“nginx”
Normal Created               31m kubelet, node2    Created container
Normal Started               31m kubelet, node2    Started container

Take a look to each of the pod settings. You can see they are not hard to understand.

Updating Kubernetes Pod

The update options are limited when you directly deploy a Kubernetes pod. For example you cannot add or remove containers from a pod. You can update a pod on different ways. You will learn how to update a pod using patch and replace.

With kubectl patch you modify on fly the pod configuration. Let’s patch the Nginx pod running in the production namespace with a different Nginx version. Important to realize the container is destroyed and re-created with the new version, it means the service has an outage. The pod uptime is not restarted.

First thing to do is to check the current Nginx version with kubectl exec. The double dash after -it is required when you have more than an argument (nginx -v)

kubectl -n production exec nginx -it ‐‐ nginx -v
nginx version: nginx/1.13.6

Let’s downgrade the Nginx version to 1.12 using kubectl patch. For the patch argument you just use the JSON blob for the spec section of your pod manifest. You can see the image key value pair has been updated with nginx:1.12.

kubectl -n production patch pod nginx -p ‘{“spec”:{“containers”:[{“name”: “nginx-app”, “image”: “nginx:1.12”}]}}’
pod “nginx” patched

Check again the Nginx version.

kubectl -n production exec nginx -it ‐‐ nginx -v
nginx version: nginx/1.12.2

Before you move to the next section, deleting Kubernetes Pods, let’s replace our busybox pod in the namespace development with a new pod with two containers. The objective is to showcase how the network traffic is within a pod when you have more than a container. Remember the containers within a pod communicate each other through localhost.

First let’s update the development.yaml file to include the new Nginx container.

cp development.yaml development-update.yaml
vim development-update.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: development
‐‐‐
apiVersion: v1
kind: Pod
metadata:
  name: busybox
  namespace: development
spec:
  containers:
  ‐ name: busybox-sleep
    image: busybox
    args:
    ‐ sleep
    ‐ “1000000”
  - name: nginx-app
    image: nginx

Now you will replace the current busybox pod with the new version of the manifest file. Since add or remove containers in a pod is not possible, you will use the option ‐‐force to delete and re-create the namespace and pod. Delete the pod takes a while because a graceful shutdown of the pod is done.

kubectl -n development replace ‐‐force -f development-update.yaml
namespace “development” deleted
pod “busybox” deleted
namespace “development” replaced
pod “busybox” replaced

If you run the following command will see the pod includes now two containers (kubectl -n development describe pod busybox)

Let’s check if the busybox container is able to get the web page provided by Nginx.

kubectl -n development exec busybox -c busybox-sleep -it /bin/sh
wget localhost
Connecting to localhost (127.0.0.1:80)
index.html 100% |****************************************************|
612 0:00:00 ETA
cat index.html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed
and working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href=”http://nginx.org/”>nginx.org</a>.<br/>
Commercial support is available at
<a href=”http://nginx.com/”>nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>

Deleting Kubernetes Pods

The command to delete a pod is the same than the one to delete a namespace but changing the kind of object. When you delete a pod Kubernetes will try gratefully terminate the pod. It is important when you have volumes attached to your pod.

Let’s destroy the busybox pod in the namespace development and later destroy the rest of objects you have created for this post.

kubectl -n development delete pod busybox
pod “busybox” deleted
kubectl -n development get pod
NAME     READY  STATUS       RESTARTS  AGE
busybox  2/2    Terminating  0         11m

Let’s delete the rest of objects

kubectl delete namespace production development
namespace “production” deleted
namespace “development” deleted

Conclusion

If you have survived until here, congratulations! It was a long post but I’ve wanted to cover the most important aspects of Kubernetes Pods. This is a good start point to get familiar with the pod architecture and lifecycle.

Pods are the meat of Kubernetes. You must practice as much as you can with pods in order to troubleshoot your container platform in case of failures.

On future posts I’ll show you how the pods can be abstracted on other high-level objects also know as controllers.

Hands-on: Kubernetes Namespaces. Multi-tenancy and more

November 4, 2017 - - 2 Comments

On the first post we saw how to deploy a Kubernetes platform using Vagrant and VirtualBox. Now we want to see how the cluster resources are logically segregated using Kubernetes namespaces. A namespace gives you the way to enable multi-tenancy in Kubernetes, RBAC, and have an isolated space where you won’t have any naming conflict with other namespaces.

Kubernetes namespaces

Getting started with namespaces

The following sections will walk you through the procedure to understand when you must use namespaces. In addition you will learn how to create and operate them.

Use cases

As an illustration let’s presume you are providing Container as a Service (a.k.a. CaaS) to your organisation, customers, or yourself. As shown above on the diagram the common use cases for namespaces are:

  • Multi-tenancy. You want to isolate organisations or customer on a shared Kubernetes platform. Kubernetes namespaces cannot be nested. If you have the namespace CustomerA and later on it requires two new namespaces for development and production, you cannot create sub-namespaces under the CustomerA namespace. It will require to create two namespaces like CustomerA-dev and CustomerA-prod.
  • Environment. If you desire to keep separated your development of production, you can create two namespaces one called Dev and the other Prod.
  • Project. Keep your projects on dedicated namespaces. It helps with CI/CD as well as a better utilisation and tracking of resources.
  • Team. If you are on a DevOps environment, the partitioning based on project or team is the same. On the contrary, you can create a playground area for learning purpose.

If you want more information about use cases take a look to this post.

Naming convention

The following approach is just a suggestion to have unique and scalable namespaces. Uses hyphens to separate the groups.

  • Customer/Organisation code. At least a minimum of three letters
  • Environment code. At least a minimum of three letters
  • Project/Service code. At least a minimum of three letters
  • Digits. At least a minimum of two numbers

Remember to use lowercase for your namespace name. This is an example using the convention above: jlg-prod-blog-01.

Operating with namespaces

Before we crack on with the creation of namespaces let’s take a look to the Kubernetes command-line tool called kubectl.

Kubectl is the command-line to operate Kubernetes clusters and its applications. It’s available for the majority of the operating systems. If you have deployed your Kubernetes platform using the Vagrantfile I have created, the master node has kubectl installed and configured for you.

If you want to know more about kubectl, please visit this link.

Listing

To list any kind of Kubernetes object you use kubectl get <object_type>. Our object type is namespace so our command shows as follows:

kubectl get namespace
NAME        STATUS AGE
default     Active 20h
kube-public Active 20h
kube-system Active 20h

By default any Kubernetes configuration tool creates at least two namespaces, default and kube-system. In our case we can see three because kube-public is created when you use kubeadm as the Kubernetes configuration tool.

  • default. This namespace is used when you don’t specify a different namespace using -n <namespace_name> or –namespace <namespace_name>.
  • kube-system. This is the namespace where all the Kubernetes core components are running.
  • kube-public (only with kubeadm). This namespace is readable by everyone, including those not authenticated. This namespace is used by kubeadm to host a ConfigMap object in order to enable a secure bootstrap with a simple and short shared token.

Viewing

You can see the details of an object using kubectl describe <object_type> <object_name>. Our object type is namespace and the object name we will use is kube-system. The command shows as follows:

kubectl describe namespace kube-system
Name:        kube-system
Labels:      <none>
Annotations: <none>
Status:      Active
 
No resource quota.
 
No resource limits.

As you can see the output is pretty simple. This is because we are working with a fresh Kubernetes installation. Also, the most common additional extra settings for a namespace like resource quotas and limits are not defined yet. Those objects will be covered on a future post.

Creating

Using the kubectl command you can create objects on two ways.

The first way is to create the object from the CLI without use a manifest file. This approach is supported for a limited number of Kubernetes objects. Let’s create a namespace called no-manifest.

kubectl create namespace no-manifest
namespace “no-manifest” created
 
kubectl get namespace
NAME        STATUS AGE
default     Active 5d
kube-public Active 5d
kube-system Active 5d
no-manifest Active 1m

The second way is to use a manifest file. This is the preferred method since it enables the opportunity to track and version your infrastructure as code manifests on your source code repository. Let’s create a namespace called manifest using a YAML file.

cat <<EOF > namespace_no-manifest.yaml
> apiVersion: v1
> kind: Namespace
> metadata:
>   name: manifest
> EOF
 
kubectl create -f namespace_no-manifest.yaml
namespace “manifest” created
 
kubectl get namespace
NAME        STATUS AGE
default     Active 5d
kube-public Active 5d
kube-system Active 5d
no-manifest Active 5m
manifest    Active 1m

As you could see the manifest file for a namespace is simple. Other attributes like labels or annotations can be added on.

Editing

You can edit the Kubernetes objects on fly with the command kubectl edit <object_type> <object_name> -n <namespace>. On our next example we don’t need to use -n namespace since we are editing one. For objects self-contained in a namespace, you must specify the namespace for the object. Let’s config the annotations with a description for our namespace called no-manifest.

kubectl edit namespace no-manifest
# Please edit the object below. Lines beginning with a ‘#’
# will be ignored, and an empty file will abort the edit.
# If an error occurs while saving this file will be
# reopened with the relevant failures.
#
apiVersion: v1
kind: Namespace
metadata:
  annotations:
    description: This is just an example
  creationTimestamp: 2017-11-04T10:57:15Z
  name: no-manifest
  resourceVersion: “39333”
  selfLink: /api/v1/namespaces/no-manifest
  uid: eb25c489-c14e-11e7-bb73-0800277f7091
spec:
  finalizers:
  – kubernetes
status:
  phase: Active
 
kubectl describe namespace no-manifest
Name:        no-manifest
Labels:      <none>
Annotations: description=This is just an example
Status:      Active
 
No resource quota.
 
No resource limits.

Deleting

The deletion of a namespace removes ALL the child objects contained within the namespace. Before you delete a namespace make sure the objects in the namespace are not required anymore. Let’s delete the namespace called manifest with the command kubectl delete namespace manifest.

kubectl delete namespace manifest
namespace “manifest” deleted

Real Kubernetes Namespaces example

For the purpose of this real case scenario we are going to create a namespace to group the different kind of web servers. After you have created the namespace, you will proceed with the deployment of Nginx. Finally, you will list all the objects in the created namespace.

Int he first place let’s create a new namespace called webservers.

kubectl create namespace webservers
namespace “webservers” created

In addition, we are going to create a deployment so we can see later on how they are contain in a namespace.

kubectl create deploy dep-nginx –image=nginx -n webservers
deployment “dep-nginx” created

As a result let’s list the components in our namespace using the command kubectl get all -n webservers.

kubectl get all -n webservers
NAME             DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
deploy/dep-nginx 1       1       1          1         53s
 
NAME                    DESIRED CURRENT READY AGE
rs/dep-nginx-6f5568d8dd 1       1       1     53s
 
NAME             DESIRED CURRENT UP-TO-DATE AVAILABLE AGE
deploy/dep-nginx 1       1       1          1         53s
 
NAME                    DESIRED CURRENT READY AGE
rs/dep-nginx-6f5568d8dd 1       1       1     53s
 
NAME                          READY STATUS  RESTARTS AGE
po/dep-nginx-6f5568d8dd-49csx 1/1   Running 0        53s

On the output above you can see what a deployment creates.

  • Deployments (deploy). This controller provides declarative updates for Pods (po) and ReplicaSets (rs). The controller is responsible to ensure the desire state of your application. You can see two deployments with the same ID (dep-nginx), this is because it’s created at cluster level rather than per node. Since we have two Kubernetes nodes it shows the object available for all the nodes in the cluster.
  • ReplicaSets (rs). This controller is the next-generation Replication Controller (rc). You can still find some applications using Replication Controller instead of ReplicaSets. The objective of ReplicaSets is to ensure that a specified number of pod replicas are running at any given time.
  • Pods (po). A Pod is the basic building block of Kubernetes–the smallest and simplest unit in the Kubernetes object model that you create or deploy. A Pod represents a running process on your cluster. Since we didn’t define the number of ReplicaSets, for that reason it runs by default a single Pod. If you would like to see where the Pod is running on your cluster, you can run the same command with the flag -o wide (-o is output): kubectl get all -n webservers -o wide.

Conclusion

Kubernetes Namespaces are an important part of your platform and security. It will allow you to logically segregate and assign resources for each of them. Also, it creates a space where the object names don’t impact with the objects in other Kubernetes namespaces.

Hands-on Kubernetes: Deployment

October 29, 2017 - - 0 Comment

A month ago I started to get my head around Kubernetes, not to mention I’m still on my early journey with containers. Honestly, it has been hard get a stable platform up and running. In the first place depending what you are using underneath to spin up the platform, you may need to tweak some tools. Anyway, this is something I’ll explain you later on.

I’m pushing myself to write a series of posts where I’ll share with you what I have learnt. The series will be mostly hands-on experience and a bit of theory. You can find lot of documentation out there about how Kubernetes works under the hood. I don’t want to reinvent what is already written. Instead, I’ll share with you some useful references.

Spinning up a Kubernetes platform

You have many ways to spin up a Kubernetes platform, like using Rancher (don’t miss Deploying Rancher 2.0 on Vagrant). This time I wanted to configure Kubernetes using kubeadm. The tool doesn’t deploy the infrastructure for you. A set of pre-provisioned servers are required. Kubeadm is still under development and it’s not ready for production yet.

Vagrant as Infrastructure Orchestrator

Many people don’t have the opportunity to have dedicated infrastructure to test solutions. Vagrant together with a virtualisation software on your computer gives you the foundational infrastructure.

Vagrant stack

VirtualBox as Virtualisation Software

The reason to use VirtualBox as the Vagrant provider is because it’s free its use by Vagrant. You have other alternatives like VMware Workstation or Fusion, but if you want to use any of them as a provider you must buy VMware Integration – Vagrant.

Virtualbox stack

Cloning the Kubernetes-Vagrant GitHub repo

I have created a Vagrantfile blueprint to deploy a cluster with the following characteristics:

  • Single master
  • One or more nodes (a.k.a. worker or minion). By default two workers are deployed to test the overlay network
  • A standalone NFS server
  • Kubeadm is the tool to configure the latest Kubernetes version
  • Canal is the CNI plug-in

Kubernetes platform

As shown above, the diagram details the foundational infrastructure with the components running on each server.

  • Blue square. It represents the virtual machine with the hostname and the suffix IP
  • Purple rectangle. It represents the NFS export for persistent storage
  • Red rectangle. It represents a Linux tool or service which natively runs on the operating system
  • Green rounded rectangle. It represents a Docker container. Kubernetes components and the CNI plug-in Canal run in containers. For more information about the Kubernetes components visit this link
  • Orange dashed rectangle. It represents the free resources on a node to run pods
  • White dotted rounded rectangle. It represents a Kubernetes pod. A pod is the group of one or more containers with shared storage and/or network

To get this setup you just clone or download my GitHub repository.

~$ git clone https://github.com/pipoe2h/kubernetes-vagrant.git
~$ cd kubernetes-vagrant
# Customise your settings in the Vagrantfile
~$ vagrant up

Once the platform is up (approx. 10 minutes for two nodes setup) you can ssh into the master node with vagrant ssh master. From the master server run kubectl get nodes to list the nodes and check all of them show as ready.

kubectl get nodes

Lessons Learnt

Overall kubeadm makes simple the deployment process. In reality the challenges I faced were not related to kubeadm, my setup in Vagrant was wrong.

  1. Vagrant adds a NAT interface to perform the provisioning tasks. During this process Vagrant overrides the hosts file with the loopback address. Kubernetes requires the node IP for proper communication (you can run hostname -i). My Vagrantfile is tweaked to set the right IP in the hosts file. Otherwise, you cannot use kubectl exec command.
  2. With two network interfaces on the virtual machine the interface to use must be defined. By default the plug-in takes the main interface. To fix that behaviour the setting canal_iface in the Canal YAML blueprint must be set to the second network interface, enp0s8.

Before you start to use actively your Kubernetes platform, make sure ping works between pods on different nodes as well as the DNS resolution too (try to resolve an external domain)

In summary, this post has shown you how to deploy a Kubernetes platform. After all, now you have a functional container platform for training and testing purpose. In fact, this is the platform I will refer on the series of posts.

Deploying Rancher 2.0 on Vagrant

October 1, 2017 - - 1 Comment

A week ago Rancher Labs released (announcement) a technology preview of the What’s new, a container management platform built on Kubernetes. This post will drive you through the process to deploy without any effort a Rancher platform using Vagrant and VirtualBox.

Source: rancher.com

(more…)