使用 kubeadm 安装 Kubernetes 集群

发表于 · 归类于 代码 · 阅读完需 47 分钟 · 报告错误

Kubernetes,简称 k8s,是 Google 开源的一个容器编排引擎,其的目标是让部署容器化的应用简单并且高效,它支持自动化部署、大规模可伸缩、应用容器化管理。随着云原生技术的发展,Kubernetes 受到越来越多的关注。

本文将主要介绍如何使用 kubeadm 安装部署 Kubernetes 集群(注:安装版本为 1.17.3)。通过学习,你将学会如何从零开始搭建一个 Kubernetes 集群。

准备虚拟机环境

创建 5 台 CentOS 虚拟主机,并在本地电脑上配置 SSH 免密登录(注:下面所有操作默认在 root 下执行):

# 配置 SSH 免密登录
# ~/.ssh/conf

# k8sadm01
Host ka01
    HostName 192.168.220.31
    Port 22
    User root
    IdentityFile ~/.ssh/id_rsa

# k8sadm02
Host ka02
    HostName 192.168.220.32
    Port 22
    User root
    IdentityFile ~/.ssh/id_rsa

# k8sadm03
Host ka03
    HostName 192.168.220.33
    Port 22
    User root
    IdentityFile ~/.ssh/id_rsa

# k8sadm04
Host ka04
    HostName 192.168.220.34
    Port 22
    User root
    IdentityFile ~/.ssh/id_rsa

# k8sadm05
Host ka05
    HostName 192.168.220.35
    Port 22
    User root
    IdentityFile ~/.ssh/id_rsa

这 5 台主机中,k8sadm01~k8sadm05 将作为 Master 节点,k8sadm01~k8sadm05 将作为 Worker 节点。

使用下面命令配置 5 台主机的 hostname 分别为 k8sadm01~k8sadm05:

$ hostnamectl set-hostname k8sadm01

并在每台主机中追加如下的 Host 配置信息:

$ vim /etc/hosts
...
192.168.220.31  k8sadm01
192.168.220.32  k8sadm02
192.168.220.33  k8sadm03
192.168.220.34  k8sadm04
192.168.220.35  k8sadm05

关闭系统防火墙及 SELinux:

$ systemctl stop firewalld
$ systemctl disable firewalld

$ setenforce 0
$ sed -i 's/^SELINUX=enforcing$/SELINUX=permissive/' /etc/selinux/config

修改系统时区:

$ timedatectl set-timezone Asia/Shanghai

配置 sysctl.d(注:避免流量无法正确路由的问题),并禁用交换分区(注:提高节点稳定性):

$ cat << EOF > /etc/sysctl.d/99-k8s.conf
# Enable netfilter on bridges.
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
# Disable devices and files for paging and swapping.
vm.swappiness = 0
EOF
$ sysctl --system
$ swapoff -a

因为 kube-proxy 使用 ipvs 模式,因此还需要配置 ipvs(注:传输层负载均衡):

$ yum install -y ipvsadm ipset
$ cat << EOF > /etc/sysconfig/modules/ipvs.modules
#!/bin/sh
ipvs_modules="ip_vs ip_vs_lc ip_vs_wlc ip_vs_rr ip_vs_wrr ip_vs_lblc ip_vs_lblcr ip_vs_dh ip_vs_sh ip_vs_fo ip_vs_nq ip_vs_sed ip_vs_ftp ip_vs_ovf ip_vs_pe_sip nf_conntrack_ipv4"
for kernel_module in ${ipvs_modules}; do
    /sbin/modinfo -F filename ${kernel_module} > /dev/null 2>&1
    if [ $? -eq 0 ]; then
        /sbin/modprobe ${kernel_module}
    fi
done
EOF
$ chmod +x /etc/sysconfig/modules/ipvs.modules && sh /etc/sysconfig/modules/ipvs.modules

升级主机内核为 4.4.178:

$ yum install -y ./kernel-lt-4.4.178-1.el7.elrepo.x86_64.rpm
$ grub2-set-default 0 && grub2-mkconfig -o /boot/grub2/grub.cfg
$ yun remove -y kernel-tools kernel-tools-libs
$ yum install -y ./kernel-lt-tools-4.4.178-1.el7.elrepo.x86_64.rpm ./kernel-lt-tools-libs-4.4.178-1.el7.elrepo.x86_64.rpm

安装 HAProxy 和 Keepalived 基础组件

在 k8s 集群的 master 节点上,需要安装 HAProxy 和 Keepalived 组件。

