Lei Mao bio photo

Lei Mao

Machine Learning, Artificial Intelligence. On the Move.

Twitter Facebook LinkedIn GitHub   G. Scholar E-Mail RSS

Introduction

In the age of Internet, instead of setting up a server on our own, we deploy our website applications widely on cloud service providers. Google Cloud provides several hosting options , including App Engine and Kubernetes Engine, for our websites. For simple applications, App Engine is desired because it is simple and Google Cloud manages everything for you, so you don’t have to worry about scalability or anything else. However, when the applications become sophisticated and require complicated environment setups, Kubernetes Engine becomes a good choice.


In this blog post, I am going to present a self-contained tutorial for setting up an containerized application on Google Cloud Kubernetes Engine.

Dependencies

  • Google Cloud SDK
  • Docker
  • Git

Google Cloud Platform Console actually has all the dependencies installed. We could directly run our command in the console.

Tutorial

Application

In this project, we are going to use Google’s official application as example. In case Google is going to change their official example, I have forked the repository to my account.

# Git 
$ git clone https://github.com/leimao/kubernetes-engine-samples.git
$ cd kubernetes-engine-samples/quickstart/python/
$ ls
app.py  Dockerfile

Tha app is a simple Flask app which runs at port 8080 by default and prints “Hello World”. In app.py, we have

import os

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    target = os.environ.get('TARGET', 'World')
    return 'Hello {}!\n'.format(target)

if __name__ == "__main__":
    app.run(debug=True,host='0.0.0.0',port=int(os.environ.get('PORT', 8080)))

The Dockerfile sets up the most basic environment the app requires and starts app.

# Use the official Python image.
# https://hub.docker.com/_/python
FROM python:3.7

# Copy local code to the container image.
ENV APP_HOME /app
WORKDIR $APP_HOME
COPY . .

# Install production dependencies.
RUN pip install Flask gunicorn

# Run the web service on container startup. Here we use the gunicorn
# webserver, with one worker process and 8 threads.
# For environments with multiple CPU cores, increase the number of workers
# to be equal to the cores available.
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 app:app

We could almost treat the app as a black box for now, and just have to remember this app will run and listen to port 8080 in the container.

Docker Container

We set up some configuration variables.

$ gcloud config set project [PROJECT_ID]
$ gcloud config set compute/zone [COMPUTE_ENGINE_ZONE]

In our case, we use

$ gcloud config set project leimao-app
$ gcloud config set compute/zone us-central1-a

We have to first build Docker image for our application on our local machine.

# Set an environment variable for project_id for convenience.
$ export PROJECT_ID="$(gcloud config get-value project -q)"
$ echo $PROJECT_ID
leimao-app
# Build Docker image
$ docker build -t gcr.io/${PROJECT_ID}/hello-app:v1 .
# Check the Docker image built
$ docker images
REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
gcr.io/python-app/hello-app      v1                  c0cd4f15736d        17 seconds ago      929MB

The next step is to upload the Docker image to Google Cloud container registry. To do that, we need to authenticate Google Cloud container registry.

$ gcloud auth configure-docker

Upload the Docker image to Google Cloud container registry.

$ docker push gcr.io/${PROJECT_ID}/hello-app:v1

To check the Docker image has been uploaded successfully.

# Check the image
$ gcloud container images list --repository=gcr.io/${PROJECT_ID}
NAME
gcr.io/leimao-app/hello-app
# Check the tag
$ gcloud container images list-tags gcr.io/${PROJECT_ID}/hello-app
DIGEST        TAGS  TIMESTAMP
98c9598a764a  v1    2019-08-06T16:04:53

Create Kubernetes Container Cluster

The best way to configure a container cluster is actually to go to https://console.cloud.google.com/kubernetes, select the right configurations, and click command line to get the command line for creating the cluster.


Here, for simplicity, we create a cluster of 3 nodes and use the rest of the configurations by default. Among the 3 nodes, one node is the master node and the rest two nodes are worker nodes. This step may take a while.

$ gcloud container clusters create hello-cluster --num-nodes=3
NAME           LOCATION       MASTER_VERSION  MASTER_IP      MACHINE_TYPE   NODE_VERSION   NUM_NODES  STATUS
hello-cluster  us-central1-a  1.12.8-gke.10   35.192.51.130  n1-standard-1  1.12.8-gke.10  3          RUNNING

