环境准备
系统及软件版本
kubernetes version: v1.16.3
| 组件 | 版本 | 说明 | 
|---|---|---|
| Centos | 7.7 | 操作系统 | 
| kubeadm | v1.16.3 | 集群部署工具 | 
| etcd | 3.3.15 | 存储数据库 | 
| calico | v3.14.0 | CNI 网络插件 | 
| docker | 18.09.9 | CRI Runtime | 
| metallb | v0.8.3 | 穷人版 LoadBalancer | 
主机列表
| hostname | IP Address | components | 
|---|---|---|
| k8s-master | 10.64.144.100 | kube-apiserver,kube-controller-manager,kube-scheduler,etcd,kubelet,flannel,docker | 
| k8s-node01 | 10.64.144.101 | kube-proxy,kubelet,flannel,docker | 
| k8s-node02 | 10.64.144.102 | kube-proxy,kubelet,flannel,docker | 
| k8s-node03 | 10.64.144.103 | kube-proxy,kubelet,flannel,docker | 
| k8s-node04 | 10.64.144.104 | kube-proxy,kubelet,flannel,docker | 
| k8s-node05 | 10.64.144.105 | kube-proxy,kubelet,flannel,docker | 
| k8s-node06 | 10.64.144.106 | kube-proxy,kubelet,flannel,docker | 
网络规划
| 功能 | 网段 | 
|---|---|
| Pod 网络 | 172.16.0.0/16 | 
| Service 网络 | 10.96.0.0/12 | 
| DNS 地址 | 10.96.0.10 | 
| LoadBalance | 10.64.144.150~10.64.144.200 | 
系统初始化
所有节点上都需要操作
配置 hosts 同步
$ cat > /etc/hosts <<EOF
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
10.64.144.100   k8s-master
10.64.144.101   k8s-node01
10.64.144.102   k8s-node02
10.64.144.103   k8s-node03
10.64.144.104   k8s-node04
10.64.144.105   k8s-node05
10.64.144.106   k8s-node06
EOF
设置主机名
hostnamectl set-hostname k8s-master # 分别设置所有节点的主机名
更新系统
yum update -y
更新内核
安装稳定版主线内核
rpm --import https://www.elrepo.org/RPM-GPG-KEY-elrepo.org
yum install -y https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm
yum install -y --enablerepo=elrepo-kernel kernel-ml kernel-ml-headers kernel-ml-devel
查看系统中已安装的内核版本
$ awk -F\' '$1=="menuentry " {print i++ " : " $2}' /etc/grub2.cfg
0 : CentOS Linux (5.6.13-1.el7.elrepo.x86_64) 7 (Core)
1 : CentOS Linux (3.10.0-1062.4.1.el7.x86_64) 7 (Core)
2 : CentOS Linux (3.10.0-957.el7.x86_64) 7 (Core)
3 : CentOS Linux (0-rescue-4097349c950a4862a8fab2587d598af7) 7 (Core)
切换默认内核
0为上面列出的序号,比如我这里0代表5.6.13这个版本的内核
$ grub2-set-default 0
$ grub2-mkconfig -o /boot/grub2/grub.cfg
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-5.6.13-1.el7.elrepo.x86_64
Found initrd image: /boot/initramfs-5.6.13-1.el7.elrepo.x86_64.img
Found linux image: /boot/vmlinuz-3.10.0-1062.4.1.el7.x86_64
Found initrd image: /boot/initramfs-3.10.0-1062.4.1.el7.x86_64.img
Found linux image: /boot/vmlinuz-3.10.0-957.el7.x86_64
Found initrd image: /boot/initramfs-3.10.0-957.el7.x86_64.img
Found linux image: /boot/vmlinuz-0-rescue-4097349c950a4862a8fab2587d598af7
Found initrd image: /boot/initramfs-0-rescue-4097349c950a4862a8fab2587d598af7.img
done
配置网卡驱动(可选)
实验环境中由于使用的是普通 PC,板载网卡为 realtek r8168/r8169, 这个网卡在 5.x 版本内核中由于缺少加载 realtek.ko 模块会造成无法正常识别到网卡的情况
$ mkinitrd --force --preload realtek /boot/initramfs-`uname -r`.img `uname -r`
$ lsinitrd /boot/initramfs-`uname -r`.img | grep realtek
Arguments: -f --add-drivers ' realtek'
...
-rwxr--r--   1 root     root       127880 Jan  7 16:21 usr/lib/modules/5.6.13-1.el7.elrepo.x86_64/kernel/drivers/net/ethernet/realtek/r8169.ko
-rwxr--r--   1 root     root        27360 Jan  7 16:21 usr/lib/modules/5.6.13-1.el7.elrepo.x86_64/kernel/drivers/net/phy/realtek.ko
确保有加载 realtek.ko 和 r8169.ko 模块
重启后验证内核版本
$ uname -r
5.6.13-1.el7.elrepo.x86_64
禁用 SELinux
如果不禁用 selinux,挂载目录会出现 Permission deined 错误
sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config  # 永久关闭
setenforce 0  # 临时关闭
禁用 swap
swapoff -a
sed -i -r 's/(.+ swap .*)/# \1/' /etc/fstab
sysctl -w vm.swappiness=0
cat > /etc/sysctl.d/swap.conf <<EOF
vm.swappiness=0
EOF
sysctl -p /etc/sysctl.d/swap.conf
禁用不需要的服务
如果选择不关闭firewalld,请参考官方文档中放行相应的端口 check-required-ports
最少化安装的 Centos 中貌似没有安装 dnsmasq,如果有安装则需要将其禁用,否则它会把节点上的 DNS server 设置为 127.0.0.1,将会导致 Docker 无法解析域名
systemctl disable --now firewalld NetworkManager dnsmasq
安装必要工具
kube-proxy 的 ipvs 模式下依赖ipvsadm,ipset用于配置及管理规则
kubectl port-forward命令依赖socat做端口转发
yum install -y ipvsadm ipset sysstat conntrack libseccomp socat wget git jq curl
优化系统内核
$ cat > /etc/sysctl.d/k8s.conf <<EOF
# https://github.com/moby/moby/issues/31208
# ipvsadm -l --timout
# 修复ipvs模式下长连接timeout问题 小于900即可
net.ipv4.tcp_keepalive_time = 600
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 10
# 关闭ipv6
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
net.ipv4.neigh.default.gc_stale_time = 120
net.ipv4.conf.all.rp_filter = 0
net.ipv4.conf.default.rp_filter = 0
net.ipv4.conf.default.arp_announce = 2
net.ipv4.conf.lo.arp_announce = 2
net.ipv4.conf.all.arp_announce = 2
# 开启路由转发
net.ipv4.ip_forward = 1
net.ipv4.tcp_max_tw_buckets = 5000
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_max_syn_backlog = 1024
net.ipv4.tcp_synack_retries = 2
# 开启 bridge-netfilter, 使用iptables规则可以作用于bridge模式
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-arptables = 1
net.netfilter.nf_conntrack_max = 2310720
fs.inotify.max_user_watches=89100
fs.may_detach_mounts = 1
fs.file-max = 52706963
fs.nr_open = 52706963
vm.overcommit_memory=1
vm.panic_on_oom=0
EOF
$ sysctl --system  # 使配置生效
开启 ipvs 支持
如果内核版本小于
4.19, 需要将nf_conntrack修改为nf_conntrack_ipv4
# 临时生效
modprobe -- ip_vs
modprobe -- ip_vs_rr
modprobe -- ip_vs_wrr
modprobe -- ip_vs_sh
modprobe -- nf_conntrack
# 永久生效
$ cat > /etc/sysconfig/modules/ipvs.modules <<'EOF'
#!/bin/sh
modules=(
ip_vs
ip_vs_rr
ip_vs_wrr
ip_vs_sh
nf_conntrack_ipv4
br_netfilter
)
for mod in ${modules[@]}
do
  /sbin/modinfo -F filename ${mod} > /dev/null 2>&1
  if [ $? -eq 0 ]
  then
      /sbin/modprobe ${mod}
  fi