HAProxy 是一个提供高可用性、负载均衡以及基于 TCP 和 HTTP 应用的代理服务器,可以保护你的 Web 服务器或数据库不被暴露到网络上。

Keepalived 是一个基于虚拟路由冗余协议(缩写:VRRP)来实现的 Web 服务高可用方案,可以利用其来避免单点故障。一个 Web 服务至少会有 2 台服务器运行 Keepalived,一台为主服务器 MASTER,一台为备份服务器 BACKUP,但是对外表现为一个虚拟 IP,主服务器会发送特定的消息给备份服务器,当备份服务器收不到这个消息的时候,即主服务器宕机的时候,备份服务器就会接管虚拟 IP,继续提供服务,从而保证了高可用性。

在 k8sadm01~k8sadm03 这三台主机上安装 HAProxy 并配置:

$ yum install -y haproxy
$ cat << EOF >> /etc/haproxy/haproxy.cfg
# Kubernetes proxy rule
listen k8sadm
    bind 0.0.0.0:8443
    mode tcp
    option tcplog
    balance roundrobin
    server k8sadm01 192.168.220.31:6443 maxconn 1000 check inter 2000 fall 3 rise 2 weight 1
    server k8sadm02 192.168.220.32:6443 maxconn 1000 check inter 2000 fall 3 rise 2 weight 1
    server k8sadm03 192.168.220.33:6443 maxconn 1000 check inter 2000 fall 3 rise 2 weight 1
EOF

根据 haproxy.cfg 中的日志配置项,同时修改系统日志 /etc/rsyslog.conf 配置:

$ sed -i 's/^#[$]ModLoad imudp$/$ModLoad imudp/' /etc/rsyslog.conf
$ sed -i 's/^#[$]UDPServerRun 514$/$UDPServerRun 514/' /etc/rsyslog.conf
$ cat << EOF >> /etc/rsyslog.conf
# Save haproxy to /var/log/haproxy.log
local2.*                                                /var/log/haproxy.log
EOF
$ systemctl start haproxy
$ systemctl enable haproxy

安装 Kepalived 并配置:

$ yum install -y keepalived

配置文件 /etc/keepalived/keepalived.conf,修改内容如下:

# /etc/keepalived/keepalived.conf

global_defs {
   notification_email {
     acassen@firewall.loc
     failover@firewall.loc
     sysadmin@firewall.loc
   }
   notification_email_from Alexandre.Cassen@firewall.loc
   smtp_server 192.168.200.1
   smtp_connect_timeout 30
   # 这里分别填入对应主机名
   router_id k8sadm01
   vrrp_skip_check_adv_addr
   # 这个参数会导致 Virtual IP 无法 PING 通
   # vrrp_strict
   vrrp_garp_interval 0
   vrrp_gna_interval 0
}

vrrp_instance VI_1 {
    # k8sadm01 为 MASTER 节点,k8sadm02/03 为 BACKUP 节点
    state MASTER
    interface eth0
    virtual_router_id 51
    # MASTER 节点优先级为 100,BACKUP 节点优先级为 50
    priority 100
    advert_int 1
    authentication {
        auth_type PASS
        auth_pass Passw0rd
    }
    virtual_ipaddress {
        192.168.220.31
        192.168.200.32
        192.168.200.33
    }
}

启动服务:

$ systemctl start keepalived
$ systemctl enable keepalived

安装 Docker

配置阿里的 yum 源,并安装 Docker(注:当前最新版本为 19.03.6):

$ yum install -y yum-utils device-mapper-persistent-data lvm2
$ yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
$ yum install -y docker-ce docker-ce-cli

创建 Docker 守护进程的配置文件,包括阿里镜像仓库,并重启 Docker:

$ mkdir /etc/docker
$ cat << EOF > /etc/docker/daemon.json
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "registry-mirrors": ["https://umqjaxg5.mirror.aliyuncs.com"],
  "storage-driver": "overlay2",
  "storage-opts": [
    "overlay2.override_kernel_check=true"
  ]
}
EOF
$ systemctl daemon-reload
$ systemctl start docker

根据 k8s 的最佳实践,使用 systemd 作为 cgroup 驱动,可以更好地保证集群运行稳定。

安装 kubelet、kubeadm 和 kubectl

配置阿里的 yum 源,并安装:

$ cat << EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/
enabled=1
gpgcheck=1
repo_gpgcheck=1
gpgkey=https://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg https://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg
EOF
$ yum install -y kubelet kubeadm kubectl --disableexcludes=kubernetes
$ systemctl enable --now kubelet
$ systemctl start kubelet

