内容


Bluemix Kubernetes 实战进阶

Comments

前言

上一篇《Bluemix Kubernetes 实战入门》中,介绍了如何通过 kubectl 命令行来部署简单的应用程序到 Bluemix Kubernetes,并演示了 Kubernetes Failover,Loadbalance,Scale up/Scale down,Rolling update 等核心功能,这篇将增加简单的代码,演示部署和管理复杂的应用,以及介绍如何在 Kubernetes Cluster 环境中使用 Bluemix 已经有的丰富的 Cloud Foundry Base 的 Service。

部署一个两层的 WebApp 到 Bluemix Kubernetes

在实际的典型 Web 应用中,除了前端的 http 服务,通常还有个后端的数据库服务器,来持久化应用程序数据,为了更好的演示多层应用的开发和部署到 Kubernetes 的过程,我们给上一篇的 WebApp 加一个计时器,显示页面被访问次数,用 redis 来实现后端数据持久层。

编写 WebApp 代码

$ mkdir k8s-go-handson-02 && cd k8s-go-handson-02
$ vi webserver.go 或者其他你喜欢用的文本编辑器

package main
import (
    "fmt"
    "net/http"
    "os"
    "strconv"
    redis "github.com/go-redis/redis"
)

func indexHandler(redisClient *redis.Client) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        count, err := redisClient.Incr("counter").Result()
        if err != nil {
            fmt.Println("error at incr", err)
        }
        strCount := strconv.FormatInt(count, 10)
        fmt.Println("Accessing:", r.URL.Path, "Count:"+strCount)
        var version = "Version 1.0"
        var hostName = os.Getenv("HOSTNAME")
        var output = version + "\nHello Bluemix Kubernetes Cluster! \n" + "HostName:" + hostName + "\n" + "Review Count:" + strCount + "\n"
        fmt.Fprintf(w, output)
    })
}
func killHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Kill server!")
    os.Exit(1)
}

func getRedisClient() *redis.Client {
    var redisClient *redis.Client
    redisHost := os.Getenv("REDIS_HOST")
    if redisHost == "" {
        redisHost = "localhost"
    }
    redisPort := os.Getenv("REDIS_PORT")
    if redisPort == "" {
        redisPort = "6379"
    }
    redisClient = redis.NewClient(&redis.Options{
        Addr:     redisHost + ":" + redisPort,
        Password: "",
        DB:       0,
    })
    fmt.Println("Connected to redis host->" + redisHost + ":" + redisPort)
    return redisClient
}

func main() {

    redisDB := getRedisClient()
    http.HandleFunc("/kill", killHandler)
    http.Handle("/", indexHandler(redisDB))
    port := os.Getenv("WEBAPP_PORT")
    if port == "" {
        port = "8000"
    }
    fmt.Println("WebServer listening port:" + port)
    http.ListenAndServe(":"+port, nil)
}

代码说明

上面代码 import github.com/go-redis/redis 第三方库访问 redis,

增加了函数 getRedisClient() ,读环境变量 REDIS_HOST 和 REDIS_PORT 连接 redis 服务器

页面被访问时,对 redis 中的 counter 键值增 1,显示在首页

创建 Dockerfile

FROM golang:1.8-alpine
COPY webserver.go /go/src/webserver/
RUN  apk add --no-cache git && go get -u github.com/go-redis/redis && go install webserver && rm -rf /go/src/webserver
ENV WEBAPP_PORT=8000
EXPOSE $WEBAPP_PORT
CMD /go/bin/webserver

Dockerfile 中增加对 redis 库的引入设置,需要安装 git

Build Image

Build image and push 到 Bluemix registry

$ bx cr login
$ bx cr namespaces
$ docker build -t registry.ng.bluemix.net/<YOUR-NAMESPACE>/k8s-go-handson-02:v1 .
$ docker push registry.ng.bluemix.net/<YOUR-NAMESPACE>/k8s-go-handson-02:v1

部署 WebApp 到 Bluemix Kubernetes Cluster

#用 Docker HUB 上已经有的 redis image 并 expose 为 service
kubectl run go-redisdb  --image=redis --port=6379
kubectl expose deployment go-redisdb --type=NodePort

#部署 WebApp,通过环境变量把 redis 相关的信息传递给应用程序
kubectl run go-handson02 --image=registry.ng.bluemix.net/<YOUR-NAMESPACE>/k8s-go-handson-02:v1 --replicas=3 --env="REDIS_HOST=go-redisdb" --image-pull-policy=Always
kubectl expose deployment go-handson02 --type=NodePort --port=8000
 
