In this example we are going to deploy a very simple database driven Golang RESTful application to Kubernetes. The important point here is that the database is not a Pod. It is in the cloud so we will access it through the Internet by relying on secrets. The application will run its own HTTP server as usual and serve requests coming in.


Structure


├── Makefile
├── .env.dist
├── build
│ ├── deploy
│ │ └── k8s
│ │ ├── deployment.yaml
│ │ ├── secret.yaml
│ │ └── service.yaml
│ └── docker
│ └── dev
│ └── Dockerfile
└── main.go

Files


deployment.yaml


apiVersion: apps/v1
kind: Deployment

metadata:
name: sport-deployment
labels:
app: sport

spec:
replicas: 2
selector:
matchLabels:
app: sport
template:
metadata:
labels:
app: sport
spec:
containers:
- name: golang
image: you/sport:latest
ports:
- containerPort: 8888
env:
- name: HTTP_PORT
value: "8888"
- name: DB_PORT
value: "15209" # This comes from ngrok. You will see that later.
- name: DB_NAME
value: "football"
- name: DB_HOST
valueFrom:
secretKeyRef:
name: sport-secret
key: db-host
- name: DB_USER
valueFrom:
secretKeyRef:
name: sport-secret
key: db-user
- name: DB_PASS
valueFrom:
secretKeyRef:
name: sport-secret
key: db-pass

service.yaml


apiVersion: v1
kind: Service

metadata:
name: sport-service

spec:
type: NodePort
selector:
app: sport
ports:
- protocol: TCP
port: 80
targetPort: 8888

secret.yaml


You are not supposed to keep this in your repository as it contains secrets.


apiVersion: v1
kind: Secret

metadata:
name: sport-secret

type: Opaque

data:
# This db-host comes from ngrok. You will see that later.
db-host: NC50Y3Aubmdyb2suaW8= # echo -n '4.tcp.ngrok.io' | base64
db-user: dXNlcg== # echo -n 'user' | base64
db-pass: cGFzcw== # echo -n 'pass' | base64

Dockerfile


#
# STAGE 1: build
#
FROM golang:1.15-alpine3.12 as build

WORKDIR /source
COPY . .

RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o bin/main main.go

#
# STAGE 2: run
#
FROM alpine:3.12 as run

COPY --from=build /source/bin/main /main

ENTRYPOINT ["/main"]

main.go


package main

import (
"database/sql"
"fmt"
"log"
"net/http"
"os"

"github.com/joho/godotenv"

_ "github.com/go-sql-driver/mysql"
)

func main() {
_ = godotenv.Load(".env.dist")

dsn := fmt.Sprintf(
"%s:%s@tcp(%s:%s)/%s",
os.Getenv("DB_USER"),
os.Getenv("DB_PASS"),
os.Getenv("DB_HOST"),
os.Getenv("DB_PORT"),
os.Getenv("DB_NAME"),
)

fmt.Println("db dsn:", dsn)

db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalln("db conn:", err)
}
defer db.Close()

if err := db.Ping(); err != nil {
log.Fatalln("db ping:", err)
}

fmt.Println("db conn: up!")

rtr := http.NewServeMux()
rtr.HandleFunc("/api/v1/team", team)

if err := http.ListenAndServe(":" + os.Getenv("HTTP_PORT"), rtr); err != nil {
log.Fatalln("app start:", err)
}
}

func team(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("my team\n"))
}

Makefile


The build, run and local-test commands are for your local environment. You can use them to test your application in local environment.


## Build application binary.
.PHONY: build
build:
go build -race -ldflags "-s -w" -o bin/main main.go

## Build application binary and run it.
.PHONY: run
run: build
bin/main

## Send a dummy request to the local server.
.PHONY: local-test
local-test:
curl --request GET http://localhost:8888/api/v1/team

## -----------------------------------------------------------------------------------

## Build, tag and push application image to registry then clean up.
.PHONY: push
push:
docker build -t you/sport:latest -f ./build/docker/dev/Dockerfile .
docker push you/sport:latest
docker rmi you/sport:latest
docker system prune --volumes --force

## Apply secret and deploy application to kubernetes cluster.
.PHONY: deploy
deploy: secret
kubectl apply -f build/deploy/k8s/secret.yaml
kubectl apply -f build/deploy/k8s/deployment.yaml
kubectl apply -f build/deploy/k8s/service.yaml

## Send a dummy request to the exposed pod on kubernetes.
.PHONY: k8s-test
k8s-test:
curl --request GET $(shell minikube service sport-service --url)/api/v1/team

.env.dist


HTTP_PORT=8888

DB_HOST=localhost
DB_PORT=3306
DB_NAME=football
DB_USER=user
DB_PASS=pass

Test


Localhost


$ make run
go build -race -ldflags "-s -w" -o bin/main main.go
bin/main
db dsn: user:pass@tcp(localhost:3306)/football
db conn: up!

$ make local-test
curl --request GET http://localhost:8888/api/v1/team
my team

Let's now build and push our Docker image to Docker Hub. This image will be pulled when we run our deployment so it is important.


$ make push
docker build -t you/sport:latest -f ./build/docker/dev/Dockerfile .
Sending build context to Docker daemon 7.762MB
.......

Ngrok


Ngrok is a free tunnelling service. We are going to use it to expose our local database server to Internet so that we can access it through the Internet. Go to ngrok and sign up to it and download it.


My MySQL database server is a docker container and looks like below. It could be your AWS service too.


$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
8a7937a05baa mysql:5.7.24 "docker-entrypoint.s…" 4 hours ago Up 4 hours 0.0.0.0:3306->3306/tcp, 33060/tcp football-db

Expose it to the Internet with ngrok.


$ ./ngrok authtoken {your-token-goes-here}
Authtoken saved to configuration file: /Users/you/.ngrok2/ngrok.yml

$ ./ngrok tcp 3306
Session Status online
Account ngrok-test (Plan: Free)
Version 2.3.35
Region United States (us)
Web Interface http://127.0.0.1:4040
Forwarding tcp://4.tcp.ngrok.io:15209 -> localhost:3306

As you can see below, the domain and the port are being used in our k8s YAML files.


Deployment


$ make deploy
kubectl apply -f build/deploy/k8s/secret.yaml
secret/sport-secret created
kubectl apply -f build/deploy/k8s/deployment.yaml
deployment.apps/sport-deployment created
kubectl apply -f build/deploy/k8s/service.yaml
service/sport-service created

Let's check the environment variables in the pod.


$ kubectl exec sport-deployment-66944b6d95-plswg -- printenv
HOSTNAME=sport-deployment-66944b6d95-plswg
DB_PASS=pass
HTTP_PORT=8888
DB_PORT=15209
DB_NAME=football
DB_HOST=4.tcp.ngrok.io
DB_USER=user
.....

Let's now test if application runs fine. This means it successfully connected to database.


$ make k8s-test
curl --request GET http://192.168.99.100:32586/api/v1/team
my team