done
EOF
chmod +x /etc/sysconfig/modules/ipvs.modules
# 检查内核模块是否已经加载
$ lsmod | grep ip_vs
ip_vs_sh               12688  0
ip_vs_wrr              12697  0
ip_vs_rr               12600  4
ip_vs                 145497  10 ip_vs_rr,ip_vs_sh,ip_vs_wrr
nf_conntrack          139224  7 ip_vs,nf_nat,nf_nat_ipv4,xt_conntrack,nf_nat_masquerade_ipv4,nf_conntrack_netlink,nf_conntrack
libcrc32c              12644  4 xfs,ip_vs,nf_nat,nf_conntrack
时间同步
所有节点配置为同一时区,并使用 chrony 同步节点的时间
$ ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
$ echo "Asia/Shanghai" > /etc/timezone
$ yum install chrony -y
$ cat > /etc/chrony.conf <<EOF
server ntp.aliyun.com iburst
stratumweight 0
driftfile /var/lib/chrony/drift
rtcsync
makestep 10 3
bindcmdaddress 127.0.0.1
bindcmdaddress ::1
keyfile /etc/chrony.keys
commandkey 1
generatecommandkey
logchange 0.5
logdir /var/log/chrony
EOF
$ systemctl enable --now chronyd
运行 docker 检测脚本
docker 官方提供了脚本用于检查内核相关配置
|  |  | 
如上高亮行所示,开启 user_namespace
$ grubby --args="user_namespace.enable=1" --update-kernel="$(grubby --default-kernel)"
$ grep user_namespace /etc/grub2.cfg
        linux16 /vmlinuz-5.6.13-1.el7.elrepo.x86_64 root=/dev/mapper/centos-root ro crashkernel=auto rd.lvm.lv=centos/root rhgb quiet user_namespace.enable=1