$ kubectl get node,svc,deploy,pod

开启一个 shell 窗口,持续访问 NODEIP 的 go-handson02 service NODEPORT

$ while true; do curl http://$NODE:$NODEPORT; sleep 3; done

可以看到来自不同 container 的 response page, 计数器的值依次增加。

使用下面命令清理本次实验中所创建的资源:

$ kubectl delete deploy/go-handson02
$ kubectl delete svc/go-handson02
$ kubectl delete deploy/go-redisdb
$ kubectl delete svc/go-redisdb

通过 YAML 文件部署 WebApp 到 Bluemix Kubernetes Cluster

file k8s-go-handson-02.yml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: redisdb-handson-02
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: redisdb-handson-02
    spec:
      containers:
        - name: redisdb-handson-02
          image: "redis:3.0.7-alpine"
          
          imagePullPolicy: Always
          ports:
            - name: redisport
              containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
  name: redisdb-handson-02
  labels:
    app: redisdb-handson-02
spec:
  type: NodePort
  selector:
    app: redisdb-handson-02
  ports:
   - protocol: TCP
     port: 6379
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: k8s-go-handson-02
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: k8s-go-handson-02
    spec:
      containers:
        - name: k8s-go-handson-02
          # edit <your-namespace>
          image: "registry.ng.bluemix.net/<your-namespace>/k8s-go-handson-02:v1"
          env:
            - name: WEBAPP_PORT
              value: "8000"
            - name: REDIS_HOST
              value: "redisdb-handson-02"
          
          imagePullPolicy: Always
          ports:
            - name: http
              containerPort: 8000
---
apiVersion: v1
kind: Service
metadata:
  name: k8s-go-handson-02
  labels:
    app: k8s-go-handson-02
spec:
  type: NodePort
  selector:
    app: k8s-go-handson-02
  ports:
   - protocol: TCP
     port: 8000
     nodePort: 30280

通过 YAML 文件部署 WebApp

$ kubectl create -f k8s-go-handson-02.yml

YAML 文件说明

  • Kubernetes 对象都可以用 YAML 或者 JSON 文件描述
  • apiVersion 指定 api 版本,Deployment 用版本 extensions/v1beta1,如果是 Pod 或者 ReplicationController 用 v1,对象用什么版本的 api 请参考 Kubernetes api 指南
  • kind 指定对象类型
  • metadata 指定对象名称等元数据
  • replicas 指定副本个数,一般指 Pod 的个数
  • labels 是 key:value 描述,Selector 按 label 选择符合条件的对象
  • containers 中定义每个 container 名字,image,协议类型,端口,使用的 env,volume 等信息
  • 多个 Kind 指定的对象定义在一个文件中时,每个段用"---"分割,可以每个文件定义一个对象,然后用 kubectl create –f filename 分别创建
  • Service type 可以是 ClusterIP, NodePort 或者 LoadBalancer
  • Service type 指定为 ClusterIP 时,生成的 ClusterIP 来自 api server 启动时候指定的一个地址段,它是一个只在 Cluster 内部可以访问的地址,Kubernetes proxy 组件根据 Service 定义中的 Selector,找到满足条件的 Pod,生成 iptables 规则,把对 ClusterIP 的访问 DNAT 到 Pod 上面;
  • Service type 指定为 NodePort 时,在每个 Cluster 成员开启一个端口,对 Node 端口的访问 DNAT 到 Pod 上
  • Service type 指定为 LoadBalancer 时,需要云供应商提供外网访问 Service 的 IP 地址,访问这个 LoadBalancer 地址和端口的流量被转发到 Cluster 成员 NodeIP:NodePort 上
  • 如果不需要在外网访问 Redis DB 服务,它的 Service type 可以定义为 ClusterIP,这个 Service 就只在 Cluster 内部可见。

通过 kubectl edit 完成 Scale up/Scale down

$ kubectl edit deploy k8s-go-handson-02
编辑 WebApp replicas 数量,比如从 3 给位 5,再保存,Kubernetes 自动识别更改,进行实例伸缩

通过 kubectl edit 完成 Rolling update

更改 webserver.go 代码, 改变字符串 "Version 1.0" 为"Version 2.0", 重新 build image, tag 为 v2, push 到 registry.ng.bluemix.net

