Getting Started With Kubernetes

27 May 2016

Kubernetes is a very popular open source container management system.

The goal of the Kubernetes project is to make management of containers across multiple nodes as simple as managing containers on a single system. To accomplish this, it offers quite a few unique features such as traffic load balancing, self-healing (automatic restarts), scheduling, scaling, and rolling updates.

In this post, we'll learn about Kubernetes by deploying a simple web application across a multi-node Kubernetes cluster. Before we can start deploying containers however, we first need to set up a cluster.

Setting Up a Kubernetes Cluster

The official getting started guide walks you through deploying a Kubernetes cluster on the Google Container Engine (GCE) platform.

GCE is a quick and easy method to get up and running. But for this article, we'll be deploying Kubernetes with an alternative provider. Specifically: Vagrant. We're using Vagrant for a few reasons, but primarily because it shows how to deploy Kubernetes on a generic platform rather than GCE.

Kubernetes comes with several install scripts for different platforms. The majority of these scripts use environmental variables to change how Kubernetes is installed.

For our installation, we'll define two variables:

  • NUM_NODES: controls the number of nodes to deploy
  • KUBERNETES_PROVIDER: specifies the platform on which to install Kubernetes

Let's define that the installation scripts should use the vagrant platform and to provision a two node cluster.

We can do that like so:

$ export NUM_NODES=2
$ export KUBERNETES_PROVIDER=vagrant

If we wanted to deploy Kubernetes on AWS, for example, we could do so by changing the KUBERNETES_PROVIDER environmental variable to aws.

Using the get-kube Install Script

While there are many documented ways to install Kubernetes, I have found the easiest method is to use the get-kube install script.

This script wraps the installation scripts distributed with Kubernetes, and sets up a default cluster, which makes the process quite a bit easier. One of my favorite things about the get-kube script is that it also downloads Kubernetes for you.

To start using this script, you'll need to download it. You can do this with a quick curl command. Like so:

$ curl >

Once we've downloaded the script, we can execute it by running the bash command followed by the script name:

$ bash

The result should look something like this:

Downloading kubernetes release v1.2.2 to /var/tmp/kubernetes.tar.gz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  426M  100  426M    0     0  12.2M      0  0:00:34  0:00:34 --:--:-- 6007k
Unpacking kubernetes release v1.2.2
Creating a kubernetes on vagrant...
<output truncated>
Kubernetes master is running at
Heapster is running at
KubeDNS is running at
kubernetes-dashboard is running at
Grafana is running at
InfluxDB is running at
Installation successful!

After the script completes execution, we have a running Kubernetes cluster!

However, we still have one more step before we can start to interact with this Kubernetes cluster. We need the kubectl command to be installed.

Setting up kubectl

The kubectl command exists for both Linux and Mac OS X.

Since I'm running this installation from my MacBook, I'll be installing the Mac OS X version of kubectl. This means I'll be running the cluster via Vagrant but interacting with that cluster from my MacBook.

To download the kubectl command, we once again use curl:

$ curl > kubectl
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 38.7M  100 38.7M    0     0  10.4M      0  0:00:03  0:00:03 --:--:-- 10.4M

After, change the permissions to allow execution:

$ chmod 750 kubectl

The kubectl command is almost ready.

One more step is required before we can start interacting with our Kubernetes cluster. That step is to configure the kubectl command. You can do that like so:

$ export KUBECONFIG=~/.kube/config

As with most Kubernetes scripts, the kubectl command's configuration is driven by environmental variables.

When we executed the cluster installation script above, that script created a .kube configuration directory in my home directory. Within that directory, it also created a file named config. This file is used to store information about the Kubernetes cluster that was created.

By setting the KUBECONFIG environmental variable to ~/.kube/config, we are telling the kubectl to reference this configuration file.

Let's take a quick look at that file to get a better understanding of what is being set:

$ cat ~/.kube/config
apiVersion: v1
- cluster:
    certificate-authority-data: <SSLKEYDATA>
  name: vagrant
- context:
    cluster: vagrant
    user: vagrant
  name: vagrant
