I’m doing this purely as an experiment to gain experience with using the Google container service and Google compute services. The steps required are loosely:

  • Establish a google compute account.
  • Install the google SDK and gcloud.
  • Create a default gcloud configuration.
  • Create a minimum sized cluster to allow us to run Kubernetes and manage our blog in a container.
  • Upload my blog container to the Google Container Registry.
  • Create a “deployment” to run the container.
  • Expose a public IP address for this container.
  • Then update necessary DNS records to point the blog to this container.

Gcloud and kubectl

I’ll be doing the majority of this using two CLI tools, gcloud and kubectl.

The gcloud CLI tool is used for managing all aspects of google cloud services. It’s installed locally and uses the Google Cloud Platform APIs. It’s also what we’ll use to install the Kubernetes CLI tool kubectl.

The kubectl CLI tool is used for managing Kubernetes. When to use each tool should become apparent as we proceed.

This SDK quickstart guide covers the installation of the SDK under various OSs. I chose to follow the quickstart for Linux since I’m running Manjaro.

  • gcloud requires python 2.7 so I installed the SDK and run the gcloud CLI within a virtual environment [https://virtualenv.pypa.io/en/stable/]
    mkvirtualenv google-cloud -p python2.7
  • The steps involve downloading a tarfile, untarring it then running a setup script.
  • You’ve then got the gcloud CLI installed and the guide walks you through getting authenticated and some initialization.

Once I installed the SDK and got access to the gcloud CLI the next step was to create a ‘project’. All services, VMs and containers are run within the context of a project.

    gcloud projects create robren-blog-v1
    gcloud projects list
    PROJECT_ID       NAME            PROJECT_NUMBER
    robren-blog-v1   robren-blog-v1  840355975236

Now we update our configuration to use this project by default, to avoid having to specify the project we’re using when running the CLI commands.

gcloud config set project robren-blog-v1

Always-free Google Compute Engine tier

Google compute has an “always free” tier. As long as I stick within the usage limits outlined in the Always free description the services will be free.

I’ll need to monitor my billing to see if this is indeed the case, but it’s worthwhile trying to stay in these limits for a tiny experimental blog.

One thing specified in the always-free rules is that we can use 1 f1-micro instance per month excluding Northern Virginia. So …. I’ll want to set my default region to be a US region that’s not in Northern Virginia. The region names and locations are described [here] (https://cloud.google.com/compute/docs/regions-zones/regions-zones)


gcloud config set compute/region us-east1
gcloud config set compute/zone us-east1-c

By the time I’ve configured Kubernetes along with controller nodes etc I know I’ll be in excess of always free limits, but at least I’ll get one of the nodes for free! If I just wanted to run my blog using an nginx server inside of a compute instance I’d be able to do this with a single micro instance and no containers, but where would be the fun in that.

Create a cluster

The container will run within a cluster, this cluster being controlled by Kubernetes. To create the cluster we need to use a gcloud command. Note the machine type being specified with the -m flag as f1-micro as well as the –preemptible flag to keep costs down from the default, but still pretty cheap machine.

# Prior to creating the cluster kubectl has to be installed
gcloud components install kubectl

# To see the available options use the built in help. In fact all of the gcloud and kubectl
# have help available on a per sub command basis, this is very usefull.
gcloud container clusters create --help

# Create our Cluster
gcloud container clusters create blog-cluster -m f1-micro --num-nodes=3 --disk-size=10 --preemptible

gcloud container clusters create blog-3p-node-cluster -m f1-micro --num-nodes=3 --disk-size=10 --preemptible
Creating cluster blog-3p-node-cluster...done.
Created [https://container.googleapis.com/v1/projects/robren-blog-v1/zones/us-east1-c/clusters/blog-3p-node-cluster].
kubeconfig entry generated for blog-3p-node-cluster.
NAME                  ZONE        MASTER_VERSION  MASTER_IP      MACHINE_TYPE  NODE_VERSION  NUM_NODES  STATUS
blog-3p-node-cluster  us-east1-c  1.6.4           35.185.41.120  f1-micro      1.6.4         3          RUNNING

We’re forced to use a minimum of 3 nodes for the cluster with the f1-micro instance. I did find out, by trial and error, that I could use a g1-small instance with a cluster size of 1 but I’ll stick with this more resilient cluster of 3.

According to the google cost estimator [https://cloud.google.com/products/calculator/] this should run around $12 per month, so slightly higher than Digital Ocean but I can live with that. Perhaps one of the instances will be always-free too!

Create a container image for my blog

Previous posts Deploying using docker machine describe how I added some custom extensions to the fabric file included with pelican. The readme in [https://github.com/robren/robren-blog] explains how to install pelican, and use the fabfile to create a docker image. Pelican’s a bit ’temperamental’, my README in github.com/robren/robren-blog has a few pointers to what I needed to do to get pelican to generate a static site. If you don’t want to get embroiled in learning pelican as well as google container engine etc, then create some simple content in a subdirectory called “output” and proceed as described below as ‘build the image"

If docker is not already installed, here’s a quick reminder of what I needed to do.

Refresher: Installing docker

Skip if you’ve already got docker running locally.

This will differ between OS’s. For me with Manjaro it was

sudo pacman -S docker
# Then to make sure docker restarted on reboot
sudo systemctl start docker
sudo systemctl enable  docker
   
# Then to remove the need to run docker with sudo
sudo gpasswd -a $USER docker
newgrp docker # Or log out and back into your system
docker run hello-world

Build the image

Create your static html content.

The pelican distribution provides makefiles, fabfiles and a direct pelican command line to create content in a subdirectory called output. The simplest way to create the static content would be to directly call:

pelican /path/to/your/content/ 

Create a docker image

The Docker file used is a simple two liner:

cat ./Dockerfile
FROM nginx:alpine
COPY output /usr/share/nginx/html

This specifies that the base linux container image is the nginx image build on top of the very lightweight linux container called alpine.`The static output from Pelican is contained in the output directory and is copied into the defaule place witin the containers file system where nginx expects to serve html files from.

The next steps of building and tagging the image can all be compined into one, I’m just splitting them out for clarity. In my robren-blog github repo fabfile.py script I combine these operations along with versioning the image into one command available as fab kub_rebuild.

docker build -t alpine-blog 

Upload the container image to the Google Container Registry

The Google documentation is pretty clear and straightforward on how to do this [https://cloud.google.com/container-registry/docs/pushing-and-pulling].

Of note are the details of how the docker image must be tagged.

The container image called alpine-blog is the image created by the commands described above, it’s a lightweight alpine linux, running nginx to host the static site. This is what I want to deploy

docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
alpine-blog         latest              d3c402637e4e        29 minutes ago      20MB
nginx               alpine              a60696d9123b        5 days ago          15.5MB
hello-world         latest              1815c82652c0        2 weeks ago         1.84kB

Here’s where we tag our image to conform to Google’s container repository requirements as well as push it to the repository. The container repo documentation does not mention the need for versioning ags in the image but as I later found out, to upgrade the images and make containers restart with new versions, it’s best to give an explicit version for, here 0.1.0, for each image.

docker tag d3c402637e4e gcr.io/robren-blog-v1/alpine-blog:0.1.0
docker images -a
REPOSITORY                          TAG                 IMAGE ID            CREATED             SIZE
alpine-blog                         latest              d3c402637e4e        33 minutes ago      20MB
gcr.io/robren-blog-v1/alpine-blog   0.1.0               d3c402637e4e        33 minutes ago      20MB
nginx                               alpine              a60696d9123b        5 days ago          15.5MB
hello-world                         latest              1815c82652c0        2 weeks ago         1.84kB

Next we need to push the now correctly named image to the google container service. Note the use of the gcloud docker command not the docker push command that docker users maybe familiar with.

gcloud docker -- push gcr.io/robren-blog-v1/alpine-blog:0.1.0
The push refers to a repository [gcr.io/robren-blog-v1/alpine-blog]
1c99d108e437: Preparing
3e2835458dad: Preparing
3da1ee90cad8: Preparing
2ab3866407e2: Preparing
040fd7841192: Preparing

# The first time this is attempted I got an error but an easy to solve link to click and enable the
API from the Cloud console.
denied: Please enable Google Container Registry API in Cloud Console at https://console.cloud.google.com/apis/api/containerregistry.googleapis.com/overview?project=robren-blog-v1 before performing this operation.

# After enabling the API
(google-cloud) ➜  robren-blog git:(master) ✗ gcloud docker -- push gcr.io/robren-blog-v1/alpine-blog:0.1.0
The push refers to a repository [gcr.io/robren-blog-v1/alpine-blog]
1c99d108e437: Pushed
3e2835458dad: Pushed
3da1ee90cad8: Pushed
2ab3866407e2: Pushed
040fd7841192: Layer already exists
0.1.0: digest: sha256:f019e80d59ef82340411ada054987b56b115b6c65f24426f05f34075e6923833 size: 1364

Instruct Kubernetes to run my image.

So far everything we’ve done is easily understandable by anyone with even a small amount of experience in docker, we created images with suitable tags and uploaded to a google container repo.

Now to run copies of these containers, we’ve got multiple choices and multiple new abstractions to learn. Some of these choices are becomming obsolete, Kubernetes having evolved a lot in the last few years. I’ll cut to the chase and point out what I interpret as the right way to deploy containers within Kuberenetes. The current best practice appears to be to use so called deployments, read on.

Pods

  • Pods A group of one or more running containers is called a “pod” in Kubernetes parlance. Pods can be created and managed directly but it’s not recommended

    The following advice is from the Kubernetes documentation

Pods aren’t intended to be treated as durable entities. They won’t survive scheduling failures, node failures, or other evictions, such as due to lack of resources, or in the case of node maintenance. In general, users shouldn’t need to create pods directly. They should almost always use controllers (e.g., Deployments), even for singletons. Controllers provide self-healing with a cluster scope, as well as replication and rollout management.

This beg’s the question what’s a Deployment?

Deployments.

Deployments appear to be the “way to go!” They are an abstraction which provide declarative definitions for how to run Pods ( i.e our desired containers) and Replica sets (the documentation is particularly unclear here)

The Kuberenetes documentation states:

A ReplicaSet ensures that a specified number of pod “replicas” are running at any given time.

Deployments manage updating pods to new versions as well as managing the containers within the pods to ensure for example that they are restarted if the node they are living on dies.

We can either create the deployment from the command line, as shown below or, as a better practice, specify the parameters for the deployment in a .yaml file as shown next.

 kubectl run rr-blog --image=gcr.io/robren-blog-v1/alpine-blog --port=80

kubectl get deployments
NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
rr-blog   1         1         1            1           9h

From now on we’ll use the yaml file for specifying how to create the deployment.

cat deploy.yaml
apiVersion: apps/v1beta1 # for versions before 1.6.0 use extensions/v1beta1
kind: Deployment
metadata:
  name: rr-blog-deploy
spec:
  replicas: 3
  template:
    metadata:
      labels:
        run: rr-blog
    spec:
      containers:
      - name: alpine-blog
        image: gcr.io/robren-blog-v1/alpine-blog:0.29.0
        ports:
        - containerPort: 80
        imagePullPolicy: Always
  • The name parameters at the top level defines the name of the deployment.
  • The replicas parameter defines how many instances of the containers we want to run
  • The labels parameter containing the label run and value rr-blog maybe used as a filter in various get commands.
  • The spec section is similar to a docker compose file, specifying where the container images come from, what internal port to listen on etc.
kubectl create -f deploy.yaml
deployment "rr-blog-deploy" created
kubectl get deploy
NAME             DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
rr-blog-deploy   3         3         3            2           4s

A few seconds later we go from 2 to 3 available pods

kubectl get deploy
NAME             DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
rr-blog-deploy   3         3         3            3           7s

We can see the individual containers, within pods, be using the get command.

(robren-blog) ➜  robren-blog git:(master) ✗ kubectl get pods
NAME                              READY     STATUS    RESTARTS   AGE
rr-blog-deploy-1503925906-77sc3   1/1       Running   0          11s
rr-blog-deploy-1503925906-bsprz   1/1       Running   0          11s
rr-blog-deploy-1503925906-qfmlg   1/1       Running   0          11s

Expose the webserver to the outside world.

In order to communicate with the pods we’ve got to explicitly “expose” the relevant ports either to other nodes internally within a cluster or externally. For a webserver we’ll want to expose port 80.

kubectl expose deployment robren-blog-deployment --name rr-blog-deploy --port=80 --target-port=80  --type=LoadBalancer

After running the kubectl expose command we’ve created a new object, a “service”; another abstraction. As far as I understand from the documents and experiments, I need to expose the deployment using a LoadBalancer type of service to get an external IP address.

We can see what external IP has been exposed by using the “get service” command.

(google-cloud) ➜  robren-blog git:(master) ✗ kubectl get service
rr-blog-deploy
NAME             CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE
rr-blog-deploy   10.19.253.94   35.185.16.202   80:31741/TCP   52s

Be aware that, the LoadBalancer is an additional cost incurred when running a service. A necessary component when a real world application has many instances of say a web server which can all be reached via a single anycast IP address, but perhaps over the top for our single test blog.

After updating our DNS records, I use fastmail as my DNS provider, to point robren.net to the External-IP we can then see the blog via a browser, or curl it to prove it’s alive!

curl robren.net
<!DOCTYPE html>
<html lang="en" prefix="og: http://ogp.me/ns# fb: https://www.facebook.com/2008/fbml">
<head>
    <title>Rob Rennison's Blog</title>
Snip

Updating the blog

Assuming you’re using versioned images and have both uploaded a new image as well as modified the image tag specified within the deploy.yaml file

cat deploy.yaml
--snip
spec:
      containers:
      - name: alpine-blog
        image: gcr.io/robren-blog-v1/alpine-blog:0.2.0
--snip

There are at least two ways of updating the running containers.

Service interrupting way

There’s a way of updating the blog which is destructive, causing a few seconds of downtime, handy for development but not recommended for a production service.

kubectl replace -f deploy.yaml --force

Rolling updates

Upload a new image with tag :0.3.0 then update the desired image, in the deploy.yaml file (or in a new yaml file). This time we use the kubectl apply command to perform a rolling update whereby individual pods are gradually updated to the new version.

kubectl apply -f deploy.yaml --record

$ kubectl describe deployments
Name:                   rr-blog-deploy
Namespace:              default
CreationTimestamp:      Sun, 09 Jul 2017 22:37:21 -0400
Labels:                 run=rr-blog
Annotations:            deployment.kubernetes.io/revision=2
                        kubectl.kubernetes.io/last-applied-configuration={"apiVersion":"apps/v1beta1","kind":"Deployment","metadata":{"annotations":{},"name":"rr-blog-deploy","namespace":"default"},"spec":{"replicas":3,"temp...
                        kubernetes.io/change-cause=kubectl apply --filename=deploy.yaml --record=true
Selector:               run=rr-blog
Replicas:               3 desired | 3 updated | 3 total | 3 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
  Labels:       run=rr-blog
  Containers:
   alpine-blog:
    Image:              gcr.io/robren-blog-v1/alpine-blog:0.3.0
    Port:               80/TCP
    Environment:        <none>
    Mounts:             <none>
 
---snip

Contemplation and next steps

In software engineering an often quoted aphorism is “Any problem can be solved with an additional level of abstraction”. These abstractions are necessary in order to utilize the full power of Kubernetes as an orchestrator for containers, e.g using replication controllers, having containers within multiple domains etc.

Initially they seem a bit confusing and unclear but that’s where struggling though an example helps to cement the concepts. In retrosepct we can go a long way by understanding the follwing objects/ abstractions:

  • Pods
  • Deployments
  • Services.

The purpose of this experiment was to get some experience using the gcloud CLI as well as kubectl and get o sense for how this compares to say docker machine for a simple remote container deployment. If we wanted multiple instances of our blog using plain docker we’d be end up using docker swarm so that too would introduce more abstractions.

Using the Google Container service APIs via gcloud and Kubectl was surprisingly easy and intuitive once I’d got a handle on the various abstractions. The help is pretty good too.

There are quite a few moving parts! Clearly overkill for a tiny static site, but the point was to utlize a concrete application, my blog, and see how to deploy it using Kubernetes.

Hopefully this has provided an updated view on how to deploy a simple app, from which readers can then tackle the documentation and other exmaples in more depth to deploy more complex multi container applications back end database containers etc.

From a cost perspective this google container service is overkill for a low load static blog, (the load balancer being the major cost), but it’s designed to scale to much larger systems which do require load balancers, multiple domain resiliancy etc so this is not a criticism.

I think I’ll keep the blog on GKE for a month or just to see what the costs are, I know it will be more than Digital Ocean just for the LoadBalance alone. I’ll then move it onto be moving onto a simpler cheaper solution. I’ve heard good things about Vultr.com and will explore them next. Of course on the next Platform, it would be too simple to merely run nginx on there! I’ll be looking at either docker-swarm or perhaps installing Kuberentes there too.