注:当前最新版本为 1.17.3-0

注:可使用下面命令查看 kubelet 服务的日志:

$ journalctl -xefu kubelet

注:如果此时执行 service status kubelet 命令,将得到 kubelet 启动失败的错误提示,请忽略此错误,因为必须完成后续步骤中 kubeadm init 的操作,kubelet 才能正常启动

初始化 Master 节点

使用下面的命令获取 kubeadm 默认配置:

$ kubeadm config print init-defaults > ./kubeadm.config.yaml

下面是 kubeadm.config.yaml 配置的具体内容,需要修改的地方已在配置中进行注释说明:

apiVersion: kubeadm.k8s.io/v1beta2
kind: InitConfiguration
bootstrapTokens:
- groups:
  - system:bootstrappers:kubeadm:default-node-token
  token: abcdef.0123456789abcdef
  ttl: 24h0m0s
  usages:
  - signing
  - authentication
localAPIEndpoint:
  # 修改此处的 API 端点信息
  advertiseAddress: 192.168.220.31
  bindPort: 6443
nodeRegistration:
  criSocket: /var/run/dockershim.sock
  name: k8sadm01
  taints:
  - effect: NoSchedule
    key: node-role.kubernetes.io/master
  # 设置 cgroup 驱动方式为 systemd
  kubeletExtraArgs:
    cgroup-driver: systemd
---
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
apiServer:
  timeoutForControlPlane: 4m0s
certificatesDir: /etc/kubernetes/pki
clusterName: kubernetes
controllerManager: {}
dns:
  type: CoreDNS
etcd:
  local:
    dataDir: /var/lib/etcd
# 此处使用 Azure k8s 镜像代替 Google Container Registry
imageRepository: gcr.azk8s.cn/google_containers
kubernetesVersion: v1.17.0
networking:
  dnsDomain: cluster.local
  podSubnet: 10.80.0.0/12
  serviceSubnet: 10.96.0.0/12
scheduler: {}
---
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
bindAddress: 0.0.0.0
clientConnection:
  acceptContentTypes: ""
  burst: 0
  contentType: ""
  kubeconfig: /var/lib/kube-proxy/kubeconfig.conf
  qps: 0
clusterCIDR: ""
configSyncPeriod: 0s
conntrack:
  maxPerCore: null
  min: null
  tcpCloseWaitTimeout: null
  tcpEstablishedTimeout: null
enableProfiling: false
healthzBindAddress: ""
hostnameOverride: ""
iptables:
  masqueradeAll: false
  masqueradeBit: null
  minSyncPeriod: 0s
  syncPeriod: 0s
ipvs:
  excludeCIDRs: null
  minSyncPeriod: 0s
  scheduler: ""
  strictARP: false
  syncPeriod: 0s
metricsBindAddress: ""
# 修改此处的 kube-proxy 代理模式为 ipvs,默认 iptables
mode: ipvs
nodePortAddresses: null
oomScoreAdj: null
portRange: ""
udpIdleTimeout: 0s
winkernel:
  enableDSR: false
  networkName: ""
  sourceVip: ""

接下来,我们使用 kubeadm 初始化第一个 Master 节点。

$ kubeadm init --config ./kubeadm.config.yaml --upload-certs

当看到下面字样时,即表示安装成功:

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

  mkdir -p $HOME/.kube
  sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
  sudo chown $(id -u):$(id -g) $HOME/.kube/config

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 192.168.220.31:6443 --token abcdef.0123456789abcdef \
    --discovery-token-ca-cert-hash sha256:65254433887607816949bef40124f041ca4d37307efda546336f73541738b1e3

安装 Calico 网络插件

在部署一般应用之前,应该先安装 Pod 网络插件,以便 Pod 间可以相互通信。这里我们选择 Calico,另外你也可以选择使用 Flannel。

下载 YAML 文件(版本:v3.13.0)并修改其中的子网地址配置:

$ curl -O https://docs.projectcalico.org/manifests/calico.yaml
$ vim ./calico.yaml
...
# The default IPv4 pool to create on startup if none exists. Pod IPs will be
# chosen from this range. Changing this value after installation will have
# no effect. This should fall within `--cluster-cidr`.
- name: CALICO_IPV4POOL_CIDR
  value: "10.80.0.0/12"
...