current-context: vagrant
kind: Config
preferences: {}
- name: vagrant
    client-certificate-data: <SSLKEYDATA>
    client-key-data: <SSLKEYDATA>
    token: sometoken
- name: vagrant-basic-auth
    password: somepassword
    username: admin

The .kube/config file sets two main pieces of information:

  • Location of the cluster
  • Authentication data for communicating with that cluster

With the .kube/config file being read by kubectl, let's attempt to execute a kubectl command against our Kubernetes cluster to verify everything is working.

$ ./kubectl get nodes
NAME                STATUS    AGE
kubernetes-node-1   Ready     31m
kubernetes-node-2   Ready     24m

The output of the ./kubectl get nodes command shows that we were able to connect to our Kubernetes cluster and display the status of our two nodes: kubernetes-node-1 and kubernetes-node-2.

With this, our installation is complete.

More About Kubernetes Nodes

In the previous command, we used kubectl to show the status of the available Kubernetes nodes in our cluster. However, we really didn't explore what a node is or what role it plays within a cluster.

A Kubernetes node is a physical or (as in our case) virtual machine used to host application containers. In a traditional container-based environment, you would typically define that specific containers run on specified physical or virtual hosts. In a Kubernetes cluster, however, you simply define what application containers you wish to run. Kubernetes then determines which node the application container will run on.

This system also lets the Kubernetes cluster to perform tasks such as automated restarts when containers or nodes die.

Deploying Our Application

With our Kubernetes cluster ready, we can now start deploying application containers.

Let's install the Ghost application container. Ghost is a popular JavaScript-based blogging platform. Because it provides an official Docker image, it's pretty simple to deploy on Kubernetes.

Since we'll be using a pre-built Docker container, we won't need to first build a Docker image. However, if you are using custom-built containers, you must first build the container and push it to a Docker repository such as Docker Hub.

You can start the Ghost container with the kubectl run command:

$ ./kubectl run ghost --image=ghost --port=2368
deployment "ghost" created

Here, we created a deployment named ghost, using the image ghost, and specified that the ghost container requires the port 2368.

Before going too far, let's first verify that the container is running. We can do this with the kubectl get pods command:

$ ./kubectl get pods
NAME                     READY     STATUS    RESTARTS   AGE
ghost-3550706830-4c2n5   1/1       Running   0          20m

The get pods command lists all Kubernetes pods currently deployed to the cluster.

What Are Pods and Deployments?

A pod is a group of containers that can communicate with each other as though they are running on the same host.

For those familiar with Docker, this may sound like linking containers, but there's actually a bit more to it than that. Containers within pods are not only able to connect to each other through a localhost connection, the processes running within the containers are also able to share memory segments with other containers.

Because containers (each one running a single service or program) inside a pod can interact as if they were running on the same physical host, it is easy to deploy applications that are not specifically designed with containers in mind.

A deployment, or deployment object, is similar to the concept of a desired state. For example, earlier, when we started the Ghost container, we didn't just launch a Ghost container. We actually configured Kubernetes to ensure that at least one copy of a Ghost container is always running.

Creating a service for Ghost

Containers within pods can connect to systems external to the cluster. However, external systems, and even other pods, cannot communicate with them. This is because, by default, the port we configured for Ghost is not exposed outside of the cluster.

This is where services come into play.

In order to make our Ghost application accessible outside of the cluster, the deployment we just created needs to be exposed as a Kubernetes service.

To do this, we use the kubectl expose command:

$ ./kubectl expose deployment ghost --type="NodePort"
service "ghost" exposed

In the above command, we set the --type flag to NodePort. This flag defines the service type to expose for this service. In this case, a NodePort service type. The NodePort service type will set all nodes to listen on the specified port.

We can check this has worked with the kubectl get services command:

$ ./kubectl get services
ghost   nodes         2368/TCP   55s
kubernetes      <none>        443/TCP    19m

Service types

At the moment, Kubernetes supports three service types:

  • ClusterIP
  • NodePort
  • LoadBalancer

If we wanted to only expose this service to other pods within the cluster, we could use the ClusterIP service type. This is the default, and opens the port on each node for pod-to-pod communication.

