将自己的应用程序部署到k8s上step by step

347 阅读9分钟

创建docker镜像本地仓库

docker为私有化部署镜像仓库提供了镜像registry,通过将registry部署到docker上可以实现docker镜像仓库的本地化部署
docker run -d -p 5000:5000 --name localRegistry -v /home/xxx/data/registry:/var/lib/registry registry
上面的命令在docker上用registry镜像创建了一个名叫localRegistry的docker实例,并将本地用户目录下的data/registry子目录下挂载到localRegistry实例的/var/lib/registry目录,这样本地docker镜像仓库的镜像文件将能够存到本地用户目录的data/registry子目录。

先拿一个nginx镜像试试水

首先拉一下nginx的官方镜像docker pull nginx,这时候用docker images应该就可以看到一个nginx:latest的镜像了,这时我们再将这个镜像改下tag变成我们自己的镜像
docker tag nginx:latest 192.168.210.128:5000/nginx
这里是由于我的本地ip是192.168.210.128,因此在docker中部署的registry用192.168.210.128:5000访问,因此我们再用docker images查看就能发现多了一个192.168.210.128:5000/nginx:latest的镜像了。接着我们需要将这个镜像push到本地docker仓库中,直接使用docker push 192.168.210.128:5000/nginx即可,但这时会报下面这种错误

Using default tag: latest
The push refers to repository [192.168.210.128:5000/nginx]
Get "https://192.168.210.128:5000/v2/": http: server gave HTTP response to HTTPS client

这是因为docker的仓库默认使用https协议,但我们部署的registry是http协议的,因此我们需要在/etc/docker/daemon.json中添加配置

{
    xxx,
    "insecure-registries":["192.168.210.128:5000"]
}

然后再用sudo systemctl restart docker来重启docker,这时再执行docker push就能真正成功地将镜像存到registry上了。并且这时能在data/registry/docker/registry/v2/repositories/目录下发现多了一个nginx目录,表示nginx真的存到了本地docker仓库了。registry也提供了http的api查询已有的镜像 curl http://192.168.210.128:5000/v2/_catalog将会返回如下response

{"repositories":["nginx"]}

检验一下本地仓库是否好使

由于刚才已经用docker tag命令在本地生成了一个镜像了,因此要校验本地仓库中的镜像,得先把本地镜像给删除 docker rmi 192.168.210.128:5000/nginx 再像刚才创建registry实例一样,这次我们使用本地仓库中的镜像 run -d -p 8080:80 --name nginxTest 192.168.210.128:5000/nginx,实例名叫nginxTest,并且将nginx的80端口映射到本地的8080端口,此时再访问http://localhost:8080 将能够进入到nginx,表示用本地仓库中的nginx镜像也能在docker中进行部署了。

创建自定义镜像

这次不再使用官方的nginx镜像了,而是自己用go写个程序打成docker镜像并部署到k8s中。go程序也很简单,用go基础库启动一个http服务,在response中返回当前系统名
首先将http的response封装一下,方便返回正常数据和error

// resp.go
package main

import (
"encoding/json"
"net/http"
)

func HttpError(resp http.ResponseWriter, err error) {
    resp.WriteHeader(http.StatusOK)
    resp.Write([]byte(err.Error()))
}

func HttpSuccess(resp http.ResponseWriter, ret interface{}) {
    resp.WriteHeader(http.StatusOK)
    retData, err := json.Marshal(ret)
    if err != nil {
        HttpError(resp, err)
        return
    }
    _, err = resp.Write(retData)
    if err != nil {
        HttpError(resp, err)
    }
}

然后是主程序代码,http服务监听8080端口,使用os.Hostname()函数返回系统名并用上面的工具函数写回到response中

// main.go
package main

import (
"fmt"
"net/http"
"os"
)

func showPodName(w http.ResponseWriter, r *http.Request) {
    hostName, err := os.Hostname()
    if err != nil {
        HttpError(w, err)
        return
    }
    ret := map[string]interface{}{
        "host_name": hostName,
    }
    HttpSuccess(w, ret)
}

