Containers Homework

Every day is a school day. Why do the bare minimum when doing something different is so much fun?

At work, we have several "working groups" centred around specific technologies we use. The purpose of these is to have a way for us lowly engineers to have input into our "tech menu" that we work with. There are several software-engineering-focused groups (JVM, Nodejs, etc.) but so far only one that relates to operations in a broad sense: the container working group.

For the past 4 or 5 meetings of this group, it has felt very much like just an update on what we as a business are doing with containers, rather than anything interesting, so the chair of the group took it upon himself to set us some homework to get us all involved in working with containers. The homework task was as follows:

Apologies for the short notice but I need to cancel today's working group. In lieu of actually having the meeting I'd like to set you all a small piece of homework...! I'd like you to use Kubernetes to display a local web page which displays your name and a picture of you. Extra credit if you pull / create the name as a secret! And we'll discuss and demo your attempts in the next session.

Now it would be easy enough to just write an HTML page, store it as a secret in Kubernetes, and then mount as /var/www/html/index.html in an off-the-shelf nginx container. Where is the fun in that? I decided that I would create my own application that would display all the required information (plus more) pulling from environment variables, and put that in a container.

The application

I initially defaulted to writing a quick web page in Express, quickly changing my mind and opting for writing it in Go instead. Full disclosure before you read this. I am not a software engineer of any kind, nor am I trained in CS in any way, shape, or form. I'm most comfortable in Bash and not afraid to admit it.

package main

import (
    "net/http"
    "html/template"
    "os"
)

func main() {
        http.HandleFunc("/", Serve)
        http.ListenAndServe(":1337", nil)
}

func Serve(w http.ResponseWriter, r *http.Request) {

    tmpl, err := template.ParseFiles("index.gohtml")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }

    type Data struct {
        Name string
        ImgURL  string
        BannerCol   string
        BannerTextCol   string
    }

    data := Data{Name: os.Getenv("NAME"), ImgURL: os.Getenv("IMGURL"), BannerCol: os.Getenv("BANNERCOLOUR"), BannerTextCol: os.Getenv("BANNERTEXTCOLOUR")}

    err = tmpl.Execute(w, data)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

The two interesting bits of this code (at least as far as I'm concerned) is the use of html/template and also os.Getenv. The template file is a very simple HTML page designed to display name and picture in a box in the middle of the screen:

<!DOCTYPE html>
<html lang="en">
<head>
<style type="text/css">
    html,body{height:100%;width:100%;padding:0;margin:0;font-family:'Helvetica Neue',sans-serif;color:#{{.BannerTextCol}};}
#container{display:flex;justify-content:center;align-items:center;width:100%;height:100%;}
#contents{width:50%;border-radius:20px;border:solid 1px #ddd;box-shadow: 0px 0px 15px #bbb;text-align:center;}
h1{background-color:#{{.BannerCol}};border-top-left-radius:20px;border-top-right-radius:20px;padding:10px 0px;margin:0;}
</style>
<title>{{.Name}}'s Container WG Homework</title>
</head>
<body>
<div id="container">
<div id="contents">
<h1>My name is {{.Name}}</h1>
<img src="{{.ImgURL}}" alt="{{.Name}}'s image">
</div>
</div>
</body>
</html>

Putting it in a container

Once we have a working application, we need to get it running in a immutable container, which was accomplished with the following Dockerfile:

FROM golang:latest
LABEL maintainer="Oliver Leaver-Smith <oliver@leaversmith.com>"
WORKDIR /app
COPY ./app .
RUN go build -o main .
EXPOSE 1337
CMD ["./main"]

Onwards to the Kube cluster

In the application, we make reference to four environment variables for name, image URL, banner colour, and banner text colour. These need setting in our Kubernetes manifest along with a whole host of other configurations. The finished YAML looks like this:

apiVersion: apps/v1
kind: Deployment
metadata:
    name: ols-ccwg-hwk
    labels:
        app: ols-ccwg-hwk
spec:
    replicas: 3
    selector:
        matchLabels:
            app: ols-ccwg-hwk-app
    template:
        metadata:
            labels:
                app: ols-ccwg-hwk-app
        spec:
            containers:
            - name: ols-ccwg-hwk
                image: docker.<super secret internal artifact store>.io/ols/ols-ccwg-hwk:latest
                imagePullPolicy: Always
                ports:
                    - containerPort: 1337
                envFrom:
                - secretRef:
                        name: ols-ccwg-hwk-secrets
---
apiVersion: v1
kind: Service
metadata:
    name: ols-ccwg-hwk-service
spec:
    selector:
        app: ols-ccwg-hwk-app
    ports:
    - protocol: TCP
        port: 1337
        targetPort: 1337
    type: LoadBalancer
---
apiVersion: v1
kind: Secret
metadata:
    name: ols-ccwg-hwk-secrets
data:
    NAME: b2xz
    IMGURL: aHR0cHM6Ly9vbHMud3RmL29scy5wbmc=
    BANNERCOLOUR: MzY0ZmM3
    BANNERTEXTCOLOUR: ZmZm

The result

Et voila! The page looks like this

Finished product

Aftermath

Our next meeting is next Monday (2019-08-05), so we will all demo our attempts then. I'm aware I've skipped through quite a lot of ground, and assumed quite a lot of knowledge. If you have any comments or corrections, or would like to discuss this further, feel free to mail my public mailbox at ~ols/public-inbox@lists.sr.ht.