The LoadBalancer service type is designed to provision an external IP to act as a load balancer for the service. Since our deployment is leveraging Vagrant on a local laptop, this option does not work in our environment. It does work with Kubernetes clusters deployed in cloud environments like GCE or AWS.

Testing our Ghost instance

Since we did not specify a port to use when defining our NodePort service, Kubernetes randomly assigned a port.

To see what port it assigned, we can use kubectl describe service:

$ ./kubectl describe service ghost
Name:           ghost
Namespace:      default
Labels:         run=ghost
Selector:       run=ghost
Type:           NodePort
Port:           <unset> 2368/TCP
NodePort:       <unset> 32738/TCP
Session Affinity:   None
No events.

We can see the assigned port is 32738.

With this port, we can use the curl command to make an HTTP call to any of our Kubernetes nodes:

$ curl -vk
* Rebuilt URL to:
*   Trying
* Connected to ( port 32738 (#0)
> GET / HTTP/1.1
> Host:
> User-Agent: curl/7.43.0
> Accept: */*
< HTTP/1.1 200 OK
< X-Powered-By: Express

From the output of the curl command, we can see that the connection was successful with a 200 OK response.

What's interesting about this is that the request was made to a node that isn't running a Ghost container.

We can see this if we use the kubectl to describe the pod:

$ ./kubectl describe pod ghost-3550706830-ss4se
Name:        ghost-3550706830-ss4se
Namespace:   default
Node:        kubernetes-node-2/
Start Time:  Sat, 16 Apr 2016 21:13:20 -0700
Labels:      pod-template-hash=3550706830,run=ghost
Status:      Running
Controllers: ReplicaSet/ghost-3550706830
    Container ID:  docker://<snipped>
    Image:         ghost
    Image ID:      docker://<snipped>
    Port:          2368/TCP
    QoS Tier:
      memory:      BestEffort
      cpu:         Burstable
      cpu:         100m
    State:         Running
      Started:     Sat, 16 Apr 2016 21:14:33 -0700
    Ready:         True
    Restart Count: 0
    Environment Variables:
  Type      Status
  Ready     True
    Type:       Secret (a volume populated by a Secret)
    SecretName: default-token-imnyi
No events.

We can see that the Ghost pod is running on kubernetes-node-2. However, the HTTP request we just made was to kubernetes-node-1.

How does that work?

Well, Kubernetes runs a service called kube-proxy. Whenever traffic arrives on a service's port, kube-proxy will transparently proxy that traffic to the node and port inside the pod that is running that service.

In our case, this means that even though the HTTP request was made to kubernetes-node-1, kube-proxy service proxied that traffic to kubernetes-node-2 where the container is running.

This feature allows users to run services without having to worry about where the service is initially created, and whether or not it has moved. This is avoids a lot of potential headaches that normally come with scaling and self-healing.

Scaling a Deployment

Now that our Ghost service is running and accessible to the outside world, we need to perform our last task: scaling the application across multiple instances.

To do this, we can simply use the kubectl scale command:

$ ./kubectl scale deployment ghost --replicas=4
deployment "ghost" scaled

Here, we've specified there should be four replicas of the ghost deployment. If we execute kubectl get pods again we should see three additional pods for the ghost deployment. Like so:

./kubectl get pods
NAME                     READY     STATUS    RESTARTS   AGE
ghost-3550706830-49r81   1/1       Running   0          7h
ghost-3550706830-4c2n5   1/1       Running   0          8h
ghost-3550706830-7o2fs   1/1       Running   0          7h
ghost-3550706830-f3dae   1/1       Running   0          7h

With this last step, we now have our Ghost service running across multiple nodes and multiple pods. As requests are made to our Ghost service, those requests will be load balanced to our various Ghost instances!


In this post, we saw how to deploy a multi-node Kubernetes cluster across several Vagrant-managed virtual machines.

We also saw how to deploy an application to the cluster, make that application accessible to external systems, and finally, scale out the application to four instances.

Even though we looked at all of this, we have only scratched the surface of what Kubernetes has to offer.

This post originally appeared on the Codeship blog. Codeship is a hosted Continuous Integration as a Service platform.

Posted in Kubernetes, Intro

triangle square circle

Did you enjoy this post?