重启系统
重启系统以使配置生效
systemctl reboot
安装 Docker
所有节点上均需配置
虽然 Kubernetes 支持多种容器运行环境,如 Docker, Containerd, CRI-O 等,但目前 Docker 还是主流
关于容器运行环境可以参考官方文档 Container Runntimes
支持的 Docker 版本列表请查看官方 CHANGELOG 搜索 The list of validated docker versions 关键字
安装
官方推荐使用 18.06.2, 我这里使用的是 18.09.9
Docker 官方的 yum 源在国内可能会访问不到,这里使用了阿里云的镜像源
yum install -y yum-utils device-mapper-persistent-data lvm2
export VERSION=18.09
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
修改配置文件
主要修改以下几个参数:
- native.cgroupdriver: 官方推荐使用- systemd来管理 cgroup,且必须与 kubelet 的- --cgroup-driver参数一致
- storage-driver: 存储驱动,- overlay2会比其他驱动更好的性能。如果文件系统为- XFS,请确保- ftype=1(可使用- xfs_info查看),否则 docker 会无法启动,报错信息- Error starting daemon: error initializing graphdriver: overlay2: the backing xfs filesystem is formatted without d_type support, which leads to incorrect behavior. Reformat the filesystem with ftype=1 to enable d_type support. Backing filesystems without d_type support are not supported.
- registry-mirrors: 加速从 dockerhub 拉取镜像
mkdir /etc/docker
cat > /etc/docker/daemon.json <<EOF
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "registry-mirrors": ["https://8jg6za8m.mirror.aliyuncs.com"],
  "storage-driver": "overlay2",
  "storage-opts": [
    "overlay2.override_kernel_check=true"
  ],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "3"
  }
}
EOF
启动并检查配置是否生效
$ systemctl enable --now docker
Created symlink from /etc/systemd/system/multi-user.target.wants/docker.service to /usr/lib/systemd/system/docker.service.
$ docker version
Client:
 Version:           18.09.9
 API version:       1.39
 Go version:        go1.11.13
 Git commit:        039a7df9ba
 Built:             Wed Sep  4 16:51:21 2019
 OS/Arch:           linux/amd64
 Experimental:      false
Server: Docker Engine - Community
 Engine:
  Version:          18.09.9
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.11.13
  Git commit:       039a7df
  Built:            Wed Sep  4 16:22:32 2019
  OS/Arch:          linux/amd64
  Experimental:     false
$ docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 18.09.9
Storage Driver: overlay2
 Backing Filesystem: xfs
 Supports d_type: true
 Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: systemd
Plugins:
 Volume: local
 Network: bridge host macvlan null overlay
 Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: b34a5c8af56e510852c35414db4c1f4fa6172339
runc version: 3e425f80a8c931f88e6d94a8c831b9d5aa481657
init version: fec3683
Security Options:
 seccomp
  Profile: default
Kernel Version: 5.6.13-1.el7.elrepo.x86_64
Operating System: CentOS Linux 7 (Core)
OSType: linux
Architecture: x86_64
CPUs: 4
Total Memory: 15.39GiB
Name: k8s-master
ID: BI53:OQC7:QV6C:SP3N:FBTT:N5ST:MK3I:CGLJ:X2JX:CJOH:CBEE:4GG2
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
Labels:
Experimental: false
Insecure Registries:
 127.0.0.0/8
Registry Mirrors:
 https://8jg6za8m.mirror.aliyuncs.com/