func main() {
    http.HandleFunc("/", showPodName)
    fmt.Println("Server starting on 8080...")
    http.ListenAndServe(":8080", nil)
}

然后是Dockerfile文件,配置镜像的build

FROM golang:alpine as build
RUN apk --no-cache add tzdata
WORKDIR /app
ADD . ./
RUN CGO_ENABLE=0 GOOS=linux go build -o main .

FROM alpine as final
COPY --from=build /app/main .
COPY --from=build /app/bootstrap.sh .
CMD ["sh", "bootstrap.sh"]

这里使用了docker的多阶段构建,先用golang:alpine镜像创建go程序的编译环境:先将工程目录的所有文件add到镜像目录,然后执行go的编译命令,将go程序代码编译成main可执行文件。然后是第二阶段用alpine镜像打出更小的镜像结果:将前一个编译环境镜像中生成的main可执行文件copy到alpine镜像中,再将bootstrap.sh文件也copy到镜像中,最后用CMD命令执行sh bootstrap.sh,这个bootstrap.sh文件的内容也特别简单

#!/bin/bash
./main

就是简单地将main可执行文件运行一下
将Dockerfile文件放到go工程的根目录下,再在go工程根目录下执行docker build -t 192.168.210.128:5000/show_pod_name即可打出192.168.210.128:5000/show_pod_name这个镜像,最后再像之前操作nginx镜像一样,使用docker push 192.168.210.128:5000/show_pod_name即可将这个镜像存入到本地docker镜像仓库了。

用docker先部署一下自定义镜像

为了验证生成的自定义镜像是否能正常部署和运行,可以先直接在docker上部署,这也类似于刚才部署本地镜像仓库中的nginx镜像 docker run -d -p 8080:8080 --name showPodNameTest 192.168.210.128:5000/show_pod_name,将容器实例的8080商品映射到本地的8080端口,这时再访问http://localhost:8080 将会得到如下结果

{"host_name":"xxx"}

这里的xxx实际上就是docker容器实例的containerId,可以用docker ps看到。

将自定义镜像部署到k8s中

在上一篇文章Ubuntu上基于minikube的k8s环境搭建的最后是用kubectl命令部署了tomcat并暴露服务,其实本地仓库中的自定义的镜像也可以这样部署。但这样我们选择使用部署文件的方式来部署。
但minikube由于是在docker上创建的node,因此要想在minikube中能够拉到镜像,还得让minikube中的docker能够访问到本地镜像仓库。在使用minikube start命令创建集群时,需要使用--insecure-registry指定可访问的本地镜像仓库
minikube start --image-mirror-country='cn' --registry-mirror=https://yw1367kh.mirror.aliyuncs.com --nodes 3 -p multinode --insecure-registry="192.168.210.128:5000"
使用上面的命令将会创建名为multinode的集群,集群有3个node

接下来是k8s的部署文件了,正如之前用kubectl命令要先部署再暴露服务,因此k8s也有部署文件和服务文件

# show-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: show-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: show-app
  template:
    metadata:
      labels:
        app: show-app
    spec:
      affinity:
        # ⬇⬇⬇ This ensures pods will land on separate hosts
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions: [{ key: app, operator: In, values: [show-app] }]
              topologyKey: "kubernetes.io/hostname"
      containers:
        - name: show-app-container
          image: 192.168.210.128:5000/show_pod_name
          imagePullPolicy: Always
          ports:
          - containerPort: 8080

在部署文件中用image这个key指定了要使用的镜像为192.168.210.128:5000/show_pod_name 用kubectl apply -f show-deployment.yaml即可创建一个deployment,部署执行成功后用pod查看命令 kubectl get pods -o wide可得到如下结果