Calico 默认子网地址 192.168.0.0/16 与我们集群中 5 台主机 IP 地址范围有重合,所以需要自定义一个子网地址段(该地址段需要与 kubeadm 配置中的 podSubnet 相同)。

参考:https://docs.projectcalico.org/getting-started/kubernetes/quickstart

安装 Worker 节点

接下来我们先将 k8sadm04 主机配置为集群的一个 Worker 节点,通过下列命令来加入集群:

$ kubeadm join 192.168.220.31:6443 --token abcdef.0123456789abcdef \
    --discovery-token-ca-cert-hash sha256:65254433887607816949bef40124f041ca4d37307efda546336f73541738b1e3

注:如果 Token 会在 24h 后过期,此时可以通过以下方式重新获取:

$ kubeadm token create --print-join-command

等待数分钟,我们便可以在 k8sadm01 主机上查看到该节点:

$ kubectl get nodes
NAME       STATUS     ROLES    AGE     VERSION
k8sadm01   Ready      master   9m38s   v1.17.3
k8sadm04   Ready      <none>   6m13s   v1.17.3

安装其他 Master 节点

下面我们来安装另外两个 Master 节点。在 k8sadm02 和 k8sadm03 上分别执行下面命令即可:

$ kubeadm join 192.168.220.31:6443 --token r4t3l3.14mmuivm7xbtaeoj \
    --discovery-token-ca-cert-hash sha256:06f49f1f29d08b797fbf04d87b9b0fd6095a4693e9b1d59c429745cfa082b31d \
    --control-plane --certificate-key 7373f829c733b46fb78f0069f90185e0f00254381641d8d5a7c5984b2cf17cd3

等所有的 Master 和 Worker 节点安装完成后,可以在任意节点上执行 kubectl get nodes -o wide 查看集群内各节点状态:

$ kubectl get nodes
NAME       STATUS     ROLES    AGE     VERSION
k8sadm01   Ready      master   9m38s   v1.17.3
k8sadm02   Ready      master   4m38s   v1.17.3
k8sadm03   Ready      master   1m38s   v1.17.3
k8sadm04   Ready      <none>   6m13s   v1.17.3
k8sadm05   Ready      <none>   1m13s   v1.17.3

安装 Kubernetes Dashboard

现在我们需要一个图形化工具来管理 k8s 集群,官方出品的 Kubernetes Dashboard 是个不错的选择。

安装 Ingress 控制器

Ingress 用于向外暴露 k8s 集群内的服务,我们使用目前最常见的 ingress-nginx 作为 Ingress 控制器(注:其他可选控制器包括 Traefik 等,这里不多介绍)。与使用 LoadBalance 的 Service 不同,Ingress 只需要一个公网 IP 就能为多个 Service 提供访问,当从集群外向 Ingress 发送 HTTP 请求时,Ingress 会根据请求的主机名和路径将该请求转发到对应的服务。

首先,我们下载官方的 YAML 配置文件:

$ wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/nginx-0.30.0/deploy/static/mandatory.yaml

官方的配置文件中使用 Deployment 方式来部署 Controller,我们需要将其修改为 DaemonSet 方式进行部署。注:DaemonSet 确保全部(或者某些)节点上运行一个 Pod 的副本。当有节点加入集群时, 也会为他们新增一个 Pod 。当有节点从集群移除时,这些 Pod 也会被回收。DaemonSet 常被用在集群中每个节点的日志收集、监控、存储等场景。