Live Restore Enabled: false
Product License: Community Engine
安装 kubernetes 相关工具
所有节点上操作
添加阿里云 YUM 源
默认官方的源在国内环境下可能会访问不到
cat > /etc/yum.repos.d/kubernetes.repo <<EOF
[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 --disablerepo="*" --enablerepo="kubernetes" list available --showduplicates  | grep "1.16"
kubeadm.x86_64                       1.16.0-0                         kubernetes
kubeadm.x86_64                       1.16.1-0                         kubernetes
kubeadm.x86_64                       1.16.2-0                         kubernetes
kubeadm.x86_64                       1.16.3-0                         kubernetes
kubectl.x86_64                       1.16.0-0                         kubernetes
kubectl.x86_64                       1.16.1-0                         kubernetes
kubectl.x86_64                       1.16.2-0                         kubernetes
kubectl.x86_64                       1.16.3-0                         kubernetes
kubelet.x86_64                       1.16.0-0                         kubernetes
kubelet.x86_64                       1.16.1-0                         kubernetes
kubelet.x86_64                       1.16.2-0                         kubernetes
kubelet.x86_64                       1.16.3-0                         kubernetes
安装 kubeadm、kubectl、kubelet 工具
本次安装
1.16.3版本
worker 节点可以不安装 kubectl
yum install kubeadm-1.16.3 kubectl-1.16.3 kubelet-1.16.3 -y
添加 kubelet 开机自启动
$ systemctl enable --now kubelet
Created symlink from /etc/systemd/system/multi-user.target.wants/kubelet.service to /usr/lib/systemd/system/kubelet.service.
$ systemctl status kubelet
● kubelet.service - kubelet: The Kubernetes Node Agent
   Loaded: loaded (/usr/lib/systemd/system/kubelet.service; enabled; vendor preset: disabled)
  Drop-In: /usr/lib/systemd/system/kubelet.service.d
           └─10-kubeadm.conf
   Active: activating (auto-restart) (Result: exit-code) since Thu 2019-11-21 10:27:34 CST; 898ms ago
     Docs: https://kubernetes.io/docs/
  Process: 3947 ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS (code=exited, status=255)
 Main PID: 3947 (code=exited, status=255)
Nov 21 10:27:34 k8s-master systemd[1]: Unit kubelet.service entered failed state.
Nov 21 10:27:34 k8s-master systemd[1]: kubelet.service failed.
如上查看服务状态你会发现启动失败,不过没关系,这里可以暂时忽略,因为我们还没开始初始化集群,所以还没有生成kubelet的配置文件
如果 enable 时提示 Unit kubelet.service could not be found.,执行systemctl daemon-reload重载配置就可以了
初始化集群
在 master 上操作
生成 kubeadm 配置文件
生成默认配置
kubeadm config print init-defaults --component-configs KubeProxyConfiguration,KubeletConfiguration > kubeadm-config.yaml
修改配置
主要改动如下:
|  |  | 
下载所需镜像(可选)
初始化的时候也会自动下载所有的镜像,提前下载好镜像可减少后续初始化时的等待时间
$ kubeadm config images list --config kubeadm-config.yaml # 查看镜像列表
registry.cn-hangzhou.aliyuncs.com/google_containers/kube-apiserver:v1.16.3
registry.cn-hangzhou.aliyuncs.com/google_containers/kube-controller-manager:v1.16.3
registry.cn-hangzhou.aliyuncs.com/google_containers/kube-scheduler:v1.16.3
registry.cn-hangzhou.aliyuncs.com/google_containers/kube-proxy:v1.16.3
registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.1
registry.cn-hangzhou.aliyuncs.com/google_containers/etcd:3.3.15-0
registry.cn-hangzhou.aliyuncs.com/google_containers/coredns:1.6.2
$ kubeadm config images pull --config kubeadm-config.yaml # 下载镜像
[config/images] Pulled registry.cn-hangzhou.aliyuncs.com/google_containers/kube-apiserver:v1.16.3
[config/images] Pulled registry.cn-hangzhou.aliyuncs.com/google_containers/kube-controller-manager:v1.16.3
[config/images] Pulled registry.cn-hangzhou.aliyuncs.com/google_containers/kube-scheduler:v1.16.3
[config/images] Pulled registry.cn-hangzhou.aliyuncs.com/google_containers/kube-proxy:v1.16.3
[config/images] Pulled registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.1
[config/images] Pulled registry.cn-hangzhou.aliyuncs.com/google_containers/etcd:3.3.15-0
[config/images] Pulled registry.cn-hangzhou.aliyuncs.com/google_containers/coredns:1.6.2
初始化
$ kubeadm init --config kubeadm-config.yaml
[init] Using Kubernetes version: v1.16.3
[preflight] Running pre-flight checks
[preflight] Pulling images required for setting up a Kubernetes cluster
[preflight] This might take a minute or two, depending on the speed of your internet connection
[preflight] You can also perform this action in beforehand using 'kubeadm config images pull'
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Activating the kubelet service
[certs] Using certificateDir folder "/etc/kubernetes/pki"
[certs] Generating "ca" certificate and key
[certs] Generating "apiserver" certificate and key
[certs] apiserver serving cert is signed for DNS names [k8s-master kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs [10.96.0.1 10.64.144.100]
[certs] Generating "apiserver-kubelet-client" certificate and key
[certs] Generating "front-proxy-ca" certificate and key
[certs] Generating "front-proxy-client" certificate and key
[certs] Generating "etcd/ca" certificate and key
[certs] Generating "etcd/server" certificate and key
[certs] etcd/server serving cert is signed for DNS names [k8s-master localhost] and IPs [10.64.144.100 127.0.0.1 ::1]
[certs] Generating "etcd/peer" certificate and key
[certs] etcd/peer serving cert is signed for DNS names [k8s-master localhost] and IPs [10.64.144.100 127.0.0.1 ::1]
[certs] Generating "etcd/healthcheck-client" certificate and key
[certs] Generating "apiserver-etcd-client" certificate and key
[certs] Generating "sa" key and public key
[kubeconfig] Using kubeconfig folder "/etc/kubernetes"
[kubeconfig] Writing "admin.conf" kubeconfig file
[kubeconfig] Writing "kubelet.conf" kubeconfig file
[kubeconfig] Writing "controller-manager.conf" kubeconfig file
[kubeconfig] Writing "scheduler.conf" kubeconfig file
[control-plane] Using manifest folder "/etc/kubernetes/manifests"
[control-plane] Creating static Pod manifest for "kube-apiserver"
[control-plane] Creating static Pod manifest for "kube-controller-manager"
[control-plane] Creating static Pod manifest for "kube-scheduler"
[etcd] Creating static Pod manifest for local etcd in "/etc/kubernetes/manifests"
[wait-control-plane] Waiting for the kubelet to boot up the control plane as static Pods from directory "/etc/kubernetes/manifests". This can take up to 4m0s
[apiclient] All control plane components are healthy after 34.001666 seconds
[upload-config] Storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace
[kubelet] Creating a ConfigMap "kubelet-config-1.16" in namespace kube-system with the configuration for the kubelets in the cluster
[upload-certs] Skipping phase. Please see --upload-certs
[mark-control-plane] Marking the node k8s-master as control-plane by adding the label "node-role.kubernetes.io/master=''"
[mark-control-plane] Marking the node k8s-master as control-plane by adding the taints [node-role.kubernetes.io/master:NoSchedule]
[bootstrap-token] Using token: d7cb0e.605fb37fed0895d3
[bootstrap-token] Configuring bootstrap tokens, cluster-info ConfigMap, RBAC Roles
[bootstrap-token] configured RBAC rules to allow Node Bootstrap tokens to post CSRs in order for nodes to get long term certificate credentials
[bootstrap-token] configured RBAC rules to allow the csrapprover controller automatically approve CSRs from a Node Bootstrap Token
[bootstrap-token] configured RBAC rules to allow certificate rotation for all node client certificates in the cluster
[bootstrap-token] Creating the "cluster-info" ConfigMap in the "kube-public" namespace
[addons] Applied essential addon: CoreDNS
[addons] Applied essential addon: kube-proxy
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 10.64.144.100:6443 --token d7cb0e.605fb37fed0895d3 \
    --discovery-token-ca-cert-hash sha256:dcabbe4b86492eb5da1f22c2a24fb1b5698557185abd481d858eda25a8d926aa
如上提示有三条信息
- 需要配置 kubeconfig ,以便 kubectl 可以操作集群
- 需要选择一个网络插件进行安装,以使集群可以连接网络通信
- 保存最后一条命令,后续 worker 节点加入集群时会用到
配置 kubeconfig
按上面提示操作就行
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
使用 kubectl 查看集群当前状态,顺便检验 kubeconfig 是否配置正确
$ kubectl get pods -n kube-system # 查看系统组件运行状态
NAME                                 READY   STATUS    RESTARTS   AGE
coredns-67c766df46-5d5pp             0/1     Pending   0          4m40s
coredns-67c766df46-kjd9f             0/1     Pending   0          4m40s
etcd-k8s-master                      1/1     Running   0          3m34s
kube-apiserver-k8s-master            1/1     Running   0          3m41s
kube-controller-manager-k8s-master   1/1     Running   0          3m44s
kube-proxy-r2w74                     1/1     Running   0          4m39s
kube-scheduler-k8s-master            1/1     Running   0          3m44s
$ kubectl get nodes  # 查看节点状态
NAME         STATUS     ROLES    AGE    VERSION
k8s-master   NotReady   master   5m6s   v1.16.3
以上两个 coredns 的 pod 处于 Pending 状态,以及 master 节点 NotReady 状态,是由于我们集群中还没有安装可通信的网络插件,暂时先忽略即可
加入 worker 节点到集群
在 woker 节点上操作
还记得kubeadm init后输出的最后一条命令吗,worker 节点上只需要执行这一条命令就行了
$ kubeadm join 10.64.144.100:6443 --token d7cb0e.605fb37fed0895d3 --discovery-token-ca-cert-hash sha256:dcabbe4b86492eb5da1f22c2a24fb1b5698557185abd481d858eda25a8d926aa
[preflight] Running pre-flight checks
[preflight] Reading configuration from the cluster...
[preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml'
[kubelet-start] Downloading configuration for the kubelet from the "kubelet-config-1.16" ConfigMap in the kube-system namespace
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Activating the kubelet service
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...
This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.
Run 'kubectl get nodes' on the control-plane to see this node join the cluster.
master 节点上操作
所有节点上都执行后,在 master 上查看节点状态
$ kubectl get nodes
NAME         STATUS     ROLES    AGE   VERSION
k8s-master   NotReady   master   18m   v1.16.3
k8s-node01   NotReady   <none>   57s   v1.16.3
k8s-node02   NotReady   <none>   20s   v1.16.3
k8s-node03   NotReady   <none>   9s    v1.16.3
k8s-node04   NotReady   <none>   2s    v1.16.3
k8s-node05   NotReady   <none>   2s    v1.16.3
k8s-node06   NotReady   <none>   2s    v1.16.3
到目前为止,我们集群已经创建好的,但是节点间还不能相互通信,所以状态都还是NotReady
接下来我们将选择一款适合自己的网络插件来部署
安装 CNI 网络插件
所有操作均在 master 节点上
注意事项及支持的网络插件列表请参考官网 Installing a pod network add-on 及 Networking and Network Policy
关于各 CNI 插件的功能对比可以参考 Kubernetes CNI 网络最强对比:Flannel、Calico、Canal 和 Weave 和性能基准测试 K8S 网络插件(CNI)超过 10Gbit/s 的基准测试结果
由于我们实验环境是二层可达,所以选用 Calico BGP 模式,如果二层不可达,可以考虑使用 Calico IPIP 或者 Flannel VXLAN
下载 calico.yaml
curl -O https://docs.projectcalico.org/manifests/calico.yaml
默认情况下,不做修改也可以,calico 会自动发现 cluster cidr, 这里我们自行修改下
主要改动如下:
- CALICO_IPV4POOL_IPIP设置为- Never,关闭 IPIP 模式
- CALICO_IPV4POOL_CIDR设置为- 172.16.0.0/16,对应 kubeadm 中的- cluster-cidr
部署
kubectl apply -f caclico.yml
通过 calicoctl 查看 BGP 节点状态
calicoctl 可以支持通过直接操作 etcd 或者对接 kubernetes apiserver 两种数据源,我这里使用 kubernetes 方式
具体的安装及配置方式请参考官网 Install calicoctl 与 Configure calicoctl
$ export CALICO_DATASTORE_TYPE=kubernetes
$ export CALICO_KUBECONFIG=/etc/kubernetes/admin.conf
$ calicoctl node status
Calico process is running.
IPv4 BGP status
+---------------+-------------------+-------+----------+-------------+
| PEER ADDRESS  |     PEER TYPE     | STATE |  SINCE   |    INFO     |
+---------------+-------------------+-------+----------+-------------+
| 10.64.144.101 | node-to-node mesh | up    | 18:11:48 | Established |
| 10.64.144.102 | node-to-node mesh | up    | 18:10:48 | Established |
| 10.64.144.103 | node-to-node mesh | up    | 18:08:10 | Established |
| 10.64.144.104 | node-to-node mesh | up    | 18:22:31 | Established |
| 10.64.144.105 | node-to-node mesh | up    | 18:22:31 | Established |
| 10.64.144.106 | node-to-node mesh | up    | 18:07:43 | Established |
+---------------+-------------------+-------+----------+-------------+
IPv6 BGP status
No IPv6 peers found.
确认查看集群状态
$ kubectl get pods -n kube-system
NAME                                       READY   STATUS    RESTARTS   AGE
calico-kube-controllers-7d4d547dd6-qr98t   1/1     Running   0          22s
calico-node-8d8bm                          1/1     Running   0          22s
calico-node-hlv2h                          1/1     Running   0          22s
calico-node-nbt4b                          1/1     Running   0          22s
calico-node-p4s45                          1/1     Running   0          22s
calico-node-srb88                          1/1     Running   0          22s
calico-node-vfpfv                          1/1     Running   0          22s
coredns-67c766df46-5d5pp                   1/1     Running   0          15m
coredns-67c766df46-kjd9f                   1/1     Running   0          15m
etcd-k8s-master                            1/1     Running   0          15m
kube-apiserver-k8s-master                  1/1     Running   0          14m
kube-controller-manager-k8s-master         1/1     Running   0          15m
kube-proxy-5tx57                           1/1     Running   0          14m
kube-proxy-6zvzv                           1/1     Running   0          14m
kube-proxy-hv8zt                           1/1     Running   0          14m
kube-proxy-rfsjl                           1/1     Running   0          14m
kube-proxy-tm7jz                           1/1     Running   0          15m
kube-proxy-kkssc                           1/1     Running   0          13m
kube-scheduler-k8s-master                  1/1     Running   0          14m
$ kubectl get nodes
NAME         STATUS   ROLES    AGE   VERSION
k8s-master   Ready    master   16m   v1.16.3
k8s-node01   Ready    <none>   14m   v1.16.3
k8s-node02   Ready    <none>   14m   v1.16.3
k8s-node03   Ready    <none>   14m   v1.16.3
k8s-node04   Ready    <none>   14m   v1.16.3
k8s-node05   Ready    <none>   15m   v1.16.3
k8s-node06   Ready    <none>   14m   v1.16.3
此时可以发现所有节点已经是 Ready 状态,且 coredns 也已经正常 Running
配置节点之间流量加密 (可选)
所有节点上操作
calico 支持通过 WireGuard 对节点之间的流量进行加密传输(仅支持 BGP 模式)
目前处于 preview 状态,不可用于生产环境
安装 WireGuard 可参考 WireGuard Installation
yum install epel-release https://www.elrepo.org/elrepo-release-7.el7.elrepo.noarch.rpm
yum install yum-plugin-elrepo
yum install kmod-wireguard wireguard-tools
加载 WireGuard 内核模块
$ modprobe wireguard
$ lsmod | grep wireguard
wireguard              86016  0
curve25519_x86_64      40960  1 wireguard
libchacha20poly1305    16384  1 wireguard
ip6_udp_tunnel         16384  1 wireguard
udp_tunnel             16384  1 wireguard
libblake2s             16384  1 wireguard
libcurve25519_generic    53248  2 curve25519_x86_64,wireguard
添加模块开机自动加载
$ cat > /etc/sysconfig/modules/wireguard.modules <<'EOF'
#!/bin/bash
/sbin/modinfo -F filename wireguard > /dev/null 2>&1
if [ $? -eq 0 ]; then
    /sbin/modprobe wireguard
fi
EOF
$ chmod +x wireguard.modules
修改配置开启 wireguard 功能
$ calicoctl patch felixconfiguration default --type='merge' -p '{"spec":{"wireguardEnabled":true}}'
Successfully patched 1 'FelixConfiguration' resource
查看节点状态
$ calicoctl get node k8s-master -oyaml
apiVersion: projectcalico.org/v3
kind: Node
....
status:
  wireguardPublicKey: 1OP5d3RVhBK7FE9tJ6l+eG38isdiy3LHzfqPzBSwI34=
测试
创建 pod 测试集群是否能正常工作
尝试创建名为 busybox 的 pod
由于 1.28 以上版本的 busybox 镜像,使用 nslookup 的时候有些 bug 导致不能解析域名,此处使用官方提供提供的 1.28 版本 busybox 的 pod 配置
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/website/master/content/en/examples/admin/dns/busybox.yaml
pod/busybox created
测试 coredns 能否正常工作
在 busybox pod 中测试
$ kubectl exec -it busybox -- nslookup kubernetes.default.svc.cluster.local
Server:    10.96.0.10
Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
Name:      kubernetes.default.svc.cluster.local
Address 1: 10.96.0.1 kubernetes.default.svc.cluster.local
在节点中使用 dig 命令测试
$ dig kubernetes.default.svc.cluster.local @10.96.0.10
; <<>> DiG 9.11.4-P2-RedHat-9.11.4-9.P2.el7 <<>> kubernetes.default.svc.cluster.local @10.96.0.10
;; global options: +cmd
;; Got answer:
;; WARNING: .local is reserved for Multicast DNS
;; You are currently testing what happens when an mDNS query is leaked to DNS
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 56913
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;kubernetes.default.svc.cluster.local. IN A
;; ANSWER SECTION:
kubernetes.default.svc.cluster.local. 14 IN A   10.96.0.1
;; Query time: 1 msec
;; SERVER: 10.96.0.10#53(10.96.0.10)
;; WHEN: Fri Nov 22 12:21:58 CST 2019
;; MSG SIZE  rcvd: 117
安装 Ingress
配置文件参考官方 mandatory.yaml
不过官方是使用 Deployment 来部署的,我这里改成了 DaemonSet 方式,同时补上了 Service 的部分
完整配置如下
$ cat > ingress-nginx.yaml <<'EOF'
apiVersion: v1
kind: Namespace
metadata:
  name: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
  name: nginx-configuration
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
  name: tcp-services
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
---
kind: ConfigMap
apiVersion: v1
metadata:
  name: udp-services
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nginx-ingress-serviceaccount
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: nginx-ingress-clusterrole
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
rules:
  - apiGroups:
      - ""
    resources:
      - configmaps
      - endpoints
      - nodes
      - pods
      - secrets
    verbs:
      - list
      - watch
  - apiGroups:
      - ""
    resources:
      - nodes
    verbs:
      - get
  - apiGroups:
      - ""
    resources:
      - services
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - ""
    resources:
      - events
    verbs:
      - create
      - patch
  - apiGroups:
      - "extensions"
      - "networking.k8s.io"
    resources:
      - ingresses
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - "extensions"
      - "networking.k8s.io"
    resources:
      - ingresses/status
    verbs:
      - update
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: Role
metadata:
  name: nginx-ingress-role
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
rules:
  - apiGroups:
      - ""
    resources:
      - configmaps
      - pods
      - secrets
      - namespaces
    verbs:
      - get
  - apiGroups:
      - ""
    resources:
      - configmaps
    resourceNames:
      # Defaults to "<election-id>-<ingress-class>"
      # Here: "<ingress-controller-leader>-<nginx>"
      # This has to be adapted if you change either parameter
      # when launching the nginx-ingress-controller.
      - "ingress-controller-leader-nginx"
    verbs:
      - get
      - update
  - apiGroups:
      - ""
    resources:
      - configmaps
    verbs:
      - create
  - apiGroups:
      - ""
    resources:
      - endpoints
    verbs:
      - get
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
  name: nginx-ingress-role-nisa-binding
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: nginx-ingress-role
subjects:
  - kind: ServiceAccount
    name: nginx-ingress-serviceaccount
    namespace: ingress-nginx
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: nginx-ingress-clusterrole-nisa-binding
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: nginx-ingress-clusterrole
subjects:
  - kind: ServiceAccount
    name: nginx-ingress-serviceaccount
    namespace: ingress-nginx
---
apiVersion: apps/v1
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:
  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:
        kubernetes.io/os: linux
      containers:
        - name: nginx-ingress-controller
          image: quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.26.1
          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 -> 33
            runAsUser: 33
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          ports:
            - name: http
              containerPort: 80
            - name: https
              containerPort: 443
          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
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-ingress-controller
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
spec:
  type: LoadBalancer
  externalTrafficPolicy: Local
  selector:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: 80
    - name: https
      port: 443
      protocol: TCP
      targetPort: 443
EOF
部署
$ kubectl apply -f ingress-nginx.yaml  # 执行部署
namespace/ingress-nginx created
configmap/nginx-configuration created
configmap/tcp-services created
configmap/udp-services created
serviceaccount/nginx-ingress-serviceaccount created
clusterrole.rbac.authorization.k8s.io/nginx-ingress-clusterrole created
role.rbac.authorization.k8s.io/nginx-ingress-role created
rolebinding.rbac.authorization.k8s.io/nginx-ingress-role-nisa-binding created
clusterrolebinding.rbac.authorization.k8s.io/nginx-ingress-clusterrole-nisa-binding created
daemonset.apps/nginx-ingress-controller created
service/nginx-ingress-controller created
$ kubectl get pods -n ingress-nginx  # 查看pod运行状态
NAME                             READY   STATUS    RESTARTS   AGE
nginx-ingress-controller-2hc8m   1/1     Running   0          4m36s
nginx-ingress-controller-69rfm   1/1     Running   0          4m36s
nginx-ingress-controller-ps7gt   1/1     Running   0          4m36s
nginx-ingress-controller-zfq56   1/1     Running   0          4m36s
$ kubectl get service -n ingress-nginx  # 查看service状态
NAME                       TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)                      AGE
nginx-ingress-controller   LoadBalancer   10.96.76.163   <pending>     80:31832/TCP,443:30219/TCP   6m10s
此时发现 pod 已经正常运行了,但是 service 的 EXTERNAL-IP 字段却是 pending 状态
这是由于我们集群中还没有能处理 LoadBalancer 类型 service 的程序
在云环境中都会有个额外的组件 Cloud-Controller-Manager(CCM) 来对接云平台的负载均衡服务,如阿里云的 SLB,AWS 的 ALB 等
那么线下环境有没有办法实现类似 CCM 的功能呢,答案是 metallb
部署 Metallb
metallb 是一款为祼机 Kubernetes 环境提供标准路由协议的开源软负载均衡器,支持基于二层网络 ARP 或基于 BGP 方式进行地址广播
关于裸机下基于 Metallb 的负载均衡方案可参考 ingress-nginx 官方文档 A pure software solution: MetalLB
由于当前的物理环境中不支持 BGP,所以使用最简单的二层 ARP 方式
部署:
$ kubectl apply -f https://raw.githubusercontent.com/google/metallb/v0.8.3/manifests/metallb.yaml
namespace/metallb-system created
podsecuritypolicy.policy/speaker created
serviceaccount/controller created
serviceaccount/speaker created
clusterrole.rbac.authorization.k8s.io/metallb-system:controller created
clusterrole.rbac.authorization.k8s.io/metallb-system:speaker created
role.rbac.authorization.k8s.io/config-watcher created
clusterrolebinding.rbac.authorization.k8s.io/metallb-system:controller created
clusterrolebinding.rbac.authorization.k8s.io/metallb-system:speaker created
rolebinding.rbac.authorization.k8s.io/config-watcher created
daemonset.apps/speaker created
deployment.apps/controller created
$ kubectl get pods -n metallb-system
NAME                          READY   STATUS    RESTARTS   AGE
controller-65895b47d4-fbs7t   1/1     Running   0          3m21s
speaker-2skdk                 1/1     Running   0          3m21s
speaker-6mg76                 1/1     Running   0          3m21s
speaker-7rr8q                 1/1     Running   0          3m21s
speaker-dv2rh                 1/1     Running   0          3m21s
speaker-mwkqr                 1/1     Running   0          3m21s
创建配置文件
addresses 表示 LoadBalancer 可使用的 IP 池范围
$ cat > metallb-configmap.yaml <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |
    address-pools:
    - name: default
      protocol: layer2
      addresses:
      - 10.64.144.150-10.64.144.250
EOF
$ kubectl apply -f metallb-configmap.yaml
configmap/config created
而此时再去查看 ingree-nginx 的 service 可以发现已经 EXTERNAL-IP 字段已经能正常拿到 IP 了
$ kubectl get service -n ingress-nginx
NAME                       TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)                      AGE
nginx-ingress-controller   LoadBalancer   10.96.76.163   10.64.144.150   80:31832/TCP,443:30219/TCP   50m
测试 ingress-nginx 及 metallb
前面已经创建了 ingress-nginx 及 metallb,但到目前为止也只是看到了集群中的状态,并不能说明它俩是真的可用
所以我们来创建一个简单的应用来测试下
$ cat > nginx-test.yaml <<EOF
---
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  name: nginx
  labels:
    run: nginx
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
    name: http
  selector:
    run: nginx
status:
  loadBalancer: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    run: nginx
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      run: nginx
  template:
    metadata:
      creationTimestamp: null
      labels:
        run: nginx
    spec:
      containers:
      - image: nginx:alpine
        name: nginx
        ports:
        - containerPort: 80
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: nginx
  labels:
    run: nginx
spec:
  rules:
  - host: test.nginx.com
    http:
      paths:
      - path: /
        backend:
          serviceName: nginx
          servicePort: http
EOF
$ kubectl apply -f nginx-test.yaml
service/nginx created
deployment.apps/nginx created
ingress.networking.k8s.io/nginx created
$ curl -H 'Host: test.nginx.com' http://10.64.144.150 # 在局域网中其他机器测试访问
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
由此 ingress-nginx 及负载均衡器 Metallb 已经部署完成