NAME                               READY   STATUS    RESTARTS      AGE   IP           NODE            NOMINATED NODE   READINESS GATES
show-deployment-6465896996-hvkwg   1/1     Running   0             57m   10.244.1.2   multinode-m02   <none>           <none>
show-deployment-6465896996-jnqrl   1/1     Running   1 (58m ago)   60m   10.244.0.2   multinode       <none>           <none>

在前面的部署文件中用replicas: 2指定了部署两个pod,因为在创建集群时指定了node=3,因此这两个pod会被创建在不同的node上。此时pod已经创建好了,要让外部可访问还得创建service,接下来是service的部署文件

# show-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: show-app
spec:
  type: NodePort
  selector:
    app: show-app
  ports:
    - protocol: TCP
      nodePort: 32000
      port: 8080
      targetPort: 8080

将前面的deployment用32000端口暴露出去,这里相当于docker创建容器实例时用-p参数进行端口映射一样,用32000映射到pod的8080端口。同样使用kubectl apply -f show-svc.yaml命令,同样,我们也能够使用kubectl命令查看我们部署的servicekubectl get services -o wide得到的结果如下:

AME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE    SELECTOR
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP          143m   <none>
show-app     NodePort    10.98.232.70   <none>        8080:32000/TCP   132m   app=show-app

此时如果直接在宿主机中访问10.98.232.70:32000也访问不通的,这是因为这个10.98.232.70是k8s中的ip,而不是我们外部能够访问的ip,同样的,如果用宿主机的ip也是访问不通的,因为32000这个端口并没有暴露到宿主机上,那么到底要用哪个ip去访问32000端口呢?跟前一篇文章的方法一样,用minikube service show-app --url来获取show-app这个service的访问url,在我的宿主要上得到的url是http://192.168.49.2:32000,因此访问的ip应该是192.168.49.2。在浏览器中输入这个url将能够得到和前面部署到docker上一样的结果,但此时Hostname返回的结果是会是podname。
刷新浏览器,会发现返回的结果是一样的,这说明每次请示都打到了同一个pod上,这其实是因为浏览器开启了长连接优化,我们在命令行使用curl访问就可以避免长连接的问题。

这个192.168.49.2的ip到底是哪里来的?我们用minikube ssh进入到k8s node进看看,在/etc/hosts文件中记录了一些ip,得到如下结果

127.0.0.1	localhost
::1	localhost ip6-localhost ip6-loopback
fe00::0	ip6-localnet
ff00::0	ip6-mcastprefix
ff02::1	ip6-allnodes
ff02::2	ip6-allrouters
192.168.49.2	multinode
192.168.49.1	host.minikube.internal
192.168.49.2	control-plane.minikube.internal

这说明192.168.49.2居然是multinode这个node的ip,那么multinode这个集群不是有三个node嘛,进入剩下的两个node查看一下ip呢?先用minikube status查看另外两个node的name,再用minikube ssh -n multinode-m02进入到第二个node中,并且查看/etc/hosts的内容,其ip是192.168.49.3。还记得show-app的两个pod分别部署在multinode和multinode-m02中,那么是不是这个192.168.49.3也能够直接访问32000端口呢?试了一下,还真可以,这说明访问的ip其实是node的ip。

更进一步,用nginx反向代理k8s服务

k8s每次都要用ip:port来访问实在是太麻烦了,一般的网站或者app的api都是通过域名+path来访问的,而nginx的反向代理即可将nginx的请示映射到指定的url上去

在ubuntu上使用sudo apt install nginx安装nginx,nginx的配置文件在/etc/nginx/nginx.conf,使用nginx.conf的内容,在server节点添加反向代理配置

server {
        listen 80 default_server;
        listen [::]:80 default_server;

        location /k8s/show_pod_name {
                proxy_pass http://192.168.49.2:32000;
        }
}

这里将所有命中nginx的/k8s/show_pod_name都映射到http://192.168.49.2:32000 这个url来,相当于我们请求http://localhost/k8s/show_pod_name 就会间接请求http://192.168.49.2:32000 这样就完成了nginx的反向代理,以后就不用再记node的ip和暴露的端口号了。