Deploy App with Kubernetes to GCP

Getting Start

In this article I will show you how to deploy a simple Go application that will consume a public Anime api called Jikan. The goal is to guide you through how to setup a kubernetes cluster on Google Cloud Platform and deploy an application there, so I won't go into details on how Go code work.

What you need to work along the way in this article is as follow:

Prepare a Demo Project

As always the source code is available on my github account and you can get it here. Before we dive into the actual deployment, lets take a quick look at what our demo project does shall we?

Our demo project is called Jikan, it's an anime searching service which consume MyAnimeList public API named (you guess it) Jikan. To consume service issue a GET request to the following endpoint

$ curl http://{server}/search?q={title}

Here is our main logic. Basically all it does is consume Jikan api, map response to our own type and return.

type service struct {
	http *http.Client

func (s *service) SearchAnime(query string) ([]*jikan.Anime, error) {
	res, err := s.http.Get(fmt.Sprintf(apiURL, query))
	if err != nil {
		return nil, err
	defer res.Body.Close()

	buf, err := ioutil.ReadAll(res.Body)
	if err != nil {
		return nil, err

	var r jikanResult
	if err := json.Unmarshal(buf, &r); err != nil {
		return nil, err

	animes := make([]*jikan.Anime, 1)
	for _, ja := range r.Results {
		status := "Finished"
		if ja.Airing {
			status = "Ongoing"

		animes = append(animes, &jikan.Anime{
			MalURL:   ja.URL,
			ImageURL: ja.ImageURL,
			Title:    ja.Title,
			ShowType: ja.Type,
			Status:   status,

	return animes, nil

To run a project locally

$ go run github.com/PrinceNorin/jikan/cmd/http

Packing Our App

The first step to deployment is to pack our application into a container. I want the image to be as small as possible so I picked scratch as base image. To summary what's going is we use multi-stage build. The first stage is to build our Go application into a binary file and then in the final stage copy that binary and execute it in an entrypoint.

FROM golang:1.13.5-alpine3.10 AS builder
LABEL builder=true

RUN mkdir /user && \
  echo 'nobody:x:65534:65534:nobody:/:' > /user/passwd && \
  echo 'nobody:x:65534:' > /user/group

WORKDIR /go/src/jikan

ADD go.mod go.sum ./
RUN go mod download

ADD . .
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -a -v -i -o /go/bin/jikan \
  -ldflags="-s -w -extldflags '-f no-PIC -static'" \
  -tags 'osusergo netgo static_build' \

FROM scratch
LABEL builder=false

COPY --from=builder /user/group /user/passwd /etc/
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /go/bin/jikan .

USER nobody:nobody
ENTRYPOINT ["/app/jikan"]

The resulting image is ~ 6MB

$ docker build -t norin/jikan:1.0.0 .
$ docker images | grep jikan
norin/jikan                1.0.0                     8cdcc06ede56        About an hour ago   6.04MB

Now push image to docker registry

$ docker login # enter credentials
$ docker push norin/jikan:1.0.0

Define Kubernetes Deployment

The following will create a deployment name jikan that manage 4 replication of our apps which expose on container port 8080. The image will be pull from my docker hub repository norin/jikan:1.0.0.

apiVersion: apps/v1
kind: Deployment
  name: jikan
    app: jikan
  replicas: 4
      app: jikan
        app: jikan
      - name: jikan
        image: norin/jikan:1.0.0
        - containerPort: 8080

Next we need a way to access our application from the web. There are many way to archive this but here I chose to create a LoadBalancer. It will distribute traffic it receive to service labeled with app: jikan and map to it port 8080.

apiVersion: v1
kind: Service
  name: jikan
    service.beta.kubernetes.io/linode-loadbalancer-throttle: "100"
    service.beta.kubernetes.io/linode-loadbalancer-default-protocol: "http"
    app: jikan
  type: LoadBalancer
  - port: 80
    targetPort: 8080
    protocol: TCP
    name: http
    app: jikan

Setup GCP Project & Cluster

The next task is to setup GCP project & cluster by run the following commands. The first command will authenticate and get access token needed to subsequence commands. Next will create a project name Jikan Anime with id jikan-anime. And finally we create a cluster name jikan-anime in Singapor. The last command switch kubernetes current context to our previous created cluster.

$ gcloud auth login # provide login credentials
$ gcloud gcloud projects create jikan-anime --name="Jikan Anime" --labels=type=jikan
$ gcloud config set project jikan-anime
$ gcloud container clusters create jikan-anime --zone=asia-southeast1-a
$ gcloud container clusters get-credentials jikan-anime --zone=asia-southeast1-a

Deploy App to GCP

Last but not least it's time to deploy our application

$ kubectl apply -f k8s/deployment.yaml
$ kubectl get svc
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)        AGE
jikan        LoadBalancer   80:30523/TCP   97m
kubernetes   ClusterIP     <none>          443/TCP        115m
$ kubectl get pods
jikan-978fdb895-4jd47   1/1     Running   0          86m
jikan-978fdb895-5bnct   1/1     Running   0          86m
jikan-978fdb895-7mztx   1/1     Running   0          86m
jikan-978fdb895-znh7s   1/1     Running   0          86m

As you can see from the output 4 pods and 1 loadbalancer are created as defined in the spec.


Try to search for anime title by issue a GET request to the following endpoint. Note the external ip from loadbalancer.

$ curl

Fix Bug

There is a bug in version 1.0.0 of our code, which didn't property escape the query string. Lets fix this and deploy the patch to production.

func (s *service) SearchAnime(query string) ([]*jikan.Anime, error) {
	res, err := s.http.Get(fmt.Sprintf(apiURL, url.QueryEscape(query)))
    // rest of the code

Rebuild image

$ docker build -t norin/jikan:1.0.1 .
$ docker push norin/jikan:1.0.1

Release patch to production and notice the finall pod image version

$ kubectl set image deployment/jikan jikan=norin/jikan:1.0.1 --record
$ kubectl rollout status deployment/jikan
$ kubectl get pods
NAME                    READY   STATUS    RESTARTS   AGE
jikan-978fdb895-4jd47   1/1     Running   0          97m
jikan-978fdb895-5bnct   1/1     Running   0          97m
jikan-978fdb895-7mztx   1/1     Running   0          97m
jikan-978fdb895-znh7s   1/1     Running   0          97m
$ kubectl describe pod jikan-978fdb895-4jd47 | grep Image
Image:          norin/jikan:1.0.1
Image ID:       docker-pullable://norin/[email protected]:a1b48661e08183a0f8bc7a13c88369ff81c49a50d1ee74908c8a13a25c7f1354

All Rights Reserved

Let's register a Viblo Account to get more interesting posts.