$ docker build -t registry.ng.bluemix.net/<YOUR-NAMESPACE>/k8s-go-handson-02:v2 .
$ docker push registry.ng.bluemix.net/<YOUR-NAMESPACE>/k8s-go-handson-02:v2

$ kubectl edit deploy k8s-go-handson-02
编辑 image 名称,更改为新的版本 k8s-go-handson-02:v2 再保存,Kubernetes 自动识别更改,update image 到新版本。

使用下面命令清理本次实验中所创建的资源:

$ kubectl delete -f k8s-go-handson-02.yml

Kubernetes Cluster 使用 Bluemix Services

Bluemix 云平台是基于 Cloud Foundry 框架的,上面有上百种做好了的 services 供使用,包括人工智能相关的 Watson, Bigdata 相关的数据分析, 存储,网络,IOT 等等服务,而且还在不断的增加,在容器应用里面如何使用这些已经成熟的 Service 呢?

上一小节我们把数据持久到 redis container 中,但由于数据没有挂载持久卷,重新启动 redis 后,原来的页面访问计数器的数据并不保留,又会从 0 开始计数。考虑到场景的连续性和演示目的,我们把 redis 放到 Bluemix service 中, 这样能更好的理解 Kubernetes 管理的 container 如何使用 Bluemix 现有的 service。

创建 Bluemix Cloud Foundry Service

创建一个 Bluemix rediscloud 类型的 service, 免费账号可以选择 30mb plan,有最大 30MB 的数据空间,

我创建的 service 名字是 rediscloud-kubernetes

$ bx login
$ bx service create rediscloud 30mb rediscloud-Kubernetes

# check list
$ bx service list

$ bx service show rediscloud-Kubernetes
Invoking 'cf service rediscloud-Kubernetes'...
Service instance: rediscloud-Kubernetes
Service: rediscloud
Tags:
Plan: 30mb
Description: Enterprise-Class Redis for Developers
Documentation url: http://redislabs.com/developers
Dashboard: https://Bluemix.marketplace.ibmcloud.com/api/custom/cloudfoundry/v2/sso/start?serviceUuid=xxxxxx-a65c-416d-9229-a7cafd30xxxx

Last Operation
Status: create succeeded
Message:
Started: 2017-05-28T12:02:20Z
Updated:

绑定 Service 到 Bluemix Kubernetes namespace

$ kubectl get namespaces

查看 Kubernetes namespace,这个 namespace 和上面用的 bx cr namespaces 意义不同,这个是 Kubernetes 的命名空间,没有指定 namespace 时候 Kubernetes 使用缺省的 namespace: default

# 绑定 Bluemix service 到 Kubernetes cluster default namespace (mycluster 是 cluster 名字)
$ bx cs cluster-service-bind mycluster default rediscloud-Kubernetes

$ bx cs cluster-services mycluster
Listing bound services in the default namespace...
OK
Service                 Instance GUID                          Key                          Namespace   
rediscloud-Kubernetes   55f1af4e-a65c-416d-9229-a7cafd30438b   kube-rediscloud-kubernetes   default   

$ kubectl get secret
NAME                            TYPE                                  DATA      AGE
binding-rediscloud-kubernetes   Opaque                                1         3m
Bluemix-default-secret          Kubernetes.io/dockercfg               1         4d
default-token-7wntf             Kubernetes.io/service-account-token   3         4d

绑定 Bluemix service 后,Kubernetes 会生成一个 secret,存放 service 相关的敏感信息,比如主机名,端口,用户密码等, secret 的命名规范为 binding-YOURSERVICENAME,我们在后面引用这个名字 binding-rediscloud-kubernetes,把 service secret 信息作为一个 volume mount 到 container,然后在程序中使用。

在代码中访问 Bluemix Service

改动一下 webserver.go 代码,从 container mount 的 binding 文件中读取 Bluemix redis service 主机,端口,密码等信息