To check the cluster available in the account.

$ gcloud container clusters list
NAME           LOCATION       MASTER_VERSION  MASTER_IP      MACHINE_TYPE   NODE_VERSION   NUM_NODES  STATUS
hello-cluster  us-central1-a  1.12.8-gke.10   35.192.51.130  n1-standard-1  1.12.8-gke.10  3          RUNNING

To check the VM instances created.

$ gcloud compute instances list
NAME                                          ZONE           MACHINE_TYPE                PREEMPTIBLE  INTERNAL_IP    EXTERNAL_IP     STATUS
gke-hello-cluster-default-pool-c162bbac-5jrp  us-central1-a  n1-standard-1                            10.128.0.28    35.222.201.163  RUNNING
gke-hello-cluster-default-pool-c162bbac-6h9v  us-central1-a  n1-standard-1                            10.128.0.17    35.202.61.204   RUNNING
gke-hello-cluster-default-pool-c162bbac-fnhz  us-central1-a  n1-standard-1                            10.128.0.76    35.225.198.205  RUNNING

To check the node information.

$ kubectl describe nodes

Deploy Application on Cluster

It is possible that you have created a Kubernetes engine cluster previously, or you would like to choose one of the many existing clusters, we need to specify the cluster we want to deploy our app container.

$ gcloud container clusters get-credentials hello-cluster

To deploy our application with app name of hello-web using the image registry we just created.

We create a yaml file named deployment.yaml.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-web
  labels:
    app: hello
spec:
  selector:
    matchLabels:
      app: hello
      tier: web
  template:
    metadata:
      labels:
        app: hello
        tier: web
    spec:
      containers:
      - name: hello-app
        image: gcr.io/leimao-app/hello-app:v1
        ports:
        - containerPort: 8080
        # Set environment variables for the container.
        env:
          - name: PORT
            value: "8080"

Then we run

$ kubectl apply -f deployment.yaml

The app would be deployed then. Apps in Kubernetes are called Pods.


To check the deployment status,

$ kubectl get deployment
NAME        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
hello-web   1         1         1            1           84s

To check the pods status,

$ kubectl get pods
NAME                         READY   STATUS    RESTARTS   AGE
hello-web-67878c7d86-v8mfj   1/1     Running   0          2m23s

Sometimes the STATUS will be abnormal values, which means that there is something going wrong in your container. You would have to delete the deployment by

$ kubectl delete deployment hello-web

In some tutorials from Google Cloud, they suggest to create deployment using command line arguments rather than using yaml file.

$ kubectl create deployment hello-web --image=gcr.io/${PROJECT_ID}/hello-app:v1

I think this method is inferior because you would not configure a lot of things in detail and you would not be able to pass environment variable to container. In our case, environment variable is necessary for the container to start correctly.

Expose Application to Internet

Although the application has been deployed, it has not been exposed to internet so outsiders would not be able to see.


To expose application to internet, again we would be using yaml file. Create a service.yaml.

# The hello service provides a load-balancing proxy over the hello-app
# pods. By specifying the type as a 'LoadBalancer', Kubernetes Engine will
# create an external HTTP load balancer.
apiVersion: v1
kind: Service
metadata:
  name: hello
spec:
  type: LoadBalancer
  selector:
    app: hello
  ports:
  - port: 80
    targetPort: 8080
$ kubectl apply -f service.yaml

Check the services. Allocating external IP might be slow.

$ kubectl get services
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
hello        LoadBalancer   10.15.244.113   <pending>     80:32759/TCP   8s

Check the services again after a while.

$ kubectl get services
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP      PORT(S)        AGE
hello        LoadBalancer   10.15.244.113   35.202.208.161   80:32759/TCP   111s
kubernetes   ClusterIP      10.15.240.1     <none>           443/TCP        136m

It is equivalent to do this using command line arguments.

$ kubectl expose deployment hello-web --type=LoadBalancer --port 80 --target-port 8080

The --port flag specifies the port number configured on the Load Balancer, and the --target-port flag specifies the port number that the hello-app container is listening on.

Check whether the application has been exposed.

$ curl 35.202.208.161
Hello World!
$ curl 35.202.208.161:80
Hello World!

Alternatively, open our web browser and go to http://35.202.208.161/ or http://35.202.208.161:80/. Note that currently this application only supports http protocol.