apiVersion: apps/v1
# 将 Deployment 修改为 DaemonSet
kind: DaemonSet
metadata:
  name: nginx-ingress-controller
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
spec:
  # 删除此处的 replicas: 1 配置,因为 DaemonSet 无此字段
  selector:
    matchLabels:
      app.kubernetes.io/name: ingress-nginx
      app.kubernetes.io/part-of: ingress-nginx
  template:
    metadata:
      labels:
        app.kubernetes.io/name: ingress-nginx
        app.kubernetes.io/part-of: ingress-nginx
      annotations:
        prometheus.io/port: "10254"
        prometheus.io/scrape: "true"
    spec:
      # wait up to five minutes for the drain of connections
      terminationGracePeriodSeconds: 300
      serviceAccountName: nginx-ingress-serviceaccount
      # 通过 nodeSelector 确保此 DaemonSet 运行在 edgenode=true 的节点上
      nodeSelector:
        kubernetes.io/os: linux
        edgenode: "true"
      # 使用 hostNetwork 暴露服务
      hostNetwork: true
      containers:
        - name: nginx-ingress-controller
          image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.30.0
          args:
            - /nginx-ingress-controller
            - --configmap=$(POD_NAMESPACE)/nginx-configuration
            - --tcp-services-configmap=$(POD_NAMESPACE)/tcp-services
            - --udp-services-configmap=$(POD_NAMESPACE)/udp-services
            - --publish-service=$(POD_NAMESPACE)/ingress-nginx
            - --annotations-prefix=nginx.ingress.kubernetes.io
          securityContext:
            allowPrivilegeEscalation: true
            capabilities:
              drop:
                - ALL
              add:
                - NET_BIND_SERVICE
            # www-data -> 101
            runAsUser: 101
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
            - name: https
              containerPort: 443
              protocol: TCP
          livenessProbe:
            failureThreshold: 3
            httpGet:
              path: /healthz
              port: 10254
              scheme: HTTP
            initialDelaySeconds: 10
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 10
          readinessProbe:
            failureThreshold: 3
            httpGet:
              path: /healthz
              port: 10254
              scheme: HTTP
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 10
          lifecycle:
            preStop:
              exec:
                command:
                  - /wait-shutdown

参考:https://kubernetes.github.io/ingress-nginx/deploy/baremetal/

同时,我们对 k8sadm04 进行打标,将其作为集群的边缘节点。打此标的所有节点上都会安装一个 Ingress 控制器:

$ kubectl label nodes k8sadm04 edgenode=true

最后,创建相关资源:

$ kubectl apply -f ./mandatory.yaml

稍等片刻,使用下面两个命令检查资源是否就绪:

$ kubectl get daemonsets -n ingress-nginx
NAME                       DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR                          AGE
nginx-ingress-controller   1         1         1       1            1           edgenode=true,kubernetes.io/os=linux   69m
$ kubectl get pods -n ingress-nginx -o wide
NAME                             READY   STATUS    RESTARTS   AGE   IP               NODE       NOMINATED NODE   READINESS GATES
nginx-ingress-controller-lwb49   1/1     Running   0          69m   192.168.220.34   k8sadm04   <none>           <none>

可以看到,nginx-ingress-controller 已经按预期运行在节点 k8sadm04 上,其 IP 地址为 192.168.220.34。 至此,Ingress 控制器便已安装完成。接下来我们在集群内创建一个服务,然后使用 Ingress 从外部进行访问。

下面的 YAML 配置文件声明了名为 mypage 的 Deployment 及对应 Service 资源,并通过 Ingress 进行服务暴露:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mypage
spec:
  selector:
    matchLabels:
      app: mypage
  # 告诉 Deployment 运行 2 个 Pod
  replicas: 2
  # 关于 Pod Tempalte 的定义
  template:
    metadata:
      labels:
        app: mypage
    spec:
      containers:
      - name: mypage
        image: registry.k8sadm.xinhua.tech/foo/nginx
        ports:
        - containerPort: 80

---

apiVersion: v1
kind: Service
metadata:
  name: mypage
spec:
  selector:
    app: mypage
  ports:
  - name: http
    port: 80
    protocol: TCP

---

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: mypage
  annotations:
    # 指定 Ingress Controller 的类型
    kubernetes.io/ingress.class: "nginx"
    # 指定我们的 rules 的 path 可以使用正则表达式
    nginx.ingress.kubernetes.io/use-regex: "true"
spec:
  rules:
  - host: mypage.k8sadm.xinhua.tech
    http:
      paths:
      - path: /
        backend:
          serviceName: mypage
          servicePort: 80

执行该文件:

$ kubectl apply -f ./mypage.yaml

稍等片刻,我们可以通过以下命令来验证:

$ curl -v -H 'Host: mypage.k8sadm.xinhua.tech' http://192.168.220.34/

或者在本地 Hosts 文件中增加从 mypage.k8sadm.xinhua.tech 到 192.168.220.34 的地址解析,然后在浏览器中访问。

至此,k8s 集群即安装完毕。

重置集群

在 k8s 安装的过程中,可能出现或多或少的错误,需要删除集群环境,所以最后,我们用少量篇幅来介绍如何使用 kubeadm 来重置 k8s 集群。

在所有的工作节点和控制节点上执行下列命令来删除集群,此命令会还原 kubeadm initkubeadm join 命令所做的修改:

$ kubeadm reset

根据提示,可能还需要做如下处理:

$ rm -rf /etc/cni/net.d
$ ipvsadm --clear

最后

本文中用到的配置文件,可点击这里查看。

(完)