...
import (
    "encoding/json"
    "fmt"
...

type BluemixServiceConfig struct {
        Hostname string `json:"hostname"`
        Port     string `json:"port"`
        Password string `json:"password"`
}

func getBluemixServiceConfig(fileName string) BluemixServiceConfig {
        raw, err := ioutil.ReadFile(fileName)
        if err != nil {
                fmt.Println("Error when get Bluemix service config", err.Error())
                //os.Exit(1)
        }
        var BluemixServiceConfig BluemixServiceConfig
        json.Unmarshal(raw, &BluemixServiceConfig)
        fmt.Printf("json:%v\n", BluemixServiceConfig)
        return BluemixServiceConfig
}


func getRedisClient() *redis.Client {
        var redisClient *redis.Client
        BluemixService := getBluemixServiceConfig("/Bluemix-rediscloud-service/binding")
        redisHost := BluemixService.Hostname
        redisPort := BluemixService.Port
        password := BluemixService.Password

        redisClient = redis.NewClient(&redis.Options{
                Addr:     redisHost + ":" + redisPort,
                Password: password,
                DB:       0,
        })
        fmt.Println("Connected to redis host->" + redisHost + ":" + redisPort)
        return redisClient
}

...

上面代码新 import encoding/json 库,操作 JSON 格式数据;

修改原来的 getRedisClient 函数,通过 Service JSON 文件解析出来的主机,端口,密码连接 redis

通过 YAML 文件部署

k8s-go-handson-03.yml

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: k8s-go-handson-03
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: k8s-go-handson-03
    spec:
      containers:
        - name: k8s-go-handson-03
          image: "registry.ng.bluemix.net/<your-namespace>/k8s-go-handson-03:v1"
          env:
            - name: WEBAPP_PORT
              value: "8000"
          imagePullPolicy: Always
          ports:
            - name: http
              containerPort: 8000
          # for Bluemix redis service
          volumeMounts:
            - mountPath: /Bluemix-rediscloud-service
              name: Bluemix-redis-volume
      volumes:
        - name: Bluemix-redis-volume
          secret:
            secretName: binding-rediscloud-kubernetes
---
apiVersion: v1
kind: Service
metadata:
  name: k8s-go-handson-03
  labels:
    app: k8s-go-handson-03
spec:
  type: NodePort
  selector:
    app: k8s-go-handson-03
  ports:
   - protocol: TCP
     port: 8000
     nodePort: 30380

上面 YAML 文件把 secret "binding-rediscloud-kubernetes"作为一个卷给 container 使用,Bluemix Kubernetes 启动 container 的时候 mount 这个卷到本地的/Bluemix-rediscloud-service 目录,并把 service 敏感信息以 json 格式放到文件/Bluemix-rediscloud-service/binding 里。

#部署 app 到 Kubernetes cluster
$ kubectl create -f k8s-go-handson-03.yml

通过 curl 访问$NODE 的 service port,可以看到来自多个 container 的响应页面,计时器的值逐渐增加,即使用 kubectl delete –f k8s-go-handson-03.yml 删除后再重建,计时器也不清零。

总结

上面通过一个非常简单的 WebApp 展示了如何把我们的 Application 部署到 Bluemix Kubernetes cluster 中,如何伸缩和升级应用,以及在 App 中如何使用 Bluemix 已有 service;目前除了 Kubernetes,比较流行的容器集群管理和编排引擎还有 Docker 的 Swarm, Apache Mesos,它们各有所长,可以预见的是在各大云厂商积极参与和容器编排引擎技术经过一段时间的 PK 过后,编排引擎的标准会逐渐统一和完善,从一种编排引擎,迁移到另一种,只需要简单的重写 YAML 部署文件即可,实施的难度会越来越低,这给企业提供了更大的选择空间,所以企业最先需要考虑的是如何把现有的应用 Docker 化,Docker 化后上云平台就水到渠成了,当然,目前来看,传统企业的应用复杂而多样,不是所有的企业应用都能很快 Docker 化,这需要容器技术越来也成熟,企业,软件开发商经过很长一段时间的努力才能完成。尽管道路崎岖,参与者多方共赢的结果值得期待。

文中提及的代码和配置文件下载地址:

https://github.com/alix2013/Bluemix-kubernetes-handson-lab

参考信息

Bluemix 文档:

https://console.ng.Bluemix.net/docs/

Bluemix cs 命令指南: https://console.ng.Bluemix.net/docs/containers/csclireference.html#csclireference

Kubernetes 文档:

https://Kubernetes.io/docs/home/

Kubernetes basic concept:

https://Kubernetes.io/docs/concepts/

Kubernetes official tutorials :

https://Kubernetes.io/docs/tutorials/

Kubectl command line usage reference:

https://Kubernetes.io/docs/user-guide/kubectl/v1.5/

Docker 文档:

https://docs.docker.com/

Docker build image:

https://docs.docker.com/engine/reference/commandline/build/

Docker official registry site:

https://hub.docker.com/

golang

https://golang.org/


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Cloud computing, Open source
ArticleID=1047300
ArticleTitle=Bluemix Kubernetes 实战进阶
publish-date=07052017