Upgrade Kubernetes Cluster

We would like to add two additional replicas (cluster candidates) to deployment for the same application. We modify the deployment.yaml file.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-web
  labels:
    app: hello
spec:
  selector:
    matchLabels:
      app: hello
      tier: web
  # Set replicas here
  replicas: 3
  template:
    metadata:
      labels:
        app: hello
        tier: web
    spec:
      containers:
      - name: hello-app
        image: gcr.io/leimao-app/hello-app:v1
        ports:
        - containerPort: 8080
        # Set environment variables for the container.
        env:
          - name: PORT
            value: "8080"

To apply the changes,

$ kubectl apply -f deployment.yaml

To check the deployment status,

$ kubectl get deployment hello-web
NAME        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
hello-web   3         3         3            3           67m

To check the pod status,

$ kubectl get pods
NAME                         READY   STATUS    RESTARTS   AGE
hello-web-67878c7d86-jgphn   1/1     Running   0          72s
hello-web-67878c7d86-v8mfj   1/1     Running   0          67m
hello-web-67878c7d86-z6mbl   1/1     Running   0          72s

But we still use one external IP.

$ kubectl get service
NAME         TYPE           CLUSTER-IP      EXTERNAL-IP      PORT(S)        AGE
hello        LoadBalancer   10.15.244.113   35.202.208.161   80:31209/TCP   49m
kubernetes   ClusterIP      10.15.240.1     <none>           443/TCP        3h4m

Now, you have multiple instances of your application running independently of each other and you can use the kubectl scale command to adjust capacity of your application.

The load balancer you provisioned in the previous step will start routing traffic to these new replicas automatically.

Deploy New Version of Application

We would like to change our “Hello World” application to “Hello Underworld” application. So we are going to change app.py.

import os

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello_world():
    target = os.environ.get('TARGET', 'Underworld')
    return 'Hello {}!\n'.format(target)

if __name__ == "__main__":
    app.run(debug=True,host='0.0.0.0',port=int(os.environ.get('PORT', 8080)))

Build and upload the new container.

$ docker build -t gcr.io/${PROJECT_ID}/hello-app:v2 .
$ docker push gcr.io/${PROJECT_ID}/hello-app:v2

We also change our deployment.yaml to use the new container.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-web
  labels:
    app: hello
spec:
  selector:
    matchLabels:
      app: hello
      tier: web
  replicas: 3
  template:
    metadata:
      labels:
        app: hello
        tier: web
    spec:
      containers:
      - name: hello-app
        image: gcr.io/leimao-app/hello-app:v2
        ports:
        - containerPort: 8080
        env:
          - name: PORT
            value: "8080"

To apply the changes,

$ kubectl apply -f deployment.yaml

We could check that the update was successful.

$ curl 35.202.208.161
Hello Underworld!

Clearn Up Application

To delete service,

$ kubectl delete service hello
service "hello" deleted

We could see the service were deleted.

$ kubectl get service
NAME         TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.15.240.1   <none>        443/TCP   3h42m

But the container cluster instance were still there.

$ gcloud container clusters list
NAME           LOCATION       MASTER_VERSION  MASTER_IP      MACHINE_TYPE   NODE_VERSION   NUM_NODES  STATUS
hello-cluster  us-central1-a  1.12.8-gke.10   35.192.51.130  n1-standard-1  1.12.8-gke.10  3          RUNNING

We delete the container cluster.

$ gcloud container clusters delete hello-cluster

The Docker image were still in the container registry. We check the container images information.

$ gcloud container images list --repository=gcr.io/${PROJECT_ID}
NAME
gcr.io/leimao-app/hello-app

Check the tag information.

$ gcloud container images list-tags gcr.io/leimao-app/hello-app
DIGEST        TAGS  TIMESTAMP
e9652084691e  v2    2019-08-06T20:04:02
98c9598a764a  v1    2019-08-06T16:04:53

We delete all the images related to hello-app.

$ gcloud container images delete gcr.io/${PROJECT_ID}/hello-app:v1 gcr.io/${PROJECT_ID}/hello-app:v2

Now there is no images for hello-app in the container registry.

$ gcloud container images list --repository=gcr.io/${PROJECT_ID}

Final Remarks

It seems that it is more desirable to use yaml to configure the Kubernetes settings instead of using